From f4356e05eafa5e8a0d5d9af80a4dee8835adb772 Mon Sep 17 00:00:00 2001 From: qiuwb <389791945@qq.com> Date: Tue, 27 Jun 2023 23:57:33 +0800 Subject: [PATCH] =?UTF-8?q?tello=E6=8E=A7=E5=88=B6?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/djitellopy/__init__.py | 2 + .../__pycache__/__init__.cpython-38.pyc | Bin 0 -> 302 bytes .../__pycache__/enforce_types.cpython-38.pyc | Bin 0 -> 2100 bytes .../__pycache__/swarm.cpython-38.pyc | Bin 0 -> 5389 bytes .../__pycache__/tello.cpython-38.pyc | Bin 0 -> 38273 bytes src/djitellopy/enforce_types.py | 65 + src/djitellopy/swarm.py | 159 +++ src/djitellopy/tello.py | 1117 +++++++++++++++++ 8 files changed, 1343 insertions(+) create mode 100644 src/djitellopy/__init__.py create mode 100644 src/djitellopy/__pycache__/__init__.cpython-38.pyc create mode 100644 src/djitellopy/__pycache__/enforce_types.cpython-38.pyc create mode 100644 src/djitellopy/__pycache__/swarm.cpython-38.pyc create mode 100644 src/djitellopy/__pycache__/tello.cpython-38.pyc create mode 100644 src/djitellopy/enforce_types.py create mode 100644 src/djitellopy/swarm.py create mode 100644 src/djitellopy/tello.py diff --git a/src/djitellopy/__init__.py b/src/djitellopy/__init__.py new file mode 100644 index 0000000..9b5f121 --- /dev/null +++ b/src/djitellopy/__init__.py @@ -0,0 +1,2 @@ +from .tello import Tello, TelloException, BackgroundFrameRead +from .swarm import TelloSwarm \ No newline at end of file diff --git a/src/djitellopy/__pycache__/__init__.cpython-38.pyc b/src/djitellopy/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9672832158fdafdb052934847f08d3a23c1ff2bb GIT binary patch literal 302 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 E0CBW8&j0`b literal 0 HcmV?d00001 diff --git a/src/djitellopy/__pycache__/tello.cpython-38.pyc b/src/djitellopy/__pycache__/tello.cpython-38.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d6f457bab5548ad22b1658c28257bf0760e4311 GIT binary patch literal 38273 zcmeHw33wdGb>`eN7y=*&9-=5&Zk;3~0tBg>rX_(SAkiWL3IIiEEM+uA^?(BoX26~X z3A6zDAZ^RB8|RX9Id|eX@&5m-?w+2( z3`9`M^?f_Q)O2+n{p!`LSFc{ZdVQj+D;vS*ZD06im3eO@@Z>8D*YCIW?QkrDrp_jKq`Wj@fK3JG&vbVYV~Z zIop-%!u^y=m%C@XbKR0BR^FJ~DE0N^HmOXZw-hOCHnZlIt0U%C)o~zV#%5x;${vV3 z7*QKk=f#NXG`C%d=dLolai>dl<4*U5*oA0rJK`Hv58^!+V!5kTY$DRP$+_cb>6BHl z7L8)nGD;QOv>DHj4wK-F=>{|RYO_Tidh@TzKFI*M*E^}QkiGx z&AynMnmlcp1?47=R;SMNCEU!|oL#C`3S~EWqJj%IF}XNrx{1T3Df`7JN~3U>S;0f6 z%)E`H+K8Ld)w^9&<&s&k^EFgu+EaX1_`}i<;9KiNU`BxV2yu?q#N2Lyb9iCOl*f%AO6JlkmCLAfu0v&VS=Eu- zpt8A6wISD~I&rop)E$LjZ>d9?Jxh-l_Zma6eZNu+XYIANouCGSk9mso) zdF{%pE=6+J;rG>Q%cW?}Ft1y=-ZU;nqY>P@L2bpo8_gS6ZZfZvdpEA!Y+gHa3$N9- z+)i~>ZkO3--fG@v?l${Dm%Gh@t0Tpj{+fH#_5%@hwc2qpV(vT~0UZvSgDZR0HF9Sc z??_G8=JuJlt5>1?9(5f`ygGLWzITGwJIu~vTp8;6i_zR&>IQY=#YpaMb(6XozxSB; zs$0~~i>!A)u6Ln-UreCH0rR!0|M{pIP!9a4u8d&r9os}aPGm;(aKgX$r~UgyP*sMjI(uort+ z9YyS@SMHb^MeLXt8&k&-8})KNqQ(&$^U@~NBx1+C*a`J0Vvl&SljvF z7kj;W0SLf9NXyqxjs2uzj)YEDSzo*o)dIrB!>J93R_*Lpn>dp8y)dh7CzeV*H z^;Y~&tGB6V@q1cecspSFdi4%Gp#+HDiFQ1v-i6qV7kjsQ4`OG$*n8Fc5G#AJ_p1*e zHtWT{L46RhiWmDv^-YLXz1WA;HzPLZ#lA&7kJwpYhd8T76Xgccd)TQ`Gl7pH$zg zz7Ka7>vz2pAA3HdKCZqWX-;FB`T^ASgX)Kn^0fLN>W2ZrlKO=D5&SNzA5}kw-)Fqm z{J8oFeER7dG!m3Ur@iO zK7*Ps;_fd2GyhY47I)u*I(`|kUs1n`*jo|%HN<{h{RU!hLr&uGH`Q+;_AJu=7h=Dy z{x@Q8M;d9_RlkGS*L$(gsozEH9bWAJsLvzzPA~Qa^?wn2&Wrt?`XXZQ@_5Sct3N>O z-Co)ssy{;PJ>CfXWA!3p@5R&kyf3LgLF|2A>`&F7A@+VR_UGy^5c_}^`?C5=#J)lO zmHKP6`h)6k)ZgOw8`a;bzsK)4scSI@e#K4799Vc9^W^bG`*gKpoVN;dbEak3)nJC# z)4P4zTs6HL^EFQYOsW=(rKwV(?9Kj#xza$^z(*cn3>`mWsOr@GEavJ0C;R=9`1-vA zw?Db-v~ADT_V3xF%KcNSGGIZ5u&Sr3c4?|MFjbx1qpIgCC*IRH#J*Qs%$!Uzy9W3W%HaVLQ7xmN#o{SMaU-5;x2sAsrR5Ryd@tZm(is+%5f-Gh6tZ+z~kyZIo*!fC5I zUr{3vCe3lm$6HSYH}gf_%~Nb1nIanej|SdD%Mal$(J>P(#+D*^2)YR4OT13l$(7_K zpe&yXuF`g<_N?8p(qVVZWbxf_DOTvTJ6F0?OxHnS%kIYYMiqZ9x|~pnr9{4GDY3HY zQp8K?l@th^OG%_8b&5(9GSlcFm3%&CZ(fQXz;kD|tZY@Om2DcnOEK2H6bC$4sR&q6 zdUV94oS(7_*0gC)LO$=X%-USFQZs9AQV7JkTe=5)3DL@wX`C#wXo$K`A_@=62L(Z;e5rm23YRZzX z%ZC7UQ+m1LriVuIM@EMy-R#8Jp@;K_9~>V#W^t*4aZ;)%i;ThDIGa(8RS(YV2IaOkKFqt_MD1KrvEljr6l$&AgdmdcjHd9Hjb>wsSWmPk z%6KL&A?|d?w#6L1HX8^EbM*V422OA-?kS(%Xls%;f+e|T@x5Rnc_2VYTqEI~ z0ix5{sIy%odUZCEY4C~TiW(ll_OrNfUS|uHU5Y*tTL#XTpv@87$7bRySb47`m#{(= z))uu>XVTVPCnIMgc8Wct5?4o7(n}Gh@@^txXU=4;H%24Nv1r8EZnKV#C86Atk;fw+ zPMy7YGIA%F=yL3t*kibJHu4z0Dv3T$Ie#!=+VgX|D-DvhkA1I4HKvVnsb-rMbSpGA z^=_;dwOm$5@3HxsZJaUvg^Y zz5XKSeCc}Umcs?R;J4BnsZTE(1&~cx1!Ucaej4q|YBrhyR?cxy%LHJJA&q*dQ}ff- zBe?cBBqiR8aytTDkef6ut7<_blu&r85S<$}o5;+17*%`_-x`IxNIDknj%A{~_$I)^ z$k&`}G@Z5N;0>fk77&oy#}NUc;XA|GJa!4h2KELep`DmX7KKEdN!&>(T!R#&%dw@H zg4$v^h8*dYj7n%y5>kxWS&-tRAUb0yy0SqfFGayF5-Xh~Ry(@{5>}}LBuWEB*#)Bf zZV+W$h?2E+FU2*{3Q>NK5M_Ef{!Cnm(g0B!Dh;B{IDfB+QgevQBg)m>gxwYJAB{ib z%E>dRiHnqfEi$zZpvNpqlbTj7@HtNy1$4ghsAK=z(nwx?^HE&fKCbA3?D4qyw{Twbp z&5=uy<%k`fiILJ*5@eM^zY!HX6SHn9VR9>x=|)zP3KV%b^5lm}_lPAx(Q&ZZ_=TuS zo{3n;&)%^VS1F8#1b)-_O)e!+bNWom`YeB!z>JquPUli;DS08foIDw+9Q7FTFM;;c zp#1?4k_;e$bi0(gB%?s?b}XesBjOGj5gp6vXVM%I6)z(r+2(?-g_1~*nZ>bWj9tm^Ofjpz7pMUT@8fSmy*t{ z!{l}biH7o!9+Kw@<#|))ji;AcoZvpa49-f4(<;r?Icl8>ku+B>*;tX973+39#LYg+ zYNUQmgBeW|Sxtx(LENM){H!ws>TVWF%CmEs%KWV8`>j^wX(H#R@vZGc5J~!sHxccI zXxSN!femM35Hh1N#CrJ4HQv`!vCim@ST^Qt(yS#kQu?A+5mhzNX%sgCfs2y4rc;Vs zB5Hltp+IIS&R4J-0&$4_YTJ57G(Bu$#w7#t*x9%Y2#8bC{Xxfzu*LcWA=o>i1jf%R zKoNp=wD57X;f1Kks7(Xj-9&L{YTmMBLxoFn?%l{OVJC_H$#{LPUBAHL5QyyDRbsj2}&iS21qnY zC}~oM@rq)`F%D4xg*<8-Xr0$#b5<2fxsiC% zG%lU_WnzL%FOtXzWjb5y_zdM+MSMi1ppnu6(WrndA-zu@E7fYGx4D8cjvR&tPDsq_ zK*-g~v?2XtRWn=97?9K$)nA2kg;Ke2stomAMboAM)fniq2zgz^aqd5j6`$;wF(%%9uEI~5ElRfk$S2w30VeFV>4)l zq#Y!rJxF`}KR(i~ze1!fZdeD>7T1Hc+<$ze-Ef6Sb2hI7 zY0f&3Hu@m=vS^>V236*?Wtsv1XwQP$m9TDHvwR%V+t@B$quL*dX(4k~s2=1;r&XKYX zuwVOF|Ih-aoryVU^)7=_AZvv3S+}eS=>|e7CGrb{A!t2`GL*U=h#!Ah5bwNPh!^i( z3*z-~ve)Kb7R0+Q7h>nWwIE&(8#(4t#n9B0S%&cvrS-LmL$-1D8--Q{tid%~AW}=X zV18jZ)R2${vN6*-Xy%wlGp|s4_O7!%i$^b`J?o*UV;)7lLha$gu)V;vx__LB%V^I! z=xv?~A@Wb`&tjv<;I=5KZr-|_$!m2XW7ns??@(VruvXui?b}8^q$|mXo}3G{@!C3j zU7hjr>2RJJCbpAX(CccbH@sSuYi=tSk?s5PDz??DCEVCTsIjlAH?~F2hSqY;6)gH? zhe<1^sCtS*NhQnJtFRKgMw9?S`wQ5(#y0xpceKx5R+npTKQ%Vcr@fhIxWH>~0V*^g z$Z1+M$pSRg@280h?iN#3l8J_wpP^h0Xh4YwQE>?0+N$N}arU^sP&#d*>um}Np%qJe z!K`I=Yd4FlI?CXi^Xu@>33I2M?@DyL%37i}ox$HJEZ%{`gBCOli zY*`P1_lxFXHL(zC-?lp0hO@M9WjCJX>j_kB3n?`2d^X%tnqXji2rK+qvrq#!qE${) zJ7rf88%E|Vmi{$SEEG=Fs^xj8Vs!7-bubeq?s+ymW@X@f=}c+PRHec|)tcVJ*q)&> z_sa3afO)m2jmv?~+Ji>EsMf@47qJ>t_J#nZX@8fsA-z35R}p@dxTG?VZIX-Gs$I2e z#%YXy?4g+z$O|?s1&Gob6!Neyx{P)g%QA$dQWgw~hz&yfaUla;7Z$UFXM>g|!-G7O zV;yZBYor?+UASmq5}TqO*YxE~V}t#B2M6nI>0h(YM6!mt5FhjIP&-1BwU^q^1~REY z-N@Yvg$=Hy=nTfe3@rj;D)wBATLm%}4qk~iwQ)1rp|u4p)*g7eG`GC}Y1#%X!(t^? znsXCkQ4tp{q1*B3gzVZJIW86h9@EZsdsZj;+SF-tR&OshE4{XoJh`VL)(Ls9#0QBK z;o|W`v^UsaY?=2(1DKd1Zu}2%l*CsUI;A6RJ#Z#t?e=E2xD8_=nB_oCdjT6Tw5piF z#v{#rcqMwrFZlG|!e+@gBna#%rl760pyuXb0Paki6}|aC!~Nz8O)jWzgU&l3k@~iC zUbZkk-7I&bU!iEx*)CVr($ccE_X8($K_-;O<8T3F4*up@RCerci*g^2>XSKD79 zFZRsz9vf2A3VUh30fVk*Ibmn8lN?>ia>osIz($Ouu+n)+3>vVnmaKi$duC#L1ls+C zwW5-&2c`_M<&-?P3%h4gluJzzzLb*K**ULVBp+Q$u5>HxsV%3S?xpk!?9FhU!LHn0 ziHO~ECi|YKmB7AS2lnN*NsXJ9I)WXzgvv}rRL641GabSHTV|zKWm$*hPOA+9QO2`7 zL;k)_=kpWT_4iFcNnO$nV&ihYP=eVEPr2B&z;2>G<6Q{{X|iY0RN1IEKUJ-k_h55v zKlUK3M$;R}lnb!w!Mre!wP>xj+ZZWOk$xT{%9yWscZ>7oave%%_enH^8hq^Dc@q(v zX)c&koX0)t;tt2az<|>mMu)bd`$}@&F81FeHWkjnp;O>Ae%r0L3T- z2=n$4>QA?LxJ7S~Z}qKFpw8o-wILQ4qfV?V>uehLtla>a2BdGZo&^u1yt`@g$g$!4 z!O4^PLu1E|4UHb2Xxa?NHe}f}=d9y=8aM4=aEGq8Vg6=T_%JE)y-n_Uc4K&0yM9j4eF2rC|$?>z`n*xD^o$B(Z(c+jSg#HW&(6 zLy|@)NKKJ7s5pjJ!g)iOmg+-`jR6}21!8YcNU2UNb-H~8Mwipq0Kl$FwN8}!;il@- zq-A{ojT`MtYZ?-gc?5TaZme(Q)i*I9YsUht-tkRr{+d@juIWDYmW6N<^o_DCE_%F&dV? z5FM2c0&?#{CO2^&7K|XG6-0E;_40PdfoZb>#$es|cm7yS@Ib(=q3)vby>I(HZ~&%{ zn1Jsa2ns^Dr;(ML93CGZ8+Vg3KRdf*`q&Sl6}u5K&(**IX>EYh8J1XFngy%^6c~^Y zWE@kB+ra~{dg7tUqOLezs4QCFiKwsp5o#$gU3k%V^GaH?NqA8h-Y>CLOp7G5F>MfY zU2GdBh(ydc>{wN%VLBkH`8|ZMROHhjEDpI?fJwp&QRkyClS;%$N^2YZDUy>%p@OBf zZDQaCGWshpN4yZTK8DsO_^S9rIHy>DHG9@w?pU-gLvXbl!)Epx6HnbyYv z1c+83yq5s|Kntj@TKz0hDJC#F^n^pWJk4jDyr8CftA+QlpZyPKEv`4E-wj-76Z@;M zwt($UjKToc6R}IMyo3FYp8Vg0b5OFb0|kPOvipREdg36WVS&|WC$D7J$_XiF$e$g^ z{pGMo!*e-7(5#J<%z$weMn+hY`ee7igN11L!GH)ezDPkRFXIulb zM>{SPew-7&HlqYboV3L>m^oO_*&Vu6P1zj^W{~JWEYSMW&V$3W*Eevi&cGpftlR5G zZl_G3ja|AJ*c<61FNSaa7BXAYSlSBUoqKoUC|{)lsc%=`PGDIdrGZr!1Y_Ug3a6d0 zyw|ozp*2GX%lEHdWr)=jjGv@bpl}d|P5W=o!`62&uo!G+Ak4IGGt0w0)(08z;Jr7) z=eoK0%cGgvek+$4A3i=|eFQn&sNK}hT%t>>wy>``4MpNO)XgUiJnp@T_{36JEGaHC6VD~wwS3yv|UajjNHtsf=it-$Z1;0+%R$cIB+w}zni4%sxM zuM_w_0*_NOMQw5?PXQkjFlERD%q*ADpcad3z}g*#rC8h(0@hOvHD+iJ?G~V;&M3G> z$opLR89B785ARsj^Taxj-FQ^Q{(u_#&OmV>=7l1}$ig(#LY(`k zo=3k;j$%qBJ-HpFT3&Z#%f8vB&%)uzrxeW-7U1#@wp*4-am-rY=49)vt1!S~GDlJU z3%M7pl9t+3{BltWJYZD>=9YA>8Jf4NI06U5Qt!F-l_wN!8Wii>QOzV3figbGJlu_w zLk|y+jf~`}ei%D3X?+rRTeZ*n6cY9ajY;+d!{4iY74&fw5a`$FzYyHs;10o^;H{BS z>nGcR^fO2}7=T3lX0>R`f>#1h;rGF}I1D(3^$9vit~f#oIvOw_0T@r&bZ;`ysQ!FP zb0jETRA{1jR8HVc_~mIZTAQDvCLMwV>K7Yp)>hLBFA(O?aT#dFuF^or99Yz0Ld965 z%t6To4Ibrg;n7+sk!JTlCLiNuX{2OSY%B`S^wrUMNZUYz{YYkQ6RWCXA?(4`++@Io zRcHN4!SJB?Dv#-}nDU%~Y>;;S_17DtRoiUT0v^#!#g|0bfSo5QXDU3<=PkW<$^kmr zZ$ka^b1?DOJ5d5}P``l>X4qL^Ta@oa1*9(=^HMa+UE}cnTpby1M)zyAg7Kiow{@@` znf)vh9ty~81IWxXJ@e~mPimZun^_mt8Y+IK!@8AMdLKdtGwzC>=>|=K;KR)%7$Ctv z+6x9V5)5c*0CoI0`D#P&*J1{-eh$fxvMpSDV_L@&p2rD%LqM-Z#bWy=tudN!p1@;; zu4R6}Ip*0}?wMlDEQWC3fxTG^OVg=Ez#g+c3_#kmou5bQQvvch#yy+Gpc0;{RwYPZ zw0yD*M0cu?PNkkfhW>y*u+z1y(3jk%i> zHwQTm49Y=5tiU>8judh&%%CV-(dfpp*>6|3E>on0=&tt;XJ@E0BM5PW@#!h zTRmqM**W+^q35$--i8!r;9i6H&s@QrMop6L3hM5HPPlIb=qD!8u9w8Z>UOxX`8i5} zpF!;27D;MxQx#$fjlWC*8n{$c$HiiL$G@m zI`zbL1Aiz0-s37)0)E;nyG8>#1x7F5J3>w96?QiePF{B-hJ!|Mhr3AtYQwr(4B7-s zuh9lg_{{XaPy@n*&$Lz0Q{B)cfs*ZO)q%(KA0DsrP#9Q_w!p{1Yn=Z+4;9bl^vB-* z+wZCyf;h>k^JJpdFLKxg?%DG7`sTcbp?Qv7O;K#fn+4Jm(0FR6G4JGKN4Ux zuA|G1mN}LFE{VB?Ea`UeaWD}n+TR<168FWkAZ?xS zToZ<;iLJX)Yt;IDJ8-h{NGb9IA$UAaT~AymK-&bMRXo~_Liy1amQHz`SLhQVXgsDH zPF^QG+XWtyP7}Lzqo>w@NOu;C{X_^HkA(_2LX8P6EnQC}-++LJpRrLnMONy6C`|_v z^>v8A0Ehl+V$jnjE+t?-gFW>nIAjQfy3cc(0F5mei-rS5X&IUdDEvIC^X+Zts&FO> z8NN<|a?%BixrmeHdI!a}V2?<}8Bfw;?Tbb8)a;(ns=NK9LwUDB=A|Tzqp&USJ8kaE zQ}-Kt8!Z5x?rEMQ5B@1eKLW&vqNOht5<>&TT7QIeH$FA1nx(wX>1uvq(b0!-ewv*{ znK{w{jf+P;;S7CgH@2|XjVv+};67tk%7L1jKJYa2*!oIdlvS=NO13- zMf^B>_W*;?TEIP!014&ADJW8CW!sO1*}g$~fZ+pcGzFvu&TiurOiV=4!B(NbU`XfE zj_CKcq2h&oZ4wsSl~`<7!f98+X;-2?gu=pjs~}db$b2J!Wqe90XR5j=y1H# z+sH1BSqeSU$?3MdQqbPAYkR`{5XZ&U7lXdd!ZgW0Y!KqA%+!~$E`yX_jozFh&-6dk zoAE1seG4uc`*J=aN8NSpS!`$odUl|fIW}4j$jwcBl*8@I!I#SR*SkYb@f#95<~+9$GxUdvLVTF$N2561-v3BG)>>z2$JJCN6QpGr($ibI|Coxv9Sen;onC=IjVT z>|wZ7wky%+6zDhs{`Xqr8AlVnZJ&Ww{(R&p4wj8oh|7U2&K(%68^uOvz&Oca?co7y zN)g6`N__`ETx}LDzUKp5R$LYA9el{q-nU8XkjG7n3PWwK;uAq*k$osq=O*ps_5I*1 zRPW2m9{WV)#ofkUf7c178FFJ^Gnhf!oUYjbcnq*L^pH2!aDJ!3v5pTm&TM)(eRa^S zX{S%~>&qy0CB5v7u#5GAUbF<4yQFVkhG%pgYc{=ujgWl1f(8bTP=Yjg_1;X)R$lZ; zdFULfQzd)Rz1q|1;1H%(z|@m^=I8S4y!xF^G9kZcg9@c##X#FF8-FSa$Nscjh6=@w z@=!K6h;jZ17SA|=^q7ASKClz8fW~R03sAGrP8A_+6%pd49#Ft=wL#y0Q7B-CRb}?M2$g&@*oZ9;{#z=NUnR#$gAABMq427I6umwZZX}@k2PFqCHOg zCsllNnwKQ-78u2}g0qy*;H!0}d$hVVplOb49JEpE!WfjeR6D~PFC*JrQ z3xvBaoEi7)8KI*!(0Q#r>``Slfv?!ivv+A*x)i~yh{OPHwDkPfzV^Kew z&@+9-`XyG_w0O--m2m(EIJzew*{HMAACvIcYz1Dr>qgvu8K)a6jnKM!6bh+3_z)T= zSi=b1YuD;st;zZ@@eKnEt5BD~e0fG{63#Z)7T%lH@wR^)BKlNk$9>P}~d-`iKoYkWdDNln$17+mH zWHwZ;oGkUjP-bERZszC$+OJwpbb3ZI=ixXS(j3mPc{ZGUc1YXk;M`=fq_L}4urGMF zw0PxJ#7eb83cPIFc^1u2SvUq#p||(iFfk*#KKKpyWtCPLUq<8Kc}SZL$+TEUg6J4l zV{3JWi(INrW1A7F?9B zqWz7Yp%Z)v2*w@`u#^OrJcol#c|b3lwEsbEQz{&&M zYq#WuWmrAYt0fjCstVO3T|Ih+T)}hTsjwgF^8!x%K)XsfcTJ8R9=pEI!aOJN!XG>x zj7Py)cE*Nw6pZ5-cHXo85?t0M8al@Fthp-JItAAq<1+X(ro|LEXd7Mda$w>$Y}$sX z#U7P~H_Am`Pry8hx>GYvT@tTmNS`WUO967bq=SF(gZy>laAv8U5y|PaQ zgqDRlScuRXa-nmxFgK)k4=l;9w11tC2c285?B(@usGhaDczk$b?C6QfBV(iaJMI}g z-l7*nIqu#U%Hi|?&!ML@qq?UZs%2KvNURs14X}-~{oz@uIU^7rFV5Aj7}Gx-U|ReI z)aa7KlPq5z!T}UEd$dlk)nFPqK9Rq(1%7#dZ@YxsLuL_NQt%cHr>D z@!R+D%({LCbP6+iy(NeIcTq`Em!q`nizX{wY-dvo8g>@sq(t{{KMZSrkd)Ix zXmK|{)r6NdNyhiFMIxLeeedp})O9rC1N3m?iHxvbZuWWvQMCXnk-B|a33WMjU_A8T z7X%xaufbalgwG=0BNaNt+8z;NGeN_Fn}dp-j920P)>Z84R@ms$?e#K#D8NU`XO#`< z>%hndACYeLaU$A%dcWFL3muy{A#V?m^B&^MO59PA3pO^tPRQ-sKcU@b!C81=j)zIr zyd|ndskdH%ljMf~8VK2>8w{fz7^Mq5mqJMK{<~=9pE~O{4&kh z3HrM|a77`8`G>$=uJL#J+{&i6tH1D=&kIQjA$Kfj1k`G^ok$c3QxT-d6~;Y?RV z6@&fvhD3=Z>szDIUf+kF`ms>U8~Q$!r=j^|CMct|B~--typFHi(vDDY3?=4#L}TH| zu@m$}QL^XZ&xWtvXoUrF9=%&Qgm`%7+LvPn+OlT->nJ6FZ>DOQM50VbG;*qjak~Gkrmd_ z$Tw(tJ_;8&lmdr+o*PC?9rcN$K6>ieu8@!#=9wg`yI=H+!drb*>j;QG;lNttRsW3aHCfHEqEgn-ei*y_o7MXZA46Pbf(EYWg7(f+38Ml z-DKstCB0o{q0op+ptAc%U;Gy+V_)RVZ+$-kA6d7xKvu6tL>QRQ1o#TQhi!GX29#mC zs}I9fBF6!u$5T*_jt8{fl7B+I1m{|nx*rKAN!21n3>gh)TQuTT8ah!y)=JZOja|M{ z#U55(97z8r+v5oxH)@r_n)%n!un{(lhwvYT@EShIT@O$0v~q z8!+;q&xP95z$Zg_*3k~}AwOYSd^_;y{OqZhl7OU>TIYA-waOYz*{o5I<79YN9*V&3 zw=`nEQ4Ssw{E)W*9Bc`zT&+)I7_4z#9VbGob36?56TQPH>7qoQ8TZ4BuFsVQu@LB(J$DH^U^N-MPV@9l%R z?nZ@3Zex@F!%df~)6*D3Zr6mIVt)u?9bPezOFsnvgh%oE-Hs7?@h+BsxelmRjtxyt z;%+CF$U%b37mpl8J@A$@20XY~czyLM!YOQ|@r~YY;>hU8Sgz;D=wx2M7jWdr@X^B) zx!#eZV?&MiEDk#Bdl=9N*WzBf-q55=A$MawkL_&F+Nze%>sPj=^m%ek*h!WUivJ|9 zPB1vhK%6Ja>jXdGnXK;?ec?Js%xPq`822V3sVzA8M z83u1)@J0r2V(?}L7Z_Y*@D>JdW$-M6w=?*92Jd38@Vrws2L+L?n)^rBGs zAtrw(gHJH{F$O=*;3pXT6oXGOkYWF`ypm!6^St^DgI{6rYYcva!EZ76Z3Zrb-(m1M z268Uw3%vS02EWhX4;g%k!Cx}?D+Yhf;O`jx1A~8L@D&FC#2|s;WMvq@4=NlKGT^3- zwUNOl2AdgdVX%$Cb_Q27*uj9Wv$3va@G1t^F}R+=jSOyLa5IBj7;v{nvlJQ@g}9mU zHOj)WcDM!?HP!^THz_*43I5`^qAMK!w*HG}u%}ITm3U!aGLcQh3^2nj@g6A^i{kH} zvo~f^nQSbY-HPvySoEgryVKnpx-;Hi8nM-X9db{H>7LGQySh8J_4M@iWO_2ane3)a zQvOnz4C?OAWHYHAFK?)>jF&69rE|xYZC%%H-O(d=kQ1()zSg;|C&l_Q$?mT1j_xdA zA{;(k9db`YmO{B+!4;WX!zYf|1Nhb^5txx&R7G+z70ty}uS%%o#b_>}QYw8hl1r+L z>cDSGWz`1!rp=7%R9zRbPRMnbcz?U<_G4OqbTY7=I_&EmhT<%_>cQS6Zx4IL4FTVsAzH zZK{I^evVJLN?uIgIa=Bc>Mp&9gMd>v^vy|S!#|&M9bOYptrTR4X{&Gwnjd+OIGbOC zLtXJ9j6WEKXSx5QQ?OMFR43678ck9L~b8gZx zt!m9p&SU+54_>6nXInAmN$JkD-h+r<+t4bv1FCGkt`gHu zo<;E<Cz4(?MoU8?3vGuiluFV$;kEFa)g8n8(62oh5M!P2f|tSr_Ta;7wp+-G@(YPU z%a$Su#+=dwUgWF=cR>y8oWyO4Za^UL9pTa6m-ni2dg^$PypI`xrIdO5FT;qb33Nl62lUDKC46_YTrQ z;GTnn!V(9r*rU><==qx=pLr#*tp#)pN*PB^5vQ7I-XYF$%u02B`n3OiS`CP^X$;!k z##|XMzrYR)to91id|4|}OBL7{@vb+Nu~F$cyn|#Ga^k+LkMCQDKqsLay2c374S@JdwnGs~VjYP8r zMIwn921zQqJ=TMP!Wk6DcZ^nK6dtY()P(Bl$8(M&LMnJSTH$QR71W!T;8gY`M%A5> zMBFv*ash)3nvyCG?Dpfw_i$O;f}M?mU)sTC$(Hd|xiTmf1NzI#<~d6w%h zfF>e@Z;ufhuOT*4x@j%B35jKdZ6H>0c1;@ zQik(5zlYJFV5J!okgVf)tgrl=L&2tg5qf`wVBrhqoQbYNZ@{qGS#&I#_x%f<4zv^+ zSO03f8a}O}GALVOmdpA_*#wVIZ*N6KoE^dPy#;h<)9Tr}m3=7dCYcU6iY@jl3^cu0 z3=5{{qMjR#qI(IUvOZ<*2QZxkhT}gIO(e6MA-`@C={TE+W)mXn_3hA4q|l<@C_jMg z?gkhg(6#|;#F`cmI^cT`LLhjd%) in YOUR CODE + # to only receive logs of the desired level and higher + + # Conversion functions for state protocol fields + INT_STATE_FIELDS = ( + # Tello EDU with mission pads enabled only + 'mid', 'x', 'y', 'z', + # 'mpry': (custom format 'x,y,z') + # Common entries + 'pitch', 'roll', 'yaw', + 'vgx', 'vgy', 'vgz', + 'templ', 'temph', + 'tof', 'h', 'bat', 'time' + ) + FLOAT_STATE_FIELDS = ('baro', 'agx', 'agy', 'agz') + + state_field_converters: Dict[str, Union[Type[int], Type[float]]] + state_field_converters = {key : int for key in INT_STATE_FIELDS} + state_field_converters.update({key : float for key in FLOAT_STATE_FIELDS}) + + # VideoCapture object + background_frame_read: Optional['BackgroundFrameRead'] = None + + stream_on = False + is_flying = False + + def __init__(self, + host=TELLO_IP, + retry_count=RETRY_COUNT, + vs_udp=VS_UDP_PORT): + + global threads_initialized, client_socket, drones + + self.address = (host, Tello.CONTROL_UDP_PORT) + self.stream_on = False + self.retry_count = retry_count + self.last_received_command_timestamp = time.time() + self.last_rc_control_timestamp = time.time() + + if not threads_initialized: + # Run Tello command responses UDP receiver on background + client_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + client_socket.bind(("", Tello.CONTROL_UDP_PORT)) + response_receiver_thread = Thread(target=Tello.udp_response_receiver) + response_receiver_thread.daemon = True + response_receiver_thread.start() + + # Run state UDP receiver on background + state_receiver_thread = Thread(target=Tello.udp_state_receiver) + state_receiver_thread.daemon = True + state_receiver_thread.start() + + threads_initialized = True + + drones[host] = {'responses': [], 'state': {}} + + self.LOGGER.info("Tello instance was initialized. Host: '{}'. Port: '{}'.".format(host, Tello.CONTROL_UDP_PORT)) + + self.vs_udp_port = vs_udp + + + def change_vs_udp(self, udp_port): + """Change the UDP Port for sending video feed from the drone. + """ + self.vs_udp_port = udp_port + self.send_control_command(f'port 8890 {self.vs_udp_port}') + + def get_own_udp_object(self): + """Get own object from the global drones dict. This object is filled + with responses and state information by the receiver threads. + Internal method, you normally wouldn't call this yourself. + """ + global drones + + host = self.address[0] + return drones[host] + + @staticmethod + def udp_response_receiver(): + """Setup drone UDP receiver. This method listens for responses of Tello. + Must be run from a background thread in order to not block the main thread. + Internal method, you normally wouldn't call this yourself. + """ + while True: + try: + data, address = client_socket.recvfrom(1024) + + address = address[0] + Tello.LOGGER.debug('Data received from {} at client_socket'.format(address)) + + if address not in drones: + continue + + drones[address]['responses'].append(data) + + except Exception as e: + Tello.LOGGER.error(e) + break + + @staticmethod + def udp_state_receiver(): + """Setup state UDP receiver. This method listens for state information from + Tello. Must be run from a background thread in order to not block + the main thread. + Internal method, you normally wouldn't call this yourself. + """ + state_socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) + state_socket.bind(("", Tello.STATE_UDP_PORT)) + + while True: + try: + data, address = state_socket.recvfrom(1024) + + address = address[0] + Tello.LOGGER.debug('Data received from {} at state_socket'.format(address)) + + if address not in drones: + continue + + data = data.decode('ASCII') + drones[address]['state'] = Tello.parse_state(data) + + except Exception as e: + Tello.LOGGER.error(e) + break + + @staticmethod + def parse_state(state: str) -> Dict[str, Union[int, float, str]]: + """Parse a state line to a dictionary + Internal method, you normally wouldn't call this yourself. + """ + state = state.strip() + Tello.LOGGER.debug('Raw state data: {}'.format(state)) + + if state == 'ok': + return {} + + state_dict = {} + for field in state.split(';'): + split = field.split(':') + if len(split) < 2: + continue + + key = split[0] + value: Union[int, float, str] = split[1] + + if key in Tello.state_field_converters: + num_type = Tello.state_field_converters[key] + try: + value = num_type(value) + except ValueError as e: + Tello.LOGGER.debug('Error parsing state value for {}: {} to {}' + .format(key, value, num_type)) + Tello.LOGGER.error(e) + continue + + state_dict[key] = value + + return state_dict + + def get_current_state(self) -> dict: + """Call this function to attain the state of the Tello. Returns a dict + with all fields. + Internal method, you normally wouldn't call this yourself. + """ + return self.get_own_udp_object()['state'] + + def get_state_field(self, key: str): + """Get a specific sate field by name. + Internal method, you normally wouldn't call this yourself. + """ + state = self.get_current_state() + + if key in state: + return state[key] + else: + raise TelloException('Could not get state property: {}'.format(key)) + + def get_mission_pad_id(self) -> int: + """Mission pad ID of the currently detected mission pad + Only available on Tello EDUs after calling enable_mission_pads + Returns: + int: -1 if none is detected, else 1-8 + """ + return self.get_state_field('mid') + + def get_mission_pad_distance_x(self) -> int: + """X distance to current mission pad + Only available on Tello EDUs after calling enable_mission_pads + Returns: + int: distance in cm + """ + return self.get_state_field('x') + + def get_mission_pad_distance_y(self) -> int: + """Y distance to current mission pad + Only available on Tello EDUs after calling enable_mission_pads + Returns: + int: distance in cm + """ + return self.get_state_field('y') + + def get_mission_pad_distance_z(self) -> int: + """Z distance to current mission pad + Only available on Tello EDUs after calling enable_mission_pads + Returns: + int: distance in cm + """ + return self.get_state_field('z') + + def get_pitch(self) -> int: + """Get pitch in degree + Returns: + int: pitch in degree + """ + return self.get_state_field('pitch') + + def get_roll(self) -> int: + """Get roll in degree + Returns: + int: roll in degree + """ + return self.get_state_field('roll') + + def get_yaw(self) -> int: + """Get yaw in degree + Returns: + int: yaw in degree + """ + return self.get_state_field('yaw') + + def get_speed_x(self) -> int: + """X-Axis Speed + Returns: + int: speed + """ + return self.get_state_field('vgx') + + def get_speed_y(self) -> int: + """Y-Axis Speed + Returns: + int: speed + """ + return self.get_state_field('vgy') + + def get_speed_z(self) -> int: + """Z-Axis Speed + Returns: + int: speed + """ + return self.get_state_field('vgz') + + def get_acceleration_x(self) -> float: + """X-Axis Acceleration + Returns: + float: acceleration + """ + return self.get_state_field('agx') + + def get_acceleration_y(self) -> float: + """Y-Axis Acceleration + Returns: + float: acceleration + """ + return self.get_state_field('agy') + + def get_acceleration_z(self) -> float: + """Z-Axis Acceleration + Returns: + float: acceleration + """ + return self.get_state_field('agz') + + def get_lowest_temperature(self) -> int: + """Get lowest temperature + Returns: + int: lowest temperature (°C) + """ + return self.get_state_field('templ') + + def get_highest_temperature(self) -> int: + """Get highest temperature + Returns: + float: highest temperature (°C) + """ + return self.get_state_field('temph') + + def get_temperature(self) -> float: + """Get average temperature + Returns: + float: average temperature (°C) + """ + templ = self.get_lowest_temperature() + temph = self.get_highest_temperature() + return (templ + temph) / 2 + + def get_height(self) -> int: + """Get current height in cm + Returns: + int: height in cm + """ + return self.get_state_field('h') + + def get_distance_tof(self) -> int: + """Get current distance value from TOF in cm + Returns: + int: TOF distance in cm + """ + return self.get_state_field('tof') + + def get_barometer(self) -> int: + """Get current barometer measurement in cm + This resembles the absolute height. + See https://en.wikipedia.org/wiki/Altimeter + Returns: + int: barometer measurement in cm + """ + return self.get_state_field('baro') * 100 + + def get_flight_time(self) -> int: + """Get the time the motors have been active in seconds + Returns: + int: flight time in s + """ + return self.get_state_field('time') + + def get_battery(self) -> int: + """Get current battery percentage + Returns: + int: 0-100 + """ + return self.get_state_field('bat') + + def get_udp_video_address(self) -> str: + """Internal method, you normally wouldn't call this youself. + """ + address_schema = 'udp://@{ip}:{port}' # + '?overrun_nonfatal=1&fifo_size=5000' + address = address_schema.format(ip=self.VS_UDP_IP, port=self.vs_udp_port) + return address + + def get_frame_read(self, with_queue = False, max_queue_len = 32) -> 'BackgroundFrameRead': + """Get the BackgroundFrameRead object from the camera drone. Then, you just need to call + backgroundFrameRead.frame to get the actual frame received by the drone. + Returns: + BackgroundFrameRead + """ + if self.background_frame_read is None: + address = self.get_udp_video_address() + self.background_frame_read = BackgroundFrameRead(self, address, with_queue, max_queue_len) + self.background_frame_read.start() + return self.background_frame_read + + def send_command_with_return(self, command: str, timeout: int = RESPONSE_TIMEOUT) -> str: + """Send command to Tello and wait for its response. + Internal method, you normally wouldn't call this yourself. + Return: + bool/str: str with response text on success, False when unsuccessfull. + """ + # Commands very consecutive makes the drone not respond to them. + # So wait at least self.TIME_BTW_COMMANDS seconds + diff = time.time() - self.last_received_command_timestamp + if diff < self.TIME_BTW_COMMANDS: + self.LOGGER.debug('Waiting {} seconds to execute command: {}...'.format(diff, command)) + time.sleep(diff) + + self.LOGGER.info("Send command: '{}'".format(command)) + timestamp = time.time() + + client_socket.sendto(command.encode('utf-8'), self.address) + + responses = self.get_own_udp_object()['responses'] + + while not responses: + if time.time() - timestamp > timeout: + message = "Aborting command '{}'. Did not receive a response after {} seconds".format(command, timeout) + self.LOGGER.warning(message) + return message + time.sleep(0.1) # Sleep during send command + + self.last_received_command_timestamp = time.time() + + first_response = responses.pop(0) # first datum from socket + try: + response = first_response.decode("utf-8") + except UnicodeDecodeError as e: + self.LOGGER.error(e) + return "response decode error" + response = response.rstrip("\r\n") + + self.LOGGER.info("Response {}: '{}'".format(command, response)) + return response + + def send_command_without_return(self, command: str): + """Send command to Tello without expecting a response. + Internal method, you normally wouldn't call this yourself. + """ + # Commands very consecutive makes the drone not respond to them. So wait at least self.TIME_BTW_COMMANDS seconds + + self.LOGGER.info("Send command (no response expected): '{}'".format(command)) + client_socket.sendto(command.encode('utf-8'), self.address) + + def send_control_command(self, command: str, timeout: int = RESPONSE_TIMEOUT) -> bool: + """Send control command to Tello and wait for its response. + Internal method, you normally wouldn't call this yourself. + """ + response = "max retries exceeded" + for i in range(0, self.retry_count): + response = self.send_command_with_return(command, timeout=timeout) + + if 'ok' in response.lower(): + return True + + self.LOGGER.debug("Command attempt #{} failed for command: '{}'".format(i, command)) + + self.raise_result_error(command, response) + return False # never reached + + def send_read_command(self, command: str) -> str: + """Send given command to Tello and wait for its response. + Internal method, you normally wouldn't call this yourself. + """ + + response = self.send_command_with_return(command) + + try: + response = str(response) + except TypeError as e: + self.LOGGER.error(e) + + if any(word in response for word in ('error', 'ERROR', 'False')): + self.raise_result_error(command, response) + return "Error: this code should never be reached" + + return response + + def send_read_command_int(self, command: str) -> int: + """Send given command to Tello and wait for its response. + Parses the response to an integer + Internal method, you normally wouldn't call this yourself. + """ + response = self.send_read_command(command) + return int(response) + + def send_read_command_float(self, command: str) -> float: + """Send given command to Tello and wait for its response. + Parses the response to an integer + Internal method, you normally wouldn't call this yourself. + """ + response = self.send_read_command(command) + return float(response) + + def raise_result_error(self, command: str, response: str) -> bool: + """Used to reaise an error after an unsuccessful command + Internal method, you normally wouldn't call this yourself. + """ + tries = 1 + self.retry_count + raise TelloException("Command '{}' was unsuccessful for {} tries. Latest response:\t'{}'" + .format(command, tries, response)) + + def connect(self, wait_for_state=True): + """Enter SDK mode. Call this before any of the control functions. + """ + self.send_control_command("command") + + if wait_for_state: + REPS = 20 + for i in range(REPS): + if self.get_current_state(): + t = i / REPS # in seconds + Tello.LOGGER.debug("'.connect()' received first state packet after {} seconds".format(t)) + break + time.sleep(1 / REPS) + + if not self.get_current_state(): + raise TelloException('Did not receive a state packet from the Tello') + + def send_keepalive(self): + """Send a keepalive packet to prevent the drone from landing after 15s + """ + self.send_control_command("keepalive") + + def turn_motor_on(self): + """Turn on motors without flying (mainly for cooling) + """ + self.send_control_command("motoron") + + def turn_motor_off(self): + """Turns off the motor cooling mode + """ + self.send_control_command("motoroff") + + def initiate_throw_takeoff(self): + """Allows you to take off by throwing your drone within 5 seconds of this command + """ + self.send_control_command("throwfly") + self.is_flying = True + + def takeoff(self): + """Automatic takeoff. + """ + # Something it takes a looooot of time to take off and return a succesful takeoff. + # So we better wait. Otherwise, it would give us an error on the following calls. + self.send_control_command("takeoff", timeout=Tello.TAKEOFF_TIMEOUT) + self.is_flying = True + + def land(self): + """Automatic landing. + """ + self.send_control_command("land") + self.is_flying = False + + def streamon(self): + """Turn on video streaming. Use `tello.get_frame_read` afterwards. + Video Streaming is supported on all tellos when in AP mode (i.e. + when your computer is connected to Tello-XXXXXX WiFi ntwork). + Currently Tello EDUs do not support video streaming while connected + to a WiFi-network. + + !!! Note: + If the response is 'Unknown command' you have to update the Tello + firmware. This can be done using the official Tello app. + """ + self.send_control_command("streamon") + self.stream_on = True + + def streamoff(self): + """Turn off video streaming. + """ + self.send_control_command("streamoff") + self.stream_on = False + + if self.background_frame_read is not None: + self.background_frame_read.stop() + self.background_frame_read = None + + def emergency(self): + """Stop all motors immediately. + """ + self.send_command_without_return("emergency") + self.is_flying = False + + def move(self, direction: str, x: int): + """Tello fly up, down, left, right, forward or back with distance x cm. + Users would normally call one of the move_x functions instead. + Arguments: + direction: up, down, left, right, forward or back + x: 20-500 + """ + self.send_control_command("{} {}".format(direction, x)) + + def move_up(self, x: int): + """Fly x cm up. + Arguments: + x: 20-500 + """ + self.move("up", x) + + def move_down(self, x: int): + """Fly x cm down. + Arguments: + x: 20-500 + """ + self.move("down", x) + + def move_left(self, x: int): + """Fly x cm left. + Arguments: + x: 20-500 + """ + self.move("left", x) + + def move_right(self, x: int): + """Fly x cm right. + Arguments: + x: 20-500 + """ + self.move("right", x) + + def move_forward(self, x: int): + """Fly x cm forward. + Arguments: + x: 20-500 + """ + self.move("forward", x) + + def move_back(self, x: int): + """Fly x cm backwards. + Arguments: + x: 20-500 + """ + self.move("back", x) + + def rotate_clockwise(self, x: int): + """Rotate x degree clockwise. + Arguments: + x: 1-360 + """ + self.send_control_command("cw {}".format(x)) + + def rotate_counter_clockwise(self, x: int): + """Rotate x degree counter-clockwise. + Arguments: + x: 1-3600 + """ + self.send_control_command("ccw {}".format(x)) + + def flip(self, direction: str): + """Do a flip maneuver. + Users would normally call one of the flip_x functions instead. + Arguments: + direction: l (left), r (right), f (forward) or b (back) + """ + self.send_control_command("flip {}".format(direction)) + + def flip_left(self): + """Flip to the left. + """ + self.flip("l") + + def flip_right(self): + """Flip to the right. + """ + self.flip("r") + + def flip_forward(self): + """Flip forward. + """ + self.flip("f") + + def flip_back(self): + """Flip backwards. + """ + self.flip("b") + + def go_xyz_speed(self, x: int, y: int, z: int, speed: int): + """Fly to x y z relative to the current position. + Speed defines the traveling speed in cm/s. + Arguments: + x: -500-500 + y: -500-500 + z: -500-500 + speed: 10-100 + """ + cmd = 'go {} {} {} {}'.format(x, y, z, speed) + self.send_control_command(cmd) + + def curve_xyz_speed(self, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, speed: int): + """Fly to x2 y2 z2 in a curve via x2 y2 z2. Speed defines the traveling speed in cm/s. + + - Both points are relative to the current position + - The current position and both points must form a circle arc. + - If the arc radius is not within the range of 0.5-10 meters, it raises an Exception + - x1/x2, y1/y2, z1/z2 can't both be between -20-20 at the same time, but can both be 0. + + Arguments: + x1: -500-500 + x2: -500-500 + y1: -500-500 + y2: -500-500 + z1: -500-500 + z2: -500-500 + speed: 10-60 + """ + cmd = 'curve {} {} {} {} {} {} {}'.format(x1, y1, z1, x2, y2, z2, speed) + self.send_control_command(cmd) + + def go_xyz_speed_mid(self, x: int, y: int, z: int, speed: int, mid: int): + """Fly to x y z relative to the mission pad with id mid. + Speed defines the traveling speed in cm/s. + Arguments: + x: -500-500 + y: -500-500 + z: -500-500 + speed: 10-100 + mid: 1-8 + """ + cmd = 'go {} {} {} {} m{}'.format(x, y, z, speed, mid) + self.send_control_command(cmd) + + def curve_xyz_speed_mid(self, x1: int, y1: int, z1: int, x2: int, y2: int, z2: int, speed: int, mid: int): + """Fly to x2 y2 z2 in a curve via x2 y2 z2. Speed defines the traveling speed in cm/s. + + - Both points are relative to the mission pad with id mid. + - The current position and both points must form a circle arc. + - If the arc radius is not within the range of 0.5-10 meters, it raises an Exception + - x1/x2, y1/y2, z1/z2 can't both be between -20-20 at the same time, but can both be 0. + + Arguments: + x1: -500-500 + y1: -500-500 + z1: -500-500 + x2: -500-500 + y2: -500-500 + z2: -500-500 + speed: 10-60 + mid: 1-8 + """ + cmd = 'curve {} {} {} {} {} {} {} m{}'.format(x1, y1, z1, x2, y2, z2, speed, mid) + self.send_control_command(cmd) + + def go_xyz_speed_yaw_mid(self, x: int, y: int, z: int, speed: int, yaw: int, mid1: int, mid2: int): + """Fly to x y z relative to mid1. + Then fly to 0 0 z over mid2 and rotate to yaw relative to mid2's rotation. + Speed defines the traveling speed in cm/s. + Arguments: + x: -500-500 + y: -500-500 + z: -500-500 + speed: 10-100 + yaw: -360-360 + mid1: 1-8 + mid2: 1-8 + """ + cmd = 'jump {} {} {} {} {} m{} m{}'.format(x, y, z, speed, yaw, mid1, mid2) + self.send_control_command(cmd) + + def enable_mission_pads(self): + """Enable mission pad detection + """ + self.send_control_command("mon") + + def disable_mission_pads(self): + """Disable mission pad detection + """ + self.send_control_command("moff") + + def set_mission_pad_detection_direction(self, x): + """Set mission pad detection direction. enable_mission_pads needs to be + called first. When detecting both directions detecting frequency is 10Hz, + otherwise the detection frequency is 20Hz. + Arguments: + x: 0 downwards only, 1 forwards only, 2 both directions + """ + self.send_control_command("mdirection {}".format(x)) + + def set_speed(self, x: int): + """Set speed to x cm/s. + Arguments: + x: 10-100 + """ + self.send_control_command("speed {}".format(x)) + + def send_rc_control(self, left_right_velocity: int, forward_backward_velocity: int, up_down_velocity: int, + yaw_velocity: int): + """Send RC control via four channels. Command is sent every self.TIME_BTW_RC_CONTROL_COMMANDS seconds. + Arguments: + left_right_velocity: -100~100 (left/right) + forward_backward_velocity: -100~100 (forward/backward) + up_down_velocity: -100~100 (up/down) + yaw_velocity: -100~100 (yaw) + """ + def clamp100(x: int) -> int: + return max(-100, min(100, x)) + + if time.time() - self.last_rc_control_timestamp > self.TIME_BTW_RC_CONTROL_COMMANDS: + self.last_rc_control_timestamp = time.time() + cmd = 'rc {} {} {} {}'.format( + clamp100(left_right_velocity), + clamp100(forward_backward_velocity), + clamp100(up_down_velocity), + clamp100(yaw_velocity) + ) + self.send_command_without_return(cmd) + + def set_wifi_credentials(self, ssid: str, password: str): + """Set the Wi-Fi SSID and password. The Tello will reboot afterwords. + """ + cmd = 'wifi {} {}'.format(ssid, password) + self.send_control_command(cmd) + + def connect_to_wifi(self, ssid: str, password: str): + """Connects to the Wi-Fi with SSID and password. + After this command the tello will reboot. + Only works with Tello EDUs. + """ + cmd = 'ap {} {}'.format(ssid, password) + self.send_control_command(cmd) + + def set_network_ports(self, state_packet_port: int, video_stream_port: int): + """Sets the ports for state packets and video streaming + While you can use this command to reconfigure the Tello this library currently does not support + non-default ports (TODO!) + """ + cmd = 'port {} {}'.format(state_packet_port, video_stream_port) + self.send_control_command(cmd) + + def reboot(self): + """Reboots the drone + """ + self.send_command_without_return('reboot') + + def set_video_bitrate(self, bitrate: int): + """Sets the bitrate of the video stream + Use one of the following for the bitrate argument: + Tello.BITRATE_AUTO + Tello.BITRATE_1MBPS + Tello.BITRATE_2MBPS + Tello.BITRATE_3MBPS + Tello.BITRATE_4MBPS + Tello.BITRATE_5MBPS + """ + cmd = 'setbitrate {}'.format(bitrate) + self.send_control_command(cmd) + + def set_video_resolution(self, resolution: str): + """Sets the resolution of the video stream + Use one of the following for the resolution argument: + Tello.RESOLUTION_480P + Tello.RESOLUTION_720P + """ + cmd = 'setresolution {}'.format(resolution) + self.send_control_command(cmd) + + def set_video_fps(self, fps: str): + """Sets the frames per second of the video stream + Use one of the following for the fps argument: + Tello.FPS_5 + Tello.FPS_15 + Tello.FPS_30 + """ + cmd = 'setfps {}'.format(fps) + self.send_control_command(cmd) + + def set_video_direction(self, direction: int): + """Selects one of the two cameras for video streaming + The forward camera is the regular 1080x720 color camera + The downward camera is a grey-only 320x240 IR-sensitive camera + Use one of the following for the direction argument: + Tello.CAMERA_FORWARD + Tello.CAMERA_DOWNWARD + """ + cmd = 'downvision {}'.format(direction) + self.send_control_command(cmd) + + def send_expansion_command(self, expansion_cmd: str): + """Sends a command to the ESP32 expansion board connected to a Tello Talent + Use e.g. tello.send_expansion_command("led 255 0 0") to turn the top led red. + """ + cmd = 'EXT {}'.format(expansion_cmd) + self.send_control_command(cmd) + + def query_speed(self) -> int: + """Query speed setting (cm/s) + Returns: + int: 1-100 + """ + return self.send_read_command_int('speed?') + + def query_battery(self) -> int: + """Get current battery percentage via a query command + Using get_battery is usually faster + Returns: + int: 0-100 in % + """ + return self.send_read_command_int('battery?') + + def query_flight_time(self) -> int: + """Query current fly time (s). + Using get_flight_time is usually faster. + Returns: + int: Seconds elapsed during flight. + """ + return self.send_read_command_int('time?') + + def query_height(self) -> int: + """Get height in cm via a query command. + Using get_height is usually faster + Returns: + int: 0-3000 + """ + return self.send_read_command_int('height?') + + def query_temperature(self) -> int: + """Query temperature (°C). + Using get_temperature is usually faster. + Returns: + int: 0-90 + """ + return self.send_read_command_int('temp?') + + def query_attitude(self) -> dict: + """Query IMU attitude data. + Using get_pitch, get_roll and get_yaw is usually faster. + Returns: + {'pitch': int, 'roll': int, 'yaw': int} + """ + response = self.send_read_command('attitude?') + return Tello.parse_state(response) + + def query_barometer(self) -> int: + """Get barometer value (cm) + Using get_barometer is usually faster. + Returns: + int: 0-100 + """ + baro = self.send_read_command_int('baro?') + return baro * 100 + + def query_distance_tof(self) -> float: + """Get distance value from TOF (cm) + Using get_distance_tof is usually faster. + Returns: + float: 30-1000 + """ + # example response: 801mm + tof = self.send_read_command('tof?') + return int(tof[:-2]) / 10 + + def query_wifi_signal_noise_ratio(self) -> str: + """Get Wi-Fi SNR + Returns: + str: snr + """ + return self.send_read_command('wifi?') + + def query_sdk_version(self) -> str: + """Get SDK Version + Returns: + str: SDK Version + """ + return self.send_read_command('sdk?') + + def query_serial_number(self) -> str: + """Get Serial Number + Returns: + str: Serial Number + """ + return self.send_read_command('sn?') + + def query_active(self) -> str: + """Get the active status + Returns: + str + """ + return self.send_read_command('active?') + def stopmove(self): + """Stop the drone from moving + """ + self.send_control_command('stop') + + def end(self): + """Call this method when you want to end the tello object + """ + try: + if self.is_flying: + self.land() + if self.stream_on: + self.streamoff() + except TelloException: + pass + + if self.background_frame_read is not None: + self.background_frame_read.stop() + + host = self.address[0] + if host in drones: + del drones[host] + + def __del__(self): + self.end() + + + +class BackgroundFrameRead: + """ + This class read frames using PyAV in background. Use + backgroundFrameRead.frame to get the current frame. + """ + + def __init__(self, tello, address, with_queue = False, maxsize = 32): + self.address = address + self.lock = Lock() + self.frame = np.zeros([300, 400, 3], dtype=np.uint8) + self.frames = deque([], maxsize) + self.with_queue = with_queue + + # Try grabbing frame with PyAV + # According to issue #90 the decoder might need some time + # https://github.com/damiafuentes/DJITelloPy/issues/90#issuecomment-855458905 + try: + Tello.LOGGER.debug('trying to grab video frames...') + self.container = av.open(self.address, timeout=(Tello.FRAME_GRAB_TIMEOUT, None)) + except av.error.ExitError: + raise TelloException('Failed to grab video frames from video stream') + + self.stopped = False + self.worker = Thread(target=self.update_frame, args=(), daemon=True) + + def start(self): + """Start the frame update worker + Internal method, you normally wouldn't call this yourself. + """ + self.worker.start() + + def update_frame(self): + """Thread worker function to retrieve frames using PyAV + Internal method, you normally wouldn't call this yourself. + """ + try: + for frame in self.container.decode(video=0): + if self.with_queue: + self.frames.append(np.array(frame.to_image())) + else: + self.frame = np.array(frame.to_image()) + + if self.stopped: + self.container.close() + break + except av.error.ExitError: + raise TelloException('Do not have enough frames for decoding, please try again or increase video fps before get_frame_read()') + + def get_queued_frame(self): + """ + Get a frame from the queue + """ + with self.lock: + try: + return self.frames.popleft() + except IndexError: + return None + + @property + def frame(self): + """ + Access the frame variable directly + """ + if self.with_queue: + return self.get_queued_frame() + + with self.lock: + return self._frame + + @frame.setter + def frame(self, value): + with self.lock: + self._frame = value + + def stop(self): + """Stop the frame update worker + Internal method, you normally wouldn't call this yourself. + """ + self.stopped = True + + +