From 1888a8dc54728d170b8033e1ec22093a2b207713 Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Wed, 2 Sep 2015 17:36:14 -0700 Subject: [PATCH] New Joystick config, new Joystick control classes --- QGCApplication.pro | 18 +- qgroundcontrol.qrc | 26 +- .../calibration/joystick/joystickCenter.png | Bin 0 -> 22277 bytes .../joystick/joystickPitchDown.png | Bin 0 -> 22001 bytes .../calibration/joystick/joystickPitchUp.png | Bin 0 -> 22147 bytes .../calibration/joystick/joystickRollLeft.png | Bin 0 -> 22000 bytes .../joystick/joystickRollRight.png | Bin 0 -> 22029 bytes .../joystick/joystickThrottleDown.png | Bin 0 -> 21948 bytes .../joystick/joystickThrottleUp.png | Bin 0 -> 22057 bytes .../calibration/joystick/joystickYawLeft.png | Bin 0 -> 21913 bytes .../calibration/joystick/joystickYawRight.png | Bin 0 -> 22009 bytes src/AutoPilotPlugins/PX4/RadioComponent.qml | 11 + src/FirmwarePlugin/FirmwarePlugin.h | 6 + .../Generic/GenericFirmwarePlugin.cc | 7 + .../Generic/GenericFirmwarePlugin.h | 1 + src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc | 5 + src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h | 1 + src/Joystick/Joystick.cc | 506 ++++++++++++ src/Joystick/Joystick.h | 167 ++++ src/Joystick/JoystickManager.cc | 158 ++++ src/Joystick/JoystickManager.h | 81 ++ src/QGCApplication.cc | 29 +- src/QGCQuickWidget.cc | 2 + src/Vehicle/MultiVehicleManager.cc | 24 + src/Vehicle/Vehicle.cc | 63 ++ src/Vehicle/Vehicle.h | 41 +- src/VehicleSetup/JoystickConfig.qml | 652 +++++++++++++++ src/VehicleSetup/JoystickConfigController.cc | 739 +++++++++++++++++ src/VehicleSetup/JoystickConfigController.h | 265 ++++++ src/VehicleSetup/SetupView.qml | 22 + .../SetupViewButtonsConnected.qml | 62 -- .../SetupViewButtonsDisconnected.qml | 40 - .../CustomCommandWidgetController.cc | 1 + src/ViewWidgets/ViewWidgetController.cc | 1 + src/input/JoystickInput.cc | 764 ------------------ src/input/JoystickInput.h | 378 --------- src/qgcunittest/FileManagerTest.cc | 1 + src/uas/UAS.cc | 186 ++++- src/uas/UAS.h | 7 +- src/uas/UASMessageHandler.cc | 1 + src/ui/HDDisplay.cc | 5 +- src/ui/HSIDisplay.cc | 1 + src/ui/JoystickAxis.cc | 98 --- src/ui/JoystickAxis.h | 87 -- src/ui/JoystickAxis.ui | 168 ---- src/ui/JoystickButton.cc | 60 -- src/ui/JoystickButton.h | 66 -- src/ui/JoystickButton.ui | 92 --- src/ui/JoystickWidget.cc | 279 ------- src/ui/JoystickWidget.h | 99 --- src/ui/JoystickWidget.ui | 191 ----- src/ui/MainWindow.cc | 24 - src/ui/MainWindow.h | 7 - src/ui/QGCMAVLinkInspector.cc | 2 + src/ui/QGCWaypointListMulti.cc | 4 +- src/ui/SettingsDialog.cc | 19 - src/ui/SettingsDialog.h | 4 - src/ui/map/MAV2DIcon.cc | 1 + src/ui/uas/UASInfoWidget.cc | 13 +- src/ui/uas/UASQuickView.cc | 9 +- 60 files changed, 2998 insertions(+), 2496 deletions(-) create mode 100644 resources/calibration/joystick/joystickCenter.png create mode 100644 resources/calibration/joystick/joystickPitchDown.png create mode 100644 resources/calibration/joystick/joystickPitchUp.png create mode 100644 resources/calibration/joystick/joystickRollLeft.png create mode 100644 resources/calibration/joystick/joystickRollRight.png create mode 100644 resources/calibration/joystick/joystickThrottleDown.png create mode 100644 resources/calibration/joystick/joystickThrottleUp.png create mode 100644 resources/calibration/joystick/joystickYawLeft.png create mode 100644 resources/calibration/joystick/joystickYawRight.png create mode 100644 src/Joystick/Joystick.cc create mode 100644 src/Joystick/Joystick.h create mode 100644 src/Joystick/JoystickManager.cc create mode 100644 src/Joystick/JoystickManager.h create mode 100644 src/VehicleSetup/JoystickConfig.qml create mode 100644 src/VehicleSetup/JoystickConfigController.cc create mode 100644 src/VehicleSetup/JoystickConfigController.h delete mode 100644 src/VehicleSetup/SetupViewButtonsConnected.qml delete mode 100644 src/VehicleSetup/SetupViewButtonsDisconnected.qml delete mode 100644 src/input/JoystickInput.cc delete mode 100644 src/input/JoystickInput.h delete mode 100644 src/ui/JoystickAxis.cc delete mode 100644 src/ui/JoystickAxis.h delete mode 100644 src/ui/JoystickAxis.ui delete mode 100644 src/ui/JoystickButton.cc delete mode 100644 src/ui/JoystickButton.h delete mode 100644 src/ui/JoystickButton.ui delete mode 100644 src/ui/JoystickWidget.cc delete mode 100644 src/ui/JoystickWidget.h delete mode 100644 src/ui/JoystickWidget.ui diff --git a/QGCApplication.pro b/QGCApplication.pro index e3de2ee52..4fccc3bb6 100644 --- a/QGCApplication.pro +++ b/QGCApplication.pro @@ -142,6 +142,7 @@ INCLUDEPATH += \ src/comm \ src/FlightDisplay \ src/input \ + src/Joystick \ src/lib/qmapcontrol \ src/QmlControls \ src/uas \ @@ -214,9 +215,6 @@ FORMS += \ !MobileBuild { FORMS += \ - src/ui/JoystickButton.ui \ - src/ui/JoystickAxis.ui \ - src/ui/JoystickWidget.ui \ src/ui/QGCHilConfiguration.ui \ src/ui/QGCHilFlightGearConfiguration.ui \ src/ui/QGCHilJSBSimConfiguration.ui \ @@ -242,6 +240,8 @@ HEADERS += \ src/FlightDisplay/FlightDisplayView.h \ src/GAudioOutput.h \ src/HomePositionManager.h \ + src/Joystick/Joystick.h \ + src/Joystick/JoystickManager.h \ src/LogCompressor.h \ src/MG.h \ src/QGC.h \ @@ -352,15 +352,12 @@ HEADERS += \ src/comm/QGCHilLink.h \ src/comm/QGCJSBSimLink.h \ src/comm/QGCXPlaneLink.h \ - src/input/JoystickInput.h \ src/ui/CameraView.h \ - src/ui/JoystickAxis.h \ - src/ui/JoystickButton.h \ - src/ui/JoystickWidget.h \ src/ui/QGCHilConfiguration.h \ src/ui/QGCHilFlightGearConfiguration.h \ src/ui/QGCHilJSBSimConfiguration.h \ src/ui/QGCHilXPlaneConfiguration.h \ + src/VehicleSetup/JoystickConfigController.h \ } SOURCES += \ @@ -379,6 +376,8 @@ SOURCES += \ src/FlightDisplay/FlightDisplayView.cc \ src/GAudioOutput.cc \ src/HomePositionManager.cc \ + src/Joystick/Joystick.cc \ + src/Joystick/JoystickManager.cc \ src/LogCompressor.cc \ src/main.cc \ src/QGC.cc \ @@ -482,15 +481,12 @@ SOURCES += \ src/comm/QGCFlightGearLink.cc \ src/comm/QGCJSBSimLink.cc \ src/comm/QGCXPlaneLink.cc \ - src/input/JoystickInput.cc \ src/ui/CameraView.cc \ - src/ui/JoystickAxis.cc \ - src/ui/JoystickButton.cc \ - src/ui/JoystickWidget.cc \ src/ui/QGCHilConfiguration.cc \ src/ui/QGCHilFlightGearConfiguration.cc \ src/ui/QGCHilJSBSimConfiguration.cc \ src/ui/QGCHilXPlaneConfiguration.cc \ + src/VehicleSetup/JoystickConfigController.cc \ } # diff --git a/qgroundcontrol.qrc b/qgroundcontrol.qrc index 36df1d219..1dce5b3d0 100644 --- a/qgroundcontrol.qrc +++ b/qgroundcontrol.qrc @@ -99,15 +99,16 @@ src/QmlControls/MissionItemIndexLabel.qml src/QmlControls/MissionItemSummary.qml - src/QmlControls/ScreenToolsFontQuery.qml - src/ViewWidgets/ParameterEditorWidget.qml - src/ViewWidgets/CustomCommandWidget.qml + src/VehicleSetup/SetupView.qml - src/VehicleSetup/SetupViewButtonsConnected.qml - src/VehicleSetup/SetupViewButtonsDisconnected.qml src/VehicleSetup/VehicleSummary.qml src/VehicleSetup/FirmwareUpgrade.qml + src/VehicleSetup/JoystickConfig.qml src/VehicleSetup/SetupParameterEditor.qml + + src/QmlControls/ScreenToolsFontQuery.qml + src/ViewWidgets/ParameterEditorWidget.qml + src/ViewWidgets/CustomCommandWidget.qml src/AutoPilotPlugins/PX4/SafetyComponent.qml src/AutoPilotPlugins/PX4/RadioComponent.qml src/AutoPilotPlugins/PX4/PowerComponent.qml @@ -222,6 +223,7 @@ resources/calibration/accel_up.png resources/calibration/accel_left.png + resources/calibration/mode1/radioCenter.png resources/calibration/mode1/radioHome.png @@ -235,6 +237,7 @@ resources/calibration/mode1/radioThrottleDown.png resources/calibration/mode1/radioSwitchMinMax.png + resources/calibration/mode2/radioCenter.png resources/calibration/mode2/radioHome.png @@ -248,6 +251,19 @@ resources/calibration/mode2/radioThrottleDown.png resources/calibration/mode2/radioSwitchMinMax.png + + + resources/calibration/joystick/joystickCenter.png + resources/calibration/joystick/joystickRollLeft.png + resources/calibration/joystick/joystickRollRight.png + resources/calibration/joystick/joystickPitchUp.png + resources/calibration/joystick/joystickPitchDown.png + resources/calibration/joystick/joystickYawLeft.png + resources/calibration/joystick/joystickYawRight.png + resources/calibration/joystick/joystickThrottleUp.png + resources/calibration/joystick/joystickThrottleDown.png + + resources/styles/style-dark.css resources/styles/style-light.css diff --git a/resources/calibration/joystick/joystickCenter.png b/resources/calibration/joystick/joystickCenter.png new file mode 100644 index 0000000000000000000000000000000000000000..556321ec4b1c3142dc40acaf8068b4fb57e923a5 GIT binary patch literal 22277 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 literal 0 HcmV?d00001 diff --git a/resources/calibration/joystick/joystickPitchDown.png b/resources/calibration/joystick/joystickPitchDown.png new file mode 100644 index 0000000000000000000000000000000000000000..6e895b1a5a50dac35d977dd496a830cd26b1a5d8 GIT binary patch literal 22001 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 a;@`Jgzj!aB@(j#bQCPEcru5UEiT?*X!VI7Q literal 0 HcmV?d00001 diff --git a/resources/calibration/joystick/joystickPitchUp.png b/resources/calibration/joystick/joystickPitchUp.png new file mode 100644 index 0000000000000000000000000000000000000000..ad9d7c03fa5dc49d5889eedf2551f900381c11fc GIT binary patch literal 22147 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 Pi)ArJS)4C7bh-OKXp%vB literal 0 HcmV?d00001 diff --git a/resources/calibration/joystick/joystickRollLeft.png b/resources/calibration/joystick/joystickRollLeft.png new file mode 100644 index 0000000000000000000000000000000000000000..fddc7dfb4217051af0ca1b51897f82ef7cd2c810 GIT binary patch literal 22000 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 UxRLuUc(bzU=@@AjXx@DCfBV%I_W%F@ literal 0 HcmV?d00001 diff --git a/resources/calibration/joystick/joystickRollRight.png b/resources/calibration/joystick/joystickRollRight.png new file mode 100644 index 0000000000000000000000000000000000000000..9556eed1e7e62df8c0ffe5e0206ad03db6c6f943 GIT binary patch literal 22029 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 KME;NSo&N%yy9fIK literal 0 HcmV?d00001 diff --git a/resources/calibration/joystick/joystickThrottleUp.png b/resources/calibration/joystick/joystickThrottleUp.png new file mode 100644 index 0000000000000000000000000000000000000000..223199991fc90272b1fae1ba6f28799b31681ca2 GIT binary patch literal 22057 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 literal 0 HcmV?d00001 diff --git a/resources/calibration/joystick/joystickYawLeft.png b/resources/calibration/joystick/joystickYawLeft.png new file mode 100644 index 0000000000000000000000000000000000000000..788a09c0b8c504db6f6c1068dd408cf8f39d7554 GIT binary patch literal 21913 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 literal 0 HcmV?d00001 diff --git a/resources/calibration/joystick/joystickYawRight.png b/resources/calibration/joystick/joystickYawRight.png new file mode 100644 index 0000000000000000000000000000000000000000..381108825e10adacbbc729dd9ea1ea22b8665f87 GIT binary patch literal 22009 zcmX_|1z43`7p4!Lf`mwefQWQ=C=JrxE#2KAE#2MHDJk9EEe#?q-94M{pP9=e9?|p0 ze%D_0+>1~-8L?N$_{b0lJAVH3f9YSC`f8LF8C&bqlB~w!Wt4Z6ce0)97hQRLJE-({;cG>c-ZP?OJL&u z;l?9PSO}Bf5;|K1f{7$avHKIzT}lD(%b42V8zJJTzaz#94GSwvO7+w!GqXoqQRPcb z%X3O!uPd%9oEsGG`>pD!$Gi%zsy&h<*jc8GM9*BM+;gKsC8Q}N4Kd<{r#YwaC3H3mJcbL4qQ>0k< zqWAK~Vy0yQB|z|9I+p`{IMZ*H8aBJJPTcPM<8~W{An9*ykB2rWt5t(tyxQH#=R4!% zDN;ujePRTCTyHx}Y;te=7L4LX3F@GT^uOaWAv>N;`XNHypp={3 zd5HAEd_Ss0O!T=zezsz(|D8Yp zAFf~)6Y=ehF{<$w$FwrEn#{GC7G)F$l{>UF%k`yNBYB=&8o&J1Jxlo7V$62DH?V3@ z+H?=*aj!fz&FR&c67Ouj=l+J6DvF?VONLmhz|;=Hx0OKIu*9glXRRGqrn*_VUyW$9 zMX8bDX2|?nv3BJphHUk{3TFcE!E1B}TjXcw zIHgeA-CRT4d++g`^MNgUi0{IJI%8k-@D_NT-Gk)rh9+WKl#qfSdtpDRbDr>_syYcL7l=FUjdpzt_Xm|{D&aq0@L?KN^>fVw2A z(d7!P!39IsI10Rmme#WUD~E=wPO9?UIgj$USmrcGhI#Wv>=5}qz7#3ZhfHw_2xN0} zGxa(;Nc>x>_tZf(Es7NrW~ixW0Blgt^q`T#$&GUbR;v%qO?ic%tZuMrE##uXd`gg- z4h>3+5f^*i7+X!w$AvpXv{PJ9G+WPoA~@zX)2D!N6@o3-{B zA@zt)#X<9>@7d(uV}?3;d@_0W8Zo=E5ib~xNYKH71=Zbc~+XvVWG5CzQO@} z?IPUTzBJSQYPlYSrf&)Pqjhh`<&Q0^z(mcxV^N=_3a`35?>mV(ic)_+%Zn84bKY9uAVDv8NHdJAZ1 zYEDi~k&zXOW=T-S4eXqYgUdBy24C^Bg7CVJFLKI%^v;#wsSzD%Xrb15>$2f*SzEZN z+-sXfn9rJWKu2pj^^J@rJvB@09(Ya26k_pbq5FOMq$bs=_qv*voDdBqiwvwMrU`A_ zAZgmD%3rn`k7bGB9Jde2leBa_Qi`N(%g5K`*vLo%aAx+{BJV>JCG&f_R5$Z}BiFgp z)cuTT>klusIL%W-Du{smwf5Di3+_c-FJY4{U8p&nSuN!!7JN|$BBrHqpRHaIdihMeDR88y*<}u7FwX7 z;MSa&Kv*wX^#X0|AVut;(X576zEJBV+-gNc@tmxZ^pif@xFlwJ0P=|la@1Wjwn%4= zP_Wdo5RupN#~~x;Al^>}c(NQ_0HL0?A7tM)u za*5F=b^rN8cC^;cu=>S=7%dpA|2JDsTC!0qD=Pz{vs+$;Vr*j*lfbZI{8Ih#+nZ(U z1_<}}>izdvep=iaI0Y8v+c;hVW+8Rl1}?))x)H~Uqn_pE<@iaE{0+JIr~%}3(+u|g zq{WEKJQJp&d?jqSjEszf4sk2C9EW5yxJuE*z z-)UYxP2jGAB>qoLoW+iF^Twt7{0hnr;r-R2|M-sPHD1!@edm^a z`F-;&w<2*NcY|Vk+n`Z3JqvW%GHk*(^t1qGWOyYNH z1H1(@8BGZMH33?c^I`S%?80Oi!elW{VdAD)Qj{^0`RqNYDkYwD^Gicx6)F_N%+P+m z+l=yX;%tk_DK->oqXuqn>YKIf%m_4JFF5eG&fOHhKzz4PX1m7Cv!{$kzzYdh0s|Qq zdiU@UPEKYt$EVl8PE+ID2LEP;&U!k^(|S5NW+)gEQ~Ok`RMObhJEGL!Uim7% z4rQoZO{`8-0#ZyLg9-~>V2#w;D=cw!=h@vJLRG~EMe<_f8uN(QDOo2oO)Bhf-5Z}QEJ}kuMpOuSGoI~$y)GC_ z@dc|U(GezXUzXDvF1keGRe*Op2{1LR!CsX%?LOSZ#Ke=`Npf0RTGFg>^0+}nzb0+t zGw1T=={;dFF$8XI?umtkk2-5k>*NtOwzjVuY}WcNE?8AmRH(P(tmX}@e&BEtqICl^ zJ#>C?coAcW>zq4k>gt_cUEdf#7cWN)EmJd%iCPeFcp$novsY(AjhWl9H|uITq0P)s z%%_5NV&&lIKEAfdT@pRQ8gk0_{q{!oAAD5uM;5eTm>6+bK0ZEN5|TBq^`#N`;~&^| zoP@3$fo~_~<|vc9LN8y`a%2R?InwWJd)wD}JE8L7_TN<;b zCz_2Pz>QlsXe#M|DpX$s2q5#&_fkj|NcDt_Kvbq z*gS$UZqSlCx?X$7N7c`U2G@{=g4W8ElQ3v}hvmZGb}VMpSVUW&J#8ia*pS8e+WL!p zTD{9k{cx650iX4Dn_y0K!5{+Lhl#?dk} z;W^B4C7)358q~9VL0U^AtcQyVkAVn_$|h7DPQ_O~$M4-tr&QnD-+=L)^yb!+KcHJ5 z*8g*DU7h~((<43tec)yBU3XdJ{9K4-b+Pks?}jZYHgagHmQyeew`!muR838dI0Yu~ zZ^|F^ZF-!l-bWb!jNosJSS@QZ`)p$J!AN~>MpYsEU5b=}v$M0dE@R$y0sHQP(e#`j zva)uxU~Zu!cZ4vR-~IJ*8k*=LD%G5bZ5~ef_&+|7rcIh+ck0PuS-p?2wzfvQK3eHs zUS1AL4PFFkd)j=*$dgP(p*RKEL%^rT_(~;eBx$*LHa5Yu!>R5wO4RRh!cqyWm3@2&7_F+!ETH72XHuy#L(2}@V3Xf+S>kxQh-{Xs zXcrW~#fS@sn;3=XC{b^$wt9e#&2))j$^NQ?GR<-^eX1&EhQ57`AwE4Z5hsEyUYvrF zg~fkwZ|}QFTSZ=zXeGvkle2TzhHc0^N+5JWVIdP2S7U|QUzLSBcHBj9|EA8GP(k|| zT3!OZ9@j@3PmlM>>FH%Yiu3>A`H0aCe&UkFgisP@j_<^XQ?TkaWV9g=JXltnxk#-_ zb`&3N|2S&Wp~V6J#>m9<6OFu{F5_#>+ep#8-Mzg@VfXL7WPeV7L3jwjV;iJobK@lj zZoS8V>6aG0^z}h#C=zf*xQSubznR~!m)3QM-r3B^*EKY#vYNpTMUWO26tF&y{JX$R z1D{6LeDH3s!YSs=QKL05H%I2MUjD?NwI(;1zI`2$+B8Gf3O-jGU9dQ>*Hg@7|@JYOqxS_xSYW4eBf?^Fbp_ zEG(>`6>}@P@s|Y-xhm{SUtQH<`h-kX*rMI+EIq`26hHv4xICAiylD?wFw5P64C zQBe`>dR>l>gED+S!8zHX-Bl{sK_~+U20rohT8aAWLf-gPzX_ZMFsTnR>96sLx0b;H zt+egVM0rS;UTQ$)QaJ7mnTx9{GMunb&vg)(w3-|x6uZo0zA3UhMYIiUp6UUwrmd~r zCC0(V7WgHJzT9HR$UjAzeW5l1Vgjpirv?SJ-qGQkd~IkPSW;<$zl@8E3zAF!?hS|` zD!B6?{8g33Q+7}pN)vRVfeTdCaf4%MyU8s=&rDA4xViatS_iRxgloSnw+SsXug>D& zzIWz$n}?&=90OUvA->YFtW&95V5wUg7sglvM%Qw+Xh9am88^5-{`@dL-`c()o&h4n zdb**J5$vgJ%RBb!#CHs$X~q|+ONV_9<9rC9N{Q-+fsj?@JzMXii|0KJ>||4YPi|2$ z+bsD?NpP_rTwh`qlnQbZ5D#mZO;u{Vt6J{3|Nb7|vZumEc73=xZPGuOJ@I1e9CRO= zkaPJCKjh##p1~6gQi*hKCuF}*9VZK^mvjYtoemiRQvt5_xy5h-E!{jfaP4a&?uyM0&=DTw03Hr&i_F$Vizoy3@Pt{OyR! zwmWM3Mndb=me9RDGyPm#ru0|75m2uCUiQr^yZ`sP1^6CXJ1|lTQQOtMSbe zts4j?O_lz3O3TRnd2tt+nH7Vtl*X-tBuUGeoJL33Sd<%x(Xv;^jkjw+?dp2Fl{7g! zn_v8%9vk_W%P}#?*+E9FJA1Cg!ZL4aWy%)WLM+h)b(V)wDM0+C>AIVfE!}H zs9emR$3=*Cs4)ATW78uM;Y^c~nU`cNmV+by&Vjao*PtMM& z3lyd5(oC=Zsqib5E|4^8p#evboSf`SJ8U4yJIV}dci=vHA_zEMNA=IMnboqj-|^al zx(Pobx3W@pI?@URhnEee9ULLWa+y;e^jxxmG{yz)H7X?qPpi=$4uo`&9fXMH{cf-! z(Q)5FXD&jt zmWQ+9pnGSKM>%24m9Hn1|J9z7DdrAaW>-R<3)w1Id%9|Xg7`f2`?Rd~tCoJO(?r*j zHvbL|3Oq?cBm;5OW?uvW8K-Ur?tJE#NXfl&-3OfXJ236+${~LWQsJZmIt;8 zyn<32`Zn!=cZKp55)umOHTY$}^_`ExD{1if+R)TgoZ|j1GMwkx;0Hm*;>W)#-c6F8 zx)E;brOkceg*6MbqGDo9?Ce3Eou6}%VkcUi4^xZJCdF06g&xxNh^hXDzQTwiDvcfHy=1?Gf9=BK71}1x#mNVllE9;BJL< zbx9t)1;*03{9nS6&C|Ax@Jg)}^l|tXkh`oWdG96J{`<}J?#F;`>D8KY+^{MVBBva|7ZlE?<&dIyyh5AV+R1u7RcR$CuYJ<2y{;+}{TWB~a1Okh9KT9(YVljFVu) zq{cWK>`4mUZYb7=iTQa15qUXU9J{@#_h1*82o;pLpM&nx;^Lw(F|mYWo<+%X7IM*S z4Gfj^y9^o7Gc;$NkTedx%lwj$+1KCC1ma4i=N8S27m6g}3A(y+)@pJ6S#bNF-AZQI zS+PpUEV9L8T3<&F+8y{sqzE zk8y!gc`QpHs-lAaC1L=_!0B;CYrXu-k4Cj(#&S@Co{#0 z(9tg$3pcm<$HTIUolAG%$|A?cWFj0?+lxW3h%s9ukK#FZ{@~3#aGODG8NIzCAPv=b zcXwB$yXE2LW@Kdz0L3Y&xwM*{M{7x!3d(c92D-Su7K)_+>#OhRh|R*nLSXKX_DPD^ z(!#=j{(x=f0c1)zZ{I>e!otE*Ns$auLAC$wL#WH8o_GrnA72DW62lH=&8dg0!xal< za%yT6A^&!Fi}Ian(u@KPonCnwk1(^c!m20A)8d>?ijgR1_!qWOzE5y=atd|lcPfzp zJ(k5(ZvVE}D7i-t(5p7@wr^jWA5!VAnUAK_B<+nZV zEN&-Br4=bz+0D%;0h|M^;djTtLxZY^EC!JfEx6!{JwYUhDl888Jgis5;8XW%TEczJ2Z)&~Yca$c`lnX9c&Gk}x<0uK-0!6n9?*4rEQ zmW@Dl^c#Zz=Z!MuwAZXNuHN3Oa%9$`Ldsau$>6L#elQ5auPC=JK@7kbg)T3utV9Q~ zq(pB&O5o|Y9P*M94;(a9%hTO~Ag4~N`y1dRs+JudE#jB1&xRRLzWCO1O+C=Sm9WE5 z>Quf(Kqtn|E@e;qSU6h}6BM)c$GQ3RYRRU1=N#Do>8V{rufBz+CagEug~ z{C7nK2fI4~pxruZZ1;HQ1OjQc8m;w#F`k5#6ZA)}s^X~RRe7C=2jncegDZ=M&%>joO^ z!3k>X>p|@t9v;3965sOFTkqQ>A3Ts_`hu;2x#y40-vM$JP&@B$N0(PreD53t(T$Rm(3N&OgH58>?iv_CWKId!|F~-ndS$W6UebFS#8C(<_#$*ztWw9v?IxQitygI6pJ~c`KppF2SKbdv7JDGQK|LA(M?)Q;= zstMdF8F5rtwj>4SXa1~t(*|}(>aiFre(%IYbQLPS)-O_HUR)%Tx@@n_2o6R@Mu>VM zaItpa!9uyt{(H2ZJUe5?42Au3%4wtyY(mJwg61V=|F=wns?t%!e1gE&tE#?Tw&p-1 z;SXj~ugcufqfLW>SVBq0}c;dCh2xk_5Ypl;sD^pN>6z1;58ULp11 zuI|x?3!>ku=eaa18=F43MG~I}JCRHnU(hD|Gpni_*uBxw)ddZnfiDPWuf%cL`pQj! ze9%*_*#L?6{{|Y2E?UJj*ZCky)at6taI zgk1Kpu@oC1DZ(Wpnhj^QWQ|aj1)2?bfesLe5B=Z~{H(1t+GSCBegX&M^?twwcLc06 z=-XY&+)elcjqdG`DkPpf*5!CryP@hX0-ul&8HZoKC|z`aGFgi&eShh@FH-p=B@VVZN?qIN!PP$MSUdjm2#`-zi%62`_eB6f8uoNK{ zMZ5vr{u}9fie z3S}ve+lTY$|$*e$QRs9!)#o*4icThVCd< zESy+dW7vMm1h%B4_L}Xx!0-s+6wkNK(_iA{;I?o2INIKMelbQUQ=|R!r#B2do!sT| z0%m!a1cl;S`d8hEML-aF$(KHnl4>!j2sK_oGsI?j0ow^@0`Xe4R|sUu95$-~xm38s z#9%*`rkN~btd~;k8L9X29Jj!T$TJEE*r+{N_EfQQZ01GrPjTiFthnhGyM3mN>&$xE z%OZ;#?AUYsGm^sc@*JjDW@7Kbl|Y8%kJV`BnL->|Kr`8fi*1H3w zrHFCKSJ{cJV66!8@Gt^lSlfP1xwYGGA#d;OBsT*8TXQBY8j87frf-3DCRi%^>(6Gr9wW4UqoU1HOJA;gZ z-=>rcqJi{vuH1HiU^#bk4L(2XTrPsAvJQe+^jEw+M|{v!tt=us5?M`4O`Y;3$$v^V zYe{!_*cB5HELQq9ywwFBgp|LV%6AzdC|x%6mku?#SR`U`sSFR3eP-KBsXncR?(V@S zE%fXOM3|dSbZ^p*i+2hd1d!11Zkw-_?>AY@J_P^gYYH;ot)D|q%xtP=CXK2upzmETk z2ZA=)wFIA$=6Ee@-F|+O4)hLTUS3{ec0Tyn*U)5KSybkjRm^ znVHEv13=y%lrQX|{}|2+%BYv9dVMeALh^Wj*_cyXkRk;S7GvIbwWiBNbe)5n8@Ue` zq^kuVJyY_mH>vIrT7uS*J$ZwR664u{WKF$(A*~dA2nS@de!+SKds%oiyh22GGp{ zhKX{>kuMrRn0Q6zD$<@SqCVzAZW@_86jmF8X5xc4^-E@_xQ@r z|By(s+$T*pfb@7Wy_Xh(AMwxQpu7R3Ut}?AZyy*nYE3%LD!o_1)(6(&?+~fSNF{h{ zya{FIN$Tt5o!TmOQ3mo~*5(PIx{{~P^mFy!p6{@AJ7>OmDlz)5)8?su5$g$#$m&pf zc}#~B2HX@JglR;a!PZ$aTD-X~+L3Vae+rl(GnA1^TLKo6xI7qsmE7YaA95I%{A^i8d)H>-Y9Qat;R+MSM%c; z*n$1L7W|Yop_1Ue!A~_N32vM)q`?!A1EtS(9O&YDMt>mwY4>Io{)s( zt#yhiYtyE-rZenKxZpgn6bNZkbaG7pCWgZGPy$Wj<2k-v&x$kD`qpp~29e(RkuT!XK9!N|;b~twv7qRJiRf%JSV(rd1+U9#&MxJ1m(FZg3WRDFTu zp`(N2!Nke=Yty1RufQ5q5xm4vK~NEt)p3oV3)FJte#@Ip3_yx>u0eUG=jMh-93^(6 z5Z)2HxD4|;-_qk_Mqs9yfkBt;O%pyT5U)UTX$vx^VM(;K@bV_X98xW${q?}fyO@qk zP=Nf{=1b`B$HhX$jq0b9Mm*~;8r{gaf?{pD*h;s6MctArarijfm|%3IaNO1^2(W|^ z(nh<@AXlB%j)br36b@@GZdjnq-fqCZjqolRRpUMvRRYUPN{)!;ylWF9QVR%kqAp+=dHELCqFom)|m(Qop;skJiUM)$X`N$4r02X(LaR>=f z)9tOB^jlOuLy@p)Z!kvjotsy3EB`uY zof08gzcy5VrC5+&BY9k7W~h)-f)2X3EjpEZ_u`B(^)po7ba{4Bu%1d^%zs$9w(7*N z=T}TrnVtVoaRl3imPTOL>o2B-sbW${Zh+|Y!t4{S=9x9gmOa;Uvx{sk1~ZnzKGa`W zL}Tkv_e&0p4wx+&)<&(di`6J19{4`oqm^cp1;2%YQB8amZ^N#jj3KJu&g3Ey6Za5-jr#E4KTGk3Z+Cb!5$r!qeym2;6MVJ! z7&jPFJtIzG;QnM{Yz&DvIZjrmcm?Oly2G9#m8(RJSAc0d26wXO<-u;1fs`yx0bn2M zJB0yO)Ih;RtFD9Dk$evh7=Cx-GrlCr_oMfg4lWjS!36i-AL5z7heo-3a z`3fJW`+^_==hM_T`ih6X*&I+L{GO&m0T&+3DX=?Qr~&LO9}gx+;D)&JfE36Zh9@3d z<#ltiQ8D7>suD8BnqgPd*BOApqkm-0a}r~mWBiFPFG%nmFtWerw?tX3x3tN>%*k9} zn3CkJDGa5OoBcjzZ)Bz&rDT3)K-{g05&dGU(hB35r@)>j!k-n~y`hF+zVB4vC#z&O zebOO@ZX^0Pgi^Y@;1EVIwxo93_?TSaK!$O{`80h|wG5u`4^s(+f+jh;75ZAtAH!ak z0E}1Yo)q#9jmu5NN_W?IDepZ7{~G<6JFbRH7~T?|MrgfIPIK9F)X2kQ$HZ8ZpEn{u z|DIVYNA=H}HOb5^pqxm${)z($?NZaJzh1p1%1kF|-&}UrQuA~gv}cq3s#iScTQc6d zG_S0uhxw}vvdXjU>+JV*aUlq?aO$di)ECs5D6%)WjY{tx^Mp6Z67~HwT={)$yKcuH z*8a30q;<1PeLQqF8 zI%I$7K&SayJOVYQ%HdRAK#A1;RI1x4@Vr9h{>{br57Egi0mKLO2=UMSnzP~Fb)=!u zNU+aSyH;np>~R^3A!wgdT79yC$IMf(&BM6zKZ-e0Kir(Yzuwiu=;;3R$?}&s-2EDPssDGYNXy7$urS|!1MdC z=S!b^Ci^hi`HD$Yy?pBcjMF&U;pne!+6*$e0C)pAg1WP9g@@w&@pYJ79bQ};s73&{ zkTz=+F+bvXJ>l1rJ9;w7Txy)-F9Yrb@w>%2qaXy4 z;E_Z9q4)X8W%3V7yewNO$;5QCY?(qa)s|KBqvsN`Ct#yuDa@qQ_&{fqu@CHT*07%@ z8KY^g6_595>;3}w9=MU(y+dd3#i`1*75re4Xdu{S(CMG$Q#-y()Jpn<3fzpH-;}Sp zN^kDKkh1bW@~U4BNcR113cq(>BzSBEJ9I;}zDafJ=HjVU+l_r}{=xC>k(<*6h%|XVu z7r~Y1&IqEAIOtPi$37`WP)@u2d*;eZ*8VRgq|DB&8-Lp8X8w3xc-*Nh-dac}fwVla z4z3TELgaHCv@Mca0w+a*FAG{AL#Hxtl+We3`@|B^c(@#I%I$y8c(;j?Ugj|W;UZUb z;iyyVQ*)b!_*Q;{**&!CZ@)9)vwbrzwDD!VtR<@y6Cuf$5-u7$pY{g@yxE&*)%W0Z zo8YVuOKZ>6H2SN1W2F4NL5Ml00r!yxk&|JA=Xu68;D`D!DY9o#T=GFN zTk>SgYjt==XF5nOeLJ3g|^rK9zr+Bg)w9#8Cg9nhSp5vu#Lxpq6c z$w}%3T@fOTz&JM11C@>HUWT+zMFJnn=I?L#1{oJ7K7R@K$Yggn95JiJ{n0{y)g6f+ zZzW7!)%^1KV#K`t>?q~LVp4Su77dOl1~KR!4}3rmRV@%YvhMM33*n;`S(Jp^Gu|^9 zR1i%E)E?GDcRN8T4+%-5Ot^;9VPj*{?2Syg0=nhY6a#v_FN3Uq4Fh`wOQbTR__|(t z(rG+QxC6#JS_?!%!cQH15vm9pkPc?hJA#?}a#D;Bsc(`kW&7P2Om&QRLyhYj_egir zY~ovIVxh65IHo$}(UJ()%NZPS_T)Ux_LNfu_w$zd#DmCpTpU{L`{e((&7)d01MUA}C;}UcSEVYT~4wLvy zgDbsed5X4Xh^AN5XCU6iE7%>{dQJvqeYBWrM}ED98Xf=U#*0u-_XZvm#q2%tmwz`) zFJB5|Ru(enW49j+kZ3498dDoxxElrK>+T7yRUeVrJ1;tv=$qbwq7C- ztd@m|r%~q7igRSSu@r~qci1LF_`7y7-uBNqFj5<8`pQL~o^Vps7VdI4sT{Ue3@Jv9 zHqCZzrYohr0YU6ed-|ivN_1{n#rlF}cMvh|j6URZC_Oz-x|J}fG6`tap+zl`atw$C zcX-dT-~!FBdJ=5u$~nuK<5_D9Pf@HJY9irBmgB{mvPRktH?GG3m}`?AC@!U|L*<-a zhx`$QE%&wRFEDy)`us7BEv4Hnos498nd#;mMt{A{IM=7snvBuuhR6%Ir$Nsm z%clvkxEMw{{ImQoPOfk!D$^1{Xee1X*raE7VqY>$;aP=W3aMelkb}>gHBDkQFyaVd zb9$;-fgLu7R8N<)6p1Q4r9%#?6SY@b7Te`rlRdaU3eX-NrbR~OE>@sjFH;Ke=#4$= z_)qVvv>%ML-B;Q#MmZ|@g+t*6`3CT#w2CLlef zRB)XxI~O)s(VWOzWh$=_5m8eoSH9h=ZT;ap|LdZZ2HE?Xhh%*^5X+Z^%9A&|5{~}& zTV!Yx)O0B6aD8Qa!87ky?a%#THw)v|$lD5)t#~y69g#MPa7VVpO_At$yP`6~YXUoS z@vnKwR2ehFoijCX`ngKZOe6hDn(1^tw*STz}aqAjy-*6 zV-+|&goI6&C4Xa%Te(E_5HUGjIuOhVn;!$kKMTVuDzVkMPpSatB<F{~>o6qwxeVMWy<3M%UUW_q3g_4DKl&^`?n$m%p z)7r(qC2dq)3CQ<&oQ(>(Y{<>^^AYsZ=<_Y__cM+hRuVN74M_CvBP2QCO@Xgpo|zAc zHSZ0)_jy)0Y08jg5v_f59Y6P`%F6HZs$6~FDED2>S5@8*H%z_^lr)FEI-UnvX*RBV z0=Ku=tGAcO8|cruDC%3sd2YH49a>#)e=uejH=>MO;uqjN6F*J*T)%F=W@rz~pZy92 z%?cTkEKy);q>xC?$go1x@4*FHCM`fxq;1Q~F#a^JoS#crw0v5pO-P$zhWk?b{$aKK zf&QuUc(rrpj{t<{ggcc;EIV6kx|E7gkDm4TKt$#C{-(Qqw~MF!iO=)wy!q=hQ)v_> zoO!2&$;0Vpe}TmzfNMv07tj{xM1No_GqZdsQYu%X?%c5LIKCEuVR3qYQg^oy@yCPv z5$|c~d4u63__?3yLDi>_sIrAM(e~LH@6Jir_YZ`2fw^_-gaozX6509SI$Ty7_-60OTPc|Omult z%N%?FD%Qi{2v%t7R#HfBnjGK$Ltqj;tiQvANONim>J0N>0L6p~mG zOI`|&ISquaMAi(bGgq3~N>KvTa2bA@hC#|@%KkYoS0Cj|9 z#oYTrV5krZ2QXH|73YA5wvC*w!A((yeX6D8VNelacr#j>nnk8}8hATAZ|>3feg4-? z;Xl)*TgjbRhe*I63+b1Bn3}kTJpsa+8Lk|I*WL$-HPEL1vQrpiW~_LbEtdCKW`EPQ zQT19hqJGZ&^AOit{0a4}N<9J_!Dn)b@O1miKH(|Q?)PG@gKd`+Pny2DPG|J z@X6Y`w(@H$!yc8C2Nyhm<{IMyU3_DdnOCMx#>UpH_wNJD31N2#0W8iA$2`_3XFtIA6U)T2 zzo2aL^LJsg0%;V;I?(z+&opT1P*henDyQB=(aI6fO`yZi#H9Z_LACM?=4)2&V!yV^ z%5kM=oG=jp2lzVB!&XQWu(*XL0U~AHxfu-C*q(1oD?V3&M)eaPrV`I*urX$w61rwo z6cUnOzaSV=UYzX8J1+mfF|G4ojU@z~fffT)PGkvV-cuygiz5KFqN2qQuAeR#ynv_p@Lh| z#Q5oN-~zxl3dFHWyq^Q4(ij*2?ccbjl-&9^7N4PrE?H8`yL)jYCnU}*0JJz)=4 zHe1BJee?#iU7r8})ave}ZT|i4+LV?08wqFwIBS#n2AquEzBL(^lEfy2$^2H zi9(90Gz9_7)iM5?n3!l%VAot?kvVreteaoIz6b0P9Lzp?DR=CFeWM%|w(aBHC7(;x zo_d9Dd!%3v1YC6J34W^!fOLBeFaViY*?N__;bBEQQvuG`2N;Rh*(zRy88U?@E4n~W zy2*4=`1P+SD#|-6HB}IF3u%XgVl>&b0oDj~$!glo442iP?<)qIO7f3>8umjVK;%=? z(qiTN0r(G~+kkl%pAnte8_E1waiycf!#6|)5v%RLeuYd<0`~?!f%-Vfj&44`0gmHG zI}E1UUTD6(6kwTv0j#F2JU=}Afv&q)-rMAP%M8f|wD*m|$oEV!F)?tf0Cxzu)t`1h zyMF}gFb;+zv)$+!sVoqbg1h}J*-ItVR>SZ~C<=KKLs#p>~@mV5?^{VfnMujXz0C0%kp$AGC-?zKiC) zIWN1a#>!^8;S_8$dt=IDphdySp}l&A zSD<&8faqFxgq_DLPmR>#d0XAr!-o0G2<=1MAd!W*oN9+yff~?r*N)#}5I+h|j=S>G zlXU?l75{|AaJki7W4BooY+fM8qSMjQ0blmZ^)#2#Olj@D3Jf=b3xN#DJZJ3ss4&D! z<_o{x0lL;fhm=>TrKikx25e zjsP||;4}(lGB6hq-Gd)=Gz9WY@>I@j+8^s})g!{g)y`fw>KB#sZXNPXcOXFC(rdl$ zM5&urKh7h=$HN0W0Y=Q~(a{mnDAXd|p^R>%GgwnQQ(%fBX+AYD$Jh6EXMZJf_@*sk zXiP42xH{Z(Vx%aU`1$Fjbh5@x206fv0VWnrfdq&!ld*IrT8pKFhxwHiII3xr&tX!RaF(tgsfk>1I_-0G6I?pMVv%gMa7Z2;^^`9X4=|A z0Tr}>%TfBoMfhAO|NW?qH`choGcWMPZTBn3g7WdHo;)6dviSusQz+1VIuIb|9VjWOIfEU`!bb13eN2T{==?i)evh$f4Y*A5B)bSMj z2cV6G{LukJ7ySoz<+f#kT=#ZKNrB-7oAFM8FAnR5};`*8x8DE)bmZc=Wu5^@@y2j*XpOf?W+)Zcw0pL)T#2492-cG<=V1x<-Aq0$cqiQ-7 z&)d;sR6jW` zQ;HO_Xj%V&X1@RA(oz`5n&*E;;A^jNI9sa97H*`H@%c6~oGCmqvSPTVdM~UFkdt8Y z`32Uxe}sN0!*m6AZU+W0%gV|~!C>FPB^iDMa*l-&v3?|e?7hFd^>hW-N;=F8W{BIaTaWd>e*LO?Rj=>8a^7I-2avWuS6Vt#|OscpiE-w8k@ZFUnX&7nrxN#@5`{Kf}w>2#*s~=>@I<6aV?;Ocv5Qzo6%LY|TYj=2Z zM%BGk62G>b4bgCV-PM{RGV}9Cq6<7!QP0N!KCkj&s4`?f$6;se<&x)LrsT)N@xVZX z-SH|PMlGP^igFe#wrRrqHw`%57;$@mrIAWt0wyzmQQOSloKC)E+ua;FxEmNl7l3|W zcGU}J`MA{H8%*MDdi&3uR4J|D$KD!0vnET%Ba`-RK| zGrE&q{Mg7ja#TvN9iz)W<-7$S)Dus)7<}+-S}$?Xh`1@nCIFUFdekQ^Z#{JZ=GeH$ovwGn zbTX)TFbN1m0Xnm5&k;E;1;Us$Ze!NlWqg|eIUeO$>jAUj6Eia}BMKlDdH81{z#_%R z#BBjwvrXG=f#`H;aBTvZOs07~dH+zfuV-y7W7w1;G4(>%0Rj$CJpkV91wiy`RQQ*h zSLd{HXiVR7st*IO;0rL?a=V=dCWzlm?*NpP{y%Wl`giF_2uOw7P9tncaZ$fVve(Pn zIPkXv-UrmL|ChrjXGgHEr#ct5&A7ZeV;Mv0abgSp7tW z{qoZcz&JX+Zd*9G310V$0VSH*hM1^cgPVQTc53EVZO@O#nyI}!gJCM1eULINy#LL7 z9JfC|MW*4aU`nl|`$~SLX3Bj^1+eb!!GRFz>(?&>@)VdzU^3U-)71F78spV>exY?D zRQSos$#==7fC`zOo(3UXV^jlb>&swuIA|ar`#j$Z(y&WH8j7pOqvpPo#iIoa#Wxj? z3Xng|R@U`ze)jPZczHsO+<_z>#>Vh~nG6*+=uXvR^Blqz^6=mRvkJgX7v$K0W;mF( z0S-hN%cpesJyghYoB0?Zg>wNZ%zZK|`BM$`Wq=9S5O{>d%i;u2Oc0=0oaGM5rNGZe zFk%)wvvKSe%y|v4Q81@^qb&_w0GR1f9<64(djjxiF32IZ!ltxFG^Uo62%_b`y}UgBmM$BERno(T6>O1 z;MJ>o_#H4j0jH$T5JF9{M*l>7Kw;oIu`4Pn0v_fF9CK2oSn9)Y20_ zV{W%Q?+t?;Hx_5%n*Gvxz6+mQvtM;;E>ioF2<#~U6+Bn=42>W?n)TOGv7O(=6Nv3} zU#fJ&Jb;N81w+~vg_*|Fc&%|7OVWHV%rW8ZhZQ% zS`q!Wh%qIS2ihJ&p1G@F{C$IbN7VgnNsKAbuB)>`hweB;BfKuY0sFdtSb5 z0Js7)Mb0*xCnOaEZt!xxA~ z8Hh%{+$P8ml2;Tg0*JQYRC-_X^D{@Et`Ki1amu#E9ABRJ3!`DCurLCGC46=4iP^28 z+~kvvo}nD?fXN}i4i5u?Ra&YhsdHTtf;GS`s2MQ51B{E|V1h^CF7K)k<;Zukp7#cS z8cuD7#v7pTWYJ>XYR(cUc(;o~4t*zFp{O~k%t;}@E(BM6A}|mKt(FfA3}BxW1MLNF zH~hh5akCVxNha@c8!^Y z(0kw`st(j@I|Jh}U=^!TTlpr`t8(AE?VVQQ` zcsX$?4B!SpTDX$n{%yKv&TMhExsZ@!+`qP#V;-Dg8f6wMbMfj`OF{JLk^E7i%@J-u z$pV)MQw$7Dl+)1=$y5qz7EoeTvPSB)mW&wV0<+k{rA-DvOIx~?`RE3<3WV=RWZm8raUupc4z2Uz&r`>;2b2#_ z-=$vArVJhn&~sOev)!4O*||$-Pl`m3jv?>E6@tnblzg!K%3Zt;Vl7U`rA4O_AkVPj2zzJgA81A>`* zb}sAngI1#Xw_MwT1BaD&2t)B4^?^dqMHHTy8?k;#j1RJ>(M7I91^r=!jhgGTx0{^C zKChQMtYfp}T-A4jkj8#LcRp$ZPVuZ}nB=%iR>V)1?N7_Gbvt4SHqO(7%V=z5yX^%TKt~!E>|S>xU}NExtW;=+WN}UAxIWj&#b`j{3n3^ZDyutO!muS5VRfJL4v-}dow40E}uU%PW1~Aj~q_H z)VLWg9$4oiM`%Xlc`TVSTi0Aeb!gV&~rjW3ZgI8(ihdeS|R4bqd8vuTa0^CPG4aa^V6k6L0wrD&|nZivPj@v%l^)uF*5x$~o3 z{+`;}B;O|$L1Y5+=4;FO5zFuDNL4Dj;4Cg{F&^pz4tEz63#m9`cGM;-s|M=OE}MAK zKgm(j4euv6_;0aR+U3JY2lCe}(aFe64KmJ=T=t7@>u^j#2Ew>{==Xw$p7pr2h7^Qt0v*qdQafsF=Y*D!oRtc^Lh{@<@F(C+8Sb(GlN;oMjJ7qT*V z3vG>tcDFs8pdFH3&ha9XvHV8gU+M%e8Lo~M%;r1sIUu)W4~0s6r(d2lBz$9&p4Z_n zK$~+ssk=`Y*vnv?7=#;C{Qa1_WrL!`YKx7~T`T;Pp&B!;!M^|qp1 zgYktLL>;|5-z~y@5t`MG&w_G2Hs%tkbVi9B?tL5jo7`5@G) z*MPj472%xj;!*IawoAD+O=`GgUV824J9#aRe}Q+kH%JEW*?Zitr?lr zSeQId`x3xhmq&FdANUaoj09Q62c$GD%<7|m2;em2qFGw zcXv#+8NHlFC|PQP<5KQRprUer*bW{(9GV+HGjC2#(wN7}_mO21Paq7ZG%zBza|vCY zW9&26){Fu=UOM5?gSD1OGUKj`b}>=t+_Ii?0isBJA1ZUtn93VMnLJiVR{a;4mY}vnS%HVsJ;hCVDk&*( zO#JHUAH6o`u|p>r8QQsRTQbw&?F;;7*{GN@eS}jVCR}FE*77OmB_fOWUb0rMxq|!y z>9?MU8I!M32#x_K%8Fn|Bx6N{wo3;HZ>MK8k8Cq~u@<1dSWLtE_!;GGRIa&AkqS3y zpKRD%cdS6@GD$>g!(USpQ~-7&aXP}Rn3ncsy8j@l<;b6H2G~h_baid5a$N%ArW?d5 zDk&-1k@P~C7fjuyc@iZO1zR*H;4reO*hBnwFE!--x?t^1Ke0JjMfyTH*$@d}XB0X3 zT4L6=$R#2h8HyYM;Pl4R{vS}h@ZTqZiF)~lo2sIZ=f9S>xT~XVUwJ$Y_ur&9EQX(< z7bsC;2rNLDE-st6YuSJGy#r@5iG?^jMBS_*yzTuuSkCj>U&-XljkkFjejt_C(EB_& z``q{(nvl52W99#ZCfXnbO^c}~i&r8nD5dAW$3fo{U>jdI)iOAkif~(A$Ds>D zULafw_>48$`WsvcX<|R+=_Vcr>a$HX$O2eSY!g(T;184k30(blF2h&H83iG&2qXQo Kr;79(|M!2_TWxOu literal 0 HcmV?d00001 diff --git a/src/AutoPilotPlugins/PX4/RadioComponent.qml b/src/AutoPilotPlugins/PX4/RadioComponent.qml index 1f9e233f3..cacafa4da 100644 --- a/src/AutoPilotPlugins/PX4/RadioComponent.qml +++ b/src/AutoPilotPlugins/PX4/RadioComponent.qml @@ -50,6 +50,9 @@ QGCView { function updateChannelCount() { if (controllerAndViewReady) { + if (joystickManager.activeJoystick && joystickManager.activeJoystick.enabled) { + showDialog(joystickEnabledDialogComponent, dialogTitle, 50, 0) + } /* FIXME: Turned off for now, since it prevents binding. Need to restructure to allow binding and still check channel count @@ -128,6 +131,14 @@ QGCView { } } + Component { + id: joystickEnabledDialogComponent + + QGCViewMessage { + message: "Radio Config is disabled since you have a Joystick enabled." + } + } + Component { id: spektrumBindDialogComponent diff --git a/src/FirmwarePlugin/FirmwarePlugin.h b/src/FirmwarePlugin/FirmwarePlugin.h index ffa423d56..d718fc8e7 100644 --- a/src/FirmwarePlugin/FirmwarePlugin.h +++ b/src/FirmwarePlugin/FirmwarePlugin.h @@ -76,6 +76,12 @@ public: /// @param[out] custom_mode Custom mode for SET_MODE mavlink message virtual bool setFlightMode(const QString& flightMode, uint8_t* base_mode, uint32_t* custom_mode) = 0; + /// Returns the number of buttons which are reserved for firmware use in the MANUAL_CONTROL mavlink + /// message. For example PX4 Flight Stack reserves the first 8 buttons to simulate rc switches. + /// The remainder can be assigned to Vehicle actions. + /// @return -1: reserver all buttons, >0 number of buttons to reserve + virtual int manualControlReservedButtonCount(void) = 0; + protected: FirmwarePlugin(QObject* parent = NULL) : QGCSingleton(parent) { } }; diff --git a/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.cc b/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.cc index 997e3f9b2..cb47d9872 100644 --- a/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.cc +++ b/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.cc @@ -89,3 +89,10 @@ bool GenericFirmwarePlugin::setFlightMode(const QString& flightMode, uint8_t* ba return false; } + +int GenericFirmwarePlugin::manualControlReservedButtonCount(void) +{ + // We don't know whether the firmware is going to used any of these buttons. + // So reserve them all. + return -1; +} diff --git a/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.h b/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.h index c0fcf0610..109e14ba4 100644 --- a/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.h +++ b/src/FirmwarePlugin/Generic/GenericFirmwarePlugin.h @@ -43,6 +43,7 @@ public: virtual QStringList flightModes(void) { return QStringList(); } virtual QString flightMode(uint8_t base_mode, uint32_t custom_mode); virtual bool setFlightMode(const QString& flightMode, uint8_t* base_mode, uint32_t* custom_mode); + virtual int manualControlReservedButtonCount(void); private: /// All access to singleton is through AutoPilotPluginManager::instance diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc index b3ff04737..f8d3aa966 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.cc @@ -176,3 +176,8 @@ bool PX4FirmwarePlugin::setFlightMode(const QString& flightMode, uint8_t* base_m return found; } + +int PX4FirmwarePlugin::manualControlReservedButtonCount(void) +{ + return 8; // 8 buttons reserved for rc switch simulation +} diff --git a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h index d6c710cc2..ea3314895 100644 --- a/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h +++ b/src/FirmwarePlugin/PX4/PX4FirmwarePlugin.h @@ -43,6 +43,7 @@ public: virtual QStringList flightModes(void); virtual QString flightMode(uint8_t base_mode, uint32_t custom_mode); virtual bool setFlightMode(const QString& flightMode, uint8_t* base_mode, uint32_t* custom_mode); + virtual int manualControlReservedButtonCount(void); private: /// All access to singleton is through AutoPilotPluginManager::instance diff --git a/src/Joystick/Joystick.cc b/src/Joystick/Joystick.cc new file mode 100644 index 000000000..22a736185 --- /dev/null +++ b/src/Joystick/Joystick.cc @@ -0,0 +1,506 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL 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, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL 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 for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "Joystick.h" +#include "QGC.h" +#include "MultiVehicleManager.h" +#include "AutoPilotPlugin.h" +#include "UAS.h" + +#include + +#ifdef Q_OS_MAC + #include +#else + #include +#endif + +QGC_LOGGING_CATEGORY(JoystickLog, "JoystickLog") + +const char* Joystick::_settingsGroup = "Joysticks"; +const char* Joystick::_calibratedSettingsKey = "Calibrated"; +const char* Joystick::_buttonActionSettingsKey = "ButtonAction%1"; +const char* Joystick::_throttleModeSettingsKey = "ThrottleMode"; +const char* Joystick::_enabledSettingsKey = "Enabled"; + +const char* Joystick::_rgFunctionSettingsKey[Joystick::maxFunction] = { + "RollAxis", + "PitchAxis", + "YawAxis", + "ThrottleAxis" +}; + +Joystick::Joystick(const QString& name, int axisCount, int buttonCount, int sdlIndex) + : _sdlIndex(sdlIndex) + , _exitThread(false) + , _name(name) + , _enabled(false) + , _calibrated(false) + , _calibrating(false) + , _axisCount(axisCount) + , _buttonCount(buttonCount) + , _lastButtonBits(0) + , _throttleMode(ThrottleModeCenterZero) +{ + for (int i=0; i<_cAxes; i++) { + _rgAxisValues[i] = 0; + } + for (int i=0; i<_cButtons; i++) { + _rgButtonValues[i] = false; + _rgButtonActions[i] = -1; + } + + _loadSettings(); +} + +Joystick::~Joystick() +{ + +} + +void Joystick::_loadSettings(void) +{ + QSettings settings; + + settings.beginGroup(_settingsGroup); + settings.beginGroup(_name); + + bool badSettings = false; + bool convertOk; + + qCDebug(JoystickLog) << "_loadSettings " << _name; + + _calibrated = settings.value(_calibratedSettingsKey, false).toBool(); + _enabled = settings.value(_enabledSettingsKey, false).toBool(); + + _throttleMode = (ThrottleMode_t)settings.value(_throttleModeSettingsKey, ThrottleModeCenterZero).toInt(&convertOk); + badSettings |= !convertOk; + + qCDebug(JoystickLog) << "_loadSettings calibrated:enabled:throttlemode:badsettings" << _calibrated << _enabled << _throttleMode << badSettings; + + QString minTpl ("Axis%1Min"); + QString maxTpl ("Axis%1Max"); + QString trimTpl ("Axis%1Trim"); + QString revTpl ("Axis%1Rev"); + + for (int axis=0; axis<_cAxes; axis++) { + Calibration_t* calibration = &_rgCalibration[axis]; + + calibration->center = settings.value(trimTpl.arg(axis), 0).toInt(&convertOk); + badSettings |= !convertOk; + + calibration->min = settings.value(minTpl.arg(axis), -32768).toInt(&convertOk); + badSettings |= !convertOk; + + calibration->max = settings.value(maxTpl.arg(axis), 32768).toInt(&convertOk); + badSettings |= !convertOk; + + calibration->reversed = settings.value(revTpl.arg(axis), false).toBool(); + + qCDebug(JoystickLog) << "_loadSettings axis:min:max:trim:reversed:badsettings" << axis << calibration->min << calibration->max << calibration->center << calibration->reversed << badSettings; + } + + for (int function=0; functioncenter); + settings.setValue(minTpl.arg(axis), calibration->min); + settings.setValue(maxTpl.arg(axis), calibration->max); + settings.setValue(revTpl.arg(axis), calibration->reversed); + + qCDebug(JoystickLog) << "_saveSettings name:axis:min:max:trim:reversed" + << _name + << axis + << calibration->min + << calibration->max + << calibration->center + << calibration->reversed; + } + + for (int function=0; function calibration.center) { + axisBasis = 1.0f; + valueNormalized = value - calibration.center; + axisLength = calibration.max - calibration.center; + } else { + axisBasis = -1.0f; + valueNormalized = calibration.center - value; + axisLength = calibration.center - calibration.min; + } + + float axisPercent = valueNormalized / axisLength; + + float correctedValue = axisBasis * axisPercent; + + if (calibration.reversed) { + correctedValue *= -1.0f; + } + +#if 0 + qCDebug(JoystickLog) << "_adjustRange corrected:value:min:max:center:reversed:basis:normalized:length" + << correctedValue + << value + << calibration.min + << calibration.max + << calibration.center + << calibration.center + << axisBasis + << valueNormalized + << axisLength; +#endif + + return correctedValue; +} + + +void Joystick::run(void) +{ + SDL_Joystick* sdlJoystick = SDL_JoystickOpen(_sdlIndex); + Vehicle * activeVehicle = MultiVehicleManager::instance()->activeVehicle(); + + if (!sdlJoystick) { + qCWarning(JoystickLog) << "SDL_JoystickOpen failed:" << SDL_GetError(); + return; + } + + while (!_exitThread) { + SDL_JoystickUpdate(); + + // Update axes + for (int axisIndex=0; axisIndex<_axisCount; axisIndex++) { + int newAxisValue = SDL_JoystickGetAxis(sdlJoystick, axisIndex); + // Calibration code requires signal to be emitted even if value hasn't changed + _rgAxisValues[axisIndex] = newAxisValue; + emit rawAxisValueChanged(axisIndex, newAxisValue); + } + + // Update buttons + for (int buttonIndex=0; buttonIndex<_buttonCount; buttonIndex++) { + bool newButtonValue = !!SDL_JoystickGetButton(sdlJoystick, buttonIndex); + if (newButtonValue != _rgButtonValues[buttonIndex]) { + _rgButtonValues[buttonIndex] = newButtonValue; + emit rawButtonPressedChanged(buttonIndex, newButtonValue); + } + } + + if (_calibrated && _enabled && !_calibrating) { + int axis = _rgFunctionAxis[rollFunction]; + float roll = _adjustRange(_rgAxisValues[axis], _rgCalibration[axis]); + + axis = _rgFunctionAxis[pitchFunction]; + float pitch = _adjustRange(_rgAxisValues[axis], _rgCalibration[axis]); + + axis = _rgFunctionAxis[yawFunction]; + float yaw = _adjustRange(_rgAxisValues[axis], _rgCalibration[axis]); + + axis = _rgFunctionAxis[throttleFunction]; + float throttle = _adjustRange(_rgAxisValues[axis], _rgCalibration[axis]); + + roll = std::max(-1.0f, std::min(roll, 1.0f)); + pitch = std::max(-1.0f, std::min(pitch, 1.0f)); + yaw = std::max(-1.0f, std::min(yaw, 1.0f)); + throttle = std::max(-1.0f, std::min(throttle, 1.0f)); + + // Adjust throttle to 0:1 range + if (_throttleMode == ThrottleModeCenterZero) { + throttle = std::max(0.0f, throttle); + throttle = (throttle * 2.0f) - 1.0f; + } else { + throttle = (throttle + 1.0f) / 2.0f; + } + + // Set up button pressed information + + // We only send the buttons the firmwware has reserved + int reservedButtonCount = activeVehicle->manualControlReservedButtonCount(); + if (reservedButtonCount == -1) { + reservedButtonCount = _buttonCount; + } + + quint16 newButtonBits = 0; // New set of button which are down + quint16 buttonPressedBits = 0; // Buttons pressed for manualControl signal + + for (int buttonIndex=0; buttonIndex<_buttonCount; buttonIndex++) { + quint16 buttonBit = 1 << buttonIndex; + + if (_rgButtonValues[buttonIndex]) { + // Button pressed down, just record it + newButtonBits |= buttonBit; + } else { + if (_lastButtonBits & buttonBit) { + // Button was down last time through, but is now up which indicates a button press + qCDebug(JoystickLog) << "button triggered" << buttonIndex; + + + if (buttonIndex >= reservedButtonCount) { + // Button is above firmware reserved set + int buttonAction =_rgButtonActions[buttonIndex]; + if (buttonAction != -1) { + qCDebug(JoystickLog) << "buttonActionTriggered" << buttonAction; + emit buttonActionTriggered(buttonAction); + } + } else { + // Button is within firmware reserved set + // Record the button press for manualControl signal + buttonPressedBits |= buttonBit; + qCDebug(JoystickLog) << "button press recorded for manualControl" << buttonIndex; + } + } + } + } + + _lastButtonBits = newButtonBits; + + emit manualControl(roll, -pitch, yaw, throttle, buttonPressedBits, activeVehicle->joystickMode()); + } + + // Sleep, update rate of joystick is approx. 25 Hz (1000 ms / 25 = 40 ms) + QGC::SLEEP::msleep(40); + } + + SDL_JoystickClose(sdlJoystick); +} + +void Joystick::startPolling(void) +{ + if (enabled()) { + UAS* uas = MultiVehicleManager::instance()->activeVehicle()->uas(); + + connect(this, &Joystick::manualControl, uas, &UAS::setExternalControlSetpoint); + connect(this, &Joystick::buttonActionTriggered, uas, &UAS::triggerAction); + } + + _exitThread = false; + start(); +} + +void Joystick::stopPolling(void) +{ + UAS* uas = MultiVehicleManager::instance()->activeVehicle()->uas(); + + disconnect(this, &Joystick::manualControl, uas, &UAS::setExternalControlSetpoint); + disconnect(this, &Joystick::buttonActionTriggered, uas, &UAS::triggerAction); + + _exitThread = true; +} + +void Joystick::setCalibration(int axis, Calibration_t& calibration) +{ + if (axis < 0 || axis > _cAxes) { + qCWarning(JoystickLog) << "Invalid axis index" << axis; + return; + } + + _calibrated = true; + _rgCalibration[axis] = calibration; + _saveSettings(); + emit calibratedChanged(_calibrated); +} + +Joystick::Calibration_t Joystick::getCalibration(int axis) +{ + if (axis < 0 || axis > _cAxes) { + qCWarning(JoystickLog) << "Invalid axis index" << axis; + } + + return _rgCalibration[axis]; +} + +void Joystick::setFunctionAxis(AxisFunction_t function, int axis) +{ + if (axis < 0 || axis > _cAxes) { + qCWarning(JoystickLog) << "Invalid axis index" << axis; + return; + } + + _calibrated = true; + _rgFunctionAxis[function] = axis; + _saveSettings(); + emit calibratedChanged(_calibrated); +} + +int Joystick::getFunctionAxis(AxisFunction_t function) +{ + if (function < 0 || function >= maxFunction) { + qCWarning(JoystickLog) << "Invalid function" << function; + } + + return _rgFunctionAxis[function]; +} + +QStringList Joystick::actions(void) +{ + QStringList list; + + foreach(QAction* action, MultiVehicleManager::instance()->activeVehicle()->uas()->getActions()) { + list += action->text(); + } + + return list; +} + +void Joystick::setButtonAction(int button, int action) +{ + if (button < 0 || button > _cButtons) { + qCWarning(JoystickLog) << "Invalid button index" << button; + return; + } + + _rgButtonActions[button] = action; + _saveSettings(); + emit buttonActionsChanged(buttonActions()); +} + +int Joystick::getButtonAction(int button) +{ + if (button < 0 || button > _cButtons) { + qCWarning(JoystickLog) << "Invalid button index" << button; + } + + return _rgButtonActions[button]; +} + +QVariantList Joystick::buttonActions(void) +{ + QVariantList list; + + for (int button=0; button<_buttonCount; button++) { + list += QVariant::fromValue(_rgButtonActions[button]); + } + + return list; +} + +int Joystick::throttleMode(void) +{ + return _throttleMode; +} + +void Joystick::setThrottleMode(int mode) +{ + if (mode < 0 || mode >= ThrottleModeMax) { + qCWarning(JoystickLog) << "Invalid throttle mode" << mode; + return; + } + + _throttleMode = (ThrottleMode_t)mode; + _saveSettings(); + emit throttleModeChanged(_throttleMode); +} + +bool Joystick::enabled(void) +{ + Fact* fact = MultiVehicleManager::instance()->activeVehicle()->autopilotPlugin()->getParameterFact(FactSystem::defaultComponentId, "COM_RC_IN_MODE"); + if (!fact) { + qCWarning(JoystickLog) << "Missing COM_RC_IN_MODE parameter"; + return false; + } + + return _enabled && _calibrated && (fact->value().toInt() == 2); +} + +void Joystick::setEnabled(bool enabled) +{ + if (!_calibrated) { + return; + } + + Fact* fact = MultiVehicleManager::instance()->activeVehicle()->autopilotPlugin()->getParameterFact(FactSystem::defaultComponentId, "COM_RC_IN_MODE"); + if (!fact) { + qCWarning(JoystickLog) << "Missing COM_RC_IN_MODE parameter"; + return; + } + + _enabled = enabled; + fact->setValue(enabled ? 2 : 0); + + _saveSettings(); + emit enabledChanged(_enabled); + + if (_enabled) { + startPolling(); + } else { + stopPolling(); + } +} diff --git a/src/Joystick/Joystick.h b/src/Joystick/Joystick.h new file mode 100644 index 000000000..a2622b117 --- /dev/null +++ b/src/Joystick/Joystick.h @@ -0,0 +1,167 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL 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, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL 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 for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#ifndef Joystick_H +#define Joystick_H + +#include +#include + +#include "QGCLoggingCategory.h" +#include "Vehicle.h" + +Q_DECLARE_LOGGING_CATEGORY(JoystickLog) + +class Joystick : public QThread +{ + Q_OBJECT + +public: + Joystick(const QString& name, int axisCount, int buttonCount, int sdlIndex); + ~Joystick(); + + typedef struct { + int min; + int max; + int center; + bool reversed; + } Calibration_t; + + typedef enum { + rollFunction, + pitchFunction, + yawFunction, + throttleFunction, + maxFunction + } AxisFunction_t; + + typedef enum { + ThrottleModeCenterZero, + ThrottleModeDownZero, + ThrottleModeMax + } ThrottleMode_t; + + Q_PROPERTY(QString name READ name CONSTANT) + + Q_PROPERTY(bool calibrated MEMBER _calibrated NOTIFY calibratedChanged) + Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged) + + Q_PROPERTY(int buttonCount MEMBER _buttonCount CONSTANT) + Q_PROPERTY(int axisCount MEMBER _axisCount CONSTANT) + + Q_PROPERTY(QStringList actions READ actions CONSTANT) + + Q_PROPERTY(QVariantList buttonActions READ buttonActions NOTIFY buttonActionsChanged) + Q_INVOKABLE void setButtonAction(int button, int action); + Q_INVOKABLE int getButtonAction(int button); + + Q_PROPERTY(int throttleMode READ throttleMode WRITE setThrottleMode NOTIFY throttleModeChanged) + + /// Start the polling thread which will in turn emit joystick signals + void startPolling(void); + void stopPolling(void); + + void setCalibration(int axis, Calibration_t& calibration); + Calibration_t getCalibration(int axis); + + void setFunctionAxis(AxisFunction_t function, int axis); + int getFunctionAxis(AxisFunction_t function); + + QStringList actions(void); + QVariantList buttonActions(void); + + QString name(void) { return _name; } + + int throttleMode(void); + void setThrottleMode(int mode); + + bool enabled(void); + void setEnabled(bool enabled); + + bool calibrating(void) { return _calibrating; } + void setCalibrating(bool calibrating) { _calibrating = calibrating; } + +signals: + void calibratedChanged(bool calibrated); + + // The raw signals are only meant for use by calibration + void rawAxisValueChanged(int index, int value); + void rawButtonPressedChanged(int index, int pressed); + + void buttonActionsChanged(QVariantList actions); + + void throttleModeChanged(int mode); + + void enabledChanged(bool enabled); + + /// Signal containing new joystick information + /// @param roll Range is -1:1, negative meaning roll left, positive meaning roll right + /// @param pitch Range i -1:1, negative meaning pitch down, positive meaning pitch up + /// @param yaw Range is -1:1, negative meaning yaw left, positive meaning yaw right + /// @param throttle Range is 0:1, 0 meaning no throttle, 1 meaning full throttle + /// @param mode See Vehicle::JoystickMode_t enum + void manualControl(float roll, float pitch, float yaw, float throttle, quint16 buttons, int joystickMmode); + + void buttonActionTriggered(int action); + +private: + void _saveSettings(void); + void _loadSettings(void); + float _adjustRange(int value, Calibration_t calibration); + + // Override from QThread + virtual void run(void); + +private: + int _sdlIndex; ///< Index for SDL_JoystickOpen + + bool _exitThread; ///< true: signal thread to exit + + QString _name; + bool _enabled; + bool _calibrated; + bool _calibrating; + int _axisCount; + int _buttonCount; + + static const int _cAxes = 4; + int _rgAxisValues[_cAxes]; + Calibration_t _rgCalibration[_cAxes]; + int _rgFunctionAxis[maxFunction]; + static const char* _rgFunctionSettingsKey[maxFunction]; + + static const int _cButtons = 12; + int _rgButtonValues[_cButtons]; + int _rgButtonActions[_cButtons]; + quint16 _lastButtonBits; + + ThrottleMode_t _throttleMode; + + static const char* _settingsGroup; + static const char* _calibratedSettingsKey; + static const char* _buttonActionSettingsKey; + static const char* _throttleModeSettingsKey; + static const char* _enabledSettingsKey; +}; + +#endif diff --git a/src/Joystick/JoystickManager.cc b/src/Joystick/JoystickManager.cc new file mode 100644 index 000000000..e8c28570f --- /dev/null +++ b/src/Joystick/JoystickManager.cc @@ -0,0 +1,158 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL 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, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL 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 for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "JoystickManager.h" + +#include + +#ifdef Q_OS_MAC + #include +#else + #include +#endif + +QGC_LOGGING_CATEGORY(JoystickManagerLog, "JoystickManagerLog") + +IMPLEMENT_QGC_SINGLETON(JoystickManager, JoystickManager) + +const char * JoystickManager::_settingsGroup = "JoystickManager"; +const char * JoystickManager::_settingsKeyActiveJoystick = "ActiveJoystick"; + +JoystickManager::JoystickManager(QObject* parent) + : QGCSingleton(parent) + , _activeJoystick(NULL) +{ + QQmlEngine::setObjectOwnership(this, QQmlEngine::CppOwnership); + + if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_NOPARACHUTE) < 0) { + qWarning() << "Couldn't initialize SimpleDirectMediaLayer:" << SDL_GetError(); + return; + } + + // Load available joysticks + + qCDebug(JoystickManagerLog) << "Available joysticks"; + + for (int i=0; iname(); + } + + setActiveJoystick(_name2JoystickMap.value(name, _name2JoystickMap.first())); + settings.setValue(_settingsKeyActiveJoystick, _activeJoystick->name()); +} + +Joystick* JoystickManager::activeJoystick(void) +{ + return _activeJoystick; +} + +void JoystickManager::setActiveJoystick(Joystick* joystick) +{ + QSettings settings; + + if (!_name2JoystickMap.contains(joystick->name())) { + qCWarning(JoystickManagerLog) << "Set active not in map" << joystick->name(); + return; + } + + if (_activeJoystick) { + _activeJoystick->stopPolling(); + } + + _activeJoystick = joystick; + + settings.beginGroup(_settingsGroup); + settings.setValue(_settingsKeyActiveJoystick, _activeJoystick->name()); + + emit activeJoystickChanged(_activeJoystick); + emit activeJoystickNameChanged(_activeJoystick->name()); +} + +QVariantList JoystickManager::joysticks(void) +{ + QVariantList list; + + foreach (QString name, _name2JoystickMap.keys()) { + list += QVariant::fromValue(_name2JoystickMap[name]); + } + + return list; +} + +QStringList JoystickManager::joystickNames(void) +{ + return _name2JoystickMap.keys(); +} + +QString JoystickManager::activeJoystickName(void) +{ + return _activeJoystick ? _activeJoystick->name() : QString(); +} + +void JoystickManager::setActiveJoystickName(const QString& name) +{ + if (!_name2JoystickMap.contains(name)) { + qCWarning(JoystickManagerLog) << "Set active not in map" << name; + return; + } + + setActiveJoystick(_name2JoystickMap[name]); +} diff --git a/src/Joystick/JoystickManager.h b/src/Joystick/JoystickManager.h new file mode 100644 index 000000000..a5c9fc96f --- /dev/null +++ b/src/Joystick/JoystickManager.h @@ -0,0 +1,81 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2014 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL 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, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL 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 for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#ifndef JoystickManager_H +#define JoystickManager_H + +#include "QGCSingleton.h" +#include "QGCLoggingCategory.h" +#include "Joystick.h" + +#include + +Q_DECLARE_LOGGING_CATEGORY(JoystickManagerLog) + +class JoystickManager : public QGCSingleton +{ + Q_OBJECT + + DECLARE_QGC_SINGLETON(JoystickManager, JoystickManager) + +public: + /// List of available joysticks + Q_PROPERTY(QVariantList joysticks READ joysticks CONSTANT) + Q_PROPERTY(QStringList joystickNames READ joystickNames CONSTANT) + + /// Active joystick + Q_PROPERTY(Joystick* activeJoystick READ activeJoystick WRITE setActiveJoystick NOTIFY activeJoystickChanged) + Q_PROPERTY(QString activeJoystickName READ activeJoystickName WRITE setActiveJoystickName NOTIFY activeJoystickNameChanged) + + QVariantList joysticks(); + QStringList joystickNames(void); + + Joystick* activeJoystick(void); + void setActiveJoystick(Joystick* joystick); + + QString activeJoystickName(void); + void setActiveJoystickName(const QString& name); + +signals: + void activeJoystickChanged(Joystick* joystick); + void activeJoystickNameChanged(const QString& name); + +private slots: + +private: + /// All access to singleton is through JoystickManager::instance + JoystickManager(QObject* parent = NULL); + ~JoystickManager(); + + void _setActiveJoystickFromSettings(void); + +private: + Joystick* _activeJoystick; + + QMap _name2JoystickMap; + + static const char * _settingsGroup; + static const char * _settingsKeyActiveJoystick; +}; + +#endif diff --git a/src/QGCApplication.cc b/src/QGCApplication.cc index 48615b088..45e04e734 100644 --- a/src/QGCApplication.cc +++ b/src/QGCApplication.cc @@ -89,6 +89,8 @@ G_END_DECLS #include "PX4/PX4FirmwarePlugin.h" #include "Vehicle.h" #include "MavlinkQmlSingleton.h" +#include "JoystickManager.h" +#include "JoystickConfigController.h" #ifdef QGC_RTLAB_ENABLED #include "OpalLink.h" @@ -327,16 +329,19 @@ void QGCApplication::_initCommon(void) qmlRegisterUncreatableType("QGroundControl.AutoPilotPlugin", 1, 0, "AutoPilotPlugin", "Can only reference, cannot create"); qmlRegisterUncreatableType("QGroundControl.AutoPilotPlugin", 1, 0, "VehicleComponent", "Can only reference, cannot create"); qmlRegisterUncreatableType("QGroundControl.Vehicle", 1, 0, "Vehicle", "Can only reference, cannot create"); + qmlRegisterUncreatableType ("QGroundControl.JoystickManager", 1, 0, "JoystickManager", "Reference only"); + qmlRegisterUncreatableType ("QGroundControl.JoystickManager", 1, 0, "Joystick", "Reference only"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "ViewWidgetController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "ParameterEditorController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "CustomCommandWidgetController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "FlightModesComponentController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "AirframeComponentController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "SensorsComponentController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "PowerComponentController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "RadioComponentController"); - qmlRegisterType("QGroundControl.Controllers", 1, 0, "ScreenToolsController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "ViewWidgetController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "ParameterEditorController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "CustomCommandWidgetController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "FlightModesComponentController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "AirframeComponentController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "SensorsComponentController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "PowerComponentController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "RadioComponentController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "ScreenToolsController"); + qmlRegisterType ("QGroundControl.Controllers", 1, 0, "JoystickConfigController"); #ifndef __mobile__ qmlRegisterType("QGroundControl.Controllers", 1, 0, "FirmwareUpgradeController"); @@ -576,6 +581,11 @@ void QGCApplication::_createSingletons(void) Q_UNUSED(multiVehicleManager); Q_ASSERT(multiVehicleManager); + // No dependencies + JoystickManager* joystickManager = JoystickManager::_createSingleton(); + Q_UNUSED(joystickManager); + Q_ASSERT(joystickManager); + // No dependencies GAudioOutput* audio = GAudioOutput::_createSingleton(); Q_UNUSED(audio); @@ -636,6 +646,7 @@ void QGCApplication::_destroySingletons(void) HomePositionManager::_deleteSingleton(); LinkManager::_deleteSingleton(); GAudioOutput::_deleteSingleton(); + JoystickManager::_deleteSingleton(); MultiVehicleManager::_deleteSingleton(); FirmwarePluginManager::_deleteSingleton(); GenericFirmwarePlugin::_deleteSingleton(); diff --git a/src/QGCQuickWidget.cc b/src/QGCQuickWidget.cc index 4b21422fd..351d48177 100644 --- a/src/QGCQuickWidget.cc +++ b/src/QGCQuickWidget.cc @@ -25,6 +25,7 @@ #include "AutoPilotPluginManager.h" #include "QGCMessageBox.h" #include "MultiVehicleManager.h" +#include "JoystickManager.h" #include #include @@ -40,6 +41,7 @@ QGCQuickWidget::QGCQuickWidget(QWidget* parent) : { rootContext()->engine()->addImportPath("qrc:/qml"); rootContext()->setContextProperty("multiVehicleManager", MultiVehicleManager::instance()); + rootContext()->setContextProperty("joystickManager", JoystickManager::instance()); } void QGCQuickWidget::setAutoPilot(AutoPilotPlugin* autoPilot) diff --git a/src/Vehicle/MultiVehicleManager.cc b/src/Vehicle/MultiVehicleManager.cc index 7bb1da915..cdb54dfe0 100644 --- a/src/Vehicle/MultiVehicleManager.cc +++ b/src/Vehicle/MultiVehicleManager.cc @@ -26,6 +26,9 @@ #include "MultiVehicleManager.h" #include "AutoPilotPlugin.h" +#include "JoystickManager.h" +#include "MAVLinkProtocol.h" +#include "UAS.h" IMPLEMENT_QGC_SINGLETON(MultiVehicleManager, MultiVehicleManager) @@ -107,6 +110,15 @@ void MultiVehicleManager::_deleteVehiclePhase1(void) qWarning() << "Vehicle not found in map!"; } + // Disconnect the vehicle from the uas + vehicle->uas()->clearVehicle(); + + // Disconnect joystick + Joystick* joystick = JoystickManager::instance()->activeJoystick(); + if (joystick) { + joystick->stopPolling(); + } + // First we must signal that a vehicle is no longer available. _activeVehicleAvailable = false; _parameterReadyVehicleAvailable = false; @@ -150,6 +162,12 @@ void MultiVehicleManager::setActiveVehicle(Vehicle* vehicle) { if (vehicle != _activeVehicle) { if (_activeVehicle) { + // Disconnect joystick + Joystick* joystick = JoystickManager::instance()->activeJoystick(); + if (joystick) { + joystick->stopPolling(); + } + // The sequence of signals is very important in order to not leave Qml elements connected // to a non-existent vehicle. @@ -195,6 +213,12 @@ void MultiVehicleManager::_autopilotPluginReadyChanged(bool pluginReady) } if (autopilot->vehicle() == _activeVehicle) { + // Connect joystick + Joystick* joystick = JoystickManager::instance()->activeJoystick(); + if (joystick && joystick->enabled()) { + joystick->startPolling(); + } + _parameterReadyVehicleAvailable = pluginReady; emit parameterReadyVehicleAvailableChanged(pluginReady); } diff --git a/src/Vehicle/Vehicle.cc b/src/Vehicle/Vehicle.cc index 69e4ad94a..cb62bdda3 100644 --- a/src/Vehicle/Vehicle.cc +++ b/src/Vehicle/Vehicle.cc @@ -28,6 +28,7 @@ #include "FirmwarePlugin.h" #include "AutoPilotPluginManager.h" #include "UASMessageHandler.h" +#include "UAS.h" QGC_LOGGING_CATEGORY(VehicleLog, "VehicleLog") @@ -35,9 +36,15 @@ QGC_LOGGING_CATEGORY(VehicleLog, "VehicleLog") #define DEFAULT_LAT 38.965767f #define DEFAULT_LON -120.083923f +const char* Vehicle::_settingsGroup = "Vehicle%1"; // %1 replace with mavlink system id +const char* Vehicle::_joystickModeSettingsKey = "JoystickMode"; + Vehicle::Vehicle(LinkInterface* link, int vehicleId, MAV_AUTOPILOT firmwareType) : _id(vehicleId) , _firmwareType(firmwareType) + , _firmwarePlugin(NULL) + , _autopilotPlugin(NULL) + , _joystickMode(JoystickModeRC) , _uas(NULL) , _mav(NULL) , _currentMessageCount(0) @@ -74,6 +81,8 @@ Vehicle::Vehicle(LinkInterface* link, int vehicleId, MAV_AUTOPILOT firmwareType) , _wpm(NULL) , _updateCount(0) { + _loadSettings(); + _addLink(link); connect(MAVLinkProtocol::instance(), &MAVLinkProtocol::messageReceived, this, &Vehicle::_mavlinkMessageReceived); @@ -809,3 +818,57 @@ void Vehicle::resetMessages() emit messageTypeChanged(); } } + +int Vehicle::manualControlReservedButtonCount(void) +{ + return _firmwarePlugin->manualControlReservedButtonCount(); +} + +void Vehicle::_loadSettings(void) +{ + QSettings settings; + + settings.beginGroup(QString(_settingsGroup).arg(_id)); + + bool convertOk; + + _joystickMode = (JoystickMode_t)settings.value(_joystickModeSettingsKey, JoystickModeRC).toInt(&convertOk); + if (!convertOk) { + _joystickMode = JoystickModeRC; + } +} + +void Vehicle::_saveSettings(void) +{ + QSettings settings; + + settings.beginGroup(QString(_settingsGroup).arg(_id)); + + settings.setValue(_joystickModeSettingsKey, _joystickMode); +} + +int Vehicle::joystickMode(void) +{ + return _joystickMode; +} + +void Vehicle::setJoystickMode(int mode) +{ + if (mode < 0 || mode >= JoystickModeMax) { + qCWarning(VehicleLog) << "Invalid joystick mode" << mode; + return; + } + + _joystickMode = (JoystickMode_t)mode; + _saveSettings(); + emit joystickModeChanged(mode); +} + +QStringList Vehicle::joystickModes(void) +{ + QStringList list; + + list << "Simulate RC" << "Attitude" << "Position" << "Force" << "Velocity"; + + return list; +} diff --git a/src/Vehicle/Vehicle.h b/src/Vehicle/Vehicle.h index 37bcb9d81..fc0271acd 100644 --- a/src/Vehicle/Vehicle.h +++ b/src/Vehicle/Vehicle.h @@ -29,13 +29,17 @@ #include #include +#include #include "LinkInterface.h" #include "QGCMAVLink.h" -#include "UAS.h" +#include "MissionItem.h" +class UAS; +class UASInterface; class FirmwarePlugin; class AutoPilotPlugin; +class UASWaypointManager; Q_DECLARE_LOGGING_CATEGORY(VehicleLog) @@ -91,6 +95,30 @@ public: //-- MissionItem management Q_PROPERTY(QQmlListProperty missionItems READ missionItems NOTIFY missionItemsChanged) + /// Returns the number of buttons which are reserved for firmware use in the MANUAL_CONTROL mavlink + /// message. For example PX4 Flight Stack reserves the first 8 buttons to simulate rc switches. + /// The remainder can be assigned to Vehicle actions. + /// @return -1: reserver all buttons, >0 number of buttons to reserve + Q_PROPERTY(int manualControlReservedButtonCount READ manualControlReservedButtonCount CONSTANT) + + typedef enum { + JoystickModeRC, ///< Joystick emulates am RC Transmitter + JoystickModeAttitude, + JoystickModePosition, + JoystickModeForce, + JoystickModeVelocity, + JoystickModeMax + } JoystickMode_t; + + /// The joystick mode associated with this vehicle. Joystick modes are stored keyed by mavlink system id. + Q_PROPERTY(int joystickMode READ joystickMode WRITE setJoystickMode NOTIFY joystickModeChanged) + int joystickMode(void); + void setJoystickMode(int mode); + + /// List of joystick mode names + Q_PROPERTY(QStringList joystickModes READ joystickModes CONSTANT) + QStringList joystickModes(void); + // Property accesors int id(void) { return _id; } MAV_AUTOPILOT firmwareType(void) { return _firmwareType; } @@ -106,6 +134,8 @@ public: QList links(void); + int manualControlReservedButtonCount(void); + typedef enum { MessageNone, MessageNormal, @@ -171,6 +201,7 @@ public slots: signals: void allLinksDisconnected(void); void coordinateChanged(QGeoCoordinate coordinate); + void joystickModeChanged(int mode); /// Used internally to move sendMessage call to main thread void _sendMessageOnThread(mavlink_message_t message); @@ -241,6 +272,9 @@ private slots: private: bool _containsLink(LinkInterface* link); void _addLink(LinkInterface* link); + void _loadSettings(void); + void _saveSettings(void); + bool _isAirplane (); void _addChange (int id); float _oneDecimal (float value); @@ -257,6 +291,8 @@ private: /// This way Link deletion works correctly. QList _links; + JoystickMode_t _joystickMode; + UAS* _uas; QGeoCoordinate _geoCoordinate; @@ -302,5 +338,8 @@ private: UASWaypointManager* _wpm; int _updateCount; QList_waypoints; + + static const char* _settingsGroup; + static const char* _joystickModeSettingsKey; }; #endif diff --git a/src/VehicleSetup/JoystickConfig.qml b/src/VehicleSetup/JoystickConfig.qml new file mode 100644 index 000000000..f10491f1f --- /dev/null +++ b/src/VehicleSetup/JoystickConfig.qml @@ -0,0 +1,652 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009 - 2015 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL 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, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL 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 for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +import QtQuick 2.2 +import QtQuick.Controls 1.2 +import QtQuick.Dialogs 1.2 + +import QGroundControl.Palette 1.0 +import QGroundControl.Controls 1.0 +import QGroundControl.ScreenTools 1.0 +import QGroundControl.Controllers 1.0 + +/// Joystick Config +QGCView { + id: rootQGCView + viewPanel: panel + + QGCPalette { id: qgcPal; colorGroupEnabled: panel.enabled } + + readonly property string dialogTitle: "Joystick Config" + readonly property real labelToMonitorMargin: defaultTextWidth * 3 + property bool controllerCompleted: false + property bool controllerAndViewReady: false + + property var _activeVehicle: multiVehicleManager.activeVehicle + property var _activeJoystick: joystickManager.activeJoystick + + function updateAxisCount() + { + if (controllerAndViewReady) { + if (controller.axisCount < controller.minAxisCount) { + showDialog(axisCountDialogComponent, dialogTitle, 50, 0) + } else { + hideDialog() + } + } + } + + JoystickConfigController { + id: controller + factPanel: panel + statusText: statusText + cancelButton: cancelButton + nextButton: nextButton + skipButton: skipButton + + onAxisCountChanged: updateAxisCount() + + Component.onCompleted: { + controllerCompleted = true + if (rootQGCView.completedSignalled) { + controllerAndViewReady = true + controller.start() + updateAxisCount() + } + } + } + + onCompleted: { + if (controllerCompleted) { + controllerAndViewReady = true + controller.start() + updateAxisCount() + } + } + + QGCViewPanel { + id: panel + anchors.fill: parent + + Component { + id: axisCountDialogComponent + + QGCViewMessage { + message: controller.axisCount == 0 ? "No joystick axes deteced." : controller.minAxisCount + " joystick axes or more are needed to fly." + } + } + + // Live axis monitor control component + Component { + id: axisMonitorDisplayComponent + + Item { + property int axisValue: 0 + + + property int __lastAxisValue: 0 + readonly property int __axisValueMaxJitter: 100 + property color __barColor: qgcPal.windowShade + + // Bar + Rectangle { + id: bar + anchors.verticalCenter: parent.verticalCenter + width: parent.width + height: parent.height / 2 + color: __barColor + } + + // Center point + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + width: defaultTextWidth / 2 + height: parent.height + color: qgcPal.window + } + + // Indicator + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: parent.height * 0.75 + height: width + x: (reversed ? (parent.width - _indicatorPosition) : _indicatorPosition) - (width / 2) + radius: width / 2 + color: qgcPal.text + visible: mapped + + property real _percentAxisValue: ((axisValue + 32768.0) / (32768.0 * 2)) + property real _indicatorPosition: parent.width * _percentAxisValue + } + + QGCLabel { + anchors.fill: parent + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: "Not Mapped" + visible: !mapped + } + + ColorAnimation { + id: barAnimation + target: bar + property: "color" + from: "yellow" + to: __barColor + duration: 1500 + } + +/* + // Axis value debugger + QGCLabel { + anchors.fill: parent + text: axisValue + } +*/ + } + } // Component - axisMonitorDisplayComponent + + // Main view Qml starts here + + QGCLabel { + id: header + font.pixelSize: ScreenTools.largeFontPixelSize + text: "JOYSTICK CONFIG" + } + + Item { + id: spacer + anchors.top: header.bottom + width: parent.width + height: 10 + } + + // Left side column + Column { + id: leftColumn + anchors.rightMargin: ScreenTools.defaultFontPixelWidth + anchors.top: spacer.bottom + anchors.left: parent.left + anchors.right: rightColumn.left + spacing: 10 + + // Attitude Controls + Column { + width: parent.width + spacing: 5 + + QGCLabel { text: "Attitude Controls" } + + Item { + width: parent.width + height: defaultTextHeight * 2 + + QGCLabel { + id: rollLabel + width: defaultTextWidth * 10 + text: "Roll" + } + + Loader { + id: rollLoader + anchors.left: rollLabel.right + anchors.right: parent.right + height: rootQGCView.defaultTextHeight + width: 100 + sourceComponent: axisMonitorDisplayComponent + + property real defaultTextWidth: rootQGCView.defaultTextWidth + property bool mapped: controller.rollAxisMapped + property bool reversed: controller.rollAxisReversed + } + + Connections { + target: controller + + onRollAxisValueChanged: rollLoader.item.axisValue = value + } + } + + Item { + width: parent.width + height: defaultTextHeight * 2 + + QGCLabel { + id: pitchLabel + width: defaultTextWidth * 10 + text: "Pitch" + } + + Loader { + id: pitchLoader + anchors.left: pitchLabel.right + anchors.right: parent.right + height: rootQGCView.defaultTextHeight + width: 100 + sourceComponent: axisMonitorDisplayComponent + + property real defaultTextWidth: rootQGCView.defaultTextWidth + property bool mapped: controller.pitchAxisMapped + property bool reversed: controller.pitchAxisReversed + } + + Connections { + target: controller + + onPitchAxisValueChanged: pitchLoader.item.axisValue = value + } + } + + Item { + width: parent.width + height: defaultTextHeight * 2 + + QGCLabel { + id: yawLabel + width: defaultTextWidth * 10 + text: "Yaw" + } + + Loader { + id: yawLoader + anchors.left: yawLabel.right + anchors.right: parent.right + height: rootQGCView.defaultTextHeight + width: 100 + sourceComponent: axisMonitorDisplayComponent + + property real defaultTextWidth: rootQGCView.defaultTextWidth + property bool mapped: controller.yawAxisMapped + property bool reversed: controller.yawAxisReversed + } + + Connections { + target: controller + + onYawAxisValueChanged: yawLoader.item.axisValue = value + } + } + + Item { + width: parent.width + height: defaultTextHeight * 2 + + QGCLabel { + id: throttleLabel + width: defaultTextWidth * 10 + text: "Throttle" + } + + Loader { + id: throttleLoader + anchors.left: throttleLabel.right + anchors.right: parent.right + height: rootQGCView.defaultTextHeight + width: 100 + sourceComponent: axisMonitorDisplayComponent + + property real defaultTextWidth: rootQGCView.defaultTextWidth + property bool mapped: controller.throttleAxisMapped + property bool reversed: controller.throttleAxisReversed + } + + Connections { + target: controller + + onThrottleAxisValueChanged: throttleLoader.item.axisValue = value + } + } + } // Column - Attitude Control labels + + // Command Buttons + Row { + spacing: 10 + + QGCButton { + id: skipButton + text: "Skip" + + onClicked: controller.skipButtonClicked() + } + + QGCButton { + id: cancelButton + text: "Cancel" + + onClicked: controller.cancelButtonClicked() + } + + QGCButton { + id: nextButton + primary: true + text: "Calibrate" + + onClicked: controller.nextButtonClicked() + } + } // Row - Buttons + + // Status Text + QGCLabel { + id: statusText + width: parent.width + wrapMode: Text.WordWrap + } + + Rectangle { + width: parent.width + height: 1 + border.color: qgcPal.text + border.width: 1 + } + + // Settings + Row { + width: parent.width + spacing: ScreenTools.defaultFontPixelWidth + + // Left column settings + Column { + width: parent.width / 2 + spacing: ScreenTools.defaultFontPixelHeight + + QGCLabel { text: "Additional Joystick settings:" } + + Column { + width: parent.width + spacing: ScreenTools.defaultFontPixelHeight + + Row { + width: parent.width + spacing: ScreenTools.defaultFontPixelWidth + + QGCLabel { + id: activeJoystickLabel + anchors.baseline: joystickCombo.baseline + text: "Active joystick:" + } + + QGCComboBox { + id: joystickCombo + width: parent.width - activeJoystickLabel.width - parent.spacing + model: joystickManager.joystickNames + + onActivated: _activeJoystick.setActiveJoystickName(textAt(index)) + } + } + + QGCCheckBox { + enabled: calibrated + text: calibrated ? "Enable joystick input, disable RC input" : "Enable joystick input (Calibrate First)" + checked: _activeJoystick.enabled + + property bool calibrated: _activeJoystick.calibrated + + onClicked: _activeJoystick.enabled = checked + } + + Column { + spacing: ScreenTools.defaultFontPixelHeight / 3 + + ExclusiveGroup { id: throttleModeExclusiveGroup } + + QGCRadioButton { + exclusiveGroup: throttleModeExclusiveGroup + text: "Center stick is zero throttle" + checked: _activeJoystick.throttleMode == 0 + + onClicked: _activeJoystick.throttleMode = 0 + } + + QGCRadioButton { + exclusiveGroup: throttleModeExclusiveGroup + text: "Full down stick is zero throttle" + checked: _activeJoystick.throttleMode == 1 + + onClicked: _activeJoystick.throttleMode = 1 + } + } + + QGCCheckBox { + id: advancedSettings + checked: _activeVehicle.joystickMode != 0 + text: "Advanced settings (careful!)" + + onClicked: { + if (!checked) { + _activeVehicle.joystickMode = 0 + } + } + } + + Row { + width: parent.width + spacing: ScreenTools.defaultFontPixelWidth + visible: advancedSettings.checked + + QGCLabel { + id: joystickModeLabel + anchors.baseline: joystickModeCombo.baseline + text: "Joystick mode:" + } + + QGCComboBox { + id: joystickModeCombo + currentIndex: _activeVehicle.joystickMode + width: ScreenTools.defaultFontPixelWidth * 20 + model: _activeVehicle.joystickModes + + onActivated: _activeVehicle.joystickMode = index + } + } + } + } // Column - left column + + // Right column settings + Column { + width: parent.width / 2 + spacing: ScreenTools.defaultFontPixelHeight + + Connections { + target: _activeJoystick + + onRawButtonPressedChanged: { + if (buttonActionRepeater.itemAt(index)) { + buttonActionRepeater.itemAt(index).pressed = pressed + } + } + } + + QGCLabel { text: "Button actions:" } + + Column { + width: parent.width + spacing: ScreenTools.defaultFontPixelHeight / 3 + + QGCLabel { + visible: _activeVehicle.manualControlReservedButtonCount != 0 + text: "Buttons 0-" + reservedButtonCount + " reserved for firmware use" + + property int reservedButtonCount: _activeVehicle.manualControlReservedButtonCount == -1 ? _activeJoystick.buttonCount : _activeVehicle.manualControlReservedButtonCount + } + + Repeater { + id: buttonActionRepeater + model: _activeJoystick.buttonCount + + Row { + spacing: ScreenTools.defaultFontPixelWidth + visible: _activeVehicle.manualControlReservedButtonCount == -1 ? false : modelData >= _activeVehicle.manualControlReservedButtonCount + + property bool pressed + + QGCCheckBox { + anchors.verticalCenter: parent.verticalCenter + checked: _activeJoystick.buttonActions[modelData] != -1 + + onClicked: _activeJoystick.setButtonAction(modelData, checked ? buttonActionCombo.currentIndex : -1) + } + + Rectangle { + anchors.verticalCenter: parent.verticalCenter + width: ScreenTools.defaultFontPixelHeight * 1.5 + height: width + border.width: 1 + border.color: qgcPal.text + color: pressed ? qgcPal.buttonHighlight : qgcPal.button + + + QGCLabel { + anchors.fill: parent + color: pressed ? qgcPal.buttonHighlightText : qgcPal.buttonText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: modelData + } + } + + QGCComboBox { + id: buttonActionCombo + width: ScreenTools.defaultFontPixelWidth * 20 + model: _activeJoystick.actions + currentIndex: _activeJoystick.buttonActions[modelData] + + onActivated: _activeJoystick.setButtonAction(modelData, index) + } + } + } // Repeater + } // Column + } // Column - right setting column + } // Row - Settings + } // Column - Left Main Column + + // Right side column + Column { + id: rightColumn + anchors.top: parent.top + anchors.right: parent.right + width: defaultTextWidth * 35 + spacing: 10 + + Image { + //width: parent.width + height: defaultTextHeight * 15 + fillMode: Image.PreserveAspectFit + smooth: true + source: controller.imageHelp + } + + // Axis monitor + Column { + width: parent.width + spacing: 5 + + QGCLabel { text: "Axis Monitor" } + + Connections { + target: controller + + onAxisValueChanged: { + if (axisMonitorRepeater.itemAt(axis)) { + axisMonitorRepeater.itemAt(axis).loader.item.axisValue = value + } + } + } + + Repeater { + id: axisMonitorRepeater + model: controller.axisCount + width: parent.width + + Row { + spacing: 5 + + // Need this to get to loader from Connections above + property Item loader: theLoader + + QGCLabel { + id: axisLabel + text: modelData + } + + Loader { + id: theLoader + anchors.verticalCenter: axisLabel.verticalCenter + height: rootQGCView.defaultTextHeight + width: 200 + sourceComponent: axisMonitorDisplayComponent + + property real defaultTextWidth: rootQGCView.defaultTextWidth + property bool mapped: true + readonly property bool reversed: false + } + } + } + } // Column - Axis Monitor + + // Button monitor + Column { + width: parent.width + spacing: ScreenTools.defaultFontPixelHeight + + QGCLabel { text: "Button Monitor" } + + Connections { + target: _activeJoystick + + onRawButtonPressedChanged: { + if (buttonMonitorRepeater.itemAt(index)) { + buttonMonitorRepeater.itemAt(index).pressed = pressed + } + } + } + + Row { + spacing: -1 + + Repeater { + id: buttonMonitorRepeater + model: _activeJoystick.buttonCount + + Rectangle { + width: ScreenTools.defaultFontPixelHeight * 1.5 + height: width + border.width: 1 + border.color: qgcPal.text + color: pressed ? qgcPal.buttonHighlight : qgcPal.button + + property bool pressed + + QGCLabel { + anchors.fill: parent + color: pressed ? qgcPal.buttonHighlightText : qgcPal.buttonText + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + text: modelData + } + } + } // Repeater + } // Row + } // Column - Axis Monitor + } // Column - Right Column + } // QGCViewPanel +} diff --git a/src/VehicleSetup/JoystickConfigController.cc b/src/VehicleSetup/JoystickConfigController.cc new file mode 100644 index 000000000..a031c2af2 --- /dev/null +++ b/src/VehicleSetup/JoystickConfigController.cc @@ -0,0 +1,739 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009, 2015 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL 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, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL 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 for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + +#include "JoystickConfigController.h" +#include "JoystickManager.h" +#include "QGCMessageBox.h" + +#include + +QGC_LOGGING_CATEGORY(JoystickConfigControllerLog, "JoystickConfigControllerLog") + +const int JoystickConfigController::_updateInterval = 150; ///< Interval for timer which updates radio channel widgets +const int JoystickConfigController::_calCenterPoint = 0; +const int JoystickConfigController::_calValidMinValue = -32768; ///< Largest valid minimum axis value +const int JoystickConfigController::_calValidMaxValue = 32768; ///< Smallest valid maximum axis value +const int JoystickConfigController::_calDefaultMinValue = -32768; ///< Default value for Min if not set +const int JoystickConfigController::_calDefaultMaxValue = 32768; ///< Default value for Max if not set +const int JoystickConfigController::_calRoughCenterDelta = 500; ///< Delta around center point which is considered to be roughly centered +const int JoystickConfigController::_calMoveDelta = 32768/2; ///< Amount of delta past center which is considered stick movement +const int JoystickConfigController::_calSettleDelta = 100; ///< Amount of delta which is considered no stick movement +const int JoystickConfigController::_calMinDelta = 1000; ///< Amount of delta allowed around min value to consider channel at min + +const int JoystickConfigController::_stickDetectSettleMSecs = 500; + +const char* JoystickConfigController::_imageFilePrefix = "calibration/"; +const char* JoystickConfigController::_imageFileMode2Dir = "joystick/"; +const char* JoystickConfigController::_imageCenter = "joystickCenter.png"; +const char* JoystickConfigController::_imageThrottleUp = "joystickThrottleUp.png"; +const char* JoystickConfigController::_imageThrottleDown = "joystickThrottleDown.png"; +const char* JoystickConfigController::_imageYawLeft = "joystickYawLeft.png"; +const char* JoystickConfigController::_imageYawRight = "joystickYawRight.png"; +const char* JoystickConfigController::_imageRollLeft = "joystickRollLeft.png"; +const char* JoystickConfigController::_imageRollRight = "joystickRollRight.png"; +const char* JoystickConfigController::_imagePitchUp = "joystickPitchUp.png"; +const char* JoystickConfigController::_imagePitchDown = "joystickPitchDown.png"; + +const char* JoystickConfigController::_settingsGroup = "Joysticks"; + +JoystickConfigController::JoystickConfigController(void) : + _currentStep(-1), + _axisCount(0), + _calState(calStateAxisWait), + _statusText(NULL), + _cancelButton(NULL), + _nextButton(NULL), + _skipButton(NULL) +{ + // FIXME: Needs to handle active joystick change + connect(JoystickManager::instance()->activeJoystick(), &Joystick::rawAxisValueChanged, this, &JoystickConfigController::_axisValueChanged); + JoystickManager::instance()->activeJoystick()->startPolling(); + _loadSettings(); + + _resetInternalCalibrationValues(); +} + +void JoystickConfigController::start(void) +{ + _stopCalibration(); + _setInternalCalibrationValuesFromSettings(); +} + +JoystickConfigController::~JoystickConfigController() +{ + _storeSettings(); +} + +/// @brief Returns the state machine entry for the specified state. +const JoystickConfigController::stateMachineEntry* JoystickConfigController::_getStateMachineEntry(int step) +{ + static const char* msgBegin = "Allow all sticks to center as shown in diagram.\n\nClick Next to continue"; + static const char* msgThrottleUp = "Move the Throttle stick all the way up and hold it there..."; + static const char* msgThrottleDown = "Move the Throttle stick all the way down and hold it there..."; + static const char* msgYawLeft = "Move the Yaw stick all the way to the left and hold it there..."; + static const char* msgYawRight = "Move the Yaw stick all the way to the right and hold it there..."; + static const char* msgRollLeft = "Move the Roll stick all the way to the left and hold it there..."; + static const char* msgRollRight = "Move the Roll stick all the way to the right and hold it there..."; + static const char* msgPitchDown = "Move the Pitch stick all the way down and hold it there..."; + static const char* msgPitchUp = "Move the Pitch stick all the way up and hold it there..."; + static const char* msgPitchCenter = "Allow the Pitch stick to move back to center..."; + static const char* msgComplete = "All settings have been captured. Click Next to Save."; + + static const stateMachineEntry rgStateMachine[] = { + //Function + { Joystick::maxFunction, msgBegin, _imageCenter, &JoystickConfigController::_inputCenterWaitBegin, &JoystickConfigController::_saveAllTrims, NULL }, + { Joystick::throttleFunction, msgThrottleUp, _imageThrottleUp, &JoystickConfigController::_inputStickDetect, NULL, NULL }, + { Joystick::throttleFunction, msgThrottleDown, _imageThrottleDown, &JoystickConfigController::_inputStickMin, NULL, NULL }, + { Joystick::yawFunction, msgYawRight, _imageYawRight, &JoystickConfigController::_inputStickDetect, NULL, NULL }, + { Joystick::yawFunction, msgYawLeft, _imageYawLeft, &JoystickConfigController::_inputStickMin, NULL, NULL }, + { Joystick::rollFunction, msgRollRight, _imageRollRight, &JoystickConfigController::_inputStickDetect, NULL, NULL }, + { Joystick::rollFunction, msgRollLeft, _imageRollLeft, &JoystickConfigController::_inputStickMin, NULL, NULL }, + { Joystick::pitchFunction, msgPitchUp, _imagePitchUp, &JoystickConfigController::_inputStickDetect, NULL, NULL }, + { Joystick::pitchFunction, msgPitchDown, _imagePitchDown, &JoystickConfigController::_inputStickMin, NULL, NULL }, + { Joystick::pitchFunction, msgPitchCenter, _imageCenter, &JoystickConfigController::_inputCenterWait, NULL, NULL }, + { Joystick::maxFunction, msgComplete, _imageCenter, NULL, &JoystickConfigController::_writeCalibration, NULL }, + }; + + Q_ASSERT(step >=0 && step < (int)(sizeof(rgStateMachine) / sizeof(rgStateMachine[0]))); + + return &rgStateMachine[step]; +} + +void JoystickConfigController::_advanceState(void) +{ + _currentStep++; + _setupCurrentState(); +} + + +/// @brief Sets up the state machine according to the current step from _currentStep. +void JoystickConfigController::_setupCurrentState(void) +{ + const stateMachineEntry* state = _getStateMachineEntry(_currentStep); + + _statusText->setProperty("text", state->instructions); + + _setHelpImage(state->image); + + _stickDetectAxis = _axisMax; + _stickDetectSettleStarted = false; + + _calSaveCurrentValues(); + + _nextButton->setEnabled(state->nextFn != NULL); + _skipButton->setEnabled(state->skipFn != NULL); +} + +void JoystickConfigController::_axisValueChanged(int axis, int value) +{ + if (axis >= 0 && axis <= _axisMax) { + // We always update raw values + _axisRawValue[axis] = value; + emit axisValueChanged(axis, _axisRawValue[axis]); + + // Signal attitude axis values to Qml if mapped + if (_rgAxisInfo[axis].function != Joystick::maxFunction) { + switch (_rgAxisInfo[axis].function) { + case Joystick::rollFunction: + emit rollAxisValueChanged(_axisRawValue[axis]); + break; + case Joystick::pitchFunction: + emit pitchAxisValueChanged(_axisRawValue[axis]); + break; + case Joystick::yawFunction: + emit yawAxisValueChanged(_axisRawValue[axis]); + break; + case Joystick::throttleFunction: + emit throttleAxisValueChanged(_axisRawValue[axis]); + break; + default: + break; + } + } + + //qCDebug(JoystickConfigControllerLog) << "Raw value" << axis << value; + + if (_currentStep == -1) { + // Track the axis count by keeping track of how many axes we see + if (axis + 1 > (int)_axisCount) { + _axisCount = axis + 1; + emit axisCountChanged(_axisCount); + } + } + + if (_currentStep != -1) { + const stateMachineEntry* state = _getStateMachineEntry(_currentStep); + Q_ASSERT(state); + if (state->rcInputFn) { + (this->*state->rcInputFn)(state->function, axis, value); + } + } + } +} + +void JoystickConfigController::nextButtonClicked(void) +{ + if (_currentStep == -1) { + // Need to have enough channels + if (_axisCount < _axisMinimum) { + QGCMessageBox::warning(tr("Joystick"), tr("Detected %1 joystick axes. To operate PX4, you need at least %2 axes.").arg(_axisCount).arg(_axisMinimum)); + return; + } + _startCalibration(); + } else { + const stateMachineEntry* state = _getStateMachineEntry(_currentStep); + Q_ASSERT(state); + Q_ASSERT(state->nextFn); + (this->*state->nextFn)(); + } +} + +void JoystickConfigController::skipButtonClicked(void) +{ + Q_ASSERT(_currentStep != -1); + + const stateMachineEntry* state = _getStateMachineEntry(_currentStep); + Q_ASSERT(state); + Q_ASSERT(state->skipFn); + (this->*state->skipFn)(); +} + +void JoystickConfigController::cancelButtonClicked(void) +{ + _stopCalibration(); +} + +void JoystickConfigController::_saveAllTrims(void) +{ + // We save all trims as the first step. At this point no axes are mapped but it should still + // allow us to get good trims for the roll/pitch/yaw/throttle even though we don't know which + // axiss they are yet. AS we continue through the process the other axes will get their + // trims reset to correct values. + + for (int i=0; i<_axisCount; i++) { + qCDebug(JoystickConfigControllerLog) << "_saveAllTrims trim" << _axisRawValue[i]; + _rgAxisInfo[i].axisTrim = _axisRawValue[i]; + } + _advanceState(); +} + +/// @brief Waits for the sticks to be centered, enabling Next when done. +void JoystickConfigController::_inputCenterWaitBegin(Joystick::AxisFunction_t function, int axis, int value) +{ + Q_UNUSED(function); + Q_UNUSED(axis); + Q_UNUSED(value); + + // FIXME: Doesn't wait for center + + _nextButton->setEnabled(true); +} + +bool JoystickConfigController::_stickSettleComplete(int value) +{ + // We are waiting for the stick to settle out to a max position + + if (abs(_stickDetectValue - value) > _calSettleDelta) { + // Stick is moving too much to consider stopped + + qCDebug(JoystickConfigControllerLog) << "_stickSettleComplete still moving, _stickDetectValue:value" << _stickDetectValue << value; + + _stickDetectValue = value; + _stickDetectSettleStarted = false; + } else { + // Stick is still positioned within the specified small range + + if (_stickDetectSettleStarted) { + // We have already started waiting + + if (_stickDetectSettleElapsed.elapsed() > _stickDetectSettleMSecs) { + // Stick has stayed positioned in one place long enough, detection is complete. + return true; + } + } else { + // Start waiting for the stick to stay settled for _stickDetectSettleWaitMSecs msecs + + qCDebug(JoystickConfigControllerLog) << "_stickSettleComplete starting settle timer, _stickDetectValue:value" << _stickDetectValue << value; + + _stickDetectSettleStarted = true; + _stickDetectSettleElapsed.start(); + } + } + + return false; +} + +void JoystickConfigController::_inputStickDetect(Joystick::AxisFunction_t function, int axis, int value) +{ + qCDebug(JoystickConfigControllerLog) << "_inputStickDetect function:axis:value" << function << axis << value; + + // If this axis is already used in a mapping we can't use it again + if (_rgAxisInfo[axis].function != Joystick::maxFunction) { + return; + } + + if (_stickDetectAxis == _axisMax) { + // We have not detected enough movement on a axis yet + + if (abs(_axisValueSave[axis] - value) > _calMoveDelta) { + // Stick has moved far enough to consider it as being selected for the function + + qCDebug(JoystickConfigControllerLog) << "_inputStickDetect starting settle wait, function:axis:value" << function << axis << value; + + // Setup up to detect stick being pegged to min or max value + _stickDetectAxis = axis; + _stickDetectInitialValue = value; + _stickDetectValue = value; + } + } else if (axis == _stickDetectAxis) { + if (_stickSettleComplete(value)) { + AxisInfo* info = &_rgAxisInfo[axis]; + + qCDebug(JoystickConfigControllerLog) << "_inputStickDetect settle complete, function:axis:value" << function << axis << value; + + // Stick detection is complete. Stick should be at max position. + // Map the axis to the function + _rgFunctionAxisMapping[function] = axis; + info->function = function; + + // Axis should be at max value, if it is below initial set point the the axis is reversed. + info->reversed = value < _axisValueSave[axis]; + qCDebug(JoystickConfigControllerLog) << "_inputStickDetect reversed:value:_axisValueSave" << info->reversed << value << _axisValueSave[axis]; + + if (info->reversed) { + _rgAxisInfo[axis].axisMin = value; + } else { + _rgAxisInfo[axis].axisMax = value; + } + + _signalAllAttiudeValueChanges(); + + _advanceState(); + } + } +} + +void JoystickConfigController::_inputStickMin(Joystick::AxisFunction_t function, int axis, int value) +{ + // We only care about the axis mapped to the function we are working on + if (_rgFunctionAxisMapping[function] != axis) { + return; + } + + if (_stickDetectAxis == _axisMax) { + // Setup up to detect stick being pegged to extreme position + if (_rgAxisInfo[axis].reversed) { + if (value > _calCenterPoint + _calMoveDelta) { + _stickDetectAxis = axis; + _stickDetectInitialValue = value; + _stickDetectValue = value; + } + } else { + if (value < _calCenterPoint - _calMoveDelta) { + _stickDetectAxis = axis; + _stickDetectInitialValue = value; + _stickDetectValue = value; + } + } + } else { + // We are waiting for the selected axis to settle out + + if (_stickSettleComplete(value)) { + AxisInfo* info = &_rgAxisInfo[axis]; + + // Stick detection is complete. Stick should be at min position. + if (info->reversed) { + _rgAxisInfo[axis].axisMax = value; + } else { + _rgAxisInfo[axis].axisMin = value; + } + + // Check if this is throttle and set trim accordingly + if (function == Joystick::throttleFunction) { + _rgAxisInfo[axis].axisTrim = value; + } + // XXX to support configs which can reverse they need to check a reverse + // flag here and not do this. + + _advanceState(); + } + } +} + +void JoystickConfigController::_inputCenterWait(Joystick::AxisFunction_t function, int axis, int value) +{ + // We only care about the axis mapped to the function we are working on + if (_rgFunctionAxisMapping[function] != axis) { + return; + } + + if (_stickDetectAxis == _axisMax) { + // Sticks have not yet moved close enough to center + + if (abs(_calCenterPoint - value) < _calRoughCenterDelta) { + // Stick has moved close enough to center that we can start waiting for it to settle + _stickDetectAxis = axis; + _stickDetectInitialValue = value; + _stickDetectValue = value; + } + } else { + if (_stickSettleComplete(value)) { + _advanceState(); + } + } +} + +/// @brief Resets internal calibration values to their initial state in preparation for a new calibration sequence. +void JoystickConfigController::_resetInternalCalibrationValues(void) +{ + // Set all raw axiss to not reversed and center point values + for (size_t i=0; i<_axisMax; i++) { + struct AxisInfo* info = &_rgAxisInfo[i]; + info->function = Joystick::maxFunction; + info->reversed = false; + info->axisMin = JoystickConfigController::_calCenterPoint; + info->axisMax = JoystickConfigController::_calCenterPoint; + info->axisTrim = JoystickConfigController::_calCenterPoint; + } + + // Initialize attitude function mapping to function axis not set + for (size_t i=0; iactiveJoystick(); + + // Initialize all function mappings to not set + + for (size_t i=0; i<_axisMax; i++) { + struct AxisInfo* info = &_rgAxisInfo[i]; + info->function = Joystick::maxFunction; + } + + for (size_t i=0; igetCalibration(axis); + info->axisTrim = calibration.center; + info->axisMin = calibration.min; + info->axisMax = calibration.max; + info->reversed = calibration.reversed; + + qCDebug(JoystickConfigControllerLog) << "Read settings axis:min:max:trim:reversed" << axis << info->axisMin << info->axisMax << info->axisTrim << info->reversed; + } + + for (int function=0; functiongetFunctionAxis((Joystick::AxisFunction_t)function); + + _rgFunctionAxisMapping[function] = paramAxis; + _rgAxisInfo[paramAxis].function = (Joystick::AxisFunction_t)function; + } + + _signalAllAttiudeValueChanges(); +} + +/// @brief Validates the current settings against the calibration rules resetting values as necessary. +void JoystickConfigController::_validateCalibration(void) +{ + for (int chan = 0; chan<_axisMax; chan++) { + struct AxisInfo* info = &_rgAxisInfo[chan]; + + if (chan < _axisCount) { + // Validate Min/Max values. Although the axis appears as available we still may + // not have good min/max/trim values for it. Set to defaults if needed. + if (info->axisMin > _calValidMinValue || info->axisMax < _calValidMaxValue) { + qCDebug(JoystickConfigControllerLog) << "_validateCalibration resetting axis" << chan; + info->axisMin = _calDefaultMinValue; + info->axisMax = _calDefaultMaxValue; + info->axisTrim = info->axisMin + ((info->axisMax - info->axisMin) / 2); + } else { + switch (_rgAxisInfo[chan].function) { + case Joystick::throttleFunction: + case Joystick::yawFunction: + case Joystick::rollFunction: + case Joystick::pitchFunction: + // Make sure trim is within min/max + if (info->axisTrim < info->axisMin) { + info->axisTrim = info->axisMin; + } else if (info->axisTrim > info->axisMax) { + info->axisTrim = info->axisMax; + } + break; + default: + // Non-attitude control axiss have calculated trim + info->axisTrim = info->axisMin + ((info->axisMax - info->axisMin) / 2); + break; + } + + } + } else { + // Unavailable axiss are set to defaults + qCDebug(JoystickConfigControllerLog) << "_validateCalibration resetting unavailable axis" << chan; + info->axisMin = _calDefaultMinValue; + info->axisMax = _calDefaultMaxValue; + info->axisTrim = info->axisMin + ((info->axisMax - info->axisMin) / 2); + info->reversed = false; + } + } +} + + +/// @brief Saves the rc calibration values to the board parameters. +void JoystickConfigController::_writeCalibration(void) +{ + Joystick* joystick = JoystickManager::instance()->activeJoystick(); + + _validateCalibration(); + + for (int axis=0; axis<_axisMax; axis++) { + Joystick::Calibration_t calibration; + + struct AxisInfo* info = &_rgAxisInfo[axis]; + + calibration.center = info->axisTrim; + calibration.min = info->axisMin; + calibration.max = info->axisMax; + calibration.reversed = info->reversed; + + joystick->setCalibration(axis, calibration); + } + + // Write function mapping parameters + for (int function=0; functionsetFunctionAxis((Joystick::AxisFunction_t)function, _rgFunctionAxisMapping[function]); + } + + _stopCalibration(); + _setInternalCalibrationValuesFromSettings(); +} + +/// @brief Starts the calibration process +void JoystickConfigController::_startCalibration(void) +{ + Q_ASSERT(_axisCount >= _axisMinimum); + + _resetInternalCalibrationValues(); + + _nextButton->setProperty("text", "Next"); + _cancelButton->setEnabled(true); + + _currentStep = 0; + _setupCurrentState(); + + JoystickManager::instance()->activeJoystick()->setCalibrating(true); +} + +/// @brief Cancels the calibration process, setting things back to initial state. +void JoystickConfigController::_stopCalibration(void) +{ + _currentStep = -1; + + _setInternalCalibrationValuesFromSettings(); + + _statusText->setProperty("text", ""); + + _nextButton->setProperty("text", "Calibrate"); + _nextButton->setEnabled(true); + _cancelButton->setEnabled(false); + _skipButton->setEnabled(false); + + _setHelpImage(_imageCenter); + + JoystickManager::instance()->activeJoystick()->setCalibrating(false); +} + +/// @brief Saves the current axis values, so that we can detect when the use moves an input. +void JoystickConfigController::_calSaveCurrentValues(void) +{ + qCDebug(JoystickConfigControllerLog) << "_calSaveCurrentValues"; + for (unsigned i = 0; i < _axisMax; i++) { + _axisValueSave[i] = _axisRawValue[i]; + } +} + +/// @brief Set up the Save state of calibration. +void JoystickConfigController::_calSave(void) +{ + _calState = calStateSave; + + _statusText->setProperty("text", + "The current calibration settings are now displayed for each axis on screen.\n\n" + "Click the Next button to upload calibration to board. Click Cancel if you don't want to save these values."); + + _nextButton->setEnabled(true); + _skipButton->setEnabled(false); + _cancelButton->setEnabled(true); + + // This updates the internal values according to the validation rules. Then _updateView will tick and update ui + // such that the settings that will be written our are displayed. + _validateCalibration(); +} + +void JoystickConfigController::_loadSettings(void) +{ + QSettings settings; + + settings.beginGroup(_settingsGroup); +} + +void JoystickConfigController::_storeSettings(void) +{ + QSettings settings; + + settings.beginGroup(_settingsGroup); +} + +void JoystickConfigController::_setHelpImage(const char* imageFile) +{ + QString file = _imageFilePrefix; + + file += _imageFileMode2Dir; + file += imageFile; + + qCDebug(JoystickConfigControllerLog) << "_setHelpImage" << file; + + _imageHelp = file; + emit imageHelpChanged(file); +} + +int JoystickConfigController::axisCount(void) +{ + return _axisCount; +} + +int JoystickConfigController::rollAxisValue(void) +{ + if (_rgFunctionAxisMapping[Joystick::rollFunction] != _axisMax) { + return _axisRawValue[Joystick::rollFunction]; + } else { + return 1500; + } +} + +int JoystickConfigController::pitchAxisValue(void) +{ + if (_rgFunctionAxisMapping[Joystick::pitchFunction] != _axisMax) { + return _axisRawValue[Joystick::pitchFunction]; + } else { + return 1500; + } +} + +int JoystickConfigController::yawAxisValue(void) +{ + if (_rgFunctionAxisMapping[Joystick::yawFunction] != _axisMax) { + return _axisRawValue[Joystick::yawFunction]; + } else { + return 1500; + } +} + +int JoystickConfigController::throttleAxisValue(void) +{ + if (_rgFunctionAxisMapping[Joystick::throttleFunction] != _axisMax) { + return _axisRawValue[Joystick::throttleFunction]; + } else { + return 1500; + } +} + +bool JoystickConfigController::rollAxisMapped(void) +{ + return _rgFunctionAxisMapping[Joystick::rollFunction] != _axisMax; +} + +bool JoystickConfigController::pitchAxisMapped(void) +{ + return _rgFunctionAxisMapping[Joystick::pitchFunction] != _axisMax; +} + +bool JoystickConfigController::yawAxisMapped(void) +{ + return _rgFunctionAxisMapping[Joystick::yawFunction] != _axisMax; +} + +bool JoystickConfigController::throttleAxisMapped(void) +{ + return _rgFunctionAxisMapping[Joystick::throttleFunction] != _axisMax; +} + +bool JoystickConfigController::rollAxisReversed(void) +{ + if (_rgFunctionAxisMapping[Joystick::rollFunction] != _axisMax) { + return _rgAxisInfo[_rgFunctionAxisMapping[Joystick::rollFunction]].reversed; + } else { + return false; + } +} + +bool JoystickConfigController::pitchAxisReversed(void) +{ + if (_rgFunctionAxisMapping[Joystick::pitchFunction] != _axisMax) { + return _rgAxisInfo[_rgFunctionAxisMapping[Joystick::pitchFunction]].reversed; + } else { + return false; + } +} + +bool JoystickConfigController::yawAxisReversed(void) +{ + if (_rgFunctionAxisMapping[Joystick::yawFunction] != _axisMax) { + return _rgAxisInfo[_rgFunctionAxisMapping[Joystick::yawFunction]].reversed; + } else { + return false; + } +} + +bool JoystickConfigController::throttleAxisReversed(void) +{ + if (_rgFunctionAxisMapping[Joystick::throttleFunction] != _axisMax) { + return _rgAxisInfo[_rgFunctionAxisMapping[Joystick::throttleFunction]].reversed; + } else { + return false; + } +} + +void JoystickConfigController::_signalAllAttiudeValueChanges(void) +{ + emit rollAxisMappedChanged(rollAxisMapped()); + emit pitchAxisMappedChanged(pitchAxisMapped()); + emit yawAxisMappedChanged(yawAxisMapped()); + emit throttleAxisMappedChanged(throttleAxisMapped()); + + emit rollAxisReversedChanged(rollAxisReversed()); + emit pitchAxisReversedChanged(pitchAxisReversed()); + emit yawAxisReversedChanged(yawAxisReversed()); + emit throttleAxisReversedChanged(throttleAxisReversed()); +} diff --git a/src/VehicleSetup/JoystickConfigController.h b/src/VehicleSetup/JoystickConfigController.h new file mode 100644 index 000000000..caeacb26e --- /dev/null +++ b/src/VehicleSetup/JoystickConfigController.h @@ -0,0 +1,265 @@ +/*===================================================================== + + QGroundControl Open Source Ground Control Station + + (c) 2009, 2015 QGROUNDCONTROL PROJECT + + This file is part of the QGROUNDCONTROL project + + QGROUNDCONTROL 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, either version 3 of the License, or + (at your option) any later version. + + QGROUNDCONTROL 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 for more details. + + You should have received a copy of the GNU General Public License + along with QGROUNDCONTROL. If not, see . + + ======================================================================*/ + + +/// @file +/// @brief Radio Config Qml Controller +/// @author Don Gagne + +#include "FactPanelController.h" +#include "QGCLoggingCategory.h" +#include "Joystick.h" + +Q_DECLARE_LOGGING_CATEGORY(JoystickConfigControllerLog) + +class RadioConfigest; + +namespace Ui { + class JoystickConfigController; +} + + +class JoystickConfigController : public FactPanelController +{ + Q_OBJECT + + friend class RadioConfigTest; ///< This allows our unit test to access internal information needed. + +public: + JoystickConfigController(void); + ~JoystickConfigController(); + + Q_PROPERTY(int minAxisCount MEMBER _axisMinimum CONSTANT) + Q_PROPERTY(int axisCount READ axisCount NOTIFY axisCountChanged) + + Q_PROPERTY(QQuickItem* statusText MEMBER _statusText) + Q_PROPERTY(QQuickItem* cancelButton MEMBER _cancelButton) + Q_PROPERTY(QQuickItem* nextButton MEMBER _nextButton) + Q_PROPERTY(QQuickItem* skipButton MEMBER _skipButton) + + Q_PROPERTY(bool rollAxisMapped READ rollAxisMapped NOTIFY rollAxisMappedChanged) + Q_PROPERTY(bool pitchAxisMapped READ pitchAxisMapped NOTIFY pitchAxisMappedChanged) + Q_PROPERTY(bool yawAxisMapped READ yawAxisMapped NOTIFY yawAxisMappedChanged) + Q_PROPERTY(bool throttleAxisMapped READ throttleAxisMapped NOTIFY throttleAxisMappedChanged) + + Q_PROPERTY(int rollAxisValue READ rollAxisValue NOTIFY rollAxisValueChanged) + Q_PROPERTY(int pitchAxisValue READ pitchAxisValue NOTIFY pitchAxisValueChanged) + Q_PROPERTY(int yawAxisValue READ yawAxisValue NOTIFY yawAxisValueChanged) + Q_PROPERTY(int throttleAxisValue READ throttleAxisValue NOTIFY throttleAxisValueChanged) + + Q_PROPERTY(int rollAxisReversed READ rollAxisReversed NOTIFY rollAxisReversedChanged) + Q_PROPERTY(int pitchAxisReversed READ pitchAxisReversed NOTIFY pitchAxisReversedChanged) + Q_PROPERTY(int yawAxisReversed READ yawAxisReversed NOTIFY yawAxisReversedChanged) + Q_PROPERTY(int throttleAxisReversed READ throttleAxisReversed NOTIFY throttleAxisReversedChanged) + + Q_PROPERTY(QString imageHelp MEMBER _imageHelp NOTIFY imageHelpChanged) + + Q_INVOKABLE void cancelButtonClicked(void); + Q_INVOKABLE void skipButtonClicked(void); + Q_INVOKABLE void nextButtonClicked(void); + Q_INVOKABLE void start(void); + + int rollAxisValue(void); + int pitchAxisValue(void); + int yawAxisValue(void); + int throttleAxisValue(void); + + bool rollAxisMapped(void); + bool pitchAxisMapped(void); + bool yawAxisMapped(void); + bool throttleAxisMapped(void); + + bool rollAxisReversed(void); + bool pitchAxisReversed(void); + bool yawAxisReversed(void); + bool throttleAxisReversed(void); + + int axisCount(void); + +signals: + void axisCountChanged(int axisCount); + void axisValueChanged(int axis, int value); + + void rollAxisMappedChanged(bool mapped); + void pitchAxisMappedChanged(bool mapped); + void yawAxisMappedChanged(bool mapped); + void throttleAxisMappedChanged(bool mapped); + + void rollAxisValueChanged(int value); + void pitchAxisValueChanged(int value); + void yawAxisValueChanged(int value); + void throttleAxisValueChanged(int value); + + void rollAxisReversedChanged(bool reversed); + void pitchAxisReversedChanged(bool reversed); + void yawAxisReversedChanged(bool reversed); + void throttleAxisReversedChanged(bool reversed); + + void imageHelpChanged(QString source); + + // @brief Signalled when in unit test mode and a message box should be displayed by the next button + void nextButtonMessageBoxDisplayed(void); + +private slots: + void _axisValueChanged(int axis, int value); + +private: + /// @brief The states of the calibration state machine. + enum calStates { + calStateAxisWait, + calStateBegin, + calStateIdentify, + calStateMinMax, + calStateCenterThrottle, + calStateDetectInversion, + calStateTrims, + calStateSave + }; + + typedef void (JoystickConfigController::*inputFn)(Joystick::AxisFunction_t function, int axis, int value); + typedef void (JoystickConfigController::*buttonFn)(void); + struct stateMachineEntry { + Joystick::AxisFunction_t function; + const char* instructions; + const char* image; + inputFn rcInputFn; + buttonFn nextFn; + buttonFn skipFn; + }; + + /// @brief A set of information associated with a radio axis. + struct AxisInfo { + Joystick::AxisFunction_t function; ///< Function mapped to this axis, Joystick::maxFunction for none + bool reversed; ///< true: axis is reverse, false: not reversed + int axisMin; ///< Minimum axis value + int axisMax; ///< Maximum axis value + int axisTrim; ///< Trim position + }; + + int _currentStep; ///< Current step of state machine + + const struct stateMachineEntry* _getStateMachineEntry(int step); + + void _advanceState(void); + void _setupCurrentState(void); + + void _inputCenterWaitBegin (Joystick::AxisFunction_t function, int axis, int value); + void _inputStickDetect (Joystick::AxisFunction_t function, int axis, int value); + void _inputStickMin (Joystick::AxisFunction_t function, int axis, int value); + void _inputCenterWait (Joystick::AxisFunction_t function, int axis, int value); + + void _switchDetect(Joystick::AxisFunction_t function, int axis, int value, bool moveToNextStep); + + void _saveFlapsDown(void); + void _skipFlaps(void); + void _saveAllTrims(void); + + bool _stickSettleComplete(int value); + + void _validateCalibration(void); + void _writeCalibration(void); + void _resetInternalCalibrationValues(void); + void _setInternalCalibrationValuesFromSettings(void); + + void _startCalibration(void); + void _stopCalibration(void); + void _calSave(void); + + void _calSaveCurrentValues(void); + + void _setHelpImage(const char* imageFile); + + void _loadSettings(void); + void _storeSettings(void); + + void _signalAllAttiudeValueChanges(void); + + // Member variables + + static const char* _imageFileMode2Dir; + static const char* _imageFilePrefix; + static const char* _imageCenter; + static const char* _imageThrottleUp; + static const char* _imageThrottleDown; + static const char* _imageYawLeft; + static const char* _imageYawRight; + static const char* _imageRollLeft; + static const char* _imageRollRight; + static const char* _imagePitchUp; + static const char* _imagePitchDown; + + static const char* _settingsGroup; + + static const int _updateInterval; ///< Interval for ui update timer + + int _rgFunctionAxisMapping[Joystick::maxFunction]; ///< Maps from joystick function to axis index. _axisMax indicates axis not set for this function. + + static const int _attitudeControls = 5; + + int _axisCount; ///< Number of actual joystick axes available + static const int _axisMax = 4; ///< Maximum number of supported joystick axes + static const int _axisMinimum = 4; ///< Minimum numner of joystick axes required to run PX4 + + struct AxisInfo _rgAxisInfo[_axisMax]; ///< Information associated with each axis + + enum calStates _calState; ///< Current calibration state + int _calStateCurrentAxis; ///< Current axis being worked on in calStateIdentify and calStateDetectInversion + bool _calStateAxisComplete; ///< Work associated with current axis is complete + int _calStateIdentifyOldMapping; ///< Previous mapping for axis being currently identified + int _calStateReverseOldMapping; ///< Previous mapping for axis being currently used to detect inversion + + static const int _calCenterPoint; + static const int _calValidMinValue; + static const int _calValidMaxValue; + static const int _calDefaultMinValue; + static const int _calDefaultMaxValue; + static const int _calRoughCenterDelta; + static const int _calMoveDelta; + static const int _calSettleDelta; + static const int _calMinDelta; + + int _axisValueSave[_axisMax]; ///< Saved values prior to detecting axis movement + + int _axisRawValue[_axisMax]; ///< Current set of raw axis values + + int _stickDetectAxis; + int _stickDetectInitialValue; + int _stickDetectValue; + bool _stickDetectSettleStarted; + QTime _stickDetectSettleElapsed; + static const int _stickDetectSettleMSecs; + + QQuickItem* _statusText; + QQuickItem* _cancelButton; + QQuickItem* _nextButton; + QQuickItem* _skipButton; + + QString _imageHelp; +}; + +#endif // JoystickConfigController_H diff --git a/src/VehicleSetup/SetupView.qml b/src/VehicleSetup/SetupView.qml index 59ffb3172..05c6ad64f 100644 --- a/src/VehicleSetup/SetupView.qml +++ b/src/VehicleSetup/SetupView.qml @@ -72,6 +72,16 @@ Rectangle { } } + function showJoystickPanel() + { + if (multiVehicleManager.activeVehicleAvailable && multiVehicleManager.activeVehicle.autopilot.armed) { + messagePanelText = armedVehicleText + panelLoader.sourceComponent = messagePanelComponent + } else { + panelLoader.source = "JoystickConfig.qml"; + } + } + function showParametersPanel() { panelLoader.source = "SetupParameterEditor.qml"; @@ -170,6 +180,18 @@ Rectangle { onClicked: showFirmwarePanel() } + SubMenuButton { + id: joystickButton + width: buttonWidth + setupIndicator: true + setupComplete: joystickManager.activeJoystick.calibrated + exclusiveGroup: setupButtonGroup + visible: multiVehicleManager.parameterReadyVehicleAvailable && joystickManager.joysticks.length != 0 + text: "JOYSTICK" + + onClicked: showJoystickPanel() + } + Repeater { model: multiVehicleManager.parameterReadyVehicleAvailable ? multiVehicleManager.activeVehicle.autopilot.vehicleComponents : 0 diff --git a/src/VehicleSetup/SetupViewButtonsConnected.qml b/src/VehicleSetup/SetupViewButtonsConnected.qml deleted file mode 100644 index df5514e4c..000000000 --- a/src/VehicleSetup/SetupViewButtonsConnected.qml +++ /dev/null @@ -1,62 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Styles 1.2 -import QtGraphicalEffects 1.0 - -import QGroundControl.FactSystem 1.0 -import QGroundControl.Palette 1.0 -import QGroundControl.Controls 1.0 - -Rectangle { - id: topLevel - - QGCPalette { id: palette; colorGroupEnabled: true } - color: palette.window - - ExclusiveGroup { id: setupButtonGroup } - - Column { - anchors.fill: parent - - SubMenuButton { - id: summaryButton; objectName: "summaryButton" - width: parent.width - text: "SUMMARY" - imageResource: "VehicleSummaryIcon.png" - setupIndicator: false - exclusiveGroup: setupButtonGroup - onClicked: controller.summaryButtonClicked() - } - - SubMenuButton { - id: firmwareButton; objectName: "firmwareButton" - width: parent.width - text: "FIRMWARE" - imageResource: "FirmwareUpgradeIcon.png" - setupIndicator: false - exclusiveGroup: setupButtonGroup - onClicked: controller.firmwareButtonClicked() - } - - Repeater { - model: autopilot ? autopilot.vehicleComponents : 0 - - SubMenuButton { - width: parent.width - text: modelData.name.toUpperCase() - imageResource: modelData.iconResource - setupComplete: modelData.setupComplete - exclusiveGroup: setupButtonGroup - onClicked: controller.setupButtonClicked(modelData) - } - } - - SubMenuButton { - width: parent.width - text: "PARAMETERS" - setupIndicator: false - exclusiveGroup: setupButtonGroup - onClicked: controller.parametersButtonClicked() - } - } -} diff --git a/src/VehicleSetup/SetupViewButtonsDisconnected.qml b/src/VehicleSetup/SetupViewButtonsDisconnected.qml deleted file mode 100644 index e5634ccbf..000000000 --- a/src/VehicleSetup/SetupViewButtonsDisconnected.qml +++ /dev/null @@ -1,40 +0,0 @@ -import QtQuick 2.3 -import QtQuick.Controls 1.2 -import QtQuick.Controls.Styles 1.2 -import QtGraphicalEffects 1.0 - -import QGroundControl.FactSystem 1.0 -import QGroundControl.Palette 1.0 -import QGroundControl.Controls 1.0 - -Rectangle { - id: topLevel - - QGCPalette { id: palette; colorGroupEnabled: true } - color: palette.window - - ExclusiveGroup { id: setupButtonGroup } - - Column { - anchors.fill: parent - - SubMenuButton { - id: firmwareButton; objectName: "firmwareButton" - width: parent.width - text: "FIRMWARE" - imageResource: "FirmwareUpgradeIcon.png" - setupIndicator: false - exclusiveGroup: setupButtonGroup - onClicked: controller.firmwareButtonClicked() - } - - Item { width: parent.width; height: 10 } // spacer - - QGCLabel { - width: parent.width - text: "Full setup options are only available when connected to vehicle and full parameter list has completed downloading." - wrapMode: Text.WordWrap - horizontalAlignment: Text.AlignHCenter - } - } -} diff --git a/src/ViewWidgets/CustomCommandWidgetController.cc b/src/ViewWidgets/CustomCommandWidgetController.cc index 538709d37..80c482cff 100644 --- a/src/ViewWidgets/CustomCommandWidgetController.cc +++ b/src/ViewWidgets/CustomCommandWidgetController.cc @@ -25,6 +25,7 @@ #include "MultiVehicleManager.h" #include "QGCMAVLink.h" #include "QGCFileDialog.h" +#include "UAS.h" #include #include diff --git a/src/ViewWidgets/ViewWidgetController.cc b/src/ViewWidgets/ViewWidgetController.cc index 71295f244..5a39032d6 100644 --- a/src/ViewWidgets/ViewWidgetController.cc +++ b/src/ViewWidgets/ViewWidgetController.cc @@ -23,6 +23,7 @@ #include "ViewWidgetController.h" #include "MultiVehicleManager.h" +#include "UAS.h" ViewWidgetController::ViewWidgetController(void) : _autopilot(NULL), diff --git a/src/input/JoystickInput.cc b/src/input/JoystickInput.cc deleted file mode 100644 index ad950d429..000000000 --- a/src/input/JoystickInput.cc +++ /dev/null @@ -1,764 +0,0 @@ -/*===================================================================== -======================================================================*/ - -/** - * @file - * @brief Joystick interface - * - * @author Lorenz Meier - * @author Andreas Romer - * @author Julian Oes -#include -#include "UAS.h" -#include "MultiVehicleManager.h" -#include "QGC.h" -#include -#include -#include -#include - -using namespace std; - -/** - * The coordinate frame of the joystick axis is the aeronautical frame like shown on this image: - * @image html http://pixhawk.ethz.ch/wiki/_media/standards/body-frame.png Aeronautical frame - */ -JoystickInput::JoystickInput() : - sdlJoystickMin(-32768.0f), - sdlJoystickMax(32767.0f), - isEnabled(false), - isCalibrating(false), - done(false), - uas(NULL), - autopilotType(0), - systemType(0), - uasCanReverse(false), - rollAxis(-1), - pitchAxis(-1), - yawAxis(-1), - throttleAxis(-1), - joystickName(""), - joystickID(-1), - joystickNumButtons(0) -{ - - // Make sure we initialize with the correct UAS. - _activeVehicleChanged(MultiVehicleManager::instance()->activeVehicle()); - - // Start this thread. This allows the Joystick Settings window to work correctly even w/o any UASes connected. - start(); -} - -JoystickInput::~JoystickInput() -{ - storeGeneralSettings(); - storeJoystickSettings(); - done = true; -} - -void JoystickInput::loadGeneralSettings() -{ - // Load defaults from settings - QSettings settings; - - // Deal with settings specific to the JoystickInput - settings.beginGroup("JOYSTICK_INPUT"); - isEnabled = settings.value("ENABLED", false).toBool(); - joystickName = settings.value("JOYSTICK_NAME", "").toString(); - mode = (JOYSTICK_MODE)settings.value("JOYSTICK_MODE", JOYSTICK_MODE_ATTITUDE).toInt(); - settings.endGroup(); -} - -/** - * @brief Restores settings for the current joystick from saved settings file. - * Assumes that both joystickName & joystickNumAxes are correct. - */ -void JoystickInput::loadJoystickSettings() -{ - // Load defaults from settings - QSettings settings; - - // Now for the current joystick - settings.beginGroup(joystickName); - rollAxis = (settings.value("ROLL_AXIS_MAPPING", -1).toInt()); - pitchAxis = (settings.value("PITCH_AXIS_MAPPING", -1).toInt()); - yawAxis = (settings.value("YAW_AXIS_MAPPING", -1).toInt()); - throttleAxis = (settings.value("THROTTLE_AXIS_MAPPING", -1).toInt()); - - // Clear out and then restore the (AUTOPILOT, SYSTEM) mapping for joystick settings - joystickSettings.clear(); - int autopilots = settings.beginReadArray("AUTOPILOTS"); - for (int i = 0; i < autopilots; i++) - { - settings.setArrayIndex(i); - int autopilotType = settings.value("AUTOPILOT_TYPE", 0).toInt(); - int systems = settings.beginReadArray("SYSTEMS"); - for (int j = 0; j < systems; j++) - { - settings.setArrayIndex(j); - int systemType = settings.value("SYSTEM_TYPE", 0).toInt(); - - // Now that both the autopilot and system type are available, update some references. - QMap* joystickAxesInverted = &joystickSettings[autopilotType][systemType].axesInverted; - QMap* joystickAxesLimited = &joystickSettings[autopilotType][systemType].axesLimited; - QMap* joystickAxesMinRange = &joystickSettings[autopilotType][systemType].axesMaxRange; - QMap* joystickAxesMaxRange = &joystickSettings[autopilotType][systemType].axesMinRange; - QMap* joystickButtonActions = &joystickSettings[autopilotType][systemType].buttonActions; - - // Read back the joystickAxesInverted QList one element at a time. - int axesStored = settings.beginReadArray("AXES_INVERTED"); - for (int k = 0; k < axesStored; k++) - { - settings.setArrayIndex(k); - int index = settings.value("INDEX", 0).toInt(); - bool inverted = settings.value("INVERTED", false).toBool(); - joystickAxesInverted->insert(index, inverted); - } - settings.endArray(); - - // Read back the joystickAxesLimited QList one element at a time. - axesStored = settings.beginReadArray("AXES_LIMITED"); - for (int k = 0; k < axesStored; k++) - { - settings.setArrayIndex(k); - int index = settings.value("INDEX", 0).toInt(); - bool limited = settings.value("LIMITED", false).toBool(); - joystickAxesLimited->insert(index, limited); - } - settings.endArray(); - - // Read back the joystickAxesMinRange QList one element at a time. - axesStored = settings.beginReadArray("AXES_MIN_RANGE"); - for (int k = 0; k < axesStored; k++) - { - settings.setArrayIndex(k); - int index = settings.value("INDEX", 0).toInt(); - float min = settings.value("MIN_RANGE", false).toFloat(); - joystickAxesMinRange->insert(index, min); - } - settings.endArray(); - - // Read back the joystickAxesMaxRange QList one element at a time. - axesStored = settings.beginReadArray("AXES_MAX_RANGE"); - for (int k = 0; k < axesStored; k++) - { - settings.setArrayIndex(k); - int index = settings.value("INDEX", 0).toInt(); - float max = settings.value("MAX_RANGE", false).toFloat(); - joystickAxesMaxRange->insert(index, max); - } - settings.endArray(); - - // Read back the button->action mapping. - int buttonsStored = settings.beginReadArray("BUTTONS_ACTIONS"); - for (int k = 0; k < buttonsStored; k++) - { - settings.setArrayIndex(k); - int index = settings.value("INDEX", 0).toInt(); - int action = settings.value("ACTION", 0).toInt(); - joystickButtonActions->insert(index, action); - } - settings.endArray(); - } - settings.endArray(); - } - settings.endArray(); - - settings.endGroup(); - - emit joystickSettingsChanged(); -} - -void JoystickInput::storeGeneralSettings() const -{ - QSettings settings; - settings.beginGroup("JOYSTICK_INPUT"); - settings.setValue("ENABLED", isEnabled); - settings.setValue("JOYSTICK_NAME", joystickName); - settings.setValue("JOYSTICK_MODE", mode); - settings.endGroup(); -} - -void JoystickInput::storeJoystickSettings() const -{ - // Store settings - QSettings settings; - settings.beginGroup(joystickName); - settings.setValue("ROLL_AXIS_MAPPING", rollAxis); - settings.setValue("PITCH_AXIS_MAPPING", pitchAxis); - settings.setValue("YAW_AXIS_MAPPING", yawAxis); - settings.setValue("THROTTLE_AXIS_MAPPING", throttleAxis); - settings.beginWriteArray("AUTOPILOTS"); - QMapIterator > i(joystickSettings); - int autopilotIndex = 0; - while (i.hasNext()) - { - i.next(); - settings.setArrayIndex(autopilotIndex++); - - int autopilotType = i.key(); - settings.setValue("AUTOPILOT_TYPE", autopilotType); - - settings.beginWriteArray("SYSTEMS"); - QMapIterator j(i.value()); - int systemIndex = 0; - while (j.hasNext()) - { - j.next(); - settings.setArrayIndex(systemIndex++); - - int systemType = j.key(); - settings.setValue("SYSTEM_TYPE", systemType); - - // Now that both the autopilot and system type are available, update some references. - QMapIterator joystickAxesInverted(joystickSettings[autopilotType][systemType].axesInverted); - QMapIterator joystickAxesLimited(joystickSettings[autopilotType][systemType].axesLimited); - QMapIterator joystickAxesMinRange(joystickSettings[autopilotType][systemType].axesMaxRange); - QMapIterator joystickAxesMaxRange(joystickSettings[autopilotType][systemType].axesMinRange); - QMapIterator joystickButtonActions(joystickSettings[autopilotType][systemType].buttonActions); - - settings.beginWriteArray("AXES_INVERTED"); - int k = 0; - while (joystickAxesInverted.hasNext()) - { - joystickAxesInverted.next(); - int inverted = joystickAxesInverted.value(); - // Only save this axes' inversion status if it's not the default - if (inverted) - { - settings.setArrayIndex(k++); - int index = joystickAxesInverted.key(); - settings.setValue("INDEX", index); - settings.setValue("INVERTED", inverted); - } - } - settings.endArray(); - - settings.beginWriteArray("AXES_MIN_RANGE"); - k = 0; - while (joystickAxesMinRange.hasNext()) - { - joystickAxesMinRange.next(); - float min = joystickAxesMinRange.value(); - settings.setArrayIndex(k++); - int index = joystickAxesMinRange.key(); - settings.setValue("INDEX", index); - settings.setValue("MIN_RANGE", min); - } - settings.endArray(); - - settings.beginWriteArray("AXES_MAX_RANGE"); - k = 0; - while (joystickAxesMaxRange.hasNext()) - { - joystickAxesMaxRange.next(); - float max = joystickAxesMaxRange.value(); - settings.setArrayIndex(k++); - int index = joystickAxesMaxRange.key(); - settings.setValue("INDEX", index); - settings.setValue("MAX_RANGE", max); - } - settings.endArray(); - - settings.beginWriteArray("AXES_LIMITED"); - k = 0; - while (joystickAxesLimited.hasNext()) - { - joystickAxesLimited.next(); - int limited = joystickAxesLimited.value(); - if (limited) - { - settings.setArrayIndex(k++); - int index = joystickAxesLimited.key(); - settings.setValue("INDEX", index); - settings.setValue("LIMITED", limited); - } - } - settings.endArray(); - - settings.beginWriteArray("BUTTONS_ACTIONS"); - k = 0; - while (joystickButtonActions.hasNext()) - { - joystickButtonActions.next(); - int action = joystickButtonActions.value(); - if (action != -1) - { - settings.setArrayIndex(k++); - int index = joystickButtonActions.key(); - settings.setValue("INDEX", index); - settings.setValue("ACTION", action); - } - } - settings.endArray(); - } - settings.endArray(); // SYSTEMS - } - settings.endArray(); // AUTOPILOTS - settings.endGroup(); -} - -void JoystickInput::_activeVehicleChanged(Vehicle* vehicle) -{ - if (this->uas) - { - disconnect(this, SIGNAL(joystickChanged(float,float,float,float,qint8,qint8,quint16,quint8)), uas, SLOT(setExternalControlSetpoint(float,float,float,float,qint8,qint8,quint16,quint8))); - disconnect(this, SIGNAL(actionTriggered(int)), uas, SLOT(triggerAction(int))); - uasCanReverse = false; - uas = NULL; - } - - // Save any settings for the last UAS - if (joystickID > -1) - { - storeJoystickSettings(); - } - - if (vehicle) - { - uas = vehicle->uas(); - connect(this, SIGNAL(joystickChanged(float,float,float,float,qint8,qint8,quint16,quint8)), uas, SLOT(setExternalControlSetpoint(float,float,float,float,qint8,qint8,quint16,quint8))); - qDebug() << "connected joystick"; - connect(this, SIGNAL(actionTriggered(int)), uas, SLOT(triggerAction(int))); - uasCanReverse = uas->systemCanReverse(); - - // Update the joystick settings for a new UAS. - autopilotType = uas->getAutopilotType(); - systemType = uas->getSystemType(); - } - - // Make sure any UI elements know we've updated the UAS. The signal is re-emitted here so that UI elements know to - // update their UAS-specific UI. - emit activeVehicleChanged(vehicle); - - // Load any joystick-specific settings now that the UAS has changed. - if (joystickID > -1) - { - loadJoystickSettings(); - } -} - -void JoystickInput::setEnabled(bool enabled) -{ - this->isEnabled = enabled; - storeJoystickSettings(); -} - -void JoystickInput::init() -{ - // Initialize SDL Joystick support and detect number of joysticks. - if (SDL_InitSubSystem(SDL_INIT_JOYSTICK | SDL_INIT_NOPARACHUTE) < 0) { - printf("Couldn't initialize SimpleDirectMediaLayer: %s\n", SDL_GetError()); - } - - // Enumerate joysticks and select one - numJoysticks = SDL_NumJoysticks(); - - // If no joysticks are connected, there's nothing we can do, so just keep - // going back to sleep every second unless the program quits. - while (!numJoysticks && !done) - { - QGC::SLEEP::sleep(1); - } - if (done) - { - return; - } - - // Now that we've detected a joystick, load in the joystick-agnostic settings. - loadGeneralSettings(); - - // Enumerate all found devices - qDebug() << QString("%1 Input devices found:").arg(numJoysticks); - int activeJoystick = 0; - for(int i=0; i < numJoysticks; i++ ) - { - QString name = SDL_JoystickName(i); - qDebug() << QString("\t%1").arg(name); - - // If we've matched this joystick to what was last opened, note it. - // Note: The way this is implemented the LAST joystick of a given name will be opened. - if (name == joystickName) - { - activeJoystick = i; - } - - SDL_Joystick* x = SDL_JoystickOpen(i); - qDebug() << QString("\tNumber of Axes: %1").arg(QString::number(SDL_JoystickNumAxes(x))); - qDebug() << QString("\tNumber of Buttons: %1").arg(QString::number(SDL_JoystickNumButtons(x))); - SDL_JoystickClose(x); - } - - // Set the active joystick based on name, if a joystick was found in the saved settings, otherwise - // default to the first one. - setActiveJoystick(activeJoystick); - - // Now make sure we know what the current UAS is and track changes to it. - _activeVehicleChanged(MultiVehicleManager::instance()->activeVehicle()); - connect(MultiVehicleManager::instance(), &MultiVehicleManager::activeVehicleChanged, this, &JoystickInput::_activeVehicleChanged); -} - -void JoystickInput::shutdown() -{ - done = true; -} - -/** - * @brief Execute the Joystick process - * Note that the SDL procedure is polled. This is because connecting and disconnecting while the event checker is running - * fails as of SDL 1.2. It is therefore much easier to just poll for the joystick we want to sample. - */ -void JoystickInput::run() -{ - init(); - - forever - { - if (done) - { - done = false; - exit(); - return; - } - - // Poll the joystick for new values. - SDL_JoystickUpdate(); - - // Emit all necessary signals for all axes. - for (int i = 0; i < joystickNumAxes; i++) - { - // First emit the uncalibrated values for each axis based on their ID. - // This is generally not used for controlling a vehicle, but a UI representation, so it being slightly off is fine. - // Here we map the joystick axis value into the initial range of [0:1]. - float axisValue = SDL_JoystickGetAxis(joystick, i); - - // during calibration save min and max values - if (isCalibrating) - { - if (joystickSettings[autopilotType][systemType].axesMinRange.value(i) > axisValue) - { - joystickSettings[autopilotType][systemType].axesMinRange[i] = axisValue; - } - - if (joystickSettings[autopilotType][systemType].axesMaxRange.value(i) < axisValue) - { - joystickSettings[autopilotType][systemType].axesMaxRange[i] = axisValue; - } - } - - if (joystickSettings[autopilotType][systemType].axesInverted[i]) - { - axisValue = (axisValue - joystickSettings[autopilotType][systemType].axesMinRange.value(i)) / - (joystickSettings[autopilotType][systemType].axesMaxRange.value(i) - - joystickSettings[autopilotType][systemType].axesMinRange.value(i)); - } - else - { - axisValue = (axisValue - joystickSettings[autopilotType][systemType].axesMaxRange.value(i)) / - (joystickSettings[autopilotType][systemType].axesMinRange.value(i) - - joystickSettings[autopilotType][systemType].axesMaxRange.value(i)); - } - axisValue = 1.0f - axisValue; - - // For non-throttle axes or if the UAS can reverse, go ahead and convert this into the range [-1:1]. - - //if (uasCanReverse || throttleAxis != i) - // don't take into account if UAS can reverse. This means to reverse position but not throttle - // therefore deactivated for now - if (throttleAxis != i) - { - axisValue = axisValue * 2.0f - 1.0f; - } - // Otherwise if this vehicle can only go forward, scale it to [0:1]. - else if (throttleAxis == i && joystickSettings[autopilotType][systemType].axesLimited.value(i)) - { - if (axisValue < 0.0f) - { - axisValue = 0.0f; - } - } - - // Bound rounding errors - if (axisValue > 1.0f) axisValue = 1.0f; - if (axisValue < -1.0f) axisValue = -1.0f; - if (joystickAxes[i] != axisValue) - { - joystickAxes[i] = axisValue; - emit axisValueChanged(i, axisValue); - } - } - - // Build up vectors describing the hat position - int hatPosition = SDL_JoystickGetHat(joystick, 0); - qint8 newYHat = 0; - if ((SDL_HAT_UP & hatPosition) > 0) newYHat = 1; - if ((SDL_HAT_DOWN & hatPosition) > 0) newYHat = -1; - qint8 newXHat = 0; - if ((SDL_HAT_LEFT & hatPosition) > 0) newXHat = -1; - if ((SDL_HAT_RIGHT & hatPosition) > 0) newXHat = 1; - if (newYHat != yHat || newXHat != xHat) - { - xHat = newXHat; - yHat = newYHat; - emit hatDirectionChanged(newXHat, newYHat); - } - - // Emit signals for each button individually - for (int i = 0; i < joystickNumButtons; i++) - { - // If the button was down, but now it's up, trigger a buttonPressed event - quint16 lastButtonState = joystickButtons & (1 << i); - if (SDL_JoystickGetButton(joystick, i) && !lastButtonState) - { - emit buttonPressed(i); - joystickButtons |= 1 << i; - } - else if (!SDL_JoystickGetButton(joystick, i) && lastButtonState) - { - emit buttonReleased(i); - if (isEnabled && joystickSettings[autopilotType][systemType].buttonActions.contains(i)) - { - emit actionTriggered(joystickSettings[autopilotType][systemType].buttonActions.value(i)); - } - joystickButtons &= ~(1 << i); - } - } - - // Now signal an update for all UI together. - if (isEnabled) - { - float roll = rollAxis > -1?joystickAxes[rollAxis]:numeric_limits::quiet_NaN(); - float pitch = pitchAxis > -1?joystickAxes[pitchAxis]:numeric_limits::quiet_NaN(); - float yaw = yawAxis > -1?joystickAxes[yawAxis]:numeric_limits::quiet_NaN(); - float throttle = throttleAxis > -1?joystickAxes[throttleAxis]:numeric_limits::quiet_NaN(); - emit joystickChanged(roll, pitch, yaw, throttle, xHat, yHat, joystickButtons, mode); - } - - // Sleep, update rate of joystick is approx. 25 Hz (1000 ms / 25 = 40 ms) - QGC::SLEEP::msleep(40); - } -} - -void JoystickInput::setActiveJoystick(int id) -{ - // If we already had a joystick, close that one before opening a new one. - if (joystick && SDL_JoystickOpened(joystickID)) - { - storeJoystickSettings(); - SDL_JoystickClose(joystick); - joystick = NULL; - joystickID = -1; - } - - joystickID = id; - joystick = SDL_JoystickOpen(joystickID); - if (joystick && SDL_JoystickOpened(joystickID)) - { - // Update joystick configuration. - joystickName = QString(SDL_JoystickName(joystickID)); - joystickNumButtons = SDL_JoystickNumButtons(joystick); - joystickNumAxes = SDL_JoystickNumAxes(joystick); - - // Restore saved settings for this joystick. - loadJoystickSettings(); - - // Update cached joystick axes values. - // Also emit any signals for currently-triggering events - joystickAxes.clear(); - for (int i = 0; i < joystickNumAxes; i++) - { - joystickAxes.append(numeric_limits::quiet_NaN()); - } - - // Update cached joystick button values. - // Emit signals for any button events. - joystickButtons = 0; - } - else - { - joystickNumButtons = 0; - joystickNumAxes = 0; - } - - // Specify that a new joystick has been selected, so that any UI elements can update. - emit newJoystickSelected(); - // And then trigger an update of this new UI. - emit joystickSettingsChanged(); -} - -void JoystickInput::setCalibrating(bool active) -{ - if (active) - { - setEnabled(false); - isCalibrating = true; - - // set range small so that limits can be re-found - for (int i = 0; i < joystickNumAxes; i++) - { - joystickSettings[autopilotType][systemType].axesMinRange[i] = -10.0f; - joystickSettings[autopilotType][systemType].axesMaxRange[i] = 10.0f; - } - - } else { - - // store calibration values - storeJoystickSettings(); - - qDebug() << "Calibration result:"; - for (int i = 0; i < joystickNumAxes; i++) - { - qDebug() << i << ": " << - joystickSettings[autopilotType][systemType].axesMinRange[i] << - " - " << - joystickSettings[autopilotType][systemType].axesMaxRange[i]; - } - setEnabled(true); - isCalibrating = false; - } -} - -void JoystickInput::setAxisMapping(int axis, JOYSTICK_INPUT_MAPPING newMapping) -{ - switch (newMapping) - { - case JOYSTICK_INPUT_MAPPING_ROLL: - rollAxis = axis; - break; - case JOYSTICK_INPUT_MAPPING_PITCH: - pitchAxis = axis; - break; - case JOYSTICK_INPUT_MAPPING_YAW: - yawAxis = axis; - break; - case JOYSTICK_INPUT_MAPPING_THROTTLE: - throttleAxis = axis; - break; - case JOYSTICK_INPUT_MAPPING_NONE: - default: - if (rollAxis == axis) - { - rollAxis = -1; - } - if (pitchAxis == axis) - { - pitchAxis = -1; - } - if (yawAxis == axis) - { - yawAxis = -1; - } - if (throttleAxis == axis) - { - throttleAxis = -1; - joystickSettings[autopilotType][systemType].axesLimited.remove(axis); - } - break; - } - storeJoystickSettings(); -} - -void JoystickInput::setAxisInversion(int axis, bool inverted) -{ - if (axis < joystickNumAxes) - { - joystickSettings[autopilotType][systemType].axesInverted[axis] = inverted; - storeJoystickSettings(); - } -} - -void JoystickInput::setAxisRangeLimit(int axis, bool limitRange) -{ - if (axis < joystickNumAxes) - { - joystickSettings[autopilotType][systemType].axesLimited[axis] = limitRange; - storeJoystickSettings(); - } -} - -void JoystickInput::setAxisRangeLimitMin(int axis, float min) -{ - if (axis < joystickNumAxes) - { - joystickSettings[autopilotType][systemType].axesMinRange[axis] = min; - storeJoystickSettings(); - } -} - -void JoystickInput::setAxisRangeLimitMax(int axis, float max) -{ - if (axis < joystickNumAxes) - { - joystickSettings[autopilotType][systemType].axesMaxRange[axis] = max; - storeJoystickSettings(); - } -} - -void JoystickInput::setButtonAction(int button, int action) -{ - if (button < joystickNumButtons) - { - joystickSettings[autopilotType][systemType].buttonActions[button] = action; - storeJoystickSettings(); - } -} - -float JoystickInput::getCurrentValueForAxis(int axis) const -{ - if (axis < joystickNumAxes) - { - return joystickAxes.at(axis); - } - return 0.0f; -} - -bool JoystickInput::getInvertedForAxis(int axis) const -{ - if (axis < joystickNumAxes) - { - return joystickSettings[autopilotType][systemType].axesInverted.value(axis); - } - return false; -} - -bool JoystickInput::getRangeLimitForAxis(int axis) const -{ - if (axis < joystickNumAxes) - { - return joystickSettings[autopilotType][systemType].axesLimited.value(axis); - } - return false; -} - -float JoystickInput::getAxisRangeLimitMinForAxis(int axis) const -{ - if (axis < joystickNumAxes) - { - return joystickSettings[autopilotType][systemType].axesMinRange.value(axis); - } - return sdlJoystickMin; -} - -float JoystickInput::getAxisRangeLimitMaxForAxis(int axis) const -{ - if (axis < joystickNumAxes) - { - return joystickSettings[autopilotType][systemType].axesMaxRange.value(axis); - } - return sdlJoystickMax; -} - -int JoystickInput::getActionForButton(int button) const -{ - if (button < joystickNumButtons && joystickSettings[autopilotType][systemType].buttonActions.contains(button)) - { - return joystickSettings[autopilotType][systemType].buttonActions.value(button); - } - return -1; -} diff --git a/src/input/JoystickInput.h b/src/input/JoystickInput.h deleted file mode 100644 index c3ebc7c7a..000000000 --- a/src/input/JoystickInput.h +++ /dev/null @@ -1,378 +0,0 @@ -/*===================================================================== - -PIXHAWK Micro Air Vehicle Flying Robotics Toolkit - -(c) 2009, 2010 PIXHAWK PROJECT - -This file is part of the PIXHAWK project - - PIXHAWK 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, either version 3 of the License, or - (at your option) any later version. - - PIXHAWK 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 for more details. - - You should have received a copy of the GNU General Public License - along with PIXHAWK. If not, see . - -======================================================================*/ - -/** - * @file - * @brief Definition of joystick interface - * - * This class defines a new thread to operate the reading of any joystick/controllers - * via the Simple Directmedia Library (libsdl.org). This relies on polling of the SDL, - * which is not their recommended method, instead they suggest use event checking. That - * does not seem to support switching joysticks after their internal event loop has started, - * so it was abandoned. - * - * All joystick-related functionality is done in this class, though the JoystickWidget provides - * a UI around modifying its settings. Additionally controller buttons can be mapped to - * actions defined by any UASInterface object through the `UASInterface::getActions()` function. - * - * @author Lorenz Meier - * @author Andreas Romer - * @author Julian Oes - */ - -#ifndef _JOYSTICKINPUT_H_ -#define _JOYSTICKINPUT_H_ - -#include -#include -#include -#ifdef Q_OS_MAC -#include -#else -#include -#endif - -#include "UAS.h" - -struct JoystickSettings { - QMap axesInverted; ///< Whether each axis should be used inverted from what was reported. - QMap axesLimited; ///< Whether each axis should be limited to only the positive range. Currently this only applies to the throttle axis, but is kept generic here to possibly support other axes. - QMap axesMaxRange; ///< The maximum values per axis - QMap axesMinRange; ///< The minimum values per axis - QMap buttonActions; ///< The index of the action associated with every button. -}; -Q_DECLARE_METATYPE(JoystickSettings) - -/** - * @brief Joystick input - */ -class JoystickInput : public QThread -{ - Q_OBJECT - -public: - JoystickInput(); - ~JoystickInput(); - void run(); - void shutdown(); - - /** - * @brief The JOYSTICK_INPUT_MAPPING enum storing the values for each item in the mapping combobox. - * This should match the order of items in the mapping combobox in JoystickAxis.ui. - */ - enum JOYSTICK_INPUT_MAPPING - { - JOYSTICK_INPUT_MAPPING_NONE = 0, - JOYSTICK_INPUT_MAPPING_YAW = 1, - JOYSTICK_INPUT_MAPPING_PITCH = 2, - JOYSTICK_INPUT_MAPPING_ROLL = 3, - JOYSTICK_INPUT_MAPPING_THROTTLE = 4 - }; - - /** - * @brief The JOYSTICK_MODE enum stores the values for each item in the mode combobox. - * This should match the order of items in the mode combobox in JoystickWidget.ui. - */ - enum JOYSTICK_MODE - { - JOYSTICK_MODE_ATTITUDE = 0, - JOYSTICK_MODE_POSITION = 1, - JOYSTICK_MODE_FORCE = 2, - JOYSTICK_MODE_VELOCITY = 3, - JOYSTICK_MODE_MANUAL = 4 - }; - - /** - * @brief Load joystick-specific settings. - */ - void loadJoystickSettings(); - /** - * @brief Load joystick-independent settings. - */ - void loadGeneralSettings(); - - /** - * @brief Store joystick-specific settings. - */ - void storeJoystickSettings() const; - /** - * @brief Store joystick-independent settings. - */ - void storeGeneralSettings() const; - - bool enabled() const - { - return isEnabled; - } - - bool calibrating() const - { - return isCalibrating; - } - - int getMappingThrottleAxis() const - { - return throttleAxis; - } - - int getMappingRollAxis() const - { - return rollAxis; - } - - int getMappingPitchAxis() const - { - return pitchAxis; - } - - int getMappingYawAxis() const - { - return yawAxis; - } - - int getJoystickNumButtons() const - { - return joystickNumButtons; - } - - int getJoystickNumAxes() const - { - return joystickNumAxes; - } - - int getJoystickID() const - { - return joystickID; - } - - const QString& getName() const - { - return joystickName; - } - - int getNumJoysticks() const - { - return numJoysticks; - } - - JOYSTICK_MODE getMode() const - { - return mode; - } - - QString getJoystickNameById(int id) const - { - return QString(SDL_JoystickName(id)); - } - - float getCurrentValueForAxis(int axis) const; - bool getInvertedForAxis(int axis) const; - bool getRangeLimitForAxis(int axis) const; - float getAxisRangeLimitMinForAxis(int axis) const; - float getAxisRangeLimitMaxForAxis(int axis) const; - int getActionForButton(int button) const; - - const double sdlJoystickMin; - const double sdlJoystickMax; - -protected: - - bool isEnabled; ///< Track whether the system should emit the higher-level signals: joystickChanged & actionTriggered. - bool isCalibrating; ///< Track if calibration in progress - bool done; - - SDL_Joystick* joystick; - UAS* uas; ///< Track the current UAS. - int autopilotType; ///< Cache the autopilotType - int systemType; ///< Cache the systemType - bool uasCanReverse; ///< Track whether the connect UAS can drive a reverse speed. - - // Store the mapping between axis numbers and the roll/pitch/yaw/throttle configuration. - // Value is one of JoystickAxis::JOYSTICK_INPUT_MAPPING. - int rollAxis; - int pitchAxis; - int yawAxis; - int throttleAxis; - - // Cache information on the joystick instead of polling the SDL everytime. - int numJoysticks; ///< Total number of joysticks detected by the SDL. - QString joystickName; - int joystickID; - int joystickNumAxes; - int joystickNumButtons; - - // mode of joystick (attitude, position, force, ... (see JOYSTICK_MODE enum)) - JOYSTICK_MODE mode; - - // Track axis/button settings based on a Joystick/AutopilotType/SystemType triplet. - // This is only a double-map, because settings are stored/loaded based on joystick - // name first, so only the settings for the current joystick need to be stored at any given time. - // Pointers are kept to the various settings field to reduce lookup times. - // Note that the mapping (0,0) corresponds to when no UAS is connected. Since this corresponds - // to a generic vehicle type and a generic autopilot, this is a pretty safe default. - QMap > joystickSettings; - - // Track the last state of the axes, buttons, and hats for only emitting change signals. - QList joystickAxes; ///< The values of every axes during the last sample. - quint16 joystickButtons; ///< The state of every button. Bitfield supporting 16 buttons with 1s indicating that the button is down. - qint8 xHat, yHat; ///< The horizontal/vertical hat directions. Values are -1, 0, 1, with (-1,-1) indicating bottom-left. - - /** - * @brief Called before main run() event loop starts. Waits for joysticks to be connected. - */ - void init(); - -signals: - - /** - * @brief Signal containing all joystick raw positions - * - * @param roll forward / pitch / x axis, front: 1, center: 0, back: -1. If the roll axis isn't defined, NaN is transmit instead. - * @param pitch left / roll / y axis, left: -1, middle: 0, right: 1. If the roll axis isn't defined, NaN is transmit instead. - * @param yaw turn axis, left-turn: -1, centered: 0, right-turn: 1. If the roll axis isn't defined, NaN is transmit instead. - * @param throttle Throttle, -100%:-1.0, 0%: 0.0, 100%: 1.0. If the roll axis isn't defined, NaN is transmit instead. - * @param xHat hat vector in forward-backward direction, +1 forward, 0 center, -1 backward - * @param yHat hat vector in left-right direction, -1 left, 0 center, +1 right - * @param mode (setpoint type) see JOYSTICK_MODE enum - */ - void joystickChanged(float roll, float pitch, float yaw, float throttle, qint8 xHat, qint8 yHat, quint16 buttons, quint8 mode); - - /** - * @brief Emit a new value for an axis - * - * @param value Value of the axis, between -1.0 and 1.0. - */ - void axisValueChanged(int axis, float value); - - /** - * @brief Joystick button has changed state from unpressed to pressed. - * @param key index of the pressed key - */ - void buttonPressed(int key); - - /** - * @brief Joystick button has changed state from pressed to unpressed. - * - * @param key index of the released key - */ - void buttonReleased(int key); - - /** - * @brief A joystick button was pressed that had a corresponding action. - * @param action The index of the action to trigger. Dependent on UAS. - */ - void actionTriggered(int action); - - /** - * @brief Hat (8-way switch on the top) has changed position - * - * Coordinate frame for joystick hat: - * - * y - * ^ - * | - * | - * 0 ----> x - * - * - * @param x vector in left-right direction - * @param y vector in forward-backward direction - */ - void hatDirectionChanged(qint8 x, qint8 y); - - /** @brief Signal that the UAS has been updated for this JoystickInput - * Note that any UI updates should NOT query this object for joystick details. That should be done in response to the joystickSettingsChanged signal. - */ - void activeVehicleChanged(Vehicle* vehicle); - - /** @brief Signals that new joystick-specific settings were changed. Useful for triggering updates that at dependent on the current joystick. */ - void joystickSettingsChanged(); - - /** @brief The JoystickInput has switched to a different joystick. UI should be adjusted accordingly. */ - void newJoystickSelected(); - -public slots: - /** @brief Enable or disable emitting the high-level control signals from the joystick. */ - void setEnabled(bool enable); - /** @brief Switch to a new joystick by ID number. Both buttons and axes are updated with the proper signals emitted. */ - void setActiveJoystick(int id); - /** @brief Switch calibration mode active */ - void setCalibrating(bool active); - /** - * @brief Change the control mapping for a given joystick axis. - * @param axisID The axis to modify (0-indexed) - * @param newMapping The mapping to use. - * @see JOYSTICK_INPUT_MAPPING - */ - void setAxisMapping(int axis, JoystickInput::JOYSTICK_INPUT_MAPPING newMapping); - /** - * @brief Specify if an axis should be inverted. - * @param axis The ID of the axis. - * @param inverted True indicates inverted from normal. Varies by controller. - */ - void setAxisInversion(int axis, bool inverted); - - /** - * @brief Specify that an axis should only transmit the positive values. Useful for controlling throttle from auto-centering axes. - * @param axis Which axis has its range limited. - * @param limitRange If true only the positive half of this axis will be read. - */ - void setAxisRangeLimit(int axis, bool limitRange); - - /** - * @brief Specify minimum value for axis. - * @param axis Which axis should be set. - * @param min Value to be set. - */ - void setAxisRangeLimitMin(int axis, float min); - - /** - * @brief Specify maximum value for axis. - * @param axis Which axis should be set. - * @param max Value to be set. - */ - void setAxisRangeLimitMax(int axis, float max); - - /** - * @brief Specify a button->action mapping for the given uas. - * This mapping is applied based on UAS autopilot type and UAS system type. - * Connects the buttonEmitted signal for the corresponding button to the corresponding action for the current UAS. - * @param button The numeric ID for the button - * @param action The numeric ID of the action for this UAS to map to. - */ - void setButtonAction(int button, int action); - - /** - * @brief Specify which setpoints should be sent to the UAS when moving the joystick - * @param newMode the mode (setpoint type) see the JOYSTICK_MODE enum - */ - void setMode(int newMode) - { - mode = (JOYSTICK_MODE)newMode; - } - -private slots: - void _activeVehicleChanged(Vehicle* vehicle); -}; - -#endif // _JOYSTICKINPUT_H_ diff --git a/src/qgcunittest/FileManagerTest.cc b/src/qgcunittest/FileManagerTest.cc index 5bce2a431..1ea71193b 100644 --- a/src/qgcunittest/FileManagerTest.cc +++ b/src/qgcunittest/FileManagerTest.cc @@ -26,6 +26,7 @@ #include "FileManagerTest.h" #include "MultiVehicleManager.h" +#include "UAS.h" //UT_REGISTER_TEST(FileManagerTest) diff --git a/src/uas/UAS.cc b/src/uas/UAS.cc index e766ebca2..75bb69abb 100644 --- a/src/uas/UAS.cc +++ b/src/uas/UAS.cc @@ -37,6 +37,7 @@ #include "QGCMessageBox.h" #include "QGCLoggingCategory.h" #include "Vehicle.h" +#include "Joystick.h" QGC_LOGGING_CATEGORY(UASLog, "UASLog") @@ -1372,7 +1373,7 @@ void UAS::receiveMessage(mavlink_message_t message) */ void UAS::setHomePosition(double lat, double lon, double alt) { - if (blockHomePositionChanges) + if (!_vehicle || blockHomePositionChanges) return; QString uasName = (getUASName() == "")? @@ -1409,6 +1410,10 @@ void UAS::setHomePosition(double lat, double lon, double alt) **/ void UAS::setLocalOriginAtCurrentGPSPosition() { + if (!_vehicle) { + return; + } + QMessageBox::StandardButton button = QGCMessageBox::question(tr("Set the home position at the current GPS position?"), tr("Do you want to set a new origin? Waypoints defined in the local frame will be shifted in their physical location"), QMessageBox::Yes | QMessageBox::Cancel, @@ -1453,6 +1458,10 @@ void UAS::setLocalPositionOffset(float x, float y, float z, float yaw) void UAS::startCalibration(UASInterface::StartCalibrationType calType) { + if (!_vehicle) { + return; + } + int gyroCal = 0; int magCal = 0; int airspeedCal = 0; @@ -1510,6 +1519,10 @@ void UAS::startCalibration(UASInterface::StartCalibrationType calType) void UAS::stopCalibration(void) { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), @@ -1530,7 +1543,11 @@ void UAS::stopCalibration(void) void UAS::startBusConfig(UASInterface::StartBusConfigType calType) { - int actuatorCal = 0; + if (!_vehicle) { + return; + } + + int actuatorCal = 0; switch (calType) { case StartBusConfigActuators: @@ -1558,6 +1575,10 @@ void UAS::startBusConfig(UASInterface::StartBusConfigType calType) void UAS::stopBusConfig(void) { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), @@ -1578,6 +1599,10 @@ void UAS::stopBusConfig(void) void UAS::startDataRecording() { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, uasId, 0, MAV_CMD_DO_CONTROL_VIDEO, 1, -1, -1, -1, 2, 0, 0, 0); _vehicle->sendMessage(msg); @@ -1585,7 +1610,11 @@ void UAS::startDataRecording() void UAS::stopDataRecording() { - mavlink_message_t msg; + if (!_vehicle) { + return; + } + + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, uasId, 0, MAV_CMD_DO_CONTROL_VIDEO, 1, -1, -1, -1, 0, 0, 0, 0); _vehicle->sendMessage(msg); } @@ -1752,7 +1781,11 @@ void UAS::setMode(uint8_t newBaseMode, uint32_t newCustomMode) */ void UAS::setModeArm(uint8_t newBaseMode, uint32_t newCustomMode) { - if (receivedMode) + if (!_vehicle) { + return; + } + + if (receivedMode) { mavlink_message_t msg; mavlink_msg_set_mode_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, (uint8_t)uasId, newBaseMode, newCustomMode); @@ -1882,7 +1915,11 @@ QImage UAS::getImage() void UAS::requestImage() { - qDebug() << "trying to get an image from the uas..."; + if (!_vehicle) { + return; + } + + qDebug() << "trying to get an image from the uas..."; // check if there is already an image transmission going on if (imagePacketsArrived == 0) @@ -1944,6 +1981,10 @@ bool UAS::isFixedWing() */ void UAS::enableAllDataTransmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -1972,6 +2013,10 @@ void UAS::enableAllDataTransmission(int rate) */ void UAS::enableRawSensorDataTransmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -1996,6 +2041,10 @@ void UAS::enableRawSensorDataTransmission(int rate) */ void UAS::enableExtendedSystemStatusTransmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -2020,6 +2069,10 @@ void UAS::enableExtendedSystemStatusTransmission(int rate) */ void UAS::enableRCChannelDataTransmission(int rate) { + if (!_vehicle) { + return; + } + #if defined(MAVLINK_ENABLED_UALBERTA_MESSAGES) mavlink_message_t msg; mavlink_msg_request_rc_channels_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, enabled); @@ -2049,6 +2102,10 @@ void UAS::enableRCChannelDataTransmission(int rate) */ void UAS::enableRawControllerDataTransmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -2095,6 +2152,10 @@ void UAS::enableRawControllerDataTransmission(int rate) */ void UAS::enablePositionTransmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -2119,6 +2180,10 @@ void UAS::enablePositionTransmission(int rate) */ void UAS::enableExtra1Transmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -2144,6 +2209,10 @@ void UAS::enableExtra1Transmission(int rate) */ void UAS::enableExtra2Transmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -2169,6 +2238,10 @@ void UAS::enableExtra2Transmission(int rate) */ void UAS::enableExtra3Transmission(int rate) { + if (!_vehicle) { + return; + } + // Buffers to write data to mavlink_message_t msg; mavlink_request_data_stream_t stream; @@ -2303,6 +2376,10 @@ void UAS::setUASName(const QString& name) void UAS::executeCommand(MAV_CMD command) { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_command_long_t cmd; cmd.command = (uint16_t)command; @@ -2321,6 +2398,10 @@ void UAS::executeCommand(MAV_CMD command) } void UAS::executeCommandAck(int num, bool success) { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_command_ack_t ack; ack.command = num; @@ -2331,6 +2412,10 @@ void UAS::executeCommandAck(int num, bool success) void UAS::executeCommand(MAV_CMD command, int confirmation, float param1, float param2, float param3, float param4, float param5, float param6, float param7, int component) { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_command_long_t cmd; cmd.command = (uint16_t)command; @@ -2354,6 +2439,10 @@ void UAS::executeCommand(MAV_CMD command, int confirmation, float param1, float */ void UAS::launch() { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, this->getUASID(), 0, MAV_CMD_NAV_TAKEOFF, 1, 0, 0, 0, 0, 0, 0, 0); _vehicle->sendMessage(msg); @@ -2405,11 +2494,12 @@ void UAS::toggleAutonomy() * This can only be done if the system has manual inputs enabled and is armed. */ #ifndef __mobile__ -void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float thrust, qint8 xHat, qint8 yHat, quint16 buttons, quint8 joystickMode) +void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float thrust, quint16 buttons, int joystickMode) { - Q_UNUSED(xHat); - Q_UNUSED(yHat); - + if (!_vehicle) { + return; + } + // Store the previous manual commands static float manualRollAngle = 0.0; static float manualPitchAngle = 0.0; @@ -2447,7 +2537,7 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t mavlink_message_t message; - if (joystickMode == JoystickInput::JOYSTICK_MODE_ATTITUDE) { + if (joystickMode == Vehicle::JoystickModeAttitude) { // send an external attitude setpoint command (rate control disabled) float attitudeQuaternion[4]; mavlink_euler_to_quaternion(roll, pitch, yaw, attitudeQuaternion); @@ -2465,7 +2555,7 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t 0, thrust ); - } else if (joystickMode == JoystickInput::JOYSTICK_MODE_POSITION) { + } else if (joystickMode == Vehicle::JoystickModePosition) { // Send the the local position setpoint (local pos sp external message) static float px = 0; static float py = 0; @@ -2495,7 +2585,7 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t yaw, 0 ); - } else if (joystickMode == JoystickInput::JOYSTICK_MODE_FORCE) { + } else if (joystickMode == Vehicle::JoystickModeForce) { // Send the the force setpoint (local pos sp external message) float dcm[3][3]; mavlink_euler_to_dcm(roll, pitch, yaw, dcm); @@ -2523,7 +2613,7 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t 0, 0 ); - } else if (joystickMode == JoystickInput::JOYSTICK_MODE_VELOCITY) { + } else if (joystickMode == Vehicle::JoystickModeVelocity) { // Send the the local velocity setpoint (local pos sp external message) static float vx = 0; static float vy = 0; @@ -2555,7 +2645,7 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t 0, yawrate ); - } else if (joystickMode == JoystickInput::JOYSTICK_MODE_MANUAL) { + } else if (joystickMode == Vehicle::JoystickModeRC) { // Save the new manual control inputs manualRollAngle = roll; @@ -2566,7 +2656,7 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t // Store scaling values for all 3 axes const float axesScaling = 1.0 * 1000.0; - + // Calculate the new commands for roll, pitch, yaw, and thrust const float newRollCommand = roll * axesScaling; // negate pitch value because pitch is negative for pitching forward but mavlink message argument is positive for forward @@ -2574,6 +2664,8 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t const float newYawCommand = yaw * axesScaling; const float newThrustCommand = thrust * axesScaling; + //qDebug() << newRollCommand << newPitchCommand << newYawCommand << newThrustCommand; + // Send the MANUAL_COMMAND message mavlink_msg_manual_control_pack(mavlink->getSystemId(), mavlink->getComponentId(), &message, this->uasId, newPitchCommand, newRollCommand, newThrustCommand, newYawCommand, buttons); } @@ -2588,7 +2680,11 @@ void UAS::setExternalControlSetpoint(float roll, float pitch, float yaw, float t #ifndef __mobile__ void UAS::setManual6DOFControlCommands(double x, double y, double z, double roll, double pitch, double yaw) { - // If system has manual inputs enabled and is armed + if (!_vehicle) { + return; + } + + // If system has manual inputs enabled and is armed if(((base_mode & MAV_MODE_FLAG_DECODE_POSITION_MANUAL) && (base_mode & MAV_MODE_FLAG_DECODE_POSITION_SAFETY)) || (base_mode & MAV_MODE_FLAG_HIL_ENABLED)) { mavlink_message_t message; @@ -2649,7 +2745,11 @@ bool UAS::isAirplane() */ void UAS::halt() { - mavlink_message_t msg; + if (!_vehicle) { + return; + } + + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, uasId, MAV_COMP_ID_ALL, MAV_CMD_OVERRIDE_GOTO, 1, MAV_GOTO_DO_HOLD, MAV_GOTO_HOLD_AT_CURRENT_POSITION, 0, 0, 0, 0, 0); _vehicle->sendMessage(msg); } @@ -2659,7 +2759,11 @@ void UAS::halt() */ void UAS::go() { - mavlink_message_t msg; + if (!_vehicle) { + return; + } + + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, uasId, MAV_COMP_ID_ALL, MAV_CMD_OVERRIDE_GOTO, 1, MAV_GOTO_DO_CONTINUE, MAV_GOTO_HOLD_AT_CURRENT_POSITION, 0, 0, 0, 0, 0); _vehicle->sendMessage(msg); } @@ -2669,6 +2773,10 @@ void UAS::go() */ void UAS::home() { + if (!_vehicle) { + return; + } + mavlink_message_t msg; double latitude = HomePositionManager::instance()->getHomeLatitude(); @@ -2685,7 +2793,11 @@ void UAS::home() */ void UAS::land() { - mavlink_message_t msg; + if (!_vehicle) { + return; + } + + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, uasId, MAV_COMP_ID_ALL, MAV_CMD_NAV_LAND, 1, 0, 0, 0, 0, 0, 0, 0); _vehicle->sendMessage(msg); @@ -2696,6 +2808,10 @@ void UAS::land() */ void UAS::pairRX(int rxType, int rxSubType) { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, uasId, MAV_COMP_ID_ALL, MAV_CMD_START_RX_PAIR, 0, rxType, rxSubType, 0, 0, 0, 0, 0); @@ -2928,6 +3044,10 @@ void UAS::sendHilState(quint64 time_us, float roll, float pitch, float yaw, floa float pitchspeed, float yawspeed, double lat, double lon, double alt, float vx, float vy, float vz, float ind_airspeed, float true_airspeed, float xacc, float yacc, float zacc) { + if (!_vehicle) { + return; + } + if (this->base_mode & MAV_MODE_FLAG_HIL_ENABLED) { float q[4]; @@ -3006,6 +3126,10 @@ float UAS::addZeroMeanNoise(float truth_meas, float noise_var) void UAS::sendHilSensors(quint64 time_us, float xacc, float yacc, float zacc, float rollspeed, float pitchspeed, float yawspeed, float xmag, float ymag, float zmag, float abs_pressure, float diff_pressure, float pressure_alt, float temperature, quint32 fields_changed) { + if (!_vehicle) { + return; + } + if (this->base_mode & MAV_MODE_FLAG_HIL_ENABLED) { float xacc_corrupt = addZeroMeanNoise(xacc, xacc_var); @@ -3043,6 +3167,10 @@ void UAS::sendHilSensors(quint64 time_us, float xacc, float yacc, float zacc, fl void UAS::sendHilOpticalFlow(quint64 time_us, qint16 flow_x, qint16 flow_y, float flow_comp_m_x, float flow_comp_m_y, quint8 quality, float ground_distance) { + if (!_vehicle) { + return; + } + // FIXME: This needs to be updated for new mavlink_msg_hil_optical_flow_pack api Q_UNUSED(time_us); @@ -3077,6 +3205,10 @@ void UAS::sendHilOpticalFlow(quint64 time_us, qint16 flow_x, qint16 flow_y, floa #ifndef __mobile__ void UAS::sendHilGps(quint64 time_us, double lat, double lon, double alt, int fix_type, float eph, float epv, float vel, float vn, float ve, float vd, float cog, int satellites) { + if (!_vehicle) { + return; + } + // Only send at 10 Hz max rate if (QGC::groundTimeMilliseconds() - lastSendTimeGPS < 100) return; @@ -3139,6 +3271,10 @@ void UAS::stopHil() void UAS::shutdown() { + if (!_vehicle) { + return; + } + QMessageBox::StandardButton button = QGCMessageBox::question(tr("Shutting down the UAS"), tr("Do you want to shut down the onboard computer?"), QMessageBox::Yes | QMessageBox::Cancel, @@ -3160,6 +3296,10 @@ void UAS::shutdown() */ void UAS::setTargetPosition(float x, float y, float z, float yaw) { + if (!_vehicle) { + return; + } + mavlink_message_t msg; mavlink_msg_command_long_pack(mavlink->getSystemId(), mavlink->getComponentId(), &msg, uasId, MAV_COMP_ID_ALL, MAV_CMD_NAV_PATHPLANNING, 1, 1, 1, 0, yaw, x, y, z); _vehicle->sendMessage(msg); @@ -3268,6 +3408,10 @@ void UAS::stopLowBattAlarm() void UAS::sendMapRCToParam(QString param_id, float scale, float value0, quint8 param_rc_channel_index, float valueMin, float valueMax) { + if (!_vehicle) { + return; + } + mavlink_message_t message; char param_id_cstr[MAVLINK_MSG_PARAM_MAP_RC_FIELD_PARAM_ID_LEN] = {}; @@ -3298,6 +3442,10 @@ void UAS::sendMapRCToParam(QString param_id, float scale, float value0, quint8 p void UAS::unsetRCToParameterMap() { + if (!_vehicle) { + return; + } + char param_id_cstr[MAVLINK_MSG_PARAM_MAP_RC_FIELD_PARAM_ID_LEN] = {}; for (int i = 0; i < 3; i++) { diff --git a/src/uas/UAS.h b/src/uas/UAS.h index 977b3914e..e6abf766a 100644 --- a/src/uas/UAS.h +++ b/src/uas/UAS.h @@ -37,8 +37,9 @@ This file is part of the QGROUNDCONTROL project #include #include "QGCMAVLink.h" #include "FileManager.h" +#include "Vehicle.h" + #ifndef __mobile__ -#include "JoystickInput.h" #include "QGCHilLink.h" #include "QGCFlightGearLink.h" #include "QGCJSBSimLink.h" @@ -110,6 +111,8 @@ public: Q_PROPERTY(double altitudeWGS84 READ getAltitudeWGS84 WRITE setAltitudeWGS84 NOTIFY altitudeWGS84Changed) Q_PROPERTY(double altitudeRelative READ getAltitudeRelative WRITE setAltitudeRelative NOTIFY altitudeRelativeChanged) + void clearVehicle(void) { _vehicle = NULL; } + void setGroundSpeed(double val) { groundSpeed = val; @@ -847,7 +850,7 @@ public slots: /** @brief Set the values for the manual control of the vehicle */ #ifndef __mobile__ - void setExternalControlSetpoint(float roll, float pitch, float yaw, float thrust, qint8 xHat, qint8 yHat, quint16 buttons, quint8); + void setExternalControlSetpoint(float roll, float pitch, float yaw, float thrust, quint16 buttons, int joystickMode); #endif /** @brief Set the values for the 6dof manual control of the vehicle */ diff --git a/src/uas/UASMessageHandler.cc b/src/uas/UASMessageHandler.cc index 3ad901b1e..d95b77021 100644 --- a/src/uas/UASMessageHandler.cc +++ b/src/uas/UASMessageHandler.cc @@ -30,6 +30,7 @@ This file is part of the QGROUNDCONTROL project #include "QGCApplication.h" #include "UASMessageHandler.h" #include "MultiVehicleManager.h" +#include "UAS.h" UASMessage::UASMessage(int componentid, int severity, QString text) { diff --git a/src/ui/HDDisplay.cc b/src/ui/HDDisplay.cc index f732d1edf..a9ba7f281 100644 --- a/src/ui/HDDisplay.cc +++ b/src/ui/HDDisplay.cc @@ -18,14 +18,17 @@ #include #include #include +#include + #include + #include "HDDisplay.h" #include "ui_HDDisplay.h" #include "MG.h" #include "QGC.h" #include "QGCApplication.h" -#include #include "MultiVehicleManager.h" +#include "UAS.h" HDDisplay::HDDisplay(const QStringList &plotList, QString title, QWidget *parent) : QGraphicsView(parent), diff --git a/src/ui/HSIDisplay.cc b/src/ui/HSIDisplay.cc index d0c25e21f..3df5d60c1 100644 --- a/src/ui/HSIDisplay.cc +++ b/src/ui/HSIDisplay.cc @@ -46,6 +46,7 @@ This file is part of the QGROUNDCONTROL project #include #include "MAV2DIcon.h" #include "QGCApplication.h" +#include "UAS.h" HSIDisplay::HSIDisplay(QWidget *parent) : HDDisplay(QStringList(), "HSI", parent), diff --git a/src/ui/JoystickAxis.cc b/src/ui/JoystickAxis.cc deleted file mode 100644 index cfe741b21..000000000 --- a/src/ui/JoystickAxis.cc +++ /dev/null @@ -1,98 +0,0 @@ -#include "JoystickAxis.h" -#include "JoystickInput.h" -#include "ui_JoystickAxis.h" -#include "MultiVehicleManager.h" -#include - -JoystickAxis::JoystickAxis(int id, QWidget *parent) : - QWidget(parent), - id(id), - ui(new Ui::JoystickAxis) -{ - ui->setupUi(this); - mappingComboBoxChanged(JoystickInput::JOYSTICK_INPUT_MAPPING_NONE); - ui->label->setText(QString::number(id)); - connect(ui->comboBox, SIGNAL(currentIndexChanged(int)), this, SLOT(mappingComboBoxChanged(int))); - connect(ui->invertedCheckBox, SIGNAL(clicked(bool)), this, SLOT(inversionCheckBoxChanged(bool))); - connect(ui->rangeCheckBox, SIGNAL(clicked(bool)), this, SLOT(rangeCheckBoxChanged(bool))); -} - -JoystickAxis::~JoystickAxis() -{ - delete ui; -} - -void JoystickAxis::setValue(float value) -{ - ui->progressBar->setValue(100.0f * value); -} - -void JoystickAxis::setMapping(JoystickInput::JOYSTICK_INPUT_MAPPING newMapping) -{ - ui->comboBox->setCurrentIndex(newMapping); - if (newMapping == JoystickInput::JOYSTICK_INPUT_MAPPING_THROTTLE) - { - ui->rangeCheckBox->show(); - } - else - { - ui->rangeCheckBox->hide(); - } - this->activeVehicleChanged(MultiVehicleManager::instance()->activeVehicle()); -} - -void JoystickAxis::setInverted(bool newValue) -{ - ui->invertedCheckBox->setChecked(newValue); -} - -void JoystickAxis::setRangeLimit(bool newValue) -{ - ui->rangeCheckBox->setChecked(newValue); -} - -void JoystickAxis::mappingComboBoxChanged(int newMapping) -{ - JoystickInput::JOYSTICK_INPUT_MAPPING mapping = (JoystickInput::JOYSTICK_INPUT_MAPPING)newMapping; - emit mappingChanged(id, mapping); - updateUIBasedOnUAS(MultiVehicleManager::instance()->activeVehicle(), mapping); -} - -void JoystickAxis::inversionCheckBoxChanged(bool inverted) -{ - emit inversionChanged(id, inverted); -} - -void JoystickAxis::rangeCheckBoxChanged(bool limited) -{ - emit rangeChanged(id, limited); -} - -void JoystickAxis::activeVehicleChanged(Vehicle* vehicle) -{ - updateUIBasedOnUAS(vehicle, (JoystickInput::JOYSTICK_INPUT_MAPPING)ui->comboBox->currentIndex()); -} - -void JoystickAxis::updateUIBasedOnUAS(Vehicle* vehicle, JoystickInput::JOYSTICK_INPUT_MAPPING axisMapping) -{ - UAS* uas = NULL; - - if (vehicle) { - uas = vehicle->uas(); - } - - // Set the throttle display to only positive if: - // * This is the throttle axis AND - // * The current UAS can't reverse OR there is no current UAS - // This causes us to default to systems with no negative throttle. - if (((uas && !uas->systemCanReverse()) || !uas) && axisMapping == JoystickInput::JOYSTICK_INPUT_MAPPING_THROTTLE) - { - ui->progressBar->setRange(0, 100); - ui->rangeCheckBox->show(); - } - else - { - ui->progressBar->setRange(-100, 100); - ui->rangeCheckBox->hide(); - } -} diff --git a/src/ui/JoystickAxis.h b/src/ui/JoystickAxis.h deleted file mode 100644 index 9693ee994..000000000 --- a/src/ui/JoystickAxis.h +++ /dev/null @@ -1,87 +0,0 @@ -/*===================================================================== - -QGroundControl Open Source Ground Control Station - -This file is part of the QGROUNDCONTROL project - - QGROUNDCONTROL 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, either version 3 of the License, or - (at your option) any later version. - - QGROUNDCONTROL 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 for more details. - - You should have received a copy of the GNU General Public License - along with QGROUNDCONTROL. If not, see . - -======================================================================*/ - -/** - * @file - * This class defines a UI element to represent a single controller axis. - * It is used by the JoystickWidget to simplify some of the logic in that class. - */ - -#ifndef JOYSTICKAXIS_H -#define JOYSTICKAXIS_H - -#include -#include "JoystickInput.h" -#include "Vehicle.h" - -namespace Ui { -class JoystickAxis; -} - -class JoystickAxis : public QWidget -{ - Q_OBJECT - -public: - explicit JoystickAxis(int id, QWidget *parent = 0); - ~JoystickAxis(); - void setMapping(JoystickInput::JOYSTICK_INPUT_MAPPING newMapping); - void setInverted(bool newValue); - void setRangeLimit(bool newValue); - void setAxisRangeMin(float min); - void setAxisRangeMax(float max); - -signals: - /** @brief Signal a change in this axis' yaw/pitch/roll mapping */ - void mappingChanged(int id, JoystickInput::JOYSTICK_INPUT_MAPPING newMapping); - /** @brief Signal a change in this axis' inversion status */ - void inversionChanged(int id, bool inversion); - /** @brief Signal a change in this axis' range setting. If limited is true then only the positive values should be read from this axis. */ - void rangeChanged(int id, bool limited); - -public slots: - /** @brief Update the displayed value of the included progressbar. - * @param value A value between -1.0 and 1.0. - */ - void setValue(float value); - /** @brief Specify the UAS that this axis should track for displaying throttle properly. */ - void activeVehicleChanged(Vehicle* vehicle); - -private: - int id; ///< The ID for this axis. Corresponds to the IDs used by JoystickInput. - Ui::JoystickAxis *ui; - /** - * @brief Update the UI based on both the current UAS and the current axis mapping. - * @param uas The currently-active UAS. - * @param axisMapping The new mapping for this axis. - */ - void updateUIBasedOnUAS(Vehicle* vehicle, JoystickInput::JOYSTICK_INPUT_MAPPING axisMapping); - -private slots: - /** @brief Handle changes to the mapping dropdown bar. */ - void mappingComboBoxChanged(int newMapping); - /** @brief Emit signal when the inversion checkbox is changed. */ - void inversionCheckBoxChanged(bool inverted); - /** @brief Emit signal when the range checkbox is changed. */ - void rangeCheckBoxChanged(bool inverted); -}; - -#endif // JOYSTICKAXIS_H diff --git a/src/ui/JoystickAxis.ui b/src/ui/JoystickAxis.ui deleted file mode 100644 index 212133c50..000000000 --- a/src/ui/JoystickAxis.ui +++ /dev/null @@ -1,168 +0,0 @@ - - - JoystickAxis - - - - 0 - 0 - 80 - 200 - - - - - 0 - 0 - - - - - 40 - 200 - - - - Form - - - - QLayout::SetMinimumSize - - - 1 - - - 2 - - - 1 - - - 2 - - - - - - - - Qt::AlignCenter - - - - - - - - 0 - 0 - - - - Specify what property of the UAS this axis should command - - - - -- - - - - - Yaw - - - - - Pitch - - - - - Roll - - - - - Throttle - - - - - - - - Half range - - - - - - - true - - - - 0 - 0 - - - - - 0 - 100 - - - - Only use the positive values from this axis for control - - - -100 - - - 0 - - - Qt::AlignCenter - - - true - - - Qt::Vertical - - - QProgressBar::TopToBottom - - - %v - - - - - - - - 0 - 0 - - - - - 0 - 25 - - - - Reverse the values for this axis - - - Inverted - - - - - - - - diff --git a/src/ui/JoystickButton.cc b/src/ui/JoystickButton.cc deleted file mode 100644 index 5f094ff51..000000000 --- a/src/ui/JoystickButton.cc +++ /dev/null @@ -1,60 +0,0 @@ -#include "JoystickButton.h" -#include "ui_JoystickButton.h" -#include "MultiVehicleManager.h" - -JoystickButton::JoystickButton(int id, QWidget *parent) : - QWidget(parent), - id(id), - m_ui(new Ui::JoystickButton) -{ - m_ui->setupUi(this); - m_ui->joystickButtonLabel->setText(QString::number(id)); - this->activeVehicleChanged(MultiVehicleManager::instance()->activeVehicle()); - connect(m_ui->joystickAction, SIGNAL(currentIndexChanged(int)), this, SLOT(actionComboBoxChanged(int))); -} - -JoystickButton::~JoystickButton() -{ - delete m_ui; -} - -void JoystickButton::activeVehicleChanged(Vehicle* vehicle) -{ - // Disable signals so that changes to joystickAction don't trigger JoystickInput updates. - disconnect(m_ui->joystickAction, SIGNAL(currentIndexChanged(int)), this, SLOT(actionComboBoxChanged(int))); - if (vehicle) - { - UAS* uas = vehicle->uas(); - - m_ui->joystickAction->setEnabled(true); - m_ui->joystickAction->clear(); - m_ui->joystickAction->addItem("--"); - QList actions = uas->getActions(); - foreach (QAction* a, actions) - { - m_ui->joystickAction->addItem(a->text()); - } - m_ui->joystickAction->setCurrentIndex(0); - } else { - m_ui->joystickAction->setEnabled(false); - m_ui->joystickAction->clear(); - m_ui->joystickAction->addItem("--"); - m_ui->joystickAction->setCurrentIndex(0); - } - connect(m_ui->joystickAction, SIGNAL(currentIndexChanged(int)), this, SLOT(actionComboBoxChanged(int))); -} - -void JoystickButton::setAction(int index) -{ - // Disable signals so that changes to joystickAction don't trigger JoystickInput updates. - disconnect(m_ui->joystickAction, SIGNAL(currentIndexChanged(int)), this, SLOT(actionComboBoxChanged(int))); - // Add one because the default no-action takes the 0-spot. - m_ui->joystickAction->setCurrentIndex(index + 1); - connect(m_ui->joystickAction, SIGNAL(currentIndexChanged(int)), this, SLOT(actionComboBoxChanged(int))); -} - -void JoystickButton::actionComboBoxChanged(int index) -{ - // Subtract one because the default no-action takes the 0-spot. - emit actionChanged(id, index - 1); -} diff --git a/src/ui/JoystickButton.h b/src/ui/JoystickButton.h deleted file mode 100644 index 4e76cde21..000000000 --- a/src/ui/JoystickButton.h +++ /dev/null @@ -1,66 +0,0 @@ -/*===================================================================== - -QGroundControl Open Source Ground Control Station - -This file is part of the QGROUNDCONTROL project - - QGROUNDCONTROL 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, either version 3 of the License, or - (at your option) any later version. - - QGROUNDCONTROL 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 for more details. - - You should have received a copy of the GNU General Public License - along with QGROUNDCONTROL. If not, see . - -======================================================================*/ - -/** - * @file - * This class defines a UI element to represent a single controller axis. - * It is used by the JoystickWidget to simplify some of the logic in that class. - */ - -#ifndef JOYSTICKBUTTON_H -#define JOYSTICKBUTTON_H - -#include - -#include "Vehicle.h" - -namespace Ui -{ -class JoystickButton; -} - -class JoystickButton : public QWidget -{ - Q_OBJECT - -public: - explicit JoystickButton(int id, QWidget *parent = 0); - virtual ~JoystickButton(); - -public slots: - /** @brief Specify the UAS that this axis should track for displaying throttle properly. */ - void activeVehicleChanged(Vehicle* vehicle); - /** @brieft Specify which action this button should correspond to. - * Values 0 and higher indicate a specific action, while -1 indicates no action. */ - void setAction(int index); - -signals: - /** @brief Signal a change in this buttons' action mapping */ - void actionChanged(int id, int index); - -private: - int id; - Ui::JoystickButton *m_ui; - -private slots: - void actionComboBoxChanged(int index); -}; -#endif // JOYSTICKBUTTON_H diff --git a/src/ui/JoystickButton.ui b/src/ui/JoystickButton.ui deleted file mode 100644 index 1598d2227..000000000 --- a/src/ui/JoystickButton.ui +++ /dev/null @@ -1,92 +0,0 @@ - - - JoystickButton - - - - 0 - 0 - 125 - 29 - - - - - 0 - 0 - - - - - 50 - 0 - - - - Form - - - - QLayout::SetMinimumSize - - - 2 - - - 1 - - - 2 - - - 1 - - - - - - 0 - 0 - - - - - 20 - 0 - - - - - - - Qt::AlignCenter - - - - - - - false - - - - 0 - 0 - - - - - 60 - 0 - - - - QComboBox::AdjustToContents - - - - - - - - diff --git a/src/ui/JoystickWidget.cc b/src/ui/JoystickWidget.cc deleted file mode 100644 index 5926322be..000000000 --- a/src/ui/JoystickWidget.cc +++ /dev/null @@ -1,279 +0,0 @@ -#include "JoystickWidget.h" -#include "QGCApplication.h" -#include "ui_JoystickWidget.h" -#include "JoystickButton.h" -#include "JoystickAxis.h" - -#include - -JoystickWidget::JoystickWidget(JoystickInput* joystick, QWidget *parent) : - QWidget(parent), - joystick(joystick), - m_ui(new Ui::JoystickWidget) -{ - m_ui->setupUi(this); - - // Center the window on the screen. - QRect position = frameGeometry(); - position.moveCenter(QDesktopWidget().availableGeometry().center()); - move(position.topLeft()); - - // Initialize the UI based on the current joystick - initUI(); - - // Watch for button, axis, and hat input events from the joystick. - connect(this->joystick, SIGNAL(buttonPressed(int)), this, SLOT(joystickButtonPressed(int))); - connect(this->joystick, SIGNAL(buttonReleased(int)), this, SLOT(joystickButtonReleased(int))); - connect(this->joystick, SIGNAL(axisValueChanged(int,float)), this, SLOT(updateAxisValue(int,float))); - connect(this->joystick, SIGNAL(hatDirectionChanged(qint8,qint8)), this, SLOT(setHat(qint8,qint8))); - - // Also watch for when new settings were loaded for the current joystick to do a mass UI refresh. - connect(this->joystick, SIGNAL(joystickSettingsChanged()), this, SLOT(updateUI())); - - // If the selected joystick is changed, update the JoystickInput. - connect(m_ui->joystickNameComboBox, SIGNAL(currentIndexChanged(int)), this->joystick, SLOT(setActiveJoystick(int))); - // Also wait for the JoystickInput to switch, then update our UI. - connect(this->joystick, SIGNAL(newJoystickSelected()), this, SLOT(createUIForJoystick())); - - // Initialize the UI to the current JoystickInput state. Also make sure to listen for future changes - // so that the UI can be updated. - connect(m_ui->enableCheckBox, SIGNAL(toggled(bool)), m_ui->joystickFrame, SLOT(setEnabled(bool))); - m_ui->enableCheckBox->setChecked(this->joystick->enabled()); // Needs to be after connecting to the joystick frame and before watching for enabled events from JoystickInput. - connect(m_ui->enableCheckBox, SIGNAL(toggled(bool)), this->joystick, SLOT(setEnabled(bool))); - - // Update the button label colors based on the current theme and watch for future theme changes. - styleChanged(qgcApp()->styleIsDark()); - connect(qgcApp(), &QGCApplication::styleChanged, this, &JoystickWidget::styleChanged); - - // change mode when mode combobox is changed - connect(m_ui->joystickModeComboBox, SIGNAL(currentIndexChanged(int)), this->joystick, SLOT(setMode(int))); - - // Display the widget above all other windows. - this->raise(); - this->show(); -} - -void JoystickWidget::initUI() -{ - // Add the joysticks to the top combobox. They're indexed by their item number. - // And set the currently-selected combobox item to the current joystick. - int joysticks = joystick->getNumJoysticks(); - if (joysticks) - { - for (int i = 0; i < joysticks; i++) - { - m_ui->joystickNameComboBox->addItem(joystick->getJoystickNameById(i)); - } - m_ui->joystickNameComboBox->setCurrentIndex(joystick->getJoystickID()); - // And if joystick support is enabled, show the UI. - if (m_ui->enableCheckBox->isChecked()) - { - m_ui->joystickFrame->setEnabled(true); - } - - // mode combo box - m_ui->joystickModeComboBox->addItem("Attitude"); - m_ui->joystickModeComboBox->addItem("Position"); - m_ui->joystickModeComboBox->addItem("Force"); - m_ui->joystickModeComboBox->addItem("Velocity"); - m_ui->joystickModeComboBox->addItem("Manual"); - m_ui->joystickModeComboBox->setCurrentIndex(joystick->getMode()); - - // Create the initial UI. - createUIForJoystick(); - } - // But if there're no joysticks, disable everything and hide empty UI. - else - { - m_ui->enableCheckBox->setEnabled(false); - m_ui->joystickNameComboBox->addItem(tr("No joysticks found. Connect and restart QGC to add one.")); - m_ui->joystickNameComboBox->setEnabled(false); - m_ui->joystickFrame->hide(); - } -} - -void JoystickWidget::styleChanged(bool styleIsDark) -{ - if (styleIsDark) - { - buttonLabelColor = QColor(0x14, 0xC6, 0x14); - } - else - { - buttonLabelColor = QColor(0x73, 0xD9, 0x5D); - } -} - -JoystickWidget::~JoystickWidget() -{ - delete m_ui; -} - -void JoystickWidget::changeEvent(QEvent *e) -{ - switch (e->type()) { - case QEvent::LanguageChange: - m_ui->retranslateUi(this); - break; - default: - break; - } -} - -void JoystickWidget::updateUI() -{ - // Update the actions for all of the buttons - for (int i = 0; i < buttons.size(); i++) - { - JoystickButton* button = buttons[i]; - int action = joystick->getActionForButton(i); - button->setAction(action); - } - - // Update the axis mappings - int rollAxis = joystick->getMappingRollAxis(); - int pitchAxis = joystick->getMappingPitchAxis(); - int yawAxis = joystick->getMappingYawAxis(); - int throttleAxis = joystick->getMappingThrottleAxis(); - for (int i = 0; i < axes.size(); i++) - { - JoystickAxis* axis = axes[i]; - JoystickInput::JOYSTICK_INPUT_MAPPING mapping = JoystickInput::JOYSTICK_INPUT_MAPPING_NONE; - if (i == rollAxis) - { - mapping = JoystickInput::JOYSTICK_INPUT_MAPPING_ROLL; - } - else if (i == pitchAxis) - { - mapping = JoystickInput::JOYSTICK_INPUT_MAPPING_PITCH; - } - else if (i == yawAxis) - { - mapping = JoystickInput::JOYSTICK_INPUT_MAPPING_YAW; - } - else if (i == throttleAxis) - { - mapping = JoystickInput::JOYSTICK_INPUT_MAPPING_THROTTLE; - } - axis->setMapping(mapping); - bool inverted = joystick->getInvertedForAxis(i); - axis->setInverted(inverted); - bool limited = joystick->getRangeLimitForAxis(i); - axis->setRangeLimit(limited); - } -} - -void JoystickWidget::createUIForJoystick() -{ - // Delete all the old UI elements - foreach (JoystickButton* b, buttons) - { - delete b; - } - buttons.clear(); - foreach (JoystickAxis* a, axes) - { - delete a; - } - axes.clear(); - - // And add the necessary button displays for this joystick. - int newButtons = joystick->getJoystickNumButtons(); - if (newButtons) - { - m_ui->buttonBox->show(); - for (int i = 0; i < newButtons; i++) - { - JoystickButton* button = new JoystickButton(i, m_ui->buttonBox); - button->setAction(joystick->getActionForButton(i)); - connect(button, SIGNAL(actionChanged(int,int)), this->joystick, SLOT(setButtonAction(int,int))); - connect(this->joystick, &JoystickInput::activeVehicleChanged, button, &JoystickButton::activeVehicleChanged); - m_ui->buttonLayout->addWidget(button); - buttons.append(button); - } - } - else - { - m_ui->buttonBox->hide(); - } - - // Do the same for the axes supported by this joystick. - int rollAxis = joystick->getMappingRollAxis(); - int pitchAxis = joystick->getMappingPitchAxis(); - int yawAxis = joystick->getMappingYawAxis(); - int throttleAxis = joystick->getMappingThrottleAxis(); - int newAxes = joystick->getJoystickNumAxes(); - if (newAxes) - { - for (int i = 0; i < newAxes; i++) - { - JoystickAxis* axis = new JoystickAxis(i, m_ui->axesBox); - axis->setValue(joystick->getCurrentValueForAxis(i)); - if (i == rollAxis) - { - axis->setMapping(JoystickInput::JOYSTICK_INPUT_MAPPING_ROLL); - } - else if (i == pitchAxis) - { - axis->setMapping(JoystickInput::JOYSTICK_INPUT_MAPPING_PITCH); - } - else if (i == yawAxis) - { - axis->setMapping(JoystickInput::JOYSTICK_INPUT_MAPPING_YAW); - } - else if (i == throttleAxis) - { - axis->setMapping(JoystickInput::JOYSTICK_INPUT_MAPPING_THROTTLE); - } - axis->setInverted(joystick->getInvertedForAxis(i)); - axis->setRangeLimit(joystick->getRangeLimitForAxis(i)); - connect(axis, SIGNAL(mappingChanged(int,JoystickInput::JOYSTICK_INPUT_MAPPING)), this->joystick, SLOT(setAxisMapping(int,JoystickInput::JOYSTICK_INPUT_MAPPING))); - connect(axis, SIGNAL(inversionChanged(int,bool)), this->joystick, SLOT(setAxisInversion(int,bool))); - connect(axis, SIGNAL(rangeChanged(int,bool)), this->joystick, SLOT(setAxisRangeLimit(int,bool))); - connect(this->joystick, &JoystickInput::activeVehicleChanged, axis, &JoystickAxis::activeVehicleChanged); - m_ui->axesLayout->addWidget(axis); - axes.append(axis); - } - } - else - { - m_ui->axesBox->hide(); - } - - connect(m_ui->calibrationButton, SIGNAL(clicked()), this, SLOT(cycleCalibrationButton())); -} - -void JoystickWidget::updateAxisValue(int axis, float value) -{ - if (axis < axes.size()) - { - axes.at(axis)->setValue(value); - } -} - -void JoystickWidget::setHat(qint8 x, qint8 y) -{ - m_ui->statusLabel->setText(tr("Hat position: x: %1, y: %2").arg(x).arg(y)); -} - -void JoystickWidget::joystickButtonPressed(int key) -{ - QString colorStyle = QString("QLabel { background-color: %1;}").arg(buttonLabelColor.name()); - buttons.at(key)->setStyleSheet(colorStyle); -} - -void JoystickWidget::joystickButtonReleased(int key) -{ - buttons.at(key)->setStyleSheet(""); -} - -void JoystickWidget::cycleCalibrationButton() -{ - if (this->joystick->calibrating()) { - this->joystick->setCalibrating(false); - m_ui->calibrationButton->setText("Calibrate range"); - } else { - this->joystick->setCalibrating(true); - m_ui->calibrationButton->setText("End calibration"); - } -} diff --git a/src/ui/JoystickWidget.h b/src/ui/JoystickWidget.h deleted file mode 100644 index 6f7cbbb05..000000000 --- a/src/ui/JoystickWidget.h +++ /dev/null @@ -1,99 +0,0 @@ -/*===================================================================== - -PIXHAWK Micro Air Vehicle Flying Robotics Toolkit - -(c) 2009, 2010 PIXHAWK PROJECT - -This file is part of the PIXHAWK project - - PIXHAWK 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, either version 3 of the License, or - (at your option) any later version. - - PIXHAWK 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 for more details. - - You should have received a copy of the GNU General Public License - along with PIXHAWK. If not, see . - -======================================================================*/ - -/** - * @file - * @brief Definition of joystick widget. Provides a UI for configuring the joystick settings. - * @author Lorenz Meier - * - */ - -#ifndef JOYSTICKWIDGET_H -#define JOYSTICKWIDGET_H - -#include -#include -#include -#include "JoystickInput.h" -#include "MainWindow.h" -#include "JoystickAxis.h" -#include "JoystickButton.h" - -namespace Ui -{ -class JoystickWidget; -} - -class JoystickWidget : public QWidget -{ - Q_OBJECT - Q_DISABLE_COPY(JoystickWidget) -public: - explicit JoystickWidget(JoystickInput* joystick, QWidget *parent = 0); - virtual ~JoystickWidget(); - -public slots: - /** @brief Update the UI for a new joystick based on SDL ID. */ - void createUIForJoystick(); - /** - * @brief Update a given axis with a new value - * @param axis The index of the axis to update. - * @param value The new value for the axis, [-1.0:1.0]. - * @see JoystickInput:axisValueChanged - */ - void updateAxisValue(int axis, float value); - /** @brief Update the UI with new values for the hat. - * @see JoystickInput::hatDirectionChanged - */ - void setHat(qint8 x, qint8 y); - /** @brief Trigger a UI change based on a button being pressed */ - void joystickButtonPressed(int key); - /** @brief Trigger a UI change based on a button being released */ - void joystickButtonReleased(int key); - /** @brief Toggle the calibration button */ - void cycleCalibrationButton(); - /** @brief Update the UI color scheme when the MainWindow theme changes. */ - void styleChanged(bool styleIsDark); - /** Update the UI assuming the joystick has stayed the same. */ - void updateUI(); - -protected: - /** @brief UI change event */ - virtual void changeEvent(QEvent *e); - JoystickInput* joystick; ///< Reference to the joystick - /** @brief a list of all button labels generated for this joystick. */ - QList buttons; - /** @brief a lit of all joystick axes generated for this joystick. */ - QList axes; - /** @brief The color to use for button labels when their corresponding button is pressed */ - QColor buttonLabelColor; - -private: - Ui::JoystickWidget *m_ui; - /** @brief Initialize all dynamic UI elements (button list, joystick names, etc.). - * Only done once at startup. - */ - void initUI(); -}; - -#endif // JOYSTICKWIDGET_H diff --git a/src/ui/JoystickWidget.ui b/src/ui/JoystickWidget.ui deleted file mode 100644 index dbd28428a..000000000 --- a/src/ui/JoystickWidget.ui +++ /dev/null @@ -1,191 +0,0 @@ - - - JoystickWidget - - - - 0 - 0 - 368 - 274 - - - - - 0 - 0 - - - - - 368 - 274 - - - - Joystick Settings - - - - 8 - - - QLayout::SetDefaultConstraint - - - 8 - - - 8 - - - 8 - - - 8 - - - - - true - - - Enable controllers - - - - - - - true - - - - 0 - 0 - - - - - - - - - - - false - - - QFrame::StyledPanel - - - QFrame::Sunken - - - - QLayout::SetMinimumSize - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - - 16777215 - 16777215 - - - - Buttons - - - Qt::AlignCenter - - - false - - - - 1 - - - QLayout::SetMinimumSize - - - 3 - - - 3 - - - 3 - - - 3 - - - - - - - - - 0 - 0 - - - - - 100 - 0 - - - - Axes - - - Qt::AlignCenter - - - - QLayout::SetMinimumSize - - - - - - - - - - - - - - - - - Calibrate range - - - - - - - - - - - diff --git a/src/ui/MainWindow.cc b/src/ui/MainWindow.cc index 09545d01c..e2bc21778 100644 --- a/src/ui/MainWindow.cc +++ b/src/ui/MainWindow.cc @@ -48,9 +48,6 @@ This file is part of the QGROUNDCONTROL project #include "MAVLinkProtocol.h" #include "QGCWaypointListMulti.h" #include "MainWindow.h" -#ifndef __mobile__ -#include "JoystickWidget.h" -#endif #include "GAudioOutput.h" #include "QGCMAVLinkLogPlayer.h" #include "SettingsDialog.h" @@ -200,10 +197,6 @@ MainWindow::MainWindow(QSplashScreen* splashScreen) // Create actions connectCommonActions(); // Connect user interface devices - emit initStatusChanged(tr("Initializing joystick interface"), Qt::AlignLeft | Qt::AlignBottom, QColor(62, 93, 141)); -#ifndef __mobile__ - joystick = new JoystickInput(); -#endif #ifdef QGC_MOUSE_ENABLED_WIN emit initStatusChanged(tr("Initializing 3D mouse interface"), Qt::AlignLeft | Qt::AlignBottom, QColor(62, 93, 141)); mouseInput = new Mouse3DInput(this); @@ -325,15 +318,6 @@ MainWindow::MainWindow(QSplashScreen* splashScreen) MainWindow::~MainWindow() { -#ifndef __mobile__ - if (joystick) - { - joystick->shutdown(); - joystick->wait(5000); - joystick->deleteLater(); - joystick = NULL; - } -#endif // Delete all UAS objects for (int i=0;i<_commsWidgetList.size();i++) { @@ -809,11 +793,7 @@ void MainWindow::showRoadMap() void MainWindow::showSettings() { -#ifndef __mobile__ - SettingsDialog settings(joystick, this); -#else SettingsDialog settings(this); -#endif settings.exec(); } @@ -1057,11 +1037,7 @@ void MainWindow::hideSplashScreen(void) void MainWindow::manageLinks() { -#ifndef __mobile__ - SettingsDialog settings(joystick, this, SettingsDialog::ShowCommLinks); -#else SettingsDialog settings(this, SettingsDialog::ShowCommLinks); -#endif settings.exec(); } diff --git a/src/ui/MainWindow.h b/src/ui/MainWindow.h index beb428601..8a31ed2f5 100644 --- a/src/ui/MainWindow.h +++ b/src/ui/MainWindow.h @@ -46,9 +46,6 @@ This file is part of the QGROUNDCONTROL project #include "WaypointList.h" #include "CameraView.h" #include "UASListWidget.h" -#ifndef __mobile__ -#include "input/JoystickInput.h" -#endif #if (defined QGC_MOUSE_ENABLED_WIN) | (defined QGC_MOUSE_ENABLED_LINUX) #include "Mouse6dofInput.h" #endif // QGC_MOUSE_ENABLED_WIN @@ -241,10 +238,6 @@ protected: QPointer fileWidget; -#ifndef __mobile__ - JoystickInput* joystick; ///< The joystick manager for QGC -#endif - #ifdef QGC_MOUSE_ENABLED_WIN /** @brief 3d Mouse support (WIN only) */ Mouse3DInput* mouseInput; ///< 3dConnexion 3dMouse SDK diff --git a/src/ui/QGCMAVLinkInspector.cc b/src/ui/QGCMAVLinkInspector.cc index bd0f2a0e3..fd5c01f65 100644 --- a/src/ui/QGCMAVLinkInspector.cc +++ b/src/ui/QGCMAVLinkInspector.cc @@ -3,6 +3,8 @@ #include "QGCMAVLink.h" #include "QGCMAVLinkInspector.h" #include "MultiVehicleManager.h" +#include "UAS.h" + #include "ui_QGCMAVLinkInspector.h" #include diff --git a/src/ui/QGCWaypointListMulti.cc b/src/ui/QGCWaypointListMulti.cc index d272868cd..12cd90de9 100644 --- a/src/ui/QGCWaypointListMulti.cc +++ b/src/ui/QGCWaypointListMulti.cc @@ -1,6 +1,8 @@ #include "QGCWaypointListMulti.h" -#include "ui_QGCWaypointListMulti.h" #include "MultiVehicleManager.h" +#include "UAS.h" + +#include "ui_QGCWaypointListMulti.h" void* QGCWaypointListMulti::_offlineUAS = NULL; diff --git a/src/ui/SettingsDialog.cc b/src/ui/SettingsDialog.cc index fe885ae37..8200c9f69 100644 --- a/src/ui/SettingsDialog.cc +++ b/src/ui/SettingsDialog.cc @@ -28,9 +28,6 @@ #include "MainWindow.h" #include "ui_SettingsDialog.h" -#ifndef __mobile__ -#include "JoystickWidget.h" -#endif #include "LinkManager.h" #include "MAVLinkProtocol.h" #include "MAVLinkSettingsWidget.h" @@ -41,11 +38,7 @@ #include "QGCMessageBox.h" #include "MainToolBar.h" -#ifndef __mobile__ -SettingsDialog::SettingsDialog(JoystickInput *joystick, QWidget *parent, int showTab, Qt::WindowFlags flags) : -#else SettingsDialog::SettingsDialog(QWidget *parent, int showTab, Qt::WindowFlags flags) : -#endif QDialog(parent, flags), _mainWindow(MainWindow::instance()), _ui(new Ui::SettingsDialog) @@ -58,17 +51,10 @@ _ui(new Ui::SettingsDialog) move(position.topLeft()); QGCLinkConfiguration* pLinkConf = new QGCLinkConfiguration(this); -#ifndef __mobile__ - JoystickWidget* pJoystickConf = new JoystickWidget(joystick, this); -#endif MAVLinkSettingsWidget* pMavsettings = new MAVLinkSettingsWidget(MAVLinkProtocol::instance(), this); // Add the link settings pane _ui->tabWidget->addTab(pLinkConf, "Comm Links"); -#ifndef __mobile__ - // Add the joystick settings pane - _ui->tabWidget->addTab(pJoystickConf, "Controllers"); -#endif // Add the MAVLink settings pane _ui->tabWidget->addTab(pMavsettings, "MAVLink"); @@ -113,11 +99,6 @@ _ui(new Ui::SettingsDialog) case ShowCommLinks: _ui->tabWidget->setCurrentWidget(pLinkConf); break; -#ifndef __mobile__ - case ShowControllers: - _ui->tabWidget->setCurrentWidget(pJoystickConf); - break; -#endif case ShowMavlink: _ui->tabWidget->setCurrentWidget(pMavsettings); break; diff --git a/src/ui/SettingsDialog.h b/src/ui/SettingsDialog.h index bf1c85eaf..ac1a31108 100644 --- a/src/ui/SettingsDialog.h +++ b/src/ui/SettingsDialog.h @@ -45,11 +45,7 @@ public: ShowMavlink }; -#ifdef __mobile__ SettingsDialog(QWidget *parent = 0, int showTab = ShowDefault, Qt::WindowFlags flags = Qt::Sheet); -#else - SettingsDialog(JoystickInput *joystick, QWidget *parent = 0, int showTab = ShowDefault, Qt::WindowFlags flags = Qt::Sheet); -#endif ~SettingsDialog(); public slots: diff --git a/src/ui/map/MAV2DIcon.cc b/src/ui/map/MAV2DIcon.cc index 8f3bd0177..9446eb350 100644 --- a/src/ui/map/MAV2DIcon.cc +++ b/src/ui/map/MAV2DIcon.cc @@ -5,6 +5,7 @@ #include "QGC.h" #include "MultiVehicleManager.h" +#include "UAS.h" MAV2DIcon::MAV2DIcon(mapcontrol::MapGraphicItem* map,mapcontrol::OPMapWidget* parent, UASInterface* uas, int radius, int type) : UAVItem(map,parent), diff --git a/src/ui/uas/UASInfoWidget.cc b/src/ui/uas/UASInfoWidget.cc index 3fce66945..18e954c5a 100644 --- a/src/ui/uas/UASInfoWidget.cc +++ b/src/ui/uas/UASInfoWidget.cc @@ -30,17 +30,18 @@ This file is part of the PIXHAWK project */ #include - -#include -#include -#include -#include #include #include +#include + +#include #include #include -#include +#include "UASInfoWidget.h" +#include "MultiVehicleManager.h" +#include "QGC.h" +#include "UAS.h" UASInfoWidget::UASInfoWidget(QWidget *parent, QString name) : QWidget(parent) { diff --git a/src/ui/uas/UASQuickView.cc b/src/ui/uas/UASQuickView.cc index b52ab4a71..93e946f4e 100644 --- a/src/ui/uas/UASQuickView.cc +++ b/src/ui/uas/UASQuickView.cc @@ -1,11 +1,14 @@ #include "UASQuickView.h" -#include -#include #include "UASQuickViewItemSelect.h" #include "UASQuickViewTextItem.h" +#include "MultiVehicleManager.h" +#include "UAS.h" + +#include +#include #include #include -#include "MultiVehicleManager.h" + UASQuickView::UASQuickView(QWidget *parent) : QWidget(parent), uas(NULL) { -- 2.22.0