From ec67f11bc5984a80a5f05ddff397f7688849a8ae Mon Sep 17 00:00:00 2001 From: Don Gagne Date: Sun, 28 Jun 2015 09:25:11 -0700 Subject: [PATCH] Major re-write of Firmware Upgrade Usability plus, new features: - 3DR Radio support - APM Stack support --- QGCApplication.pro | 12 +- qgroundcontrol.qrc | 6 + resources/firmware/3drradio.png | Bin 0 -> 9566 bytes resources/firmware/apm.png | Bin 0 -> 46209 bytes resources/firmware/px4.png | Bin 0 -> 36252 bytes src/QGCLoggingCategory.cc | 2 +- src/QGCLoggingCategory.h | 2 +- .../{PX4Bootloader.cc => Bootloader.cc} | 352 ++++++++-- src/VehicleSetup/Bootloader.h | 156 +++++ src/VehicleSetup/FirmwareImage.cc | 393 +++++++++++ src/VehicleSetup/FirmwareImage.h | 103 +++ src/VehicleSetup/FirmwareUpgrade.qml | 411 ++++++++---- src/VehicleSetup/FirmwareUpgradeController.cc | 630 +++++++----------- src/VehicleSetup/FirmwareUpgradeController.h | 117 ++-- src/VehicleSetup/PX4Bootloader.h | 161 ----- src/VehicleSetup/PX4FirmwareUpgradeThread.cc | 404 ++++++----- src/VehicleSetup/PX4FirmwareUpgradeThread.h | 170 ++--- src/main.cc | 4 + 18 files changed, 1891 insertions(+), 1032 deletions(-) create mode 100644 resources/firmware/3drradio.png create mode 100644 resources/firmware/apm.png create mode 100644 resources/firmware/px4.png rename src/VehicleSetup/{PX4Bootloader.cc => Bootloader.cc} (52%) create mode 100644 src/VehicleSetup/Bootloader.h create mode 100644 src/VehicleSetup/FirmwareImage.cc create mode 100644 src/VehicleSetup/FirmwareImage.h delete mode 100644 src/VehicleSetup/PX4Bootloader.h diff --git a/QGCApplication.pro b/QGCApplication.pro index f7a22ebcc..61c579de8 100644 --- a/QGCApplication.pro +++ b/QGCApplication.pro @@ -591,8 +591,10 @@ HEADERS+= \ !MobileBuild { HEADERS += \ src/VehicleSetup/FirmwareUpgradeController.h \ - src/VehicleSetup/PX4Bootloader.h \ - src/VehicleSetup/PX4FirmwareUpgradeThread.h + src/VehicleSetup/Bootloader.h \ + src/VehicleSetup/PX4FirmwareUpgradeThread.h \ + src/VehicleSetup/FirmwareImage.h \ + } SOURCES += \ @@ -621,8 +623,10 @@ SOURCES += \ !MobileBuild { SOURCES += \ src/VehicleSetup/FirmwareUpgradeController.cc \ - src/VehicleSetup/PX4Bootloader.cc \ - src/VehicleSetup/PX4FirmwareUpgradeThread.cc + src/VehicleSetup/Bootloader.cc \ + src/VehicleSetup/PX4FirmwareUpgradeThread.cc \ + src/VehicleSetup/FirmwareImage.cc \ + } # Fact System code diff --git a/qgroundcontrol.qrc b/qgroundcontrol.qrc index 20fd3daf3..56201fbca 100644 --- a/qgroundcontrol.qrc +++ b/qgroundcontrol.qrc @@ -201,6 +201,12 @@ resources/QGroundControlConnect.svg + + resources/firmware/px4.png + resources/firmware/apm.png + resources/firmware/3drradio.png + + resources/mavs/helicopter.svg resources/mavs/unknown.svg diff --git a/resources/firmware/3drradio.png b/resources/firmware/3drradio.png new file mode 100644 index 0000000000000000000000000000000000000000..7dd84905b38c8518f2a52ff1ce1289cfe6ba3772 GIT binary patch literal 9566 zcmaJ{bySp5n;&|B0f(*uhL92wkPc}WI;2ZdX+cp+y1N7kK{}+nOG-o}1nE{0knZ%m zvuF38J$se|%z=sb#&du5ypdWON<;*-1P};>NEs!s10FwvAA2Y+`0Y>aGzA`TtyGod z!6W#(kZbb?5BRPqqn8i}rUPLYZrdbmR@f21fNv~nGmAYs{>CQgH#YkTa9t`>z6mw*XviD9v`2J|3cu* zf1hBGkc7&UXtOj2SduU~&1#X7BH>AC>B?-wtW3kKbzL6*{@uR5b+&=hR!5y15_g_1 zy?D2`;dkak1HtAWVog@E`o)x>%tnI*e_+Y8rP~C2W=)2{(`}M4n3L7`rEQWnuhbS$ z!Yfq@XGCwvdZk^V7NWRZPjKp~VnphiNc7m}x{t77y#esGLnv05B?N+jfniv0M{;|6 z*2TidM-l=rUM}oAk`UP)s78psz|ItMmQ_~9$CiOW4vgZ~Et=-}>?c^AW@~X&-|>W2 zRXu<*X>pLd>>MbDz^l0lgL@#cior!iY=2I?C#R-Xx2DQUK7I_+FT2@`8mP4}q6|Vt zIJeU0ITPXI_kbr4F1M>#`1wg+x6!K<9yTtiUkw~d2+$I_i)GC;MMV*qHoIb+oSdxP zs1%Y)jNVc0x_t)W=WyM}G>i+KP{L4G5RN*^&@!38m!YOc$jHc`r(*8)iz0?ms`H5} zGzbzZivbbwI%K`Q81>Jqt)0L}1lz{+c*F-Hp->_s1q%zLj*bqPm1W)R*z7DP|M!cd z4Q*p%0x*&6jUL{Z&kOmHxe_O_wnpG18OF!QT}2Spvrf%^H)54$ZDDI`R^;U5IRypS z#Nk+AS#6ErvS9*o)|AK)b_)vF+eZX;_k6Uj-N+oGs(L53AuIFWC|LniXa;aStXwy9~1@RBE1VPPR}x|#4aK0w$vL(h_pHI|XWZ6){DQv7rR}^;*fk_9 z%xZI@(6Gscp8w&)T%FQO9K2p$Ffxw&_xqP`Pld@TD0-ZP?s(ljI=+>m3lTQZu+N-y z5WK#=hEq|6&&(Lw*w|RyUSEi~tzm!5mW&Py!!3TM)cbLMnHg#8-C!Jhus+Db#)f-v zaF8bCOnZ2EsHLM5w7cu##;sJVxln?OHymI~BVZ<&yT8shN&f27r1hB?hP$+!tZcRW zrcSw88|BN5Z=uE&3~+>EJ_%Sh&#Rp#x9fAaeF=X9nz%;wFXR)a1VI5SD;6OkAxI`B zjPFJ2y_2Pn6F+>22gQpXJ)zuU>8;AfV8e67Ad{#l4G9TJL}a9lt1GX@dy&A?(^C>W zd|~6Bo>~|b!`MNc;~EwJF?pvto!%miu1j@rcx#3XK#r$*_|(kK}HseAjUIH z?axz)MnW;aK79x#71r7Lhz1Ff#tzWqMK)u^CHZ>4|Cfg&{cpvB8n?5vv-#ic)xlcy z11nIh`v(U!a0F$v!dnD60?dRs-1>8+jsb;LNh6-G1{_@vbY?9$u zfqasNLk&-HrHvicIKF*fzYa{zbi=`w*qb#`=;3?z4dFCbkI%`;nb-7%0-?(fuqwPa zaAHEoX}*ytoR}VS^r#7(;oiU>KlJMx8$S&W;t6G{+tl_Xs1ydp$KN3*z%fh>mCc0= zx-nyE3zyzIFVWVxdgyg%gZ^BhOrnd6i$9y2G___j(7;8P@F5%8*NO#8LbN8i(Qh5Q zMB;*bSHXLm_d2h%o;-=q$e;zwY!)NAX!Z@U8dTav`JkBuTbEbsvevGBB0TTTNr{M6 zGs!=Tj^JQ#+25ld;{C7J8JjnxL#dcFd>l;GzMsBD0+K~6{)2MRaf&SdU3$p`=8svifd{}y8hkl zp^pu9tm460psobK4%WMOnI1kQ1Jodgdnx@Q=uPC`k2BJNbN9N!#2r6kkCJy+Sjd*dkql%?b`jgb0xF^Uxw zJRnLP2A~>4==oAiUS6I_t!RNm;FN=cnHl}o_BLJxX7fkQ`VqvSOsOt2pa%4*y}md~ zOi2l?uNUATCtyEOy(FG{lD$DfM;8rhnq+Ztk%NmXXv%>iP+UvO!O?MiY;5JH3j>dp z5hXZ1k&%(-2l4(fdS&`d_wTF6)IUx@1gtFG{1qTr-(?LoU`+NV`Jk539Fk~D5xv_vR5+?qWgouiK}mFS_hnI z=j0?0c7&9SOaapNjh=`bvpeOUftHDh_$j3Jcv^YP?5~XSQY^Hkbe@OHZ>e2xeIT~C z)^>RRZ{4WXxA&sx1F7}Gkel-D+qV|OsrNa!x!o}bUCUD^&QQzSJBK!OC}*~^si?|I z9sxnY+{Q-AhK2?xF|neqF1apOO0o*GuQ?q1{QMj#CPqt7Pe10+fUf)`_qNAWDQWhgD}rRqUmQW~?(UvbSxFol8w-WO$H)0TAg?z9{pyQJD$3T2KjEfHsLBxYyF0U8+p_99CP{F^^r zOG_(YX=$kj2VR!0bds)-7W?{LK!mtRP*6~{%@Aq1NfYtW#;6A&ruRgJkB^Vr?t-Y( zawk3B9R_lu!|{w%Cg*p$g0=HIB^4E+Jr>e-b|qve31yO0TM`5!2JG@Up}<59$(-=q zgSy$6r6qH<_aY?r_VzA1<5P97wkA2jy2joQsJB;ho$*{iNgQ+UZbmhG9#~x;zU%61 z_d3F*U@G2ZX=!OW2YV(zkUOwz$6h+Fl3vu=$zWz?hUO?!Q^cD$Z~A;pd-C+ldU#>9 zw6t1gX1>GPx8mau<^Ymm0M)>4-cGGjI6=fZF%guP$86f+eNx%@s*kV;J3FVV+*E9w zkdnK5AeL@%lI!_OPGtCe)APYj-z%Vp6g0=qVoS6&gG1Og-gk#H`b7XW#^=JKKJ<ix)&mF=o7ML)mJw6P6)7zUD= z853OQfzIQ{N`>wPLi#2qnFd1|cm3_`{>&U%d6^gi@mV&NCg~%zkY*jA%5nCA zgB|f!bTF1K{O?~UiL1YLpTB-3Po59^NYK*iMiUnJ;RECK%?%JrfgvH-1w}<`K*PcT58Wx@oMHGWoBR`ZKr1Q{6_yug`My`loN6%idxHkFcC+fk4KNHB-q+sV-txcQY~@QQ zVfgrgRaI5VKRZ)y7rrS(2Cdf&GRS#)Hc#6qgjtR)F2(|hU1J|S=3C}>*0yZ zn^;L&)i`^LQ^AxVM>&~om8$C;2Ta8E`uqUn?9+=1sxFFG&qY`tKnyGN0sFKmt8GdG zCk7CA|L92W$&+QXckb>Qr5~G|X89#fCLgEqS%--{$}W%`J+gCf_|)=pLoq~334yG8 zI`T>NK}SgGxgxfGCy}v;9JTecIFo^e+w!H<%7?ny#Fyh2-Es z9j7V>BK6#26j{pkx2yR-9Gek`%atRKf0$>~t~#&gc9rRJxgywk;7x>1rIW;jgmQ|C zxTz1G#FdwG_YDk)^%qE_C^P}qTxfY&QeKY8geap>(w3G?Kt0{1e6Sd2%nZeRt8!OX zR+iWD6Si8m1PvLR8hqH*ezD~qszk{aoOaK(NcQTN8ybA-3sd*x9aWiGSPsgb+^D!K zsIn~T?g^)>d2VERXc-%GRW9^q?J`3@1rSCz{k64UURz&J%*u)_C}4Ga@^SutcHUBN zNePEqIzP_$V$H#3xApf0O~-T-wRWkt+6D&Ua^b|0J2Wo^H@&>Pn2(4JHa7b{PC1~Z z7^n__ukv7SB5`D~y3-gNUJO8Fdv28$yB`11o}8TYEY~g`#l^*C1}}a1^y_{z|MxRS zDL(`ik11R@kM30Yrw9PAg+uBIoS~<6Xid2Nqk>FtakV@lQpGQ#}zf~>geF$ zy3&n7PDv?t_kJ8_z}S7Wu;){8T72>u>t1DR#lB0tyvjP(^MlRyPYPo7$Rs z1cX{IkZ%~WP;&y_&D_Y$_1Q;rS|w1iYVec(;+U!T#HEb89h{~6dumL!)^ zM@dGu{`+@o#lFpltVvA3Tm9^g-lysD3~gn;bq^7OG?rM z?yx%Dm)?pLe)l^|Bcee25J>V?4EZ@8ps3>Fh-aJ{{is;8H^vthV&bK4sR83sm5~(g zF%TP6xtp+jJgns=%pZ>k#=B!Rnk~uk-~rY50L71LtNxb|`pw_??hTS4%;>zmbc~;O zSD8v}ZWaMd#KOxvw$3F&J+=^gi-V1go-tD8I0cjBUDGNqjQ#FR-lvYRz-6 z<1FFbyYRU=V-ZB$yv{4rLa`eUP0c$zrp*Co%l?f2K=zc?%{O5^Y8jK@uEX=B69z)0 zw{OXXAt)MdW@g!i6(@nI*s+;dz>PXOq)$gOe*I*4Wy=jJsl)tEtl~N=(G?c{E0I6$ zV(nMEZIA###~)x3lln#h}TUI3eyDKGe?cY|1RDk_M8 zUEBsEu78B3D$2?Trz_3KzH8(5dQL#HHHzh2TzEXQ z2Oo|~1Opn3=Zm@hY*o|!v@USN>-gkErucg{ntNA>PzVs`kfen9#ex2;`58 zHHTm7 zp|s9;Dk?O5pgQ9pU2=d6u8-#{PTg-U`~&EZ~{n|Zr@zE`mNdfd5JM}nG@^YcC?iSw(b z_=RHrYHBbB|7$lJTieJ)dxQDl(}vt!jQ;-qwb!CI7}*Q$re0emyA?Vb@q|qs` z$HH~);mmi>?|ZhkwyPH#ACz9asQu$mx2CDieG%YGWp{YqwBz9ouAG>$6M&T95{yrxvs+~pNkLd zOFU?`HcIVBujFgbBv##w(2x*|Utdy?Ar@eRn_X9f()LA-?;zK9f@hQdo{MugDnZ{;Vu$lSJqpTB;|6zZ&QNc|HXh-ciZw+vxVKv&$w z;V(EgMw`c;qLER$ok6UAf~z$S!o6_h>({=dyQ<)*?Y@2+;4bLm?A-6M?8{Eucr(k@ zYuVhUm{tf>R$@j*loEp%cD&>%H4seg*>kEV-oSx8eD)3aFE~(2=K15o+3YL2u)N&^ zrM0g}Q_01^tu+leoJ2aCRoU8_Wy$}aFGq?BdSU=VG{UKr&wdm-7&z5lcb(Xca zztIg2p(BF55ndW~LlkLbMNyf1V$jggXdDntvwRDnt4RV;AKJO?J@ZE<22D**Qvqa6 zd*myoUuhZ%Dvnt~Vqu#EIzbx;Qn=O6i7d^pcjj^$1(~t6@Z6d=!gHaKdc8CDN~-G1 zBYcN2Qd+e)XNw*^wcPqu=4du$Z~Oc`oe>>d)mY(BhXsy2?FFCuJ}xyGW6kQ#5`L0~ z8c=m?n%OZ8j?-2rTT>b*yA$}u0X4HuB0i_wbMy1zw!v7f-o(0rpKG~+iGsu70dDI9 z=l~E+AYh?+U;b7rJ`X4!X~mHN0utl{{lFG!nVQ1#$7i1NkU8U%Kzf*@e+@F*T5^p=9FAh#ig$`b>rbv;t;?18C)ZJsyPW+5$_3$V3%smV`N`kM#sZSrHS@~K91Vq19+ypl z(0ZpSBYt;)0i>(|B5j^NWd>1AGMhT-C{|*tf*FyWvE2Cdba;Ea#NXvEe^jvo-(RBY z{nwcKJxD=u(wHEbo=QdPtt)9nkdm@&A;7X;5a@LuN&KiZiw0OGjIS;ud$e96tWm1F zb~61$NmVsESxy!g8c_VfZ*Dt|wG#Gs^O5HK!Wj8JI>H60Qd(NN_IqR^O19MU-r}+M za;I-eWhJGDdAr^aN~ZPc2rU?76l~Wx|646LkXmpGicp~A`T9doLtAAwcQ-%;5c8~V z*7ffUpVMM11#N86vKfbDBxx81b!uQQTesx~pS2My1Q(};$gXq~2a3_lj|_Tg&%NU) zCx`WVcL6_hB#ekoD56K+;oVrHmR{6`7hogHkqkkQ3siNS(!y|iOE8BbuzrpRFSq9p zw8-YdlaKxwd4da*S%TgsK&<_8gw(Amh)UKc(T=*C$o}6!TKFPDvzqP<6`o$FHHb0tA%6f^YsX zX>th#nNJ-ZE+JIYIS2Wwf!;Lvu%LlN;N(-XkNzA_sx1gL;y_N@e)7Y_YX14NJu_U$ zFfT|FICKfrvKa|vsW}dhOCIbULPJo}6B(xD?0vh8D+WI5 zl>wiBQ00H43Ui99_i*gP;7>aa3e+tNT!SvfJxgX=ux~K_JJ&!6%q;ye$>(6cc1gab zrsa08V$&}69TQ()Nnk0e0Ws(r7)0N++Ga*xQ{9CPgPzlhE;ri<5pDz|^=^syvM&~E zFpTYX^^QM$ajP;DLq&$)WkICeum)toSRj~|RZ!@Uz$5E2FfZ?ER>6D0)W$EcC~Cd8 z)Nu}6BXi0@%$S6Vik)^d`?n4p8;!U9Q4}C_=r~)F^YUs* zUS!6yNz*I8r5<@&m)qJ(dv&&l7Jr>UQp|spYhJtz?SV`<39ZjHIEuXd4U1!xikq02 zSUtPt-Tj!~O{j&(;esNCuHazqgzjb{X++XWbV~PsCTog0_x!Z~jw=I)gTNBRAJKa> z$_XF|+qJn=^gjqoO{LV;)m`Zh#6r&oM4(whK5Z1z3z3+NUqpbKva_?3*3+XPARutP zJhrzJpq+6NDyBFmhUGya*;FfZIen5xvd;_RXR!ZNor*24+y6cBmVEK@Wse(z!RIeu zaYS1&77Cv!B0iofI1;RQN=gb#5}EMR7d0jCJ_2P^QBi^JkaeD~5xxN}l(ixoxkW@k zQT&~0^L8D@CgTdw^#K8&jE|2vk9m6p@UVHn0~lBQ7{c)t781;Xz!8{lu0_;3q+l?t z_cMiZZ3D0+9#6BJ5<<_xIC@CHRGC3^kGuihcM8@r{R+%p-kC;cI&fv%09g2B&X5}r zz{>sgYcZPJrcXBY?z&ssT`59G5)XdJAD^D)6&B)v zRI>Ta1wXnG_T`I;yN5?xVc$Yxavn+w*+Z1T@s_*j-=8qTW{YQ7Fymx{gl1ead?wGs zA$AbftTb*%-$Q})wZ{9z0nlCh!2q4rY&9&w#71C?#QynnB|wG8-jZN5w6(PZpLbs1 z8dRFP9hxsjjmYfnZEp6A=PTs`53on97o=rUQddU?$Q95;s77(HV!n){V_9>2iY`$i zgQM$oAs^%uBq%Ffwp4mRGtZ(9PX2RiEcfBf8N<-S2@B@?%X%E-hFyM=pL%;2*B)2) z^nLz}LgPe*zBNcCJi59-!#7=GzU&bGzk9vPxDjgnLy*M(vmyN7PqsK4S6g84n6+ZJ zE$$w``ao9&z02rk83`k!|MYwl@GzjLfW3_J_W@>iY3JT}cmW6>;(}Pz($!_v%;T2XU9fGM$7c!wGd}itVr-nz z>>zb}zRtoW14%eOb|)nvVH(ZlmQlnrYxhJa%aDYO46(JH((-b75C=-|)h0gjj!0bq z81Kc4?uHj$?R7DNO@pjx?`fh?-Hp!kuD91-Aic;hF2;MuWA@4a_6XhK@bZ0bXh^;E z=15&vPcKsNB>WEJ0O)bGKT1wYVs`nr83}@cZ2wy?besoL?rKBsF*|`7ASp@`{A#VQ zaMK_V>}B-507QpUHo+jpNqqkvF;0n20~U6@{^W*7%sPQ(`g9~QzUeLxJl)>z?k=0M zva-u)VNp>M=#h^%))-gx*$G7LdF{4rL@4IF)yceg@dC;eQs2xt}c_oj+x$p-9T;P|&d{E^BHsuqg)+a=2AiMVe}xl^q`!#|AuZ2wWEQ zFlE-(*53Px1KGnAlCaq3Ax;;@eUJYI;LV|L-`KcPqN=%70fwPKI|huU0LY9n;2h$B z2ZYN)`KVzwKpV@07|(gO)39Qy$3uO|@9Laar<{)Qo#wASnaC=;wcdV?geloLkN9uaa+6U~} zbb>B-s_jq=&BM=29X<+0Ce5z7&xDzVNee-M@HI_F$Z1CU_1_tp&g(?F*8$# zG8MG6%x{CoWZsJA-0(@ zWjHn(YB!5ND56l%)cYo36(&s}(dpe;+WcN@z40x}Wg{5eHIO{?^Wf%jks1c%fZR~i z{q(O<)6w`5C+$@&m1>a*TlU65vD6Jdgx7ii_Vt6*@<_fe zXy+T%Jwl-rg*J2aeLwXti`Lio(riOwh0 zB|#A76=WN*@F?ZoyY-nGYe>M8f0xr5UCyA-czb&bB7s4m&Z}*QDbY7afXZA8%*;lW zO<_VnjC>i}2D@7k>M~11Prq}*5Gu>mTDHBt{c9)%gI}T^xO+%>5QB7lkv-NH0PJ&E zX{%VU2T@gx<1fX$1{pjiG z*{tzKvmn<(#4ww-Ypk;rPiy)j3KSAaP zDiMGzCaH<7Z}ZnXOfL z!^Xw+Wz!ejUUApEW2tm01Q!>d>1QaFIb23)2IcMKRQ@P$ z4eaeXYvy-?jkS$Z?*hMDP*@lWLhfjPO=V>!AST$2D^fw*EpN(UXm_z~=#Jk$R-P6I z3iKH4U0hD7J)neyEScbTqN%B=KC2pdaE~-G<0Cc72j<)=6#y=@#m4s<8XJ>3e9pX` zid71kAGGgdiuzs(w1AsY!0DAQn@U<(Fy;5XboW{^1|ZlENm_S_?E039hIqIgJ@Q1H2p`FB1W>jRCO;@0dB${&#Te_k}Ye zoG&@r0Kl(aK0aoK@Z`_0&gd`R{DSzeOkoIsljiRxSU@Nk5iqW6h|KM?Tg zpjqGaMp#Ul{_?&wT~a~)4{l~;$n;4Ji|+*e$0H57&TY`*Cw1>n)tKC$j<08Xjh>W-P#A^?B!-YX zNMOB4@N7sR&ObuLLIbICg}qc9v{3-Rv}|>~L_91t<3pu&b=7J^<>B;{^~Hc%;`o2Z z)+G0HF#KIXzyV0rL;g+AWB@&U!2Pp96a~~YG2&mxof?X2=)aoK)7+wp3Q{kU{h)yD zU*O|^K{|=Rjfvo4a$(YQp#}{>bp9f*|HXa(g4`Ysk4TM1Up)t@c-%3*u;- zgsmUgim>5;NYG0Z`Ik$skFGO>{2Afw$8RJgn7-(1lD<%Il8{o8)lg#NfMsHaP)?Lz ze@K|6Sn9!60{4l8`JgL1|g%!0KZbiC@|6~7kxwM{naTx|K=FdvbcLA?>yg1Z9o2NjG` zYI_{fG64z*B@qq1o;xCrWvzI+)R%PC=5%v)d zaqe_ni#}t&vjhljlMf@q{fGv~Ar6Zb<&tO>Y8CeoNhPmjVPg5gTFRoy^33#)$&@vb zCDWpH54JbGM=b=t5VR0@NpMMmA@OLCXmSM(1y=>cNw!IuN!-i?T`UG^2Ha|@YEs<^ z-Ko~aR){wF*1$GICr&599sQl1Bj!Ws!;u54UF;pLJ@lQX?WleF{jO2o-jIWY1Ls58 z1KwWu6#KZGK!cc^$(*sR@h=eodMvsF@-l;iOs=A#mXg%843olj>h{lGabgByle`~! z#(5bcuzlx!!F@Y@1AQ%jw!$33Q$xkV5+l};wen$(*@aZUlQdTKKBdTct4(k>#A zWX1FtBV9eg zm4^AT`O~>fMZ-mwC8<>&z?*{j*7z8eJe8VRdo%b0s)M=FJsMjY9kqHOk6L%>X!$IV z7MN46JWsd4vaq@!P%%~pRgt4aIA^$2yR=+0Q*a}zD$6R|pzJIxt4k|XA!DPKA-X30 z7W?RnpBY`IP(10HMcAI+F5N!So^y|OPx3H%HgP6<0(F+Nn8S?3o{K$?C5ID^1&O7O ztAL{%Whv1lF*j&DC_HEpbtGphMca-)kZ1bIuE#vbzGk&(7R}tkGR00 zQd;X_Ms1C4y<*sAT4dC;Q9g!a@zX}nlHZ!tQnaP4MQb&`$?PbZx7k!jV-04_idU-x zyaT~|-Y?=q`7H$j1%Vks1HX4>@TPR{PeP(`PTd_!vTpzJ{BI|I%Ic z)`r|hbZblLwRcv)8dg0aoW zqLu;t7T%V{T48h1Pw(xaBg_N${+yMZ!!49wu-9;0gx3j&QWZt?P^;kT5DYUD$7c6W z!h^yC!mz!Nz2d#j#E`_|#CLg+d6J@-eUW`K{TTfdeYAaolA4m4k(yyHk(Ckap|_FN z;pU+Tq1_n$7^v9Jl(<41%3;gz zk{2<}=n-f~82+?3jGjv6`d;;sYRS4BT6P+nesb@cy>&V0M(CtT#Yyr&P#{!=dNdTi zzf<3{*q(H6f+yutd4I)_b*1&HHMxzf;*MfbrkD5Xp34>X1?z*w%j99!2M|-0({u7* zJ?%zV9J2V5iftNe?_HBdt+F>@vMe-mm ziblEiijMPIZ1a6{QH7BPzk@2~8GuC*?Tu9i#O^`1|mr{1QJRm2*BS)cjE zl9{E|nwc%0T<+;d%`?Zh+)2Zs*w5(+N(sgGkcp5h{wB{?%YR0u$7yd3b;?^aBXzlT zFGtE#f<026bpqKBC{HZzzs>URlm1q$F0s^PP_R?l%cUtk$ag5jOif#C_DK8OIlEoy zjIPac@3g&oF1(z%% z#S@PoN#+hKN_S1nitCSM4XgOlUF zeA~F@co>Fegh4W`y6oZ|M}p3vE!6qts)4(uW9PBUX4~dL=b3m5#nTu8eKx(mMrX}) zqwUYfuFk5Fnvovv+^j|XWl4X6dyZ$FVF@e~E{#8yg}R9njmnYomEO7zKGuCZ{~DxQ z;)hfON|5KoSsI&p&vUQj){-)`l7U?v6mR?4w_TEt2m7{Lg#JYPxr)`I*(1bJ{j)Ei zlFKkyuwmknl3L-OGNnaMW~T>`Lv%Ffv>O%81SL74d}(S~Kvg9j=kka9e%6{PKi(lF zMhE?}-uvHg1Ji?z#b7Bij7uDxjg7h|mR$>OPp5&8pEjcSq**3QL-w8BFNce7)dvLQ zKPP!--5;*FA7-yg1crSzA4e>~o*=)fuQV8B;8U=u#KmBV9E^O! zgvvw#9Vk72rEL}bPqpUI6{l6?bM=cY?oJmxr}nG$J*T;*xvGt>g}r!-z$L9KoSg8g zf$or&i>I;2DY)T6GpNJa7)T(*QjjjpPsB5r9>Ns}5QKI9s1JAPZhvx@Weo8{1V1=h@hYyZ_@KL30)+cv} z*VmmRycQ-5gE}V%`$p^i^{v$1S@H|5Y*oci+>2+MM1*LC?lTfjGW*djl{1#(&DlL) z-cyCcje@6BFW$>1!Bn5;BE+>RcA^(elWCXX%doTBBh?(_EepSbPZN+SF#YL4D@H5S zMvca$`;pqR))RczHhUU#+)cO1hY3YGACD*Jxc7jof_=&X@}emfgVr8fpRCu#F5M5` zrSPlrOT=u#t)<{4#t*)C+TE6e;AQ{&ub!SS5I2+%0Sgih)F(=PEdd_?qQ9!d;kDgT zWKDAS#mkM)_R{Lq<&Bk*k(G&fz-+|;@wifO8H1!FwL!J3il(xh1*)0#0`!_L`5|=; zSw*=VY5xVoL5JzmvDSh8OsgiXBF`}K?gPIHx-+(nR{FeVKR3Ha4us(7bOo777KZrh zUAz2CHAEvt5Jr7_2EyhlQ-eMZ(>2B(!cSU|ibhu+oPS8;czx(JND}ZVk+D%~;VMy! zp)+CZ;SJ&L3fGE}vZFE#a%Iw8nTtuETqeeEZ}=4*({d^po}A3MBcIJj6PqP(UQX$c zgo3f&Ox!8S7XDIiwVDFXpEaa}FvYM27*Y86^aQk?`aJ3!>H=QpS_5;VJLP$MF^cy& zc%=m8p6a}grxzzhtTwL8&WT==cOvldm}rdUoTnVvdMq8bo3=B?eglmQtrOejb{&iC zO$bE_ZD;YEV)if_yH~qc7^lcRRSuG8)hGOK`crQKi1`G?e52>Q(*|5hpU?jcCzLJ{ zPIt;n%t(W#ZY7?gFMP6JN!<9(&Hny*qFq{2NSIX3o(o{Tv9Gs~-OSE7%e*p3^?5Qw zehb(uc&1z+51cwQpzG20W7Xk!t!~M0E58-BlXueFr@Uf(+)9LhAB6lX*nk26bW;31 z5e7Vc0NjEAl=lF#?^!nmfD-0c210JJBmk%iruH&__yh(*hZF$e@e9T&7s7lS?h^?% zO$pF64TES1+D$_CC;XViPKqu*VxIUZRtqggrohdTPdnPJAOcH2lAQuo3NrWm7=!d3AwMx~?MoR&upoXPJd}4pb$)<3s{H5hnuT+CkT~s$x z^Q>AeR?j6UGAb!6Coi0+?k^n6AF5JhS7Kpfabk~VTCveKS^OzrB3&Eb)Z7HURX^3d zT0Q4CZZr_KwDOmH`5{!NJSJi~T1rbB_GncLEPe zI1L^dX{$PQ$G*(E>uwl7jv#4<11M4hP@jd_&j0w724l2B?bZH4LLwG}brd0q!`nxj zjgrg%K@+T*0>nT#e49QG+z5JyIZn46oET_qkQ9s4yq7 zb>90#J&jI_m)oul`$ZziMO>B~p-Ah%Ey-uWkln-;jeucY5`C_`b6K}-Y4DfrY+z{%g^HTDc{ zS9Op7NcxfiUO2-5)r}aBfB;{H_<>YSP)B=f#AYvNYOBF#FCw#RS?@P`!ghF{g-z%_ z?G|muWAR|Ic5$+Glz6xQYpgeqFGLjat}5mpV-*b^ts|wACW6kOf=f-Df75fe6-SPDr$c++<)Qh9;byCB9qr_iKiXBDY9kM%7l@sCI@DYLmT?gt8QqpTiU9$PQI@Aj^RW8 ziQ42eGWJ{=KXNwmz!cI6(lc&X(R0*v$R%bau`OdD@qnNL|Lg~D z)CKAr0|G^~wE}R`Ten+xt2O3&`0oB@@Vf5K^%e@Y7TN`R5IXD!Jj@k_GPi7dzP2)t16_8gzg{?g?@3z`002EJ+r#?qD#hadi>_OM=>aLNy3tkNew-W^*`(U z{Na31U0Oc6Ey=IZFE=mt@TK`(`4JG};0Dm*eVKhWr0V&21-Qo2jx#r;Va3uD)GB2Q zC!p9#yx?w&=A-6K<{1~B?TmCsM*+Wetv8GMAzu(-_+YjO6D001IWRls^hR8&no;19_!gCJbovnE{J9t99XeNY$i4_1FDz~2S{nsh#+qkkWGguBHB0Bt~om{=4YrAZ+nn|4*$3?0FqBek-v6 zX)$3nkJa-mS0CWe<6}-*SIoZ+RJs7N){YKU-Ea&W>H(RECbXY<1NbmOv%8mNgV zMtp{<#EhXmx}0u4@4N2y)MyA{xWsqgzm(20PR*_g#+8`Ss11C-bB9+ld@Z3Z`R}!P zrSBh)qa4=~@^q4H)t07Z-Cq7zBYq$c^{(EDAjQ(C?+w3aee5EoS3Zu*O5p{|f1e!7 zqUHsFqoWuJJqqfHlP4;PK--6WWN_r7{lIcWp+NhK3wb=Njho<1)s6of(^roFk^&D@2+qUz z4P7E(CwO3wMMY%A&h}oN=$w>i`qrVJ%>>I7;gQ@hvqfZgZ0h6l3FqNlBITnA3(s_} z2)1u9pD8F&>eop*ZQl1NpE19x(@xITC26ur@E5Q9-FK@`jvs8&%2TeY?of`{s;U}1 zWvxho1xbe&!raDgcDgw;9E=+%or3iz`qf<@CSkUv^XZvZj(#g?Y zV!<~%6|}(CR3v`LbuVJ^TCOB<)e8U{SCZia`OjDUqw~Y41B9d`aAxCz0=<^5|AcP>q($N8{41dEAe8lYx#NRn@_JBOP$;OhNeSF@qNQsJIMI8Z81Z{0;Th zU8{nm-0v?Hey=Ce7Jt}_!m~?*&?^7HL2LgZC8YjxNlN~<+*ic=jhO&QYq(7-qy<+$ zHb-p;J|2w!xgpf!;uR_upihmA6s5=rs^xz45W^EJSwq@n$5E?dK{AWcx|~RbEv~$- z#tL$6lJol2srCn3^v~Eyyr8_G8sCc9xhiuf$_M%NK2Q!YMG!!dVKBmDtdR~D&-L4r zst7cB;x(m5L#noU0UR7$iUY2@oTxlQvGpv&Nn~UA(!tf8v_0Jei^4Y9yu)wQ^BsWq z@?U5LhY;3GBMA2SpdM-fQhlx@Nw!c3+WcRv)2G=?f5waBaz4F_d#p{}YU>ZV_OLgR zWU^+=`qd0}6Z-LfOM00$?iuj@8-(g~#f#t+d{f|HLHYS8il;b$z81$lPm<2MAJXs3 z1(@>Rhe`2lNX0Sw15Hy(6k%E}O@C?c>-Dxm+^=a??83sTXe4|UPuH|hEgB%)Yfs8nVM9Oisc@geZZ$tikJ zenoqsQ*XR^lftTq+J`TyXzrE(BM9&S&Vl7wIwf)QFfIz7?gl&?#kOB$+dNFQu;^f) zb-p0z{0H)g6bAt4V8y{BmhLFAXwZLq`KR_~^El%#5J=0sfM7SkUqllI9|y;M){mF_ zJM#sBlX9P~d3nMH&bvCA50euUKbf@dqPOH^e$q~-1$f}l!G{y-0?C5ZNtmvBvIjig zza&omGfCS4w{uAbh8-pvquP_UW21B5YRSX8A767r&p*%DI;NJLTujkpquOTm)nQL& zA$>yT%FoWJZHV1&N%^Lj4d9q2tRNTEDi}W)t$Xe)bDa#CJF$PMFGs)*3xjL_MKA+l zlKOH%{SQLEFsaff*kE~%ugN8h;JOFhY~`15X>dW^HhJ7mo?nLFg#kX_N_hMMXYn6~B3$(AR}jSHL2n zt;l;A1R3kjj?aMA2)Ve$V-VkO05TQayNm5(xsQkYf;09KH zg-c-6i$l^El6OMtOeTX4i%xD7)NpiadyYKK7thJ=noIf+aL+e3Z952K=oMqZ6j&un z;0#RZF5=O37oW~A7o@!Xcxv~fl$9+C+|Yh~c>1Z;u2-h+_wmtlf4O&8cG@XZA(KQ~ zhFd!B1m#)dBNA@xs+q73`^)O`3ATYl9H`JkoYnXuv!PbW=3Zp=BiFn>jQ=pFJh&fu zj!Z`&b7J;S{az=x-oKR4T7G%_aSa<_!kKoeFm_xCLa#Zd+KE(!NE+K= zz^V+97mSzV+f`vEEGpS~%E>W8)#s%%a~lMBIFel}+C)&d)mG}_-@Bp+$0XUt7=5Z8 z_DPWsj38?K+pYdX+%JlK!RM}m)X*@#l#I4CfqXWZf)zCRk**Amlj%ZNnT_;28v?=E z*ZmGk?>}A}C+?gSUxj{Vn=EvwDFZ*L6&FWD(n+HdHxqD2VfSlR^nUug#VO3fSYv-^ zjc27EG5ULWo*a0O+TH0lNT`V`y0}j{IXimqR6ML54jl|`A}BDD?tQ#HH@STc7iBx^ zcsA;W2-erfGfJIs5XuTlJvK8QH25Szu1A^OCe0HXRG8NtT4rO=1;=iY*ZfYxPWmmK zo>N1gQI)leR_~eIAtX@z+Pg?s z6H8PxH8Zr3JDv*kp`v`Do-FA9SN+WI@%qrQ|u&ONUXV60arCvQ9lXSfQzqhFYO%8=>{CVHtAAPmLD6S z>!@uuCaiN;2l33h59WR~Y4AV%EJ(YW7gG(Kh_3)E4M=JY$K#VUSthL&Q~8fw7~E+@ zoSapEh7C)CEEI3JAmKPsVqy&NUUN{RyP`YoDO7SfUZ@38x?g6ux|3}Idvw0|5oXnJ zC^r%*M+;6^afAYq!kPF&&?^lgKp;S=_%6jDB!EnSuCibFMo_63=+mL@gHOw)TC|g4 zYd+AXuVft^r@`S)lF|u9kUqiOv%2EV6FC-|cF&uKvK6<@O}IG~N%NI9Q7h<29V=~+ zt<{aI6&sZpk6Ggv|Ae-hXFE9VGHumz)=Y0HxcV@5NbW<$otOXCnC=*S<3{J?Rt9^x z5C99yZ5zWb;(^2fsb|=Ky930wZ`YeJ=qc3FSI^h!x{M|+`1oZe1g_F(4 zGH0;mnteVHN693$_iu0SUpmP=p`W59I>`uRHtllE8uf9T`40>o4$$r#16eCfS!Mzj3s;&b!{66<(i0AqmWsBLMYAdGD4s!1|6ll1Jakz+>Y8N;zO$r-Qc>=L7i#Cj{su z@a&ZGMt0-=nD3tBB!QpVGZvsrt?a1mAaz$NQ$sNr#k`#=W#f*;Rht%3)x<$h$MpL; zg@aRP>W{>92!yTf!(rJpLR^73f#OJS8qJX-J@y%t(~lck_Sd&^3X-SiVDoP29dx7j z$r|Ho!*BGm?5V7+eIOhDcUjkUPQ;Om=!H26^vpB*Z@%^{I3-9~jN!)~Q;^~Cn{`4p z{!cVv65Dzy7GV3n8}<$_aS63M>9U8J066Z zxuaXycB=RU0N?X_Kiv;83_a@t5SS9))KDvvEQ57ba;p^ za`9kFEcqm`M83Ut8C_J9z8FBbPfRGQgqpMy1h+qWWF!>*O7JR*`6%QnnUsBaiezO_ z=dV9e#J<>ArF3Ub?^;Gp(6w=9Lnu2{n3ddjOsggWTtv2TfLYjvXH3l%3Md!Tl%?AF z!ugW^?kPBka;v`}{8^oFT4f{#Dj6m;-_$KF;?tjMm@Ze(*R)P&S^Z$irv%0NXPRZk5Vhq_x{5}RfOwr1t*vd1K2>6*LEHHE7U*?^ z-iF+UwWe$3-~1Z!CWodNLw~Cl6fmtHo`{45c6oHVex>>qTKSy(9eNwGRy||%8wq!A7)wD59-3}4qtjIwZHAy1QddhQ*n1r>_2DvL73!JKcs0#0L zH;6Z2j*_p!7sZ3fsRHz8eWU$J4gR2Yv~@BD#)>inFNE-h=igS(T@rqRgMY0hk-btAA*8v9*tc|p*sQx%WgBTx>z6;I5t0m zmE0hR`aHl`Pir(VY^29JTsAcM zkdJQ2BH-m`W*ZYsBc!bkr-~cMxgC0^#>)#p@Y?VFf$dcHPc{HRj0p;}_S@1lV~rRc zb9rdcZ?OOJ!3wrRnPLUcMxp1azYXT7Wh%S1?I@Z1tiJkZ_h1g941Q+&lnUHh-+bhp zYiG>UG1q9pZcX)VbDQ&jeYHR8@ic$6DZN%?9p8>6E}-1(|521=T;U;OQiwjwH^wM% z`D%JR*Ns`B+v(saH(y%{%P4vuE}-+34K%1oQeR@iTJ*@dRH9%Hz|}P}u{aNx=XNSNlK>Hb6GfSOZ7FNJKk6ztFs~;CCMD`UvwbxLG_*Q$D^Yc+;(U5*IoFc!? z5|Pp4yIWXB?>g1uNJOQ%!Qn5=gom*HiK1Hb!oKsOyp5tZaE2c%fJf|N6PiI2lEHrX zNrz9|z!-XR%x~tLoJ_)xJuHQJjG`y0$|hoRoE!3qn0)mu+w7rD?!bLzD(>ZMV-xHt z8}U~d@J@bQ5g~dG2eMZh$sZk)=oQPTrCI+4fJG{aLI90W@^_7;wf_+ZunHay$<%>k z^!M$+XtZ`7RD4+s+`H0oD50wGYOf^)7jr6Oc8Rg-{ZDMN+K zCzLldXs}YT^2lf9t-e*s8ppSp#l0%puLU{R70oGss1RpAQ)4WuE;T){KNIYA%yp|X zhXF_~?|=`QtmD=^&}Q6jMGr8Fd`cdTePyS-C<@IT`=$ff`L=smG&y7koalNvgZ%K` zkJc^yvpOY+@Zx?5c|LzdA*%lZ#mWt8W=g-l+Ie`a3dbpp8uIwn;lGC-G5!^ts$Cno zkk8br8XO6j7iTw%jV~yaHIOn=!2@7Dv^3c3cXB-)4rgQ${`5Y1VByP?u~QOe53F+a zy>L$tZw=`PEmXb#&DFULd1Qdu2cHPi`!sq)t@5a`UuDZE-O;eS>qmm1 ztiJJG?qk7f*AR{95xjEB$psZRU#>!;l+`)bopH5&+o+mcFWYPA;0zgR+OC|_e1|fW z!Rw*mQ(HV`iC$fR(_hiGsbEAk_PvEVn1^&cYesakNGT8UG#l$6miGQD{F@;P?j zg@;CopV4kju0WfnrkcPI-@*^_ zNK8fAC4QN&R`)5)7RJ`)h-I&(m~jENVM+jgm~iD%aCTFUUrlEGa3)bxkrkkB&+c@X zyKH|)Hb)TnXJEk`k=|-MrbXXscAN6jF=ja%Gv$qPYHy@f;}yuo$)9AgQ&1$0cA*FA z+fI{?VuYuYW$=E2%CAx!Mdm5yd^6f46!Zp*Xj#Vf>=Q?N7_=iH$ zi>)bLVP{m}P`7qb$N!P2i4ncSV&aH*5Q4@+!<(v!{?Olr6Zpq8$FeM1^Eo&_ecyGd zcchpZ0-wE_uX3kv=EWt0(2zIaA!k~%49;;AMc%Wxznknq*7pK>*}q}e*^2ETyIH)V zZu1tJnZ?cP%vj-7wmPcCmM)O{bD&CrT1O_n_{`u`asT3N|9bx>L7;l6WHYH<%kmFJ zX^5OD3GAgqh>oKy14j+)lY(vkWT-FlmiRLxRa5QTQWm=jdlS2bn5+?F_(!CDFXUpa z1jwwzub*ZKX;D1d8w&VkdKD|xRzK(ZnsYAnLWH0gL%JA}bl>$}z5UO+7AvZzAaf`M zsxL9u&0D)~aV~s&JAV6!jwYediO~AoA(D+LEcklX{G6sT#)0)i#j)Vz=wa!OR4sCg@I&VZ(WESw5W?LIDPavbNr=QCgStyGAtKo3 z+9JDHqeZDo?zi2dk@-H4&h_v^#q^j*KecX~zbcMNm$UrpxDmCfo-F8ZRm@u~Z|b7( z?5=qX^AYa*4;~dRLyhzIiVV8#wGX>+R@rKj>H7`C7K!dU$P5ahH?<-B=ixd*J|Kj{ zg0&fTEq#U-&+AOfN)gF(DUI>$#*~^}g2(!gL$#WaKqX=F=7%eef2Z*E5eJJ?1r~b=PAf;ySPMSo4L(b^; z39QX53AuQj8VHCm!M?a3UD819@OUPj>*93eapX)|iI20d*Q;*RiTT{Bd9jmfTQva1 z6s^Sk2Pm1Dtk$gG1bvi(Dy@yi;@J64@nO3<92U zSLh$3mcQl8qX|jQvsK#&K8b4Lnjy^8@ZM<=*ozF9X9M&(|6ZY3VM}NWwIMv0euoA0 z6H==lXRN12J4{Yme|3f8Of@fHS{{EDWcTd{o8`;*SPtN)GFS;cB$uMo5|zsTjxzO6 zCsY}qt!Si%L1?N*p6Tc(1bc+Oc`)!Nqdh8Y-gWY-W70HI`FoeTW<)M#$oiZ3&b{Vy z$*fsXc8pp#XxrcOHPHnj>`);@Fz=GyK;IparXIz;7hK`#WaA{fk~9V2(ViU;MkPf) zMC#Q7#%kNJ!VL8T1rHEk@b75u)bHZfDcaVYYQiA$TaJ~v6wv=FRSxXs^s3-`4vThc zl5s?A5EK6K!@$Xp1TNB;p>=@{K&xIuB!ALt5W+*iH1|SsSOyPTMrJIB&DoM<3>KFz zmM@kjm!SZ?7LYQ+7P%KEEUBDglU~^_x*F9b7YVri#vSM{S0~Ocv>_VL=Zww!3E>Hw-maPK@c^xw*LXN_bsjit5=ga*k6qd2c>6mzIO-L_KWGCup%# za4B(8rs2{hX~PriZ#6lTD4a#m#XX#miQI9}M()2aV^9Oz+yk z()5kf;qb-{%VVsbB>ryRBNw9X#?@}^ZlCEA-^ipC-Qbcuye{8oYNE0O>l4O4;Ku+K z$y8rvi-K!E%>0aPoot}{sr#4o5p1n@5%Y8MD!uA6<3sL?hVIQB<_9RNGE5c|f?}Nk zg0b{0^%j&irFT;Opr%0^=GsPU&?`9Q!ot!LLL?zq(U94fb&=v*E5VIhhMbL{h?3sAG#Ta$Gz>JWeC^^Tj$!Lz^~M;Iq-#@9KF*SIA^bS<;}Z=%;j zTu?*rI^b3(nVHBV^|Qze@l`O?pX;t{jMeLqt{W-jAB$TwzWEVS6qXn!_3jz`w5M*~ z*mzn1^M;eJg)il6D#pxiUrdDEl8i(Yb6H@fL6+E+!7jGk%INVWuzqsRRI%hh??i2;l3wx|%ggh` zx`Djhi{SPQV_>wCQx1$M8zm{QA_4AScll5n5OQ)Awcyb(6jJh4`$$uiS7el z@MlpeNjrByY6PEk&9*~SLd987ny#$l+0t2xS*Xo8^5IwWjjJ+2cw4$k$WI4oiHxR~hh634y})-XumS|PO|&(~ z4^RRzv|0XwDd4ci?8>N#-qFp8Zkb0_;rbt17@UlSV~Sb4{-&rEEe&bE7Isw&-l)OA zNv{58T{PN45ti8Ih7=U0u2bnru7~3hbBvRZBO3{CR(8#&m8YrJqyc0qy#$Z7U#vB# z3Ukz%hX>>>Vk%#^j=fqB9faufxjg%cnb}93X&OKm3!KNF$(r${+(h0G zPsz!-*{U0Fk!l8o2zNk&5W%12yBF7#XYdeE{7ywtp8jwAXqa9XB_zL*>9oTY@>!W> zh>P(HKZMpE>mH@EC?9IZJ9*xmz=B$5FgbK?x0VZ4w{7=(>sFD#tdNz+&Sn?9t>N7p#J90B97Av}Eny|q@?vJN zbJl=~eC(dgB@bqBAG@Ubf5Z-xCT6zgT{$^+XkUnvg+I#t<7BF%*-WR=i!G=Z8IxW| zmrV&b12wP{S54JS+D2(wPVJ!n5Ju&5=o&ST!*DYZ5tbX!Gt*(Hd?$m@!0(@B6Ia(` z?`oPgN(XI6U1v|(8#*>VcH!)Y*E;VMJwYa1!F&GJ9IreUjtH2kgm)$iJO+AN8`?N> zqLv~%C12TQ-2=ho!oN+m%^sOke34N%&%`OaL$0=Yk8!o-WWygi!O;*{o0Jfnmg(IR zHjF7(q}}4POpaDO-O{s+OO_>?qpO7N?}gp>NeN&4ugM@}VIlKDX{O=Z6`!8X{K9kS zEVHk)rng7ZDY+Go4tNfuNA&tkJkb2bi0^-Ic8dP2OBTorK?`4Njj7$|l|GC2-7_|B z82|!nfcC2X;*g`1%8_zb_I=;{zSwlM5D!QQ1amz|9HmZVktwTwrgGJUIGlQ~#fqpc z#4^*?z2W9+#iA6zXed-yaW1=^pycGY=kLDs=s!6W9uNhO80@4*+!s!b3DVTW;=8r* zY61i-TQ`SSsg=`ih_&Rd1uTIM)r+1pt!XV2hHtm&k`(s@nLS!)rL+G6qY3pjPJfP- z@3m@vc_}EoVi74|)) zW$?0Jq}rfR#)4^jXr8sOTs~!8Z^fBau@(bVz>>`ADbrKA~E=zCTTPm2@I#m($qS_k56T(rwr1DaVa9rE1P zu0dHk_sKQ(YMmms33Pk3jHGkPQAqxpB=alRnxI9&GRZ`IOMmvMvbgEaQx_=~m-RM8 zHfk%B`}`t}M+p(LefALjQ$ke-+nsg&$W`G*M{`FhPxLgixhcKomx%OhQ_&y`E+{}r z=Tr`?=&2AyWo_)NhD$drYW2zKaLl1CqZ3FV&sHH_e%zdukT8v~nX8T^7IZ&+7`>Z8pm$)n{f(;87d zhb0N5|4H2yzm-_4MOoMP+X}8_9OWx6Qc}7^G3Ef;;?E1UMJGz(A&8+?1RpX55rO%q zYVwQ2Cr*B+P4n(5U9L2~26_!55%1bQ5fAp*o%DAC@NCo55bZ)5%Re~ceB)GEbyDW6 z6?3|d9)Y7giJn|%(WiMd;!SCOg(fcG=t>A6{sCp-fq`}Gw&*(4+2JW9JXpedfhf;H zo50uH>72%4W4-Sn-hYUET0;&pDa@}s9vWiegg;&L>c)&iMNPX7)i{|Ka}t%s?~0tJTe}`NZR~r@!)0 zn8kJG&z`YQkPkiVpnR=Xn@^z|X}@jyW8&|9Xq-{cb1)PYS** zxGDR}4ze9S#iy9|w;bp93?T?%Ti7P{ogvB~6(pIyH}5ddEc~=A%A)LFW>WYyMwnnK z{F6hCf{q8!}y8JyBL@Q|ptu4l!#RaZ#rV%uhMxh*Pqf8ldnM!2_^d8;&d9Ry2 zr}fnJi0ojF>DjqggvkE4YE{{73#ZUtzE_jxfBdd!heaHxkycf;G*jZ|Lja_!a{LBZY`K z4q!hHCN@a=Bq<^8M7&5Cq=q_Qb%CPj6{Y;e=jD3B13s$ps!voylGBqPB{$cd$=#z% z$35J|eHuX{XmpW$V>e(u)`~b0D>7ILyTCp@nRF`hRA5xZ%f~Oz@<6EruSa+2Jl%&8 z1u*;t7E%jF7-7Z{9K}9!J99Jhq~KSqOLPa{7ipB06)vomyQw?Qi&g!#U0gs{N|F;!}EGe9gYbI@T8gW;6zXLd&E2 z=lc{8Xo(hRLlvnuH89*YY&WEw*mq+7sp)Aw)8f)*Xj^K1w0WYh7$AlfZZ*8gh2D~+ zLk&BqVMYPWAeccw5nebG@?pXg#3K~VX)~*)Gw3$xt~nVTjgAJ?LQT~8tHL7xzk4PS zov#n6)v7V!vW_SQS>C_R&5%P>eGDQ5NNBG<`x7)qd(swN!gpO~0BB z4MOYBt=A0$0sQ2p@cGZ*8Cm%6QWyr@vAr|%uFb*WhujaP=k(1PmD`SKm@_jGh>_*4 zi$)Hzk&UsKjIpA+$fL@NM%kpCF8nkeafthkZ&|#0S7Q@4VHQ5cgyig$n<+aJk`wPG z?6exConh5VY!;WrmcL+8{CZPNq>1#Yq?MSInybt8(j{hf&F+`A47HGqnkCKYJy;TZk|U~3XVQf<(5ia80Hba_0>yv(p|~jW^$DLq-b2Ey3_$W$iG>W zmXWP8A`y+V{5iiNA2*yZuIcrJ9%K>>$c zD1SfADDw+FOl8R8k$53qN^7LW(!5*qZau%{6IS(khv#X$GI!#Q@G+)hLZSccuVvJ- zX_JjCWMdkp;}aIn4zj1H03Vcx9tF@BF?f*!bdFBZd0v6{VnO=;y88O=&_E4Mq2w9= zhHWUzi=>4H8mz)PtczS3Sut|AImH}rzKUd|APw2bE~1gaa-X$P8?{B8NEVsu!RnUk zQWcI>XkOu%@{O`e*=bf3NpV$j6ob$xS&ClB$~EP>^4M%vWW|nj1X$PES7%ObeaovY zMzsjYYn9h4Z#JLIKjI@HU@q2c?*s1y+hroztl!*BYCAR zN?DZpScy}fD=Ey2c`z4jzy^H#t8W!pWK_%&GsTw{8I5*(rH|fx)c?rcV@(QX7DN_Y zRdrLzsxowduFxrzfgei$wSxgZ!!&#nMF?( z^uH~p78$jO&NF1=IWD09wit{w49GZalJe(~mS0wGgm+Kam$dQO;;_dNikF(t;6qH^ zX#(wMGZ2PHklvW$L=6pDp*PxTOQ9l0KX1>kGJX9HjkY%EQoCpAv!!zs7x96zkmchE z?+AthEQT#KXa(S>){%Z*+H0W|=^|U?@k-o@`zF;)nwqrt(zwg@u3Xbc>0R}{s_#{6 zRdc8U1yUtg!5UV-aZi@SYtC?mBWj>N+L&*eH<;7OmRgVtTA&u16)|BQ0l0)~xWLRT zovmIYqHJ?4 zAF`Qj8k_d(+MudbooXQmX-LleGV@wSMtr}7t_iaCzNWrrrI7FuX&J*}tjFdj46qU^d^ z`s@z->m2yz;K_`xnFBMYsaL6YsRz-=G@s@o7!?ro7yAoK;E>plY#aMQToK=kD-~50 z2UXn0BG^54t8hK%1SdEay~zJnF@0S|Um_ndm3qX9Li3NXIGG5x36-*sH&)ulkGv|*+0uv^h9>cMOisvw!UGaL3J6v%J^zzeqsev$)=w0(nRSVHNPLwTZ_I3oI< zo_(gtS!2SPgxiTvwRYMZt(RycI*1l1i?Z8{a>`k1+Nt|VsSQX}*8g}w+I7dnguuu`lPe!wsgmnNy{^qXz0>61HL^ z)}acjpladozdmPIV1^mZ(E`oca`rX*ns#F*zDFfAK_!?_05ghWFnJBui}hu_#SL*p zT(D|ny~w)1V`HbAj^mLBJ@T*(yZ^svE(sDOI!hPn2EM=-n4SD>vLPifrf*DSthf5K z`l5O*rBfQ|V1oHC_ph+jb9GcjO({-JmP1m{rff}B>!Nkfbk4Ac9qi}_+DbqEVn#V1 z^Dsx`36mi1#3R`9Q?_RuPq&X~7uhDVnZ%@0k`-;Eb+iiw$VdL)VtLoW06mLfcUhS6 zlxd|YCRh{PENF!kDaA>Vg{^Gy6)7_Rdt&<2*`EMI67rFb1xQ8|cETA=^1l*;jRWH& zOv`i|9P(6Ad}qkcluc>++kPRVBdp8yGks**YbeWnSt7fK1^5;}<$n4XmRBtsnaIWx zHkfr}D@>0~(Cu-2*I{wb*v6Y0Zm4k2ew@PzF;0wAB>oQXEq#uBxF8TR7|Z~qpPSHP zbF>82Z5ErvPN{7sN}7o*G9xz^rRJF4dOig$w>1ZPVJrFqf@ahxvEc~n6aR4!6|UdClyWsEVFuREyU ztuG}Ohy`Lp;irFXx|5%*r7~noouqx8wmhw3djGW1=?&Nh_AUDk5AhHWiczK?O2b$5 z5?#c5+KJk(nnu9^L5%}#&<3qgpLJv%S*thh8pk043G5-e$L``we1-Wrrksg63)wy- zu=Tizm$(W&GK-ji*)p4c7lSZB8LsqHCfOdb)7g%6Yw3Q^?J2Bb18er2MX{&9_|^-& zKmV>Z4>Zj)Rm#gJAr-8KDVxsF^{4x~T|q)LBTEBv}y9LbGbRm(?O+VR%lx&=~FWcc(%Fe;$}GLV6c;%3GD z-xE{IJRu84WMMVZa1QHH2F(k?#WU0B`0A!yT~(tKofcuNt29Z~HYFtScJdP4(3t*l z{_5$ftE!`vNt~?Vj(iw?C8dBBw6F$)HNfq79EV|(QEgh$q+O%$8ibWL2MrI>E9b-+ zv6i*q$E6hH!3|EZf)RQE($ArymJVcb8L3ZAs3ktar-UxOET+bx;|#g+`PSBwtibQ(dp{P2i~t1L+PuqN^;M<+9x0FdA6y zgk#|v>v!dh2GTHkPkJF;l`dyL&hDJuC+E|g^Ep2v2tf#B zd$1n|3;j=;h?b(06eyLG+@gj@U5(No%{%(wxUQgb!P0_H)!Wql)RSl_{YV=uTY|5L zmLdsBNM?uGVYV0T&<<^3gz*m@e96GrPPUD07gNPtF}p%Sh0f(is-~#kS1m0R{VZf7 zySQl&|Chwn@){D%$j5x7;U?z74}FZG)WEztZlNhZXR7{3v02G+8YO)pmwIZ@e$&EZm0{CgcH7|9_S>J@m-KQ#`>#YD!J0IsbqU;_WZ? zztrXO`!APAUwCyvJ70T1`;F)z8i}?jjdCda8=j<=m*F1n;eq_QJWU>zyD?{5ZgSg) zZTh!q>3-bZ?5?PKt3Fkamnw6WRFCG+BKiUju!BR9YV|Bm;w#KL zhpk{M*g}*-Dfkp_8NF#M#*$j)2kfA|Y(5*u`lgyvAEmw{BE>Zk$2zdKtc8dc(IN@w z*>!e2^W)4enV%gTe(1*G@Qjfe(=xWItE-!HmuR} zCy_nfevpRo{pMh5P48nEA`$f)UZqA9z{u>GE0fF(%(cv8?L6!*+tz56(_(P*DvqOE zK6i@}4`?8DW((OSb_{ytBMTg=|IdrfvNX1ZEo{UmVw#xGzvf@^nITg`_J(%2v*ymp zJ4Y2q^KB)G&0$cZ!LA<-uMO!vAR%biozva$7Lq7Tqf&g=e5@@rf+E5&$**ae`xOTCSIE% z8jl&*8h?oVA+k|aJj-T@ED>jL4(E$enm4@QCFY9dVyU{eT2ZSjTrK~3z=tS}0F=im z9KkP2PK7#JpDGA4_PqNZqogE)?zuik%E{*`T{b>w$Z4k|^Jqza{zbdD|- z=}s(}G`F%XY_oZtdAoVPi?>U>%YooMK?y;#aTXVFhN3AS*{J@S`Nd)_1xciZ3_G}{ zu1(EKZ613nwoL2_^%M0u^)`y9WJ)iRb9*+jk%JECh_Ab&ElGAvL?o#)RgtReNfVPZlgH-S=C{ayA9k>TmfBJ$>c-0c`AvA=B(Fhoj2g5I9(F#`3qAbdy46=}gT&obP5bbc4kIF|SP!?qoglI$~ zx)^6VNKiqAt@sw-ibUZlyaPP~`&W3$RBSDXC~U_6+gN1OlD=dZp5X_q zKp8YuN~63O5glq=nYAX{R}42*<#B92AFt}ohGAt=ym5^2gZPA3tz!q-^-J57zQ!~| z#Z^~WIK`3<@xL19tziXgyh18s_yPVQ|2Ds2K}x~8dPD0`y$O{JmGZ0X5#7Xav6}^B zBlf}p4&bnc)o<<*eJ-9dwB zGJOC)c*E~EMrMn>DG&L`l{Atfk@2<>#*>}i>2S0CJg13HZJlbvfE*O4tE-u68XL&o zVU=k+cH(ehCoT%-aS>;k2eW2gy6d{$x~<}4F<<<2GL-?Qzi?)1nWyAJkDTZ^F=b<{ z^6c}H@;*Za1i>GvNI_aLN(+EL0;n$ypkb=Xsu8L#fm;IW2JS-^GLVexxPq&6oQ~5u znoDzNA^A}$@>fV`grg`Wd_^GN$~W+(sT)$yrnNide|YQRqpCrw@^ZZRM0_C@;To>u z>Rax?GCaf~4smQco5m&^h8e~gzVe>wRo}}acwKP2U=0nXq4bgTj?{*?EX>?$;Ya*{ zAJ`Xc2AhHd_y+GLHc50!?5eZVne`*3CDID%a~e&f=))q_#!P&H>EfoiDnhMWS@*V< zT&uedb)KTiQ(4LT=qMea8)%32X#E@CwIo=nL^V)N&XAwVktx?w-c1eAW$2pd%2O6) zQjYXodMe$zdiQ#R>rOXj-E4N#htkN6wA_{3a5F|?BtEcAAPRf3C6(VsY{K_4$wv8= z?wD?`&a>|Rx_fKSa`@2ULwi?qM{`H>U<|rt>>RWIyJGsfjE==~Y{iGDgh65ie5hZ{RKwz|dTDBrVbn`kn4?@u zSu9S|RHw@70rJ|2YAJnF6Ys4FeG%E&ue%~C%Zx|ZP&Sz@!A|Ul>a`ASc^U5DA?`}I z`EKcqzLEa1etbDrPFJ>VbI;}{o2}LuwN9F77Rqk28BEKxObZ2OD8FHi(f{kTNZL*N z>4LOcS|WXXec5&S#*ydCUz~a2tGTMVsvaxGin(GoJm3u1-`J)7X}jqW9!UvOjPy)5 zO82quXq{=b^J|%^7^<|XJWuK?HIsr-3$;*F?WlREN#x)1hddPPXb~-hO;NK7(P=tG z=UEvR$eeQ<RRZcuY#(G zFMwrsX*t&Af;dlCt#(@NvszOQ<*JuGgluFWhfm~__(;nJ6#JabWK&oii)E1#;|89^ zC-4b;WO(=R%!uVXmhZ6NIXb3kj8km3%A~SZ<p;nmzqh9q$;^%a_8&rRj*!sVD(`CTmE7G zC&gp&nDjJ-rqCpGL`QTeQmC{92@*TUPO=lClhR)d4Bi{84<0YAmFDo3aD@w83Pt=6 z@qbZF#cMLU7%#CKWe|uyG!@gSPW%Z&SXSfY@07cS2eK0zBArHeOd%Jh;1N4yuEsA) z)`>@x9;Cd?IvuTw8>YFc>ZAIR7E@VrLTP0FMqD#v7ujW;R@REImEBfdtfyEFYq7I= zY_ry$H9T&5^d>b~lN~$7j<7R7?`g{a--d=|A!Y*`*obLjnwZCT@$G!&tMV_iUwwAn z^ZL{qlZ=Or1x73BzH~;qfMFPd;e}B^zs7R7!Wyi_Dmp>?=!DJpwoh$7Y<;Co=hmk+ zXEaweu_8n0sqi(^1ZmvM zlllQYrBHfRq&g805ImTd=Ru|;rkkcyo)I1&dwv(ZqT)vtH(Cw1T5q*jnJGRNld%lT zvEq$Q)Sfz07kPp_M(&f|E^S15gmIv0hH;gyo6b*H@384`k0ayLHm99Sx7FO!oKmk7 zF2YBYLrv5~ji1v?GB9K#8`-QbtI4V<k6vu%MFYM>{ztB(L=XgrRlou(Da^A`5 zp7V{=R%$CXD$Ipel+B`iWru)Du_DJKnu-pysm1fp~7`s$4%Koc9oT^npusqm-MXN z^Xu-tOSSR4<7ba*7>mgmhWF?m>Y*lsrm=d2`Umd9H?nVN1#KW=*35=k)BE(nTNjD1 z@Cq;4P_~k-HO(`*8h00bWEgHpEKL7c6;)B?FF17iwF8i|I%NfAzet>t7@0U0+t_0E z4yNN%Oe;odMi^ma-pq&jFu^!er+TMWOYQgF=FQ!|Z!S721Ff=Qun;x9LJ)>KuDbp&`L6xjJr8;1fY9ra4QGQT%C?_xu<1p4@r~ZxWve$lY z;0A6MkQe<{B+?UvA&e$a9~w;8VWJ+14HDWVb_)3+ zWJ73dWNKt^RF>+j>Ic;s`kbcFhuDcjI0OOp&*j*`4z@@_64In0QZK1Wo}6DVe{=69 zz1H=V+~>KEa^D<&G{QL|CO;&J?z72>?u;a*FX(a#YLRQ6-_fuW6h;9 zi^>cs&2a?DxPe!QMl?#nuUKce{i2ip-{7Z^PzHtzNX8XRh829V7aQnt@@(U#oGnoc z&31YBNXNGD9()|`k^^p{fCh`1bOVFzhpGKkQff@{%#{4+eZ$-%_qcSBpUKC~XtBY%f|X+RiZx(CtO={h zsFcfNn-5qtdQk<80*U+!A? zt=DH><1r4C_-ekKe{Ggb@n(x8 z`O$Hs<0{A2E{|Q}oV!_7*T!qN+b_4TYnN+&Y0fnZc9UIWSKjEMVFN2zDV>!*%1~QB z+hp4-SN*Rpz4qCST{lMDY(dR268*Rhm$(jtF$}{Cql_$dW%>IWtic9(qr6`JJiBFf z&zy@*8#YvIEn+(>&5o;XHODjE!d>v-gpO%%ZJ+*dDf*?NUZ4 zgOv}_Wk$5a#AcnRe5H@J58P3&gOk9-Vrt$OVHD8U6TWotWZB=?@<)Qqs zG#g#0D%FGurl0Mkmc{ife2(dKm!4C+YM%P4`irKUn+$F8o!@(YU3`W3UhEVH*lBix z9r(rRlCNVK+bua9kBQUb0>8tb@=NjF@t?=P6C*Zk)BGxHm^ZP(^Z>u>DK+I!gNnBO&zG*3~^Dp!>I%6es^a!6UA zELFahGWk%c6syH-SQ9#pt2hBQ)J0BB!T{XFHQZ!r%$lX>ee(L~tNtQoujx;u{(DU( zsT`G~;Ly;pDxq$?FR#Jt;XWSVL9w$SSzefU#3LU52taw2UX`QDv~{pevwdh&$HvXZ zY%|uTvrSE#PBxWooUQ9wyIGs9+_j44m83`-DMti}N}@h(qK&i}_HclGq2&EqMlJ0g ziAY3Y5f)>iG+z2h8kwJz|02Jtjh#)9O-}!X{Ra0rU$u6XX;sRL4dMr}n?K?=`I*8J zUOy4j;tth>T2Kqj!VFAL|1#r6MpkT8tX1p}nuQwDjG|ezi59>fR=;DheeK?uEwf`P z;~--{<7_{7zp8#cU9()xu9GktAK)XW+m4Y=Kk_L4f(sf&qi7g{5sZqzQJMk)fwt&~ z&hi#{nLH~sI_;gbAI(nYO6JC@8S;B_Pr5)?={AB8h`_@BZ`p;vjLWzx^&LvJKho|!)oj&7Rj=@X@Ocp#hMfgAhOLr;Ka)~vGwq_SMG76ZJn0p7 zfn8D>DV;=<;LM;#!Jo^!(_+ytW^*0E^SEX2b`OhrC6^KvL)-9!u~A;9URDnL3N`+4fvv>x}ihn0`2 z?fZfFo@SXAV=(K?zQZ;gDSSaJK4^Dw1Gl71Ns@B)_jRA?_gA}E^?8*xbq>`XS!V`q zr@gd?nOOnTqZyi`=^J+jEwA4MjK@T5!WL{5vhrBCTzq)R_OfqGer(xTl~q5hrdIEY zOpz}#P#yuO@J2Tm0RbVMmX1i<^>_5~diU0Gt(&ye);HFFr{0gML#jop@$xqLTX~`M zLV6@!mXFH^7uwG9v}b#C|iuSxQL6m!ej=f&mENe zj!woX49ACs^C{Dxn3-Biu)KJ6(TM!QT0ZB`WAO>5U<#h$8J_*Yk}d6G+03a=^{Igf z6_FxF6bK#8=Zp?*Z66hbeHbQ+QlI_TI4HiY#xJTN31 z9DFi-CVO}C^zyjmu?6d~7AsxDT_3m%v^!^a%C3*X6rN|6+e`lyGxh0_a5%bH<=nZ>er79n1Uo0MhymF-2FkGk{j!{6!P z+{3wvb90nOFzSbwexV8v(5LAg^ey>fX%(M_%eYuXTe%*3=$VR1Fq@{E<{3{0R}VfK z1b&-e;fD)dzKR)%|63jEzZZ*)CZYi8cpnKkjaXE`2%W7`xxnFZC1Z=MC3yqU9QCX} zV=MRwcu)wu8Al@KVgc^sM}8h)JM(^&>PR99N!wE&7Yur39lKq!R-aZC82i#?n$EOn zib_zxzz~mkB=9tz#^VY;D41BV%Da{KRDvNpV!lqCfhwKCouiu%`92fmSj{xUdE$l?g_*zJcCH ze_tLezbE&jV|0+#VgZ(U*g^?aUMX?NLKZULa_8;Q9<8Z9Ri~!X zV(GXv)_apzEw3l)`|9fIPRKweGF>0JrnoM#&vr<$Kb%%N^~M>SLVHIE9mPpzJGq=bp~Z&Rx?z^_?NzoCD7K z&+-SYql(lSJ+KgR-1hjsaYKw6V+Z36?MCgl+Bwvg+86QEfUy{h4@4W$f(|&_ID0s6 za(~Z#vRgTPhM5qBLt|xB#-9qh{qLNaTIK;p7?FcHh{HLYLj{b;J0&U`r3W7y8fKKu zO`+LTQC*tdVOf+*J7_*jgb8iG8B2+Hnw62$@9xkS_OHJ1bJVZV z_cnaT`|>jECUvC|g(IjX;af|zLUY(bf(={BHnI;Io@yB0;DmpSzjwJsqJd~3+Vf3( z6I=0%CCux*9wh2WZsddh7=^JZhSXN6-(4PbnJ(|nsh&G9cZj-@+F$KL-_sAY8}4w2 z+gnCp6HG9PbdfEL8q%a|9^5N`f8G7}4vsjKemL*&%Oib{bv&~BX!)aFN4`DccGT*q ze01iqPmW|C9d)$BNt;v4PwVw*dMU4?G)x*GwZ#T(z^0-%2wkFUbd{-?j^*iY>c7>0 zNh@fl_$eia@lQwS_eCG{3z-zsF?603#8z@GA`pehf0K+_YTgGv@WE`%#%v}riE)04 zpWp}hSU#E$lCR4L5`u`j===k!A#46w*(e!u*N`Qzeu#0`p%kUPnPr79SN z{uqHYq#^aK&%^+h!jf5{xw*NWxvRUkyPJEshuXv4BLR!B6ieU?Cpft@a5?B4;?T)H z$==3%-<+UCvUzMan^W9S%U@uz$gjW(td{r5JLN@LrLwAKt#0hocz&Y|jkS$GYV@4N zu{idWb!WX7lh*^Nz|TyLt4|*pFZ8!r~*0wMZ4HdaN%; z(WGY|Ar+a(hc&El6Sr|&@|2t;Rqm@?yWBR_t5y%M-m!5(<5i8iGd&x}%Ahu?p%TVm z4BmgE@L9G#?%^(OQ#8dC{-jM zI?IMnX_SI5Igt~+LxJQ^3Ob`BY(_wn`x5Ygk|ZSh(sjfUA&6{QZ1>Hj2XGW;i`}Lc)5=3U#3TMK-^3Re zykF42V0*yD0Be8V$<(oHhZ(A^stKwIN{*rvlC(wIz&E|v@?!Q2m%K@N@8`Ko&!sf( zK|5$CWnwe7;QO~+Q9wXp$t<45o6Tlhvj{#DgrH`sp{nQdU6ev4s4bbpiv29V@o@hw zyzYeG4l&q`k5L+ZO~+`3vh!YO!H+4Xhbx;nU)S3i3^lqi;pMik;)VC}nPHh+&oXg4TxFi2FiD7Bo=9h#cgw8Egvs z)Sx#=2IfA|{i6HFEyG$kHXm!3WOu@DnTQmxL>lkTJMeaeqvfw(iGKx&+KRd2Yq5r( z<;Qu;XPcfq37K3|ogtJ(d$8_{9t)p+@126a#$tuKO`_V!JHEY3Ivrh1Y zAN=tMA$SP}#tL8N*UDrlKmo4cNBoHGNI?oxiyiNL9hY#8wP$^qul}O$vVOKWC{~Ky zzj*fI^4i=d|4IId{L8OmV|vFl$9l}doWgjOqGMTpj>AXzh)R<``DcHg{cZLM<$G~W zxG*nx!vkqZLmKR12Yb3hcX5XjDUp_81J=_9eu6*b-qv2$9@gb-)9gywHFNQC9`AJ0 z`I58jT+e&2H+etvR=kAQ_bxqMgiEUDryeamc4*pa`)e$|$z5nG?W7~@L-sx!%5!-Z zqe6p^rM8O@bwlscU>eTqvbwBx+K9A>^eQpaVo%1UTZdZpu-YS1g`bEn(mX2%M&z>L ztRDN&u){dX;Ot$+XMk5TN6E3R;}ZIY@<@ma;-a`OAD8#Y-`a1ohrO35E%HPYG(uf? z;}kC72J-$DW>SRq{OKt5@39M;@eJ2-S@S~UrrF(XNY{g1om{85nq5DkGjxtF(MyUG z`!Nbbc(-s}#JxygeUjc+|5Ej%YMts+c*7mOMY^gx+~I){7>p0Yy0WSLaDHZnsU`lj3uFe55A!WnhRV zQ$1a{(J*R8VfYLK*_Y5l0|jbmnGFL=6ON)jt*iT9odI>81x*jy7dS|1q|_5yX{q!L zABk01jg@blS*W0bOkdLyT1zK!3g@#cWG~7-bBSNxbh%0Tl=R8z6>TDImfLg_lf~C! zA$;HguVNp8_j(M;c5KJ?V%K&NvIPAGmvI%>SsT`cwbqr_eW8!1%`}dVqYc_({GYO! zW+D@rkycTUqK=qCjHiv~xtibOcM5B!=!?xj1R_v`hzRjYlcIU5x!-<4`-AOr?Pl4Y zvGs)s%drJBFc$CQBu?Tao5l{YGU|isgX)Lu1NJ^UsWobowOdq;RRdH*HHWkvG{dZy zSVdY-vv#zOvHsS<%i)^+e%wPaS^)@##TfM+jiPZh8Iv&{AF^hwKC6OUWZ|cH`Jx}_ z@(sSh5`LZE;5TWsXi4J|rX^HP)EI{tx)|ozhS&_Z=_}fau40ho=Vs}st2lv*-Ntz=lJ%i*1K}g?}B#(y9G@^BP_u!)Imek zkhGGOWSuYab@?Y#-%Y)k^1X6U{3sf$b67`Kfy`7uqA0D;cCp=Tn|LWgM7ZzQzDIp+ z?2g-gZM#`e;G)nlfkJv!gp2xH9ESUz=ZERp2VUPJd7Iy5J`6|t(XUZkkmF>#!ocwX- z?%YjrhZBC3Y*aT?OEAJv!?YNO*jZ?C3%77ba*=E$mOCl;Rjzf#xfP#QjB2^5Wsers zq@L0cX$p0su5<^-a2PA!=$dX>+AA;v)Ps6c3rXgac*f1Ww}#)|{-o5?^k);T`de+Y zDlhtof#MuoU=OFi_$1N489Gbn=>m&pFW9}@+qz!4gT+Cyo@N06#{VgsX@B%b{}BI> zhM{-4neXNKaDfY)iXo$x7STWrwP+|>hz>Tvwq=RSRf0=EUdwog}*-w z6*2SYZ}<55Z_CgSi+}#FpCaLg0PqIT2lWB+q|#IY@1iGqvis~IyTf*{?d(T*zyqEj z(9d(mqMrp|@i*u~-RK=Qg-v0TvjVe6WbKHl8(S}SlIF2SrwI{j#JA#moW|L=?rvLb zh(=~$M$;kFVbgK%UfwUfZ@c@r&-G}G3%G=yIE_>61RKTP#|LPI)@bQ!bnWN*u0xhX zbqD|K+1V4bx5(LYgq%>MG1uZGw+Y*@MV=^kko#mG&i*P})nH(Q^!hEE+cw|UER1LI z3?7Ad&>MZQ605O{-C}px_2}c#3!_u>4(nIseJ7>y`qB&RggqW(I7VXhTdr$yhYDj) z@Wkw9E~oSh4h*gzWRh>l17s@P{_=wleEwk1{EZ9#XY#M(?Kn0e8i(mC_@O?!pbvEo z*^yr+dFZo_;;7Lm>v$!pEt^J>bPo=6oXuhr*=USqeG!Y!JQ@-k!sfHO*ke2+R*Ci= zpKENadfKeYJ*;FDgohw1KWJ^%@T~E&L3`h-zjBf_$2c|?YTRUgtQ7MwZZ(}S9dogD zKIS~8#TU(|G`-<))8P~Q(&Co5Bf|JGewgolk+YMVN>Ad=Edsce1(cD>jd! zD3YRzltIT*{=f17k%$v(E!6Vc|J=7~k33jl99eGh{@{@)C3+w8-t85I*;s>S%*YfNaS`WPu*DnK z#oHypd4oN(ceWpyemVU?dZ=7mE-h;?8B{Y4J96jq1NUn@OmsP@vQs;XIAIW# z(G~WvLIM&H&v&uqY?V366k%#nV@b`vH6p68D#NQ}D$kWyN_XjFX`(a|S8)|r-ng_k z!wiK$L=qfe4eR{%`4jR_TyA@1)0GB^TM~m4*4vb|G25ILiV#AAKm1Y7@~t=4Pd0Lp zg8~?khs%h^Bf|v4bi>!mCgmGtGwaA&u^~uB%3JqyEelMG!?`n@;HEaJnL3?KW7F9u z*o@8CQuIS^aS>N>nRRFNSm)enx({`UbeY`gClRrD)E1rB9D~x{OB<8cCB0R8JuTnauZb)vF ztVucGnC#fe;cMl*azS~5lp^HKa)m40FrUq3GnA!DqT=MW)yvE4m3FLljrN3y5eXub zr|~2nMK|aQU4svN;Oge+dcf^_`*QY8?2SsE(p53DX>22#O`JHX-Z=1E=A^?h62sV5 zwur5etfg+!gD!IC#+?s%DPA+YTxcsL&_K46?PS}ahY?1E<2tUx1Re5TK8fBCT`hlK zeo+1>?P_gPZA%(SpVCMag?I5T`qDFcM4`6#?ZRxmefoL#^Lm1VsEJCriCegZdZ>%K ze^o>MCyD9nPWTUzfybnU5elo0OPCpBFf7S@c6Em`EMG_7ysLCY?m{~#NO-X_>^-?J zUB^kjn|+TIz6;HnBC=@?b=U5rr^2CJ7rBMacK22)#eO%tZ$3j^LpMa6k7${hlKpx1 z4#!qbUpRsy%`MC=aUW0ckZV%8*;}`Q6|86zZKWgpOFo}Zxf^hI?!Dgk&fmLs|66TUZF8*$)uTGp2tM$I z-&=w>?O+Q#1fl{0SR9LG8FIc@qx=}(XPFfisqK=Hh%{CUO;FVwZ{DEv${(G- zzku0%Yx9xKLR3W+RDl8o0urRR&dv%7x5TdCO4yXJcb`{52MoansE2xJRIDCFAj=R% zBWWysE>)5oq`XSCD|M>a8^bXQW7#A&fqj5hXjOQK;oJB#E>Gs|1+u1!-PAZp`kC`pzu|4RIY51V< zoqZikmi|rHj4djr%2vI~zMjppTXcKc?N!%XRfkkftWp{KaT^=iI5vSz#0o6KQi`Mm zikJ3C2c%sY{+VYpKhL_Gxg#qWjyQrc=z_lJP#TunkKJkCpsOTk}6dOkbDLNa*p3svrm9OhO!1B;^}@Gq0ZOW6H_> z!7z|bkQP{fh4JJ{*Ki4k*kfu=?XjITW)hD;HC!_#QGM~koD~7w)2duYQ?+{DHI`Lg$K--%}c#BxyM+hFsYorO%!K?;3m9lrY%WE^MO@sDf z?X%j|R##N_(%ck3iictY+s78XEq;lX(>g4tp7gPp%{KE5JYwmirA=eM%nr{^$XTQ9 zt?j6pN{y)rH7`;NN?_o08rN`|&t`UPqOPZ|OKu0(JlA}eyuwU87KxNzJDTN9SYo9m zNU#ar5!Nm2II5sAY7{QOi;iW*um)?ek-0NB#$A@X%ymBOP}|`hhiYQ5m>@>*Dm;jn z`CEJUAIB0r+KYNqUsje?WI-8KGFE0Bh#44DKW4aQs^$YtDH=xos6OhTKAOK}AKzj_ zG%+*NndX~joA#6$U1nbCv7TK#2YPOT3T3g0)nYYS#lk-P4&Fgms!VmM3C3V31~^}L zn&6bLQE5h~cZufIMR2@>8R(2dIPmkL+p_)jBer3?93=b6F4-)*VRnn!{E^MT*YHlLQ6Kp zJe;12!DVu|y}C>J6Pg~*<*g-NPgM!wFu@am3V0b8?HscyvQ%WJ$auR*ySa8*${b~; zG8YGN7{|DlD?BGJIzJ%4UG0H&Sgm@sSJ%?jdRM#>k41TYil65PphNcW#Fkj*q+{>_ z#<8tzE88tbDNV)VYwfNdyRqiwu~!pcZMN=jJ>0syxF^bpYAB1cZ#@lM3oW$FmT8&R zd`5YoT=zWZ@yPRdeTN2R>xVji;F#oi4*(DYz#hN|tS(Z$4?|}x2LLQbH+g~VAs;1$ zBvQlo4;kif;tFoDXeL;;&Q%w#9|>z%;X>idS(N3un z8nR|%*f=&moziP&9Dmh2dO}RDTC1L}zC?HE0X;=6)J4;`l$M8lXd8*jt zEj;#q{>Id>jL!V2eAxO9Yszed9@lA|Y^G{7M0$cN{5$hOc+n2c0r)Zp%|j}$d|l~V zIm0HV)CBblhoP#9Y=b<5W}^bCvBubsuX06hl5Rk#e`v4J?x=~W*o1l5f$2!ZOJwl2 zycKV(_s|FE>y#-~YH+C*?K0XnY?GrdP-m&9k_|bKH}uFxW?@1H=~qj8%QloHNYq34 zh;m$T;yDik9xi-%?Pk)gPj1drjZ@81B~dLZOV-$j!#GixlJ-ql=2nr2#7n82)Iq9| zwK!{eR$Q0ZE>4|wejoX6^j$-XXgbYDZFE6fHjm9>Ulb`aBnLUjF*}+)%@t&moGZsO zduC!E<1frHB7fmFUlcCl8ZI*%X2mMyUe#U8ZHOQ=LKTGJ8lDu%K`I5QNP`F5 z;U2Rr=A+my`EuT*{Bg{l-DIi7UX74poRuA>~P_*P1Rd&)sgSmfHwq;S~2lWjw~$=tlLZI$E*HtYhMdq$NrDk76F2dbG`k+30O1i;H4{ zxPm*lkGqPjrac9VuiR-Xj19BI01$<)b$23BSzC*h<+~}H=FmC$KD)*uP!oL+rsU8UietcK^q2Nk zPqiNDUd@L1H|jG0vhfvWAPNUC19wo0nOO_wZniO>G$+3JBK&Fi&YXU^@8#TdaCg}0 z;HK18>M0GlBbT|N->AQ*r@Bw-%&KGEaB2f%{TOnkQWU^K*#mYLZgBaXj53xumJ>LR z)7XWb_>q_5j=a?EO?MvM(T1!GyBId#X0>%+n^?L)L+Koj<1kLWrCV4rD`v$4P#&du z9Ujcjg4$3^YRy7f2n$OapXQamJ|;WnX>6n>Mq|=U zqZZVNnqvu;V98sYqGU*rxfd_Z?M(Mg2h28J0p6)z>%AhprhCr9c&HG}(peVE`Sm=V zJz+1{Gg?8V>2v#0_G9fA**CIZZ8y$*&HRyKjbk{9I5a~Wbe1+t8>Me@yX6kb{j$1O z_14v1wi)00POC8ahTKqYi|VL^YIulec=U^HTZ>)z2HmH7>>RtuPKSp?Y>F`Ir|A>* zi{yFo0{I-uA^`qaghg2NmM2x9K;aws2EM_x!nD-9IM5taE$F7YtNOY+7ejCvolq9# zij@(q$ozZ7^k13q8D=03`)M!=kS9h{xX_*)m~$p7{e>%B)x6Mwq%Sqo^9WFmd)699HI)V>UY48<*DrkaqJjQ&KLwEXucF;m*$84B3 z-6wr@`j?N456?bIvASth(<+)=$w@5WZrqxm%1_RB&G+?n@V)8Vwe8@xH`_#L+i4eT zJw&K@AVRnscj4+c&Pgq3p@ml55)Z@^zJSl=Gr~N>Mui=|_UX0W*EgzMRiCOd`D}KM z4HF$P0LR&Q_CD+PmTvI^^${d3lO{;Rv(?!i+5H--8ntK;UnQ@~+{$UpiZy25SdJA~ zhDBI_`6w=y7p4koqK4&vsGq8*Xg-uOr1sJ+){9kQHSr0?VF>$(jbeR@RAdzv%*sm> zrD0Mp-ALUs-A+18=jc?Sm@2<3ra8z#F4~|CTIlcT@8-=nFhI}G@vr$0rZ3D*jjuev z@v7?espBrkijH%r4XvahtPktL#{R}&{5huLQ}IBY7k8y$(kD^}rwxv`9G|PXda>$z zZYyo&F8m&U#SfXUnd8jSu8FR_UH5hD)xo+$J%_Un4IMVqeEO0Wvv1i}_LBkOb?+Am z5+rd%92Xb4FL&WmWSz)OQK@;g@>=J$Q=d?8R4>5}>_HqG%SN)dJ!v2U5yWS)8SFz< zWz}8PpfzXoq%xPrLuS#Psz{_-he~1Jo9J6yPh&A;bB-*<~Z| zJ-o)WQo3dZPnMjh34Nek=0jLB$;osd8%g7yMR&~s%{Qt&9BX?XQ^|JSSu*pMH1I|k zmLLKC_yB+ls^Ts}5yJeLAM?gw491`+k7&P`z{o+7-$zcf?`eO}zJp?|WGPWtjeYc) z#!ItPW2!f)?v^@@%4C#DFMUMODLNrZky1Fn4p%s%?(Z+7=n7q-TRe zX`|JRM{DnC$7qkx_q3IEu}qfAQvTGTAT7`o%`Jb`me=mp#xNh&jja|lXght$&f*Zh zLq1;MMUhH-iVP${Mj+I=hFo=SRk59R(1!q4;LEqzOrPQ@o?4vYpPzhw^m$|RGjpa{ zU8ArXm4a%|$b)X|3D;5T_@ z&c8aFAxu+PFt$kv8FYndKAO6+LHKXS=l46)Gvteq~K`qq!4GVP{1R_vY z$&h7z$Glwq`LvX@0qLET1 zy?K1|C(T_^39eYg*Ycz67Mh|yY8NG{guP@h*$W)R0qnC~ZM)cZSI@pZE_OfDby1fu zx(w%I`9}U7Hefvsg$in<6sg|2qBA+?3QZM(OA02yV_k9~m+cu)+N`wFOligX3hh|sK3+o`Qo zFXWr@IRpX#eBt{$$tf(^Bnbpm@T*X|e6?wNrpA71h5`?C!nZhHgtT)1LIVwSq^Bcz z$z8bv|IB@Z`$_jN;SFEZLSOVir$19~e>mwvPD@yO4lHB=7;e5AXNJ?^+_J zYVu0F9K7HK&x-ddP71!$_f?<3-fo6hhG;`~x<(O{>D1n-n^R1Up*8%fHD+d(#5$up zdY~r`U_bV~ame3-Z}BafqbZtdCu+87*ADOfUb|shQ%7@i({$dPwPXTe2zzU8Rts&> z7B#6VH9$=(Co4}Yt+s)-ns#8J2W}^HM2Ekx(HIdC6(^-d(Rp1)A0rueNI^V8m>Rpt z{{hd5ePw$%M_d-Nw1c;o1JwRBjrNOJen5IxeU@gMW;IzL9Z>75sH|#vRMfWMPO70; z1^}ppblgW9xS-}Q(x8|jAZQaUq-AV0o5m)EUx@fF(sf0-6}~HP@%nruuZJqAgsO&e z246!f*ZFRnT|);}8IaU}P*6gUOHey;SKJZzd2L>Y*L=&;{@1+8^YpL903!_0Kn=}* z^iFgMf1cv@z`qXz3QzzIw9sM~zQZ!17a78qT2dPth#u&HUebLjQaW1r`wws*4^aa( zQN2jL)G}bden88AA%ACLU5kvqL>69I@vSMRZSL6UZ zKpTS}oY)Y@j!GRvou2nJD#sPIZd;%BoL?xTK8V0cOn?#Tg)(XpYm1Cp#=SLIkBzJX ztHr9CqRorUm!98xap%R4IxC&E&Vi@%T%Jq!=!7^govjxy9cfeJffUY zE-R1u3;vQv6yAT)JF%tHTV(VFULYJT(F$!zA%X-uIKUZJuz^D{{cX+}VF^EBQ;fr0|oLK*Us4#D@1gY?>(}|dbPbi-Z^#9sB z@35$@ZvTJxp27@e7^;XMRRs|XU`0elv3Cde+%zueI0WVGAW3e^qus8>#j@h9#r-TkmoBwfpjuilQ6;q2 z_^bW7naMLgoSrN{CjUV`lSQ$k>?9dOULzwKXMBw`@)8Z72$O^D`?!x(^A;78Hwn}B zlXhr_w!bSNQS5f|c&2t=TF&KG%C z^6PV)a>8;_YYor0MT4ZRzL5m|O!EMBVM^cU0aHdYTt9AWX6(7`fr{|=3Xpcgs zv##YIYTuf$S2n`Bva7F6^X3<26?BEv2|bXH7z99%O7w>#f?u)(_$4OsHyu+k4K2|I ztyOQSeo$RZ-<#(8sGlfB)KWAI1JE2zk&Gws3Y-%t2~2KV7w8>$iQmZYIzxq~+CYA+u6ozp+Wh>4@;K@&>m;0DyO_?; zNs!j3PWwTT>OS3PtUTE1W08zX#mBJ-IXHn>G)FinEM9D~{H0Hth6n(_*0IHGDcsQn zE~TH9d6ln8*_d)KWtsI8>m2JCbzk*e^-j+y&rTj^2Y)wc+kgO?PH)kZw2qe3Y|O)4 z%=urij@C#cjr@c8bD#M?#}EP-5}-m6OG65dbGzAm?(pug$}gXIO31eI0-;njjiwxQA8n#>oHSbG{*_Iq61h!!{B@x{$8MMcfBm zpNs*Ixy(J<&o!TDN0YTyy~!23V!Iu73&Mx?7}0|$hA7S`4ho+MUkdB!a{3Yd;CFo5 ztDu6~ya!nf5v+wt!c<`{-9>lP?;*edwYgzxn2H%#E-PZC^b5Lzeu!g;!XZ}0YMG9K zNtlejPv_EiunRk|4R2#Arn4jL1iL`|h%a%+J={S&CSe+;3)6(j!U8&-PNn0KhDS&? z=W?6fV~^Mwc8*;$`!KbJHEhgo?&?tk6>UX*sUtFwgDjz+&_@_euhD3F5*x4q-(oj* zVV~e5_zNAe9^YdlJmCos5KvG!LIEXpqz+VsshEmM!g1k@5Jw70G07p5$apdWwottA z0c!dfY@nHCquF>ije!v+r@qvO+Tm+_hEFjQGci|i7Tg50Q}{Fh03DJ^L_t)4l%ND9 z4FY%&lgT>yf_y=i5IbT;#KL^xL*eTecBTZxyz9&j&EQIx)5UZ;R^l715z>Wpp#&~) zK@+oT#iYP#hGu9=J|G{EnZ$}n2#|_YJP?`*UP4=}zzTeBR)dtI92Mw_uINk;(gSoe zZsIzwpbdi2NeB{xgkJP0jiTG2McoUN9XEKRmGFh|g|ME+(OCKuc47-Q{x^E+hT}~O z4YX_@JIGG4hwL%Srf2CXx*xfCipS>kXK@}E*jwyvHU|qa2Xn|$vW(0_F^ce<4Q9jH z1bD*-UgQ`#Om?u>**G?h6|quQgP!PtFmT|(;{hHZ4Hw8oa)#!U9CDwzGG}smB?rkK@&ml#2~TrG1_mAi z3;=kcTBs2SW@0Ahm<+|tYTjpfhFoGxY>AX?CmYE(Y$aRGHn1{Q!RpPn2c|YRDe-*a z0Z-bVwxi8WJ^PpFMc5xhXmFgss@N0ZLw0}-;=dN^`RlviDH)XV#!SzOYk6IkZz`Xe z(KJJ=k`&zHR`5}VX!BKbSv9L*TBczo4elxYzG5&YToq!31a^!aXU9vMlscBK`E=*! zO;$R;yJ+^+`;?dPc6>87pUq#&yHzJQ3Q}MTF~lbK!|}&% zm)=>pd(rmwJ4TA1NUup(xovFb@49S8^z@Q^Mrm|`59kPRLC)+E0VP`0_ukQ|>fGIf8zvDRvT ziTg5lAGe8RwCrRVe=|OQ*{zKsCxe56GMle%zTbV5DVO`-rgnTVBPk}yCpiiUY%hzK zER)WaMi{3UHyd~I2e|@%!s~c_sf0`-)5tjbH9bOCTNhXrS+Su@hxQrt*m{EXa%<_){>P*z=6IEO zweXY%{nX)MhwrI7rNoFlP_pYr?f9Php#Rx*;cZ;bPiX+ zNjYmFS9mH^E88o5mB)IDdfx9*<=VUH8keg4@PaM{H?Lj378P^4_sQN%!#g$~>wdu9 zywSFJ-18~V@5CL6{WAVauWx#f4)=Cm=oIHPj-|6~R!CYCAL3@7lg1$y@#ums=!B)1 zg2gGeDPvM|AG)Mz)Asj%r&nHhlAF8R7Po20M-H+yR5MMpB96P+J3(SMPkB(ex6_7B zXG601UVKY_TuewzOx#xzBJCi_4_n>6B(%r18`pzl{L|K_1wM+fEw(+b7=dGmA*WQe zDvfHQ`$G41Zu!i_A;*ovcOJw(hsObr3%l z9}s_!Q1n40+r^f$?*Q_c%sbch-0TYp`EmKz3eJeF#P;G!l0s6*Ew+cPW+&V5ZJM@T z*6nQ9%U!0$-%I!|QSs>1qpI|9QH{ou=>A1jQ*f?2oPclyq9wDxZ) z6KH2?6aWisM;yv=U+B#x>2sa#dz=}`~QrzOFjq>@ra zko9?+nC|k>@X!g4iH;8)ONCxSFJU-6N6*rOe{7P^qF59=X1d788L82&q+^M+)a80+ z%v-VNhb$0c&*y59|X>`=HcDQm^0@|1Tn-9f)qe^OUaS6ydWx7~P&yUS$) z0Q`+-xg_qHYO#t}@rL8Z2gaWOY{Nw3DegX(Tf4F5P_3(BlHs1gyl;m^vuKvk>P*YP z79pNJJqCE3donBQ)2z)I7c#;!@Ab0j`EHL-9Bdp;+kdNYQHT`V3PK8f3VmzsYpZIz zadOU%`#Iu#C_1Ct^C2bEOX|Z|*v!yQtwy(;*CM)ZLS3lJc*ODO^W&~1X{GjMVZN}2 z_3fI2HPN+gH3Kz!G()fryIC+k#}x9j(e-ZZ#(r{vTqKujmejmnqe>r`J|VMG5DOiJ zZfX6}7NxZcb?zo~UF#U_c*#LEI(-x!-m`v)roGnf*sx=to!Hnry_d4*75C-tUpM+y z|A4Rios-X!Es>o))A97xb34TcB`WdPhW>`7MpInaaY)2%+(B#jz|H(=-y8if2&Zux zM=C$A%Bbq}G$z|Fd%1e0dQW}lWY?68RL(8mEypbjJdEJ^6GEbpq&lXuQBAbCDZM6j zHjjb9tUnu}CiQLWzZSdeM~F>}Bt1fcf^P(cxBsTSb6fXY9dAY4o>6nXW^T=`F@s*e zHu}275R1hYm&H-yPsDje?#0WB-?se9>XOyqe)}Uu5#NgxqDWC`?e*F>>vqIt#F6-; zRdcGYR3C7KYtN>;i5-y>>GLZkNu{g(;`}hXo6X)M zT4GHM8nq@+n_tkfpjBbhUSIWOJp!6OZ@SsV2yb|k@u_PctbR~j6<6g_-9lThbJ6Vy zTG3&2yPW~gTi5#e-M^N4{sAd;DtxQR&~HxU+CD{=mn@ga^DIIv!YqE?#@!C>5csOZ z=pPlP&!IsXrXm4r$qfXeZ>&rC-G}c-?_`4|F>}K$Mv}P!trXd=-)*H{wL>ON+`5mdjH=O6`{FDd{a)FB#|)=sU%yI&5)R zQTKJie&LuRl@R5Y{7JfGcgTiW^873?-_#1Bye4O$2_LOm0)@7ZQywrqr~udJ$Z2*8tco+R}P25%7N$f-@$sy0o!#9%k6GmItSfyK) z#(Bm~jo*DY=&m$rpHMF7STM&KKj0oJBW(xR&NB~}E#j@>Wn>NclEw@1!X05UickhU z>|hUXa*0HfZ^#i6MGhn%Nm-m47|=Fge(P#)KW`t;qo=<-{r1_pZJM^R59nieNcp33 zhbgv&+|SZYULfHm_L8p3Z@t@J4^3;lr$z~c8cBo zojqcDScb*>y7~lq&q6+OQNRj>^FoGXp~O#;ue4P;R+j>2)_vJfq=lGn>lv2>Qi zl%~3ncgZX|o{T0>>GMaYAH`(017Cecrho9 zLp-8!8D}wsbwvNHqAcfZOGS-hzhXq+HGQ^3$S%HfY02f#XQQ5#=l1YA?d9c}#4fYe zY@~R%I7+;ptRq{=*FvRG$B58U2o&-~@6sUJQFK(4DKdv#R(RP+Z4RK4XrZ=z$MTHj zQC*I9fbKgd2PcW+O3XnBUc(%m1Xw7`Wjp0J<=16P>}S{q+n*=jla-`5RnQA`qSQg^ zEd2l*u#|m+w=o+KN`+FvU~|Iefpy=seQBwW9_6{`733`w?g=&QPVL-Um)flkN9=Ff zxnLm10yIPvPFvA8MOAbdnShUAL^=S7_lmZOSJ2mKDxD;h2!hZ_crH{5Mpn#8n;*@uk{>lVpA7!|56aav?*>5w)Ca_*zKdAm5`%+jds4UM~zF`?; z3Oc25wyCgLZ;>ubm92504&5BwrJqS}O1=gF^f5on^4}S?;WzMmgsBQf)I$mhB)Eb8 z*l>4f<&O0D#4q&|w0DYMN1Q z#13r7Q!<@$*Vgac1LFfcOeOZT zCrJe`4qz%)!zg`9vuL8+K`xeEqz+U;AvemaWd|uoMKqC3W)mU-@+LKq z3Gz2&X)+nrQ!B#b4359poRx@pBnY4JD}@83CD}$?`BI+onP`D>oIwO4(2HCrSI7nW zBb`NWks$H_H*poI03AdJ(JqF?#yiF%q0K{cyQ;m~dG__F#BzL3`iRenSBpNe;4D%t z>HxR{WC9sahD%*6LZqBHOk6I0*}MB;9K^P|p>=_ENm<9TE?L3D5kg;K3Qpn#wxd1Tp)K-}j{YeK#bdep+h_cb5Lr1C!Uoj8O2@W4O}KtIgIY^;Um6CWgW<%f!* zM2kho*+fialR(H2bmv9f_q+$~CYnZr$#rs?9EAg1;Vdc^6^UZV_avRHF;!G@M7mG9 zn4X}ksTI;$9-f<9GK_}N*7^YbQ+=;C!`hq)I1zF-_)v!+6e0^pMB_z9(QWBCX@S(F zhV`c7=y)1QyOBiN2K~_j2h1&bnjA-zgqJRr6k0f0Jhy0WK0g<;FwA`H$}}vK>?3>0 z4e2uJR>^$y!G;%NYlL`^*q8W_Psn1dz)Ebu13bV(pxSj~k zvMQv9L>&PPBhE-h6*6Cl(LXy6Z3qDYVI$Z;HjF$YMWm?4xw@og@k86L?8?ohVqlOi?v;K9-_@+ zk?7`t4g&%r=aHx69(nxK>S=j)oocd zl*#4OWG=d`y2-kW${1yZt(9e8`AFFrTn7NY32(uB8J-!|8|NRreZ2Byf~8=owY;tC zuj{E>?SG*4eZM(Jg-HzUfj<@j!l%M2VH+Yb7;X6^KA%^` zufG|Y5SX$cdH?;@=!_u;qyooZl|f|WTmqGiUh?a#S{Qra1TTOt8;AjDh?_R)$OQ@s(WMWmTi4@fO#+vv=n*> z;lg0SQwSFV!ZlrVXJe-^9WU|U@jvmg^{whH>&MGHWf>OJ2F)AzTK~zc7n{PuE4(V3RF2ZS z8*B{=*&rcZn2J;YKrxE(9A9D;)*u6ENHJV6=#19!wh2uVm&j(zT3EE2wrpzB+aYWh zOA;oOO)fiJ9(dX3O5D|v_qHd8q|6N3+~I2bRTe)=*IIlSH$48mgtHIUr&&E3EBqjw z7p`d3^(C32Crj;G1fTMc)_qs|AQeOGKgS-l~|5tCeKMW z(`=$_wj{~!c_qKMWx>|ZcDzNh$#rs*3+IM#Gif57O*aRH1rvJi3VTQ0601#<#Gx!@ukvjT-MzbGS1o{C0O0f<?eBp$1te6cM`NPN-!%sQpI4*LyX7P?ip@nk4#etMV&t%229NC-X z0eML7YU(t+=97Tk0f$?ED|QjPN?M{Vq)?+R+9HrE;1ak1{Ve@#!(n{R+OxY@i$U-* zy}Vz66#6y(2L+W$!`?v8uK9^0^;ubIH79Z=>38wP#`ETssb(FVhL!(?3bQzt!ctfP zD`17D6A5GPT)f+E{DSfMZw}iyVbhS!*SY3gYc5EbBD^CkX5CmQ>;B4ef50BFG}9sH z%Xx7Tx`n#sx^qGgVTdrIVM6Y0ZU*s6SCP zQH7}1X?b09-AML~6|rJ2mP_U`b#ruI>3$OYgg~KN!}9Rs`8oU|ZML>h=fi9Gdcne6 zo*dz+@LW4iJ4<_n+sd8cVp%MUWj9~?f0bQj*G=hly>z2>tJS)CMg2roo{CqGR;8$_ z)P26uLtbTf6UbXWDo`dWk6FI!1FP&-4r-9QaZ44qA93~LPE8!o9Ys59$Zs)^cN zy--!CmZ-V_0C=pv%)$=)KuVc8r~7rL2sVnQL@_ z+reF_zg7Q0djZ>pWu?bRz)*(yOjQkAbNQxD`<@E`H(&ExHnu2S!=%2gTFBULG?3U!34L@iOj zp}wQetM6>MW_V=KuoLVwJJYcBj9(Zx8GqDW*JbF%%%26Y4(1;W%)kt=QL z!*<3CWmpAUD#25t#uZc{3w>b^U-R7WFOm9SVCaWI7>uJhiX-Zk>T&92d!OyQvA;Th zLjL>$w;9W(Z=crR>!H_kukK9A>{v5WL#j#9ziF?1HgfRH{8`r=?*GEw+FuNMns6hoP zP+{@{32Rux8fvIfk0KPI$gBaU5J3b1JOs#L2^rks1~>BpxyhYvAqr6h?Tb*!BD2?e zd)UL?oO?BDP=j)mqx_{im@oxTE<_=U;Q1-flPS710GO7 zftN$4J-|bxnN0M}wf-fh_I!-T$N~igd9kC-RDKEySGb}n9AFQ7b4;pSh?Z!H7B2}nQ%{**sJ`%lx4DIX`64%Y{3=8d zip|H~@PeoLSCgNuT;w4ij&OiIoZ$>-bG{X*M5QTc^9%1+0tE9IGo@^pqiN*mpo1RX z@P_A0n^R2V*EA|fgN-sj`>YZ}Y@QaF+Q(GdY-A(He8!aTuZ@-*JVUPep6+mmo4MrA z@C>=AK@DonXG~X`@P#jY%%j)jgUvK@C`6#<1w&I5TL(D80j_9@rZ3g26lExb3UyGK zUvmGI%U{Uv7p6QIIOxDZ4=YIioO9yegsFShVJo&^8ph!b<0Fo64^JFAJ^ZvH>q184 z;}uJ`EQwgOl!l4AiTaW@@Iez~A`=;ZdD$9iq>)Ad{1MYA5eUSvg5=*8^2DTcnunQ~ zM;@UIx|QE7%PyZ;uq^LJ!J}cm!`cmX7WEQ!6a@>pLY9z8GN~v1>u1W1G}1^T4fy*) zJ^ywRkcw2&2DPX_AQVvK+%D`@5TtxVG1R76U{IjAO}crq9DhVcJL| zjr?2rE1g5XD#l_wCSVh`VQWQAg}So)?e4cfy=@z|CM>jjq++~cg5o3CLJ1{m|9xJy z8)>AGMjGp%pD=BV8`?-Cjr=3|f7)G07*qoM6N<$g34oZNdN!< literal 0 HcmV?d00001 diff --git a/resources/firmware/px4.png b/resources/firmware/px4.png new file mode 100644 index 0000000000000000000000000000000000000000..27459899a06fa1f77d9cd9ef5def3f4d2c011e3e GIT binary patch literal 36252 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 literal 0 HcmV?d00001 diff --git a/src/QGCLoggingCategory.cc b/src/QGCLoggingCategory.cc index 7a7c3cdae..bd102e438 100644 --- a/src/QGCLoggingCategory.cc +++ b/src/QGCLoggingCategory.cc @@ -27,7 +27,7 @@ #include "QGCLoggingCategory.h" // Add Global logging categories (not class specific) here using QGC_LOGGING_CATEGORY - // There currently are no global categories +QGC_LOGGING_CATEGORY(FirmwareUpgradeLog, "FirmwareUpgradeLog") QGCLoggingCategoryRegister* _instance = NULL; diff --git a/src/QGCLoggingCategory.h b/src/QGCLoggingCategory.h index e059f833d..a77ad830f 100644 --- a/src/QGCLoggingCategory.h +++ b/src/QGCLoggingCategory.h @@ -31,7 +31,7 @@ #include // Add Global logging categories (not class specific) here using Q_DECLARE_LOGGING_CATEGORY - // There currently are no global categories +Q_DECLARE_LOGGING_CATEGORY(FirmwareUpgradeLog) /// @def QGC_LOGGING_CATEGORY /// This is a QGC specific replacement for Q_LOGGING_CATEGORY. It will register the category name into a diff --git a/src/VehicleSetup/PX4Bootloader.cc b/src/VehicleSetup/Bootloader.cc similarity index 52% rename from src/VehicleSetup/PX4Bootloader.cc rename to src/VehicleSetup/Bootloader.cc index 81eb339c7..0b85c2dea 100644 --- a/src/VehicleSetup/PX4Bootloader.cc +++ b/src/VehicleSetup/Bootloader.cc @@ -25,7 +25,8 @@ /// @brief PX4 Bootloader Utility routines /// @author Don Gagne -#include "PX4Bootloader.h" +#include "Bootloader.h" +#include "QGCLoggingCategory.h" #include #include @@ -78,13 +79,13 @@ static quint32 crc32(const uint8_t *src, unsigned len, unsigned state) return state; } -PX4Bootloader::PX4Bootloader(QObject *parent) : +Bootloader::Bootloader(QObject *parent) : QObject(parent) { } -bool PX4Bootloader::write(QextSerialPort* port, const uint8_t* data, qint64 maxSize) +bool Bootloader::_write(QextSerialPort* port, const uint8_t* data, qint64 maxSize) { qint64 bytesWritten = port->write((const char*)data, maxSize); if (bytesWritten == -1) { @@ -101,13 +102,13 @@ bool PX4Bootloader::write(QextSerialPort* port, const uint8_t* data, qint64 maxS return true; } -bool PX4Bootloader::write(QextSerialPort* port, const uint8_t byte) +bool Bootloader::_write(QextSerialPort* port, const uint8_t byte) { uint8_t buf[1] = { byte }; - return write(port, buf, 1); + return _write(port, buf, 1); } -bool PX4Bootloader::read(QextSerialPort* port, uint8_t* data, qint64 maxSize, int readTimeout) +bool Bootloader::_read(QextSerialPort* port, uint8_t* data, qint64 maxSize, int readTimeout) { qint64 bytesAlreadyRead = 0; @@ -137,11 +138,13 @@ bool PX4Bootloader::read(QextSerialPort* port, uint8_t* data, qint64 maxSize, in return true; } -bool PX4Bootloader::getCommandResponse(QextSerialPort* port, int responseTimeout) +/// Read a PROTO_SYNC command response from the bootloader +/// @param responseTimeout Msecs to wait for response bytes to become available on port +bool Bootloader::_getCommandResponse(QextSerialPort* port, int responseTimeout) { uint8_t response[2]; - if (!read(port, response, 2, responseTimeout)) { + if (!_read(port, response, 2, responseTimeout)) { _errorString.prepend("Get Command Response: "); return false; } @@ -164,18 +167,21 @@ bool PX4Bootloader::getCommandResponse(QextSerialPort* port, int responseTimeout return true; } -bool PX4Bootloader::getBoardInfo(QextSerialPort* port, uint8_t param, uint32_t& value) +/// Send a PROTO_GET_DEVICE command to retrieve a value from the PX4 bootloader +/// @param param Value to retrieve using INFO_BOARD_* enums +/// @param value Returned value +bool Bootloader::_getPX4BoardInfo(QextSerialPort* port, uint8_t param, uint32_t& value) { uint8_t buf[3] = { PROTO_GET_DEVICE, param, PROTO_EOC }; - if (!write(port, buf, sizeof(buf))) { + if (!_write(port, buf, sizeof(buf))) { goto Error; } port->flush(); - if (!read(port, (uint8_t*)&value, sizeof(value))) { + if (!_read(port, (uint8_t*)&value, sizeof(value))) { goto Error; } - if (!getCommandResponse(port)) { + if (!_getCommandResponse(port)) { goto Error; } @@ -186,15 +192,18 @@ Error: return false; } -bool PX4Bootloader::sendCommand(QextSerialPort* port, const uint8_t cmd, int responseTimeout) +/// Send a command to the bootloader +/// @param cmd Command to send using PROTO_* enums +/// @return true: Command sent and valid sync response returned +bool Bootloader::_sendCommand(QextSerialPort* port, const uint8_t cmd, int responseTimeout) { uint8_t buf[2] = { cmd, PROTO_EOC }; - if (!write(port, buf, 2)) { + if (!_write(port, buf, 2)) { goto Error; } port->flush(); - if (!getCommandResponse(port, responseTimeout)) { + if (!_getCommandResponse(port, responseTimeout)) { goto Error; } @@ -205,10 +214,10 @@ Error: return false; } -bool PX4Bootloader::erase(QextSerialPort* port) +bool Bootloader::erase(QextSerialPort* port) { // Erase is slow, need larger timeout - if (!sendCommand(port, PROTO_CHIP_ERASE, _eraseTimeout)) { + if (!_sendCommand(port, PROTO_CHIP_ERASE, _eraseTimeout)) { _errorString = tr("Board erase failed: %1").arg(_errorString); return false; } @@ -216,11 +225,20 @@ bool PX4Bootloader::erase(QextSerialPort* port) return true; } -bool PX4Bootloader::program(QextSerialPort* port, const QString& firmwareFilename) +bool Bootloader::program(QextSerialPort* port, const FirmwareImage* image) { - QFile firmwareFile(firmwareFilename); + if (image->imageIsBinFormat()) { + return _binProgram(port, image); + } else { + return _ihxProgram(port, image); + } +} + +bool Bootloader::_binProgram(QextSerialPort* port, const FirmwareImage* image) +{ + QFile firmwareFile(image->binFilename()); if (!firmwareFile.open(QIODevice::ReadOnly)) { - _errorString = tr("Unable to open firmware file %1: %2").arg(firmwareFilename).arg(firmwareFile.errorString()); + _errorString = tr("Unable to open firmware file %1: %2").arg(image->binFilename()).arg(firmwareFile.errorString()); return false; } uint32_t imageSize = (uint32_t)firmwareFile.size(); @@ -248,12 +266,12 @@ bool PX4Bootloader::program(QextSerialPort* port, const QString& firmwareFilenam Q_ASSERT(bytesToSend <= 0x8F); bool failed = true; - if (write(port, PROTO_PROG_MULTI)) { - if (write(port, (uint8_t)bytesToSend)) { - if (write(port, imageBuf, bytesToSend)) { - if (write(port, PROTO_EOC)) { + if (_write(port, PROTO_PROG_MULTI)) { + if (_write(port, (uint8_t)bytesToSend)) { + if (_write(port, imageBuf, bytesToSend)) { + if (_write(port, PROTO_EOC)) { port->flush(); - if (getCommandResponse(port)) { + if (_getCommandResponse(port)) { failed = false; } } @@ -270,7 +288,7 @@ bool PX4Bootloader::program(QextSerialPort* port, const QString& firmwareFilenam // Calculate the CRC now so we can test it after the board is flashed. _imageCRC = crc32((uint8_t *)imageBuf, bytesToSend, _imageCRC); - emit updateProgramProgress(bytesSent, imageSize); + emit updateProgress(bytesSent, imageSize); } firmwareFile.close(); @@ -284,46 +302,131 @@ bool PX4Bootloader::program(QextSerialPort* port, const QString& firmwareFilenam return true; } -bool PX4Bootloader::verify(QextSerialPort* port, const QString firmwareFilename) +bool Bootloader::_ihxProgram(QextSerialPort* port, const FirmwareImage* image) +{ + uint32_t imageSize = image->imageSize(); + uint32_t bytesSent = 0; + + for (uint16_t index=0; indexihxBlockCount(); index++) { + bool failed; + uint16_t flashAddress; + QByteArray bytes; + + if (!image->ihxGetBlock(index, flashAddress, bytes)) { + _errorString = QString("Unable to retrieve block from ihx: index %1").arg(index); + return false; + } + + qCDebug(FirmwareUpgradeLog) << QString("Bootloader::_ihxProgram - address:%1 size:%2 block:%3").arg(flashAddress).arg(bytes.count()).arg(index); + + // Set flash address + + failed = true; + if (_write(port, PROTO_LOAD_ADDRESS) && + _write(port, flashAddress & 0xFF) && + _write(port, (flashAddress >> 8) & 0xFF) && + _write(port, PROTO_EOC)) { + port->flush(); + if (_getCommandResponse(port)) { + failed = false; + } + } + + if (failed) { + _errorString = QString("Unable to set flash start address: 0x%2").arg(flashAddress, 8, 16, QLatin1Char('0')); + return false; + } + + // Flash + + int bytesIndex = 0; + uint16_t bytesLeftToWrite = bytes.count(); + + while (bytesLeftToWrite > 0) { + uint8_t bytesToWrite; + + if (bytesLeftToWrite > PROG_MULTI_MAX) { + bytesToWrite = PROG_MULTI_MAX; + } else { + bytesToWrite = bytesLeftToWrite; + } + + failed = true; + if (_write(port, PROTO_PROG_MULTI) && + _write(port, bytesToWrite) && + _write(port, &((uint8_t *)bytes.data())[bytesIndex], bytesToWrite) && + _write(port, PROTO_EOC)) { + port->flush(); + if (_getCommandResponse(port)) { + failed = false; + } + } + if (failed) { + _errorString = QString("Flash failed: %1 at address 0x%2").arg(_errorString).arg(flashAddress, 8, 16, QLatin1Char('0')); + return false; + } + + bytesIndex += bytesToWrite; + bytesLeftToWrite -= bytesToWrite; + bytesSent += bytesToWrite; + + emit updateProgress(bytesSent, imageSize); + } + } + + return true; +} + +bool Bootloader::verify(QextSerialPort* port, const FirmwareImage* image) { bool ret; - if (_bootloaderVersion <= 2) { - ret = _bootloaderVerifyRev2(port, firmwareFilename); + if (!image->imageIsBinFormat() || _bootloaderVersion <= 2) { + ret = _verifyBytes(port, image); } else { - ret = _bootloaderVerifyRev3(port); + ret = _verifyCRC(port); } - sendBootloaderReboot(port); + reboot(port); return ret; } -/// @brief Verify the flash on bootloader version 2 by reading it back and comparing it against -/// the original firmware file. -bool PX4Bootloader::_bootloaderVerifyRev2(QextSerialPort* port, const QString firmwareFilename) +/// @brief Verify the flash on bootloader eading it back and comparing it against the original image-> +bool Bootloader::_verifyBytes(QextSerialPort* port, const FirmwareImage* image) { - QFile firmwareFile(firmwareFilename); + if (image->imageIsBinFormat()) { + return _binVerifyBytes(port, image); + } else { + return _ihxVerifyBytes(port, image); + } +} + +bool Bootloader::_binVerifyBytes(QextSerialPort* port, const FirmwareImage* image) +{ + Q_ASSERT(image->imageIsBinFormat()); + + QFile firmwareFile(image->binFilename()); if (!firmwareFile.open(QIODevice::ReadOnly)) { - _errorString = tr("Unable to open firmware file %1: %2").arg(firmwareFilename).arg(firmwareFile.errorString()); + _errorString = tr("Unable to open firmware file %1: %2").arg(image->binFilename()).arg(firmwareFile.errorString()); return false; } uint32_t imageSize = (uint32_t)firmwareFile.size(); - if (!sendCommand(port, PROTO_CHIP_VERIFY)) { + if (!_sendCommand(port, PROTO_CHIP_VERIFY)) { return false; } uint8_t fileBuf[READ_MULTI_MAX]; - uint8_t flashBuf[READ_MULTI_MAX]; + uint8_t readBuf[READ_MULTI_MAX]; uint32_t bytesVerified = 0; Q_ASSERT(PROG_MULTI_MAX <= 0x8F); while (bytesVerified < imageSize) { int bytesToRead = imageSize - bytesVerified; - if (bytesToRead > (int)sizeof(fileBuf)) { - bytesToRead = (int)sizeof(fileBuf); + if (bytesToRead > (int)sizeof(readBuf)) { + bytesToRead = (int)sizeof(readBuf); } Q_ASSERT((bytesToRead % 4) == 0); @@ -337,48 +440,137 @@ bool PX4Bootloader::_bootloaderVerifyRev2(QextSerialPort* port, const QString fi Q_ASSERT(bytesToRead <= 0x8F); bool failed = true; - if (write(port, PROTO_READ_MULTI)) { - if (write(port, (uint8_t)bytesToRead)) { - if (write(port, PROTO_EOC)) { - port->flush(); - if (read(port, flashBuf, sizeof(flashBuf))) { - if (getCommandResponse(port)) { - failed = false; - } - } + if (_write(port, PROTO_READ_MULTI) && + _write(port, (uint8_t)bytesToRead) && + _write(port, PROTO_EOC)) { + port->flush(); + if (_read(port, readBuf, sizeof(readBuf))) { + if (_getCommandResponse(port)) { + failed = false; } } } if (failed) { - _errorString = tr("Verify failed: %1 at address: 0x%2").arg(_errorString).arg(bytesVerified, 8, 16, QLatin1Char('0')); + _errorString = tr("Read failed: %1 at address: 0x%2").arg(_errorString).arg(bytesVerified, 8, 16, QLatin1Char('0')); return false; } for (int i=0; iimageIsBinFormat()); + + uint32_t imageSize = image->imageSize(); + uint32_t bytesVerified = 0; + + for (uint16_t index=0; indexihxBlockCount(); index++) { + bool failed; + uint16_t readAddress; + QByteArray imageBytes; + + if (!image->ihxGetBlock(index, readAddress, imageBytes)) { + _errorString = QString("Unable to retrieve block from ihx: index %1").arg(index); + return false; + } + + qCDebug(FirmwareUpgradeLog) << QString("Bootloader::_ihxVerifyBytes - address:%1 size:%2 block:%3").arg(readAddress).arg(imageBytes.count()).arg(index); + + // Set read address + + failed = true; + if (_write(port, PROTO_LOAD_ADDRESS) && + _write(port, readAddress & 0xFF) && + _write(port, (readAddress >> 8) & 0xFF) && + _write(port, PROTO_EOC)) { + port->flush(); + if (_getCommandResponse(port)) { + failed = false; + } + } + + if (failed) { + _errorString = QString("Unable to set read start address: 0x%2").arg(readAddress, 8, 16, QLatin1Char('0')); + return false; + } + + // Read back + + int bytesIndex = 0; + uint16_t bytesLeftToRead = imageBytes.count(); + + while (bytesLeftToRead > 0) { + uint8_t bytesToRead; + uint8_t readBuf[READ_MULTI_MAX]; + + if (bytesLeftToRead > READ_MULTI_MAX) { + bytesToRead = READ_MULTI_MAX; + } else { + bytesToRead = bytesLeftToRead; + } + + failed = true; + if (_write(port, PROTO_READ_MULTI) && + _write(port, bytesToRead) && + _write(port, PROTO_EOC)) { + port->flush(); + if (_read(port, readBuf, bytesToRead)) { + if (_getCommandResponse(port)) { + failed = false; + } + } + } + if (failed) { + _errorString = tr("Read failed: %1 at address: 0x%2").arg(_errorString).arg(readAddress, 8, 16, QLatin1Char('0')); + return false; + } + + // Compare + + for (int i=0; iflush(); - if (read(port, (uint8_t*)&flashCRC, sizeof(flashCRC), _verifyTimeout)) { - if (getCommandResponse(port)) { + if (_read(port, (uint8_t*)&flashCRC, sizeof(flashCRC), _verifyTimeout)) { + if (_getCommandResponse(port)) { failed = false; } } @@ -395,7 +587,7 @@ bool PX4Bootloader::_bootloaderVerifyRev3(QextSerialPort* port) return true; } -bool PX4Bootloader::open(QextSerialPort* port, const QString portName) +bool Bootloader::open(QextSerialPort* port, const QString portName) { Q_ASSERT(!port->isOpen()); @@ -414,10 +606,10 @@ bool PX4Bootloader::open(QextSerialPort* port, const QString portName) return true; } -bool PX4Bootloader::sync(QextSerialPort* port) +bool Bootloader::sync(QextSerialPort* port) { // Send sync command - if (sendCommand(port, PROTO_GET_SYNC)) { + if (_sendCommand(port, PROTO_GET_SYNC)) { return true; } else { _errorString.prepend("Sync: "); @@ -425,10 +617,10 @@ bool PX4Bootloader::sync(QextSerialPort* port) } } -bool PX4Bootloader::getBoardInfo(QextSerialPort* port, uint32_t& bootloaderVersion, uint32_t& boardID, uint32_t& flashSize) +bool Bootloader::getPX4BoardInfo(QextSerialPort* port, uint32_t& bootloaderVersion, uint32_t& boardID, uint32_t& flashSize) { - if (!getBoardInfo(port, INFO_BL_REV, _bootloaderVersion)) { + if (!_getPX4BoardInfo(port, INFO_BL_REV, _bootloaderVersion)) { goto Error; } if (_bootloaderVersion < BL_REV_MIN || _bootloaderVersion > BL_REV_MAX) { @@ -436,11 +628,11 @@ bool PX4Bootloader::getBoardInfo(QextSerialPort* port, uint32_t& bootloaderVersi goto Error; } - if (!getBoardInfo(port, INFO_BOARD_ID, _boardID)) { + if (!_getPX4BoardInfo(port, INFO_BOARD_ID, _boardID)) { goto Error; } - if (!getBoardInfo(port, INFO_FLASH_SIZE, _boardFlashSize)) { + if (!_getPX4BoardInfo(port, INFO_FLASH_SIZE, _boardFlashSize)) { qWarning() << _errorString; goto Error; } @@ -456,7 +648,35 @@ Error: return false; } -bool PX4Bootloader::sendBootloaderReboot(QextSerialPort* port) +bool Bootloader::get3DRRadioBoardId(QextSerialPort* port, uint32_t& boardID) +{ + uint8_t buf[2] = { PROTO_GET_DEVICE, PROTO_EOC }; + + if (!_write(port, buf, sizeof(buf))) { + goto Error; + } + port->flush(); + + if (!_read(port, (uint8_t*)buf, 2)) { + goto Error; + } + if (!_getCommandResponse(port)) { + goto Error; + } + + boardID = buf[0]; + + _bootloaderVersion = 0; + _boardFlashSize = 0; + + return true; + +Error: + _errorString.prepend("Get Board Id: "); + return false; +} + +bool Bootloader::reboot(QextSerialPort* port) { - return write(port, PROTO_BOOT) && write(port, PROTO_EOC); + return _write(port, PROTO_BOOT) && _write(port, PROTO_EOC); } diff --git a/src/VehicleSetup/Bootloader.h b/src/VehicleSetup/Bootloader.h new file mode 100644 index 000000000..ccd0355f6 --- /dev/null +++ b/src/VehicleSetup/Bootloader.h @@ -0,0 +1,156 @@ +/*===================================================================== + + 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 . + + ======================================================================*/ + +/// @file +/// @author Don Gagne + +#ifndef Bootloader_H +#define Bootloader_H + +#include "FirmwareImage.h" + +#include "qextserialport.h" + +#include + +/// Bootloader Utility routines. Works with PX4 bootloader and 3DR Radio bootloader. +class Bootloader : public QObject +{ + Q_OBJECT + +public: + explicit Bootloader(QObject *parent = 0); + + /// @brief Returns the error message associated with the last failed call to one of the bootloader + /// utility routine below. + QString errorString(void) { return _errorString; } + + /// @brief Opens a port to the bootloader + bool open(QextSerialPort* port, const QString portName); + + /// @brief Read a PROTO_SYNC response from the bootloader + /// @return true: Valid sync response was received + bool sync(QextSerialPort* port); + + /// @brief Erases the current program + bool erase(QextSerialPort* port); + + /// @brief Program the board with the specified image + bool program(QextSerialPort* port, const FirmwareImage* image); + + /// @brief Verify the board flash. + bool verify(QextSerialPort* port, const FirmwareImage* image); + + /// @brief Retrieve a set of board info from the bootloader of PX4 FMU and PX4 Flow boards + /// @param bootloaderVersion Returned INFO_BL_REV + /// @param boardID Returned INFO_BOARD_ID + /// @param flashSize Returned INFO_FLASH_SIZE + bool getPX4BoardInfo(QextSerialPort* port, uint32_t& bootloaderVersion, uint32_t& boardID, uint32_t& flashSize); + + /// @brief Retrieve the board id from a 3DR Radio + bool get3DRRadioBoardId(QextSerialPort* port, uint32_t& boardID); + + /// @brief Sends a PROTO_REBOOT command to the bootloader + bool reboot(QextSerialPort* port); + + // Supported bootloader board ids + static const int boardIDPX4FMUV1 = 5; ///< PX4 V1 board + static const int boardIDPX4FMUV2 = 9; ///< PX4 V2 board + static const int boardIDPX4Flow = 6; ///< PX4 Flow board + static const int boardIDAeroCore = 98; ///< Gumstix AeroCore board + static const int boardID3DRRadio = 78; ///< 3DR Radio + +signals: + /// @brief Signals progress indicator for long running bootloader utility routines + void updateProgress(int curr, int total); + +private: + bool _binProgram(QextSerialPort* port, const FirmwareImage* image); + bool _ihxProgram(QextSerialPort* port, const FirmwareImage* image); + + bool _write(QextSerialPort* port, const uint8_t* data, qint64 maxSize); + bool _write(QextSerialPort* port, const uint8_t byte); + + bool _read(QextSerialPort* port, uint8_t* data, qint64 maxSize, int readTimeout = _readTimout); + + bool _sendCommand(QextSerialPort* port, uint8_t cmd, int responseTimeout = _responseTimeout); + bool _getCommandResponse(QextSerialPort* port, const int responseTimeout = _responseTimeout); + + bool _getPX4BoardInfo(QextSerialPort* port, uint8_t param, uint32_t& value); + + bool _verifyBytes(QextSerialPort* port, const FirmwareImage* image); + bool _binVerifyBytes(QextSerialPort* port, const FirmwareImage* image); + bool _ihxVerifyBytes(QextSerialPort* port, const FirmwareImage* image); + bool _verifyCRC(QextSerialPort* port); + + enum { + // protocol bytes + PROTO_INSYNC = 0x12, ///< 'in sync' byte sent before status + PROTO_EOC = 0x20, ///< end of command + + // Reply bytes + PROTO_OK = 0x10, ///< INSYNC/OK - 'ok' response + PROTO_FAILED = 0x11, ///< INSYNC/FAILED - 'fail' response + PROTO_INVALID = 0x13, ///< INSYNC/INVALID - 'invalid' response for bad commands + + // Command bytes + PROTO_GET_SYNC = 0x21, ///< NOP for re-establishing sync + PROTO_GET_DEVICE = 0x22, ///< get device ID bytes + PROTO_CHIP_ERASE = 0x23, ///< erase program area and reset program address + PROTO_LOAD_ADDRESS = 0x24, ///< set next programming address + PROTO_PROG_MULTI = 0x27, ///< write bytes at program address and increment + PROTO_GET_CRC = 0x29, ///< compute & return a CRC + PROTO_BOOT = 0x30, ///< boot the application + + // Command bytes - Rev 2 boootloader only + PROTO_CHIP_VERIFY = 0x24, ///< begin verify mode + PROTO_READ_MULTI = 0x28, ///< read bytes at programm address and increment + + INFO_BL_REV = 1, ///< bootloader protocol revision + BL_REV_MIN = 2, ///< Minimum supported bootlader protocol + BL_REV_MAX = 4, ///< Maximum supported bootloader protocol + INFO_BOARD_ID = 2, ///< board type + INFO_BOARD_REV = 3, ///< board revision + INFO_FLASH_SIZE = 4, ///< max firmware size in bytes + + PROG_MULTI_MAX = 64, ///< write size for PROTO_PROG_MULTI, must be multiple of 4 + READ_MULTI_MAX = 255 ///< read size for PROTO_READ_MULTI, must be multiple of 4 + }; + + uint32_t _boardID; ///< board id for currently connected board + uint32_t _boardFlashSize; ///< flash size for currently connected board + uint32_t _imageCRC; ///< CRC for image in currently selected firmware file + uint32_t _bootloaderVersion; ///< Bootloader version + + QString _firmwareFilename; ///< Currently selected firmware file to flash + + QString _errorString; ///< Last error + + static const int _eraseTimeout = 20000; ///< Msecs to wait for response from erase command + static const int _rebootTimeout = 10000; ///< Msecs to wait for reboot command to cause serial port to disconnect + static const int _verifyTimeout = 5000; ///< Msecs to wait for response to PROTO_GET_CRC command + static const int _readTimout = 2000; ///< Msecs to wait for read bytes to become avilable + static const int _responseTimeout = 2000; ///< Msecs to wait for command response bytes +}; + +#endif // PX4FirmwareUpgrade_H diff --git a/src/VehicleSetup/FirmwareImage.cc b/src/VehicleSetup/FirmwareImage.cc new file mode 100644 index 000000000..1f49de63c --- /dev/null +++ b/src/VehicleSetup/FirmwareImage.cc @@ -0,0 +1,393 @@ +/*===================================================================== + + 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 Support for Intel Hex firmware file +/// @author Don Gagne + +#include "FirmwareImage.h" +#include "QGCLoggingCategory.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +FirmwareImage::FirmwareImage(QObject* parent) : + QObject(parent), + _imageSize(0) +{ + +} + +bool FirmwareImage::load(const QString& imageFilename, uint32_t boardId) +{ + _imageSize = 0; + _boardId = boardId; + + if (imageFilename.endsWith(".bin")) { + return _binLoad(imageFilename); + _binFormat = true; + return true; + } else if (imageFilename.endsWith(".px4")) { + _binFormat = true; + return _px4Load(imageFilename); + } else if (imageFilename.endsWith(".ihx")) { + _binFormat = false; + return _ihxLoad(imageFilename); + } else { + emit errorMessage("Unsupported file format"); + return false; + } +} + +bool FirmwareImage::_readByteFromStream(QTextStream& stream, uint8_t& byte) +{ + QString hex = stream.read(2); + + if (hex.count() != 2) { + return false; + } + + bool success; + byte = (uint8_t)hex.toInt(&success, 16); + + return success; +} + +bool FirmwareImage::_readWordFromStream(QTextStream& stream, uint16_t& word) +{ + QString hex = stream.read(4); + + if (hex.count() != 4) { + return false; + } + + bool success; + word = (uint16_t)hex.toInt(&success, 16); + + return success; +} + +bool FirmwareImage::_readBytesFromStream(QTextStream& stream, uint8_t byteCount, QByteArray& bytes) +{ + bytes.clear(); + + while (byteCount) { + uint8_t byte; + + if (!_readByteFromStream(stream, byte)) { + return false; + } + bytes += byte; + + byteCount--; + } + + return true; +} + +bool FirmwareImage::_ihxLoad(const QString& ihxFilename) +{ + _imageSize = 0; + _ihxBlocks.clear(); + + QFile ihxFile(ihxFilename); + if (!ihxFile.open(QIODevice::ReadOnly | QIODevice::Text)) { + emit errorMessage(QString("Unable to open firmware file %1, error: %2").arg(ihxFilename).arg(ihxFile.errorString())); + return false; + } + + QTextStream stream(&ihxFile); + + while (true) { + if (stream.read(1) != ":") { + emit errorMessage("Incorrectly formatted .ihx file, line does not begin with :"); + return false; + } + + uint8_t blockByteCount; + uint16_t address; + uint8_t recordType; + QByteArray bytes; + uint8_t crc; + + if (!_readByteFromStream(stream, blockByteCount) || + !_readWordFromStream(stream, address) || + !_readByteFromStream(stream, recordType) || + !_readBytesFromStream(stream, blockByteCount, bytes) || + !_readByteFromStream(stream, crc)) { + emit errorMessage("Incorrectly formatted line in .ihx file, line too short"); + return false; + } + + if (!(recordType == 0 || recordType == 1)) { + emit errorMessage(QString("Unsupported record type in file: %1").arg(recordType)); + return false; + } + + if (recordType == 0) { + bool appendToLastBlock = false; + + // Can we append this block to the last one? + + if (_ihxBlocks.count()) { + int lastBlockIndex = _ihxBlocks.count() - 1; + + if (_ihxBlocks[lastBlockIndex].address + _ihxBlocks[lastBlockIndex].bytes.count() == address) { + appendToLastBlock = true; + } + } + + if (appendToLastBlock) { + _ihxBlocks[_ihxBlocks.count() - 1].bytes += bytes; + qCDebug(FirmwareUpgradeLog) << QString("_ihxLoad - append - address:%1 size:%2 block:%3").arg(address).arg(blockByteCount).arg(ihxBlockCount()); + } else { + IntelHexBlock_t block; + + block.address = address; + block.bytes = bytes; + + _ihxBlocks += block; + qCDebug(FirmwareUpgradeLog) << QString("_ihxLoad - new block - address:%1 size:%2 block:%3").arg(address).arg(blockByteCount).arg(ihxBlockCount()); + } + + _imageSize += blockByteCount; + } else if (recordType == 1) { + // EOF + qCDebug(FirmwareUpgradeLog) << QString("_ihxLoad - EOF"); + break; + } + + // Move to next line + stream.readLine(); + } + + ihxFile.close(); + + return true; +} + +bool FirmwareImage::_px4Load(const QString& imageFilename) +{ + _imageSize = 0; + + // We need to collect information from the .px4 file as well as pull the binary image out to a seperate file. + + QFile px4File(imageFilename); + if (!px4File.open(QIODevice::ReadOnly | QIODevice::Text)) { + emit errorMessage(QString("Unable to open firmware file %1, error: %2").arg(imageFilename).arg(px4File.errorString())); + return false; + } + + QByteArray bytes = px4File.readAll(); + px4File.close(); + QJsonDocument doc = QJsonDocument::fromJson(bytes); + + if (doc.isNull()) { + emit errorMessage("Supplied file is not a valid JSON document"); + return false; + } + + QJsonObject px4Json = doc.object(); + + // Make sure the keys we need are available + static const char* rgJsonKeys[] = { "board_id", "image_size", "description", "git_identity" }; + for (size_t i=0; i(static_cast(0xFF))); + } + + // Store decompressed image file in same location as original download file + QDir imageDir = QFileInfo(imageFilename).dir(); + QString decompressFilename = imageDir.filePath("PX4FlashUpgrade.bin"); + + QFile decompressFile(decompressFilename); + if (!decompressFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { + emit errorMessage(QString("Unable to open decompressed file %1 for writing, error: %2").arg(decompressFilename).arg(decompressFile.errorString())); + return false; + } + + qint64 bytesWritten = decompressFile.write(decompressedBytes); + if (bytesWritten != decompressedBytes.count()) { + emit errorMessage(QString("Write failed for decompressed image file, error: %1").arg(decompressFile.errorString())); + return false; + } + decompressFile.close(); + + _binFilename = decompressFilename; + + return true; +} + +/// Decompress a set of bytes stored in a Json document. +bool FirmwareImage::_decompressJsonValue(const QJsonObject& jsonObject, ///< JSON object + const QByteArray& jsonDocBytes, ///< Raw bytes of JSON document + const QString& sizeKey, ///< key which holds byte size + const QString& bytesKey, ///< key which holds compress bytes + QByteArray& decompressedBytes) ///< Returned decompressed bytes +{ + // Validate decompressed size key + if (!jsonObject.contains(sizeKey)) { + emit statusMessage(QString("Firmware file missing %1 key").arg(sizeKey)); + return false; + } + int decompressedSize = jsonObject.value(QString(sizeKey)).toInt(); + if (decompressedSize == 0) { + emit errorMessage(QString("Firmware file has invalid decompressed size for %1").arg(sizeKey)); + return false; + } + + // XXX Qt's JSON string handling is terribly broken, strings + // with some length (18K / 25K) are just weirdly cut. + // The code below works around this by manually 'parsing' + // for the image string. Since its compressed / checksummed + // this should be fine. + + QStringList parts = QString(jsonDocBytes).split(QString("\"%1\": \"").arg(bytesKey)); + if (parts.count() == 1) { + emit errorMessage(QString("Could not find compressed bytes for %1 in Firmware file").arg(bytesKey)); + return false; + } + parts = parts.last().split("\""); + if (parts.count() == 1) { + emit errorMessage(QString("Incorrectly formed compressed bytes section for %1 in Firmware file").arg(bytesKey)); + return false; + } + + // Store decompressed size as first four bytes. This is required by qUncompress routine. + QByteArray raw; + raw.append((unsigned char)((decompressedSize >> 24) & 0xFF)); + raw.append((unsigned char)((decompressedSize >> 16) & 0xFF)); + raw.append((unsigned char)((decompressedSize >> 8) & 0xFF)); + raw.append((unsigned char)((decompressedSize >> 0) & 0xFF)); + + QByteArray raw64 = parts.first().toUtf8(); + raw.append(QByteArray::fromBase64(raw64)); + decompressedBytes = qUncompress(raw); + + if (decompressedBytes.count() == 0) { + emit errorMessage(QString("Firmware file has 0 length %1").arg(bytesKey)); + return false; + } + if (decompressedBytes.count() != decompressedSize) { + emit errorMessage(QString("Size for decompressed %1 does not match stored size: Expected(%1) Actual(%2)").arg(decompressedSize).arg(decompressedBytes.count())); + return false; + } + + emit statusMessage(QString("Succesfully decompressed %1").arg(bytesKey)); + + return true; +} + +uint16_t FirmwareImage::ihxBlockCount(void) const +{ + return _ihxBlocks.count(); +} + +bool FirmwareImage::ihxGetBlock(uint16_t index, uint16_t& address, QByteArray& bytes) const +{ + address = 0; + bytes.clear(); + + if (index < ihxBlockCount()) { + address = _ihxBlocks[index].address; + bytes = _ihxBlocks[index].bytes; + return true; + } else { + return false; + } +} + +bool FirmwareImage::_binLoad(const QString& imageFilename) +{ + QFile binFile(imageFilename); + if (!binFile.open(QIODevice::ReadOnly)) { + emit errorMessage(QString("Unabled to open firmware file %1, %2").arg(imageFilename).arg(binFile.errorString())); + return false; + } + + _imageSize = (uint32_t)binFile.size(); + + binFile.close(); + + return true; +} diff --git a/src/VehicleSetup/FirmwareImage.h b/src/VehicleSetup/FirmwareImage.h new file mode 100644 index 000000000..6c3467dff --- /dev/null +++ b/src/VehicleSetup/FirmwareImage.h @@ -0,0 +1,103 @@ +/*===================================================================== + + 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 +/// @author Don Gagne + +#ifndef FirmwareImage_H +#define FirmwareImage_H + +#include +#include +#include +#include +#include + +#include + +/// Support for Intel Hex firmware file +class FirmwareImage : public QObject +{ + Q_OBJECT + +public: + FirmwareImage(QObject *parent = 0); + + /// Loads the specified image file. Supported formats: .px4, .bin, .ihx. + /// Emits errorMesssage and statusMessage signals while loading. + /// @param imageFilename Image file to load + /// @param boardId Board id that we are going to load this image onto + /// @return true: success, false: failure + bool load(const QString& imageFilename, uint32_t boardId); + + /// Returns the number of bytes in the image. + uint32_t imageSize(void) const { return _imageSize; } + + /// @return true: image format is .bin + bool imageIsBinFormat(void) const { return _binFormat; } + + /// @return Filename for .bin file + QString binFilename(void) const { return _binFilename; } + + /// @return Block count from .ihx image + uint16_t ihxBlockCount(void) const; + + /// Retrieves the specified block from the .ihx image + /// @param index Index of block to return + /// @param address Address of returned block + /// @param byets Bytes of returned block + /// @return true: block retrieved + bool ihxGetBlock(uint16_t index, uint16_t& address, QByteArray& bytes) const; + +signals: + void errorMessage(const QString& errorString); + void statusMessage(const QString& warningtring); + +private: + bool _binLoad(const QString& px4Filename); + bool _px4Load(const QString& px4Filename); + bool _ihxLoad(const QString& ihxFilename); + + bool _readByteFromStream(QTextStream& stream, uint8_t& byte); + bool _readWordFromStream(QTextStream& stream, uint16_t& word); + bool _readBytesFromStream(QTextStream& stream, uint8_t byteCount, QByteArray& bytes); + + bool _decompressJsonValue(const QJsonObject& jsonObject, + const QByteArray& jsonDocBytes, + const QString& sizeKey, + const QString& bytesKey, + QByteArray& decompressedBytes); + + typedef struct { + uint16_t address; + QByteArray bytes; + } IntelHexBlock_t; + + bool _binFormat; + uint32_t _boardId; + QString _binFilename; + QList _ihxBlocks; + uint32_t _imageSize; +}; + +#endif diff --git a/src/VehicleSetup/FirmwareUpgrade.qml b/src/VehicleSetup/FirmwareUpgrade.qml index f5ed9f980..f4d5627d4 100644 --- a/src/VehicleSetup/FirmwareUpgrade.qml +++ b/src/VehicleSetup/FirmwareUpgrade.qml @@ -34,158 +34,337 @@ import QGroundControl.Controllers 1.0 import QGroundControl.ScreenTools 1.0 QGCView { - viewPanel: panel + id: qgcView + viewPanel: panel + + // User visible strings + readonly property string title: "FIRMWARE UPDATE" + readonly property string highlightPrefix: "" + readonly property string highlightSuffix: "" + readonly property string welcomeText: "QGroundControl can upgrade the firmware on Pixhawk devices, 3DR Radios and PX4 Flow Smart Cameras." + readonly property string plugInText: highlightPrefix + "Plug in your device" + highlightSuffix + " via USB to " + highlightPrefix + "start" + highlightSuffix + " firmware upgrade" + readonly property string qgcDisconnectText: "All QGroundControl connections to vehicles must be disconnected prior to firmware upgrade. " + + "Click " + highlightPrefix + "Disconnect" + highlightSuffix + " in the toolbar above." + property string usbUnplugText: "Device must be disconnected from USB to start firmware upgrade. " + + highlightPrefix + "Disconnect {0}" + highlightSuffix + " from usb." property string firmwareWarningMessage + property bool controllerCompleted: false + property bool initialBoardSearch: true + property string firmwareName + + function cancelFlash() { + statusTextArea.append(highlightPrefix + "Upgrade cancelled" + highlightSuffix) + statusTextArea.append("------------------------------------------") + controller.cancel() + flashCompleteWaitTimer.running = true + } QGCPalette { id: qgcPal; colorGroupEnabled: panel.enabled } FirmwareUpgradeController { id: controller - upgradeButton: upgradeButton progressBar: progressBar statusLog: statusTextArea - firmwareType: FirmwareUpgradeController.StableFirmware - onShowMessage: { - showMessage(title, message, StandardButton.Ok) + Component.onCompleted: { + controllerCompleted = true + if (qgcView.completedSignalled) { + // We can only start the board search when the Qml and Controller are completely done loading + controller.startBoardSearch() + } + } + + onNoBoardFound: { + initialBoardSearch = false + statusTextArea.append(plugInText) } + + onBoardGone: { + initialBoardSearch = false + statusTextArea.append(plugInText) + } + + onBoardFound: { + if (initialBoardSearch) { + // Board was found right away, so something is already plugged in before we've started upgrade + if (controller.qgcConnections) { + statusTextArea.append(qgcDisconnectText) + } else { + statusTextArea.append(usbUnplugText.replace('{0}', controller.boardType)) + } + } else { + // We end up here when we detect a board plugged in after we've started upgrade + statusTextArea.append(highlightPrefix + "Found device" + highlightSuffix + ": " + controller.boardType) + if (controller.boardType == "Pixhawk") { + showDialog(pixhawkFirmwareSelectDialog, title, 50, StandardButton.Ok | StandardButton.Cancel) + } + } + } + + onError: { + hideDialog() + flashCompleteWaitTimer.running = true + } + + onFlashComplete: flashCompleteWaitTimer.running = true } - QGCViewPanel { - id: panel - anchors.fill: parent + onCompleted: { + if (controllerCompleted) { + // We can only start the board search when the Qml and Controller are completely done loading + controller.startBoardSearch() + } + } - Component { - id: firmwareWarningComponent + // After a flash completes we start this timer to trigger resetting the ui back to it's initial state of being ready to + // flash another board. We do this only after the timer triggers to leave the results of the previous flash on the screen + // for a small amount amount of time. - QGCViewMessage { - message: firmwareWarningMessage + Timer { + id: flashCompleteWaitTimer + interval: 15000 - function accept() { - hideDialog() - controller.doFirmwareUpgrade(); - } - } + onTriggered: { + initialBoardSearch = true + progressBar.value = 0 + statusTextArea.append(welcomeText) + controller.startBoardSearch() } + } + + Component { + id: pixhawkFirmwareSelectDialog - Column { + QGCViewDialog { anchors.fill: parent + + property bool showVersionSelection: apmFlightStack.checked || advancedMode.checked - QGCLabel { - text: "FIRMWARE UPDATE" - font.pixelSize: ScreenTools.largeFontPixelSize + function accept() { + hideDialog() + controller.flash(firmwareVersionCombo.model.get(firmwareVersionCombo.currentIndex).firmwareType) + } + + function reject() { + cancelFlash() + hideDialog() + } + + ExclusiveGroup { + id: firmwareGroup + } + + ListModel { + id: px4FirmwareTypeList + + ListElement { + text: qsTr("Standard Version (stable)"); + firmwareType: FirmwareUpgradeController.PX4StableFirmware + } + ListElement { + text: qsTr("Beta Testing (beta)"); + firmwareType: FirmwareUpgradeController.PX4BetaFirmware + } + ListElement { + text: qsTr("Developer Build (master)"); + firmwareType: FirmwareUpgradeController.PX4DeveloperFirmware + } + ListElement { + text: qsTr("Custom firmware file..."); + firmwareType: FirmwareUpgradeController.PX4CustomFirmware + } } + + ListModel { + id: apmFirmwareTypeList - Item { - // Just used as a spacer - height: 20 - width: 10 + ListElement { + text: "ArduCopter Quad" + firmwareType: FirmwareUpgradeController.ApmArduCopterQuadFirmware + } + ListElement { + text: "ArduCopter X8" + firmwareType: FirmwareUpgradeController.ApmArduCopterX8Firmware + } + ListElement { + text: "ArduCopter Hexa" + firmwareType: FirmwareUpgradeController.ApmArduCopterHexaFirmware + } + ListElement { + text: "ArduCopter Octo" + firmwareType: FirmwareUpgradeController.ApmArduCopterOctoFirmware + } + ListElement { + text: "ArduCopter Y" + firmwareType: FirmwareUpgradeController.ApmArduCopterYFirmware + } + ListElement { + text: "ArduCopter Y6" + firmwareType: FirmwareUpgradeController.ApmArduCopterY6Firmware + } + ListElement { + text: "ArduCopter Heli" + firmwareType: FirmwareUpgradeController.ApmArduCopterHeliFirmware + } + ListElement { + text: "ArduPlane" + firmwareType: FirmwareUpgradeController.ApmArduPlaneFirmware + } + ListElement { + text: "Rover" + firmwareType: FirmwareUpgradeController.ApmRoverFirmware + } } - Row { - spacing: 10 - - ListModel { - id: firmwareItems - ListElement { - text: qsTr("Standard Version (stable)"); - firmwareType: FirmwareUpgradeController.StableFirmware - } - ListElement { - text: qsTr("Beta Testing (beta)"); - firmwareType: FirmwareUpgradeController.BetaFirmware - } - ListElement { - text: qsTr("Developer Build (master)"); - firmwareType: FirmwareUpgradeController.DeveloperFirmware - } - ListElement { - text: qsTr("Custom firmware file..."); - firmwareType: FirmwareUpgradeController.CustomFirmware - } + Column { + anchors.fill: parent + spacing: defaultTextHeight + + QGCLabel { + width: parent.width + wrapMode: Text.WordWrap + text: "Detected Pixhawk board. You can select from the following flight stacks:" } - QGCComboBox { - id: firmwareCombo - width: 200 - height: upgradeButton.height - model: firmwareItems - } - - QGCButton { - id: upgradeButton - text: "UPGRADE" - primary: true - onClicked: { - if (controller.activeQGCConnections()) { - showMessage("Firmware Upgrade", - "There are still vehicles connected to QGroundControl. " + - "You must disconnect all vehicles from QGroundControl prior to Firmware Upgrade.", - StandardButton.Ok) - return - } + function firmwareVersionChanged(model) { + firmwareVersionWarningLabel.visible = false + // All of this bizarre, setting model to null and index to 1 and then to 0 is to work around + // strangeness in the combo box implementation. This sequence of steps correctly changes the combo model + // without generating any warnings and correctly updates the combo text with the new selection. + firmwareVersionCombo.model = null + firmwareVersionCombo.model = model + firmwareVersionCombo.currentIndex = 1 + firmwareVersionCombo.currentIndex = 0 + } - if (controller.pluggedInBoard()) { - showMessage("Firmware Upgrade", - "You vehicle is currently connected via USB. " + - "You must unplug your vehicle from USB prior to Firmware Upgrade.", - StandardButton.Ok) - return - } + QGCRadioButton { + id: px4FlightStack + checked: true + exclusiveGroup: firmwareGroup + text: "PX4 Flight Stack (full QGC support)" + + onClicked: parent.firmwareVersionChanged(px4FirmwareTypeList) + } + + QGCRadioButton { + id: apmFlightStack + exclusiveGroup: firmwareGroup + text: "APM Flight Stack (partial QGC support)" - controller.firmwareType = firmwareItems.get(firmwareCombo.currentIndex).firmwareType - - if (controller.firmwareType == 1) { - firmwareWarningMessage = "WARNING: BETA FIRMWARE\n" + - "This firmware version is ONLY intended for beta testers. " + - "Although it has received FLIGHT TESTING, it represents actively changed code. " + - "Do NOT use for normal operation.\n\n" + - "Click Cancel to abort upgrade, Click Ok to Upgrade anwyay" - showDialog(firmwareWarningComponent, "Firmware Upgrade", 50, StandardButton.Cancel | StandardButton.Ok) - } else if (controller.firmwareType == 2) { - firmwareWarningMessage = "WARNING: CONTINUOUS BUILD FIRMWARE\n" + - "This firmware has NOT BEEN FLIGHT TESTED. " + - "It is only intended for DEVELOPERS. " + - "Run bench tests without props first. " + - "Do NOT fly this without addional safety precautions. " + - "Follow the mailing list actively when using it.\n\n" + - "Click Cancel to abort upgrade, Click Ok to Upgrade anwyay" - showDialog(firmwareWarningComponent, "Firmware Upgrade", 50, StandardButton.Cancel | StandardButton.Ok) - } else { - controller.doFirmwareUpgrade(); + onClicked: parent.firmwareVersionChanged(apmFirmwareTypeList) + } + + QGCLabel { + width: parent.width + wrapMode: Text.WordWrap + visible: showVersionSelection + text: "Select which version of the above flight stack you would like to install:" + } + + QGCComboBox { + id: firmwareVersionCombo + width: 200 + visible: showVersionSelection + model: px4FirmwareTypeList + + onActivated: { + if (model.get(index).firmwareType == FirmwareUpgradeController.PX4BetaFirmware) { + firmwareVersionWarningLabel.visible = true + firmwareVersionWarningLabel.text = "WARNING: BETA FIRMWARE. " + + "This firmware version is ONLY intended for beta testers. " + + "Although it has received FLIGHT TESTING, it represents actively changed code. " + + "Do NOT use for normal operation." + } else if (model.get(index).firmwareType == FirmwareUpgradeController.PX4DeveloperFirmware) { + firmwareVersionWarningLabel.visible = true + firmwareVersionWarningLabel.text = "WARNING: CONTINUOUS BUILD FIRMWARE. " + + "This firmware has NOT BEEN FLIGHT TESTED. " + + "It is only intended for DEVELOPERS. " + + "Run bench tests without props first. " + + "Do NOT fly this without addional safety precautions. " + + "Follow the mailing list actively when using it." + } else { + firmwareVersionWarningLabel.visible = false } - } + } + } + + QGCLabel { + id: firmwareVersionWarningLabel + width: parent.width + wrapMode: Text.WordWrap + visible: false + } + } + + QGCCheckBox { + id: advancedMode + anchors.bottom: parent.bottom + text: "Advanced mode" + + onClicked: { + firmwareVersionCombo.currentIndex = 0 + firmwareVersionWarningLabel.visible = false } } - Item { - // Just used as a spacer - height: 20 - width: 10 + QGCButton { + anchors.leftMargin: ScreenTools.defaultFontPixelWidth * 2 + anchors.left: advancedMode.right + anchors.bottom: parent.bottom + text: "Help me pick a flight stack" + onClicked: Qt.openUrlExternally("http://pixhawk.org/choice") } + } // QGCViewDialog + } // Component - pixhawkFirmwareSelectDialog + + + Component { + id: firmwareWarningDialog + + QGCViewMessage { + message: firmwareWarningMessage - ProgressBar { - id: progressBar - width: parent.width + function accept() { + hideDialog() + controller.doFirmwareUpgrade(); } + } + } + + QGCViewPanel { + id: panel + anchors.fill: parent - TextArea { - id: statusTextArea + QGCLabel { + id: titleLabel + text: title + font.pixelSize: ScreenTools.largeFontPixelSize + } - width: parent.width - height: 300 - readOnly: true - frameVisible: false - font.pixelSize: ScreenTools.defaultFontPixelSize - - text: qsTr("Please disconnect all vehicles from QGroundControl before selecting Upgrade.") + ProgressBar { + id: progressBar + anchors.topMargin: ScreenTools.defaultFontPixelHeight + anchors.top: titleLabel.bottom + width: parent.width + } - style: TextAreaStyle { - textColor: qgcPal.text - backgroundColor: qgcPal.windowShade - } + TextArea { + id: statusTextArea + anchors.topMargin: ScreenTools.defaultFontPixelHeight + anchors.top: progressBar.bottom + anchors.bottom: parent.bottom + width: parent.width + readOnly: true + frameVisible: false + font.pixelSize: ScreenTools.defaultFontPixelSize + textFormat: TextEdit.RichText + text: welcomeText + + style: TextAreaStyle { + textColor: qgcPal.text + backgroundColor: qgcPal.windowShade } - } // Column - } // QGCViewPanel -} // QGCView + } + } // QGCViewPabel +} // QGCView \ No newline at end of file diff --git a/src/VehicleSetup/FirmwareUpgradeController.cc b/src/VehicleSetup/FirmwareUpgradeController.cc index 700190003..a00922c77 100644 --- a/src/VehicleSetup/FirmwareUpgradeController.cc +++ b/src/VehicleSetup/FirmwareUpgradeController.cc @@ -26,16 +26,7 @@ /// @author Don Gagne #include "FirmwareUpgradeController.h" - -#include -#include -#include -#include -#include -#include -#include -#include - +#include "Bootloader.h" #include "QGCFileDialog.h" #include "QGCMessageBox.h" @@ -43,187 +34,229 @@ FirmwareUpgradeController::FirmwareUpgradeController(void) : _downloadManager(NULL), _downloadNetworkReply(NULL), - _firmwareType(StableFirmware), - _upgradeButton(NULL), - _statusLog(NULL) + _statusLog(NULL), + _image(NULL) { _threadController = new PX4FirmwareUpgradeThreadController(this); Q_CHECK_PTR(_threadController); - connect(_threadController, &PX4FirmwareUpgradeThreadController::foundBoard, this, &FirmwareUpgradeController::_foundBoard); - connect(_threadController, &PX4FirmwareUpgradeThreadController::foundBootloader, this, &FirmwareUpgradeController::_foundBootloader); - connect(_threadController, &PX4FirmwareUpgradeThreadController::bootloaderSyncFailed, this, &FirmwareUpgradeController::_bootloaderSyncFailed); - connect(_threadController, &PX4FirmwareUpgradeThreadController::error, this, &FirmwareUpgradeController::_error); - connect(_threadController, &PX4FirmwareUpgradeThreadController::complete, this, &FirmwareUpgradeController::_complete); - connect(_threadController, &PX4FirmwareUpgradeThreadController::findTimeout, this, &FirmwareUpgradeController::_findTimeout); - connect(_threadController, &PX4FirmwareUpgradeThreadController::updateProgress, this, &FirmwareUpgradeController::_updateProgress); - + connect(_threadController, &PX4FirmwareUpgradeThreadController::foundBoard, this, &FirmwareUpgradeController::_foundBoard); + connect(_threadController, &PX4FirmwareUpgradeThreadController::noBoardFound, this, &FirmwareUpgradeController::_noBoardFound); + connect(_threadController, &PX4FirmwareUpgradeThreadController::boardGone, this, &FirmwareUpgradeController::_boardGone); + connect(_threadController, &PX4FirmwareUpgradeThreadController::foundBootloader, this, &FirmwareUpgradeController::_foundBootloader); + connect(_threadController, &PX4FirmwareUpgradeThreadController::bootloaderSyncFailed, this, &FirmwareUpgradeController::_bootloaderSyncFailed); + connect(_threadController, &PX4FirmwareUpgradeThreadController::error, this, &FirmwareUpgradeController::_error); + connect(_threadController, &PX4FirmwareUpgradeThreadController::updateProgress, this, &FirmwareUpgradeController::_updateProgress); + connect(_threadController, &PX4FirmwareUpgradeThreadController::status, this, &FirmwareUpgradeController::_status); + connect(_threadController, &PX4FirmwareUpgradeThreadController::eraseStarted, this, &FirmwareUpgradeController::_eraseStarted); + connect(_threadController, &PX4FirmwareUpgradeThreadController::eraseComplete, this, &FirmwareUpgradeController::_eraseComplete); + connect(_threadController, &PX4FirmwareUpgradeThreadController::flashComplete, this, &FirmwareUpgradeController::_flashComplete); + connect(_threadController, &PX4FirmwareUpgradeThreadController::updateProgress, this, &FirmwareUpgradeController::_updateProgress); + + connect(LinkManager::instance(), &LinkManager::linkDisconnected, this, &FirmwareUpgradeController::_linkDisconnected); + connect(&_eraseTimer, &QTimer::timeout, this, &FirmwareUpgradeController::_eraseProgressTick); } -/// @brief Cancels the current state and returns to the begin start -void FirmwareUpgradeController::_cancel(void) +void FirmwareUpgradeController::startBoardSearch(void) { - // Bootloader may still still open, reboot to close and heopfully get back to FMU - _threadController->sendBootloaderReboot(); - - Q_ASSERT(_upgradeButton); - _upgradeButton->setEnabled(true); + _bootloaderFound = false; + _startFlashWhenBootloaderFound = false; + _threadController->startFindBoardLoop(); } -/// @brief Begins the process or searching for the board -void FirmwareUpgradeController::_findBoard(void) +void FirmwareUpgradeController::flash(FirmwareType_t firmwareType) { - QString msg("Plug your board into USB now. Press Ok when board is plugged in."); - - _appendStatusLog(msg); - emit showMessage("Firmware Upgrade", msg); - - _searchingForBoard = true; - _threadController->findBoard(_findBoardTimeoutMsec); + if (_bootloaderFound) { + _getFirmwareFile(firmwareType); + } else { + // We haven't found the bootloader yet. Need to wait until then to flash + _startFlashWhenBootloaderFound = true; + _startFlashWhenBootloaderFoundFirmwareType = firmwareType; + } } -/// @brief Called when board has been found by the findBoard process -void FirmwareUpgradeController::_foundBoard(bool firstTry, const QString portName, QString portDescription) +void FirmwareUpgradeController::cancel(void) { - if (firstTry) { - // Board is still plugged - _cancel(); - emit showMessage("Board plugged in", - "Please unplug your board before beginning the Firmware Upgrade process. " - "Click Upgrade again once the board is unplugged."); - } else { - _portName = portName; - _portDescription = portDescription; + _eraseTimer.stop(); + _threadController->cancel(); +} - _appendStatusLog(tr("Board found:")); - _appendStatusLog(tr(" Port: %1").arg(_portName)); - _appendStatusLog(tr(" Description: %1").arg(_portName)); - - _findBootloader(); +void FirmwareUpgradeController::_foundBoard(bool firstAttempt, const QSerialPortInfo& info, int type) +{ + _foundBoardInfo = info; + switch (type) { + case FoundBoardPX4FMUV1: + _foundBoardType = "PX4 FMU V1"; + _startFlashWhenBootloaderFound = false; + break; + case FoundBoardPX4FMUV2: + _foundBoardType = "Pixhawk"; + _startFlashWhenBootloaderFound = false; + break; + case FoundBoardPX4Flow: + case FoundBoard3drRadio: + _foundBoardType = type == FoundBoardPX4Flow ? "PX4 Flow" : "3DR Radio"; + if (!firstAttempt) { + // PX4 Flow and Radio always flash stable firmware, so we can start right away without + // any further user input. + _startFlashWhenBootloaderFound = true; + _startFlashWhenBootloaderFoundFirmwareType = PX4StableFirmware; + } + break; } + + qCDebug(FirmwareUpgradeLog) << _foundBoardType; + emit boardFound(); } -/// @brief Begins the findBootloader process to connect to the bootloader -void FirmwareUpgradeController::_findBootloader(void) + +void FirmwareUpgradeController::_noBoardFound(void) { - _appendStatusLog(tr("Attemping to communicate with bootloader...")); - _searchingForBoard = false; - _threadController->findBootloader(_portName, _findBootloaderTimeoutMsec); + emit noBoardFound(); +} + +void FirmwareUpgradeController::_boardGone(void) +{ + emit boardGone(); } /// @brief Called when the bootloader is connected to by the findBootloader process. Moves the state machine /// to the next step. void FirmwareUpgradeController::_foundBootloader(int bootloaderVersion, int boardID, int flashSize) { + _bootloaderFound = true; _bootloaderVersion = bootloaderVersion; - _boardID = boardID; - _boardFlashSize = flashSize; + _bootloaderBoardID = boardID; + _bootloaderBoardFlashSize = flashSize; - _appendStatusLog(tr("Connected to bootloader:")); - _appendStatusLog(tr(" Version: %1").arg(_bootloaderVersion)); - _appendStatusLog(tr(" Board ID: %1").arg(_boardID)); - _appendStatusLog(tr(" Flash size: %1").arg(_boardFlashSize)); + _appendStatusLog("Connected to bootloader:"); + _appendStatusLog(QString(" Version: %1").arg(_bootloaderVersion)); + _appendStatusLog(QString(" Board ID: %1").arg(_bootloaderBoardID)); + _appendStatusLog(QString(" Flash size: %1").arg(_bootloaderBoardFlashSize)); - _getFirmwareFile(); + if (_startFlashWhenBootloaderFound) { + flash(_startFlashWhenBootloaderFoundFirmwareType); + } } /// @brief Called when the findBootloader process is unable to sync to the bootloader. Moves the state /// machine to the appropriate error state. void FirmwareUpgradeController::_bootloaderSyncFailed(void) { - _appendStatusLog(tr("Unable to sync with bootloader.")); - _cancel(); -} - -/// @brief Called when the findBoard or findBootloader process times out. Moves the state machine to the -/// appropriate error state. -void FirmwareUpgradeController::_findTimeout(void) -{ - QString msg; - - if (_searchingForBoard) { - msg = tr("Unable to detect your board. If the board is currently connected via USB. Disconnect it and try Upgrade again."); - } else { - msg = tr("Unable to communicate with Bootloader. If the board is currently connected via USB. Disconnect it and try Upgrade again."); - } - _cancel(); - emit showMessage("Error", msg); + _errorCancel("Unable to sync with bootloader."); } /// @brief Prompts the user to select a firmware file if needed and moves the state machine to the next state. -void FirmwareUpgradeController::_getFirmwareFile(void) +void FirmwareUpgradeController::_getFirmwareFile(FirmwareType_t firmwareType) { - static const char* rgPX4FMUV1Firmware[3] = - { - "http://px4-travis.s3.amazonaws.com/Firmware/stable/px4fmu-v1_default.px4", - "http://px4-travis.s3.amazonaws.com/Firmware/beta/px4fmu-v1_default.px4", - "http://px4-travis.s3.amazonaws.com/Firmware/master/px4fmu-v1_default.px4" + static DownloadLocationByFirmwareType_t rgPX4FMUV2Firmware[] = { + { PX4StableFirmware, "http://px4-travis.s3.amazonaws.com/Firmware/stable/px4fmu-v2_default.px4" }, + { PX4BetaFirmware, "http://px4-travis.s3.amazonaws.com/Firmware/beta/px4fmu-v2_default.px4" }, + { PX4DeveloperFirmware, "http://px4-travis.s3.amazonaws.com/Firmware/master/px4fmu-v2_default.px4"}, + { ApmArduCopterQuadFirmware, "http://firmware.diydrones.com/Copter/stable/PX4-quad/ArduCopter-v2.px4" }, + { ApmArduCopterX8Firmware, "http://firmware.diydrones.com/Copter/stable/PX4-octa-quad/ArduCopter-v2.px4" }, + { ApmArduCopterHexaFirmware, "http://firmware.diydrones.com/Copter/stable/PX4-hexa/ArduCopter-v2.px4" }, + { ApmArduCopterOctoFirmware, "http://firmware.diydrones.com/Copter/stable/PX4-octa/ArduCopter-v2.px4" }, + { ApmArduCopterYFirmware, "http://firmware.diydrones.com/Copter/stable/PX4-tri/ArduCopter-v2.px4" }, + { ApmArduCopterY6Firmware, "http://firmware.diydrones.com/Copter/stable/PX4-y6/ArduCopter-v2.px4" }, + { ApmArduCopterHeliFirmware, "http://firmware.diydrones.com/Copter/stable/PX4-heli/ArduCopter-v2.px4" }, + { ApmArduPlaneFirmware, "http://firmware.diydrones.com/Plane/stable/PX4/ArduPlane-v2.px4" }, + { ApmRoverFirmware, "http://firmware.diydrones.com/Plane/stable/PX4/APMrover2-v2.px4" }, }; + static const size_t crgPX4FMUV2Firmware = sizeof(rgPX4FMUV2Firmware) / sizeof(rgPX4FMUV2Firmware[0]); - static const char* rgPX4FMUV2Firmware[3] = - { - "http://px4-travis.s3.amazonaws.com/Firmware/stable/px4fmu-v2_default.px4", - "http://px4-travis.s3.amazonaws.com/Firmware/beta/px4fmu-v2_default.px4", - "http://px4-travis.s3.amazonaws.com/Firmware/master/px4fmu-v2_default.px4" + static const DownloadLocationByFirmwareType_t rgAeroCoreFirmware[] = { + { PX4StableFirmware, "http://s3-us-west-2.amazonaws.com/gumstix-aerocore/PX4/stable/aerocore_default.px4" }, + { PX4BetaFirmware, "http://s3-us-west-2.amazonaws.com/gumstix-aerocore/PX4/beta/aerocore_default.px4" }, + { PX4DeveloperFirmware, "http://s3-us-west-2.amazonaws.com/gumstix-aerocore/PX4/master/aerocore_default.px4" }, }; - - static const char* rgAeroCoreFirmware[3] = - { - "http://gumstix-aerocore.s3.amazonaws.com/PX4/stable/aerocore_default.px4", - "http://gumstix-aerocore.s3.amazonaws.com/PX4/beta/aerocore_default.px4", - "http://gumstix-aerocore.s3.amazonaws.com/PX4/master/aerocore_default.px4" + static const size_t crgAeroCoreFirmware = sizeof(rgAeroCoreFirmware) / sizeof(rgAeroCoreFirmware[0]); + + static const DownloadLocationByFirmwareType_t rgPX4FMUV1Firmware[] = { + { PX4StableFirmware, "http://px4-travis.s3.amazonaws.com/Firmware/stable/px4fmu-v1_default.px4" }, + { PX4BetaFirmware, "http://px4-travis.s3.amazonaws.com/Firmware/beta/px4fmu-v1_default.px4" }, + { PX4DeveloperFirmware, "http://px4-travis.s3.amazonaws.com/Firmware/master/px4fmu-v1_default.px4" }, }; - - static const char* rgPX4FlowFirmware[3] = - { - "http://px4-travis.s3.amazonaws.com/Flow/master/px4flow.px4", - "http://px4-travis.s3.amazonaws.com/Flow/master/px4flow.px4", - "http://px4-travis.s3.amazonaws.com/Flow/master/px4flow.px4" + static const size_t crgPX4FMUV1Firmware = sizeof(rgPX4FMUV1Firmware) / sizeof(rgPX4FMUV1Firmware[0]); + + static const DownloadLocationByFirmwareType_t rgPX4FlowFirmware[] = { + { PX4StableFirmware, "http://px4-travis.s3.amazonaws.com/Flow/master/px4flow.px4" }, }; + static const size_t crgPX4FlowFirmware = sizeof(rgPX4FlowFirmware) / sizeof(rgPX4FlowFirmware[0]); - Q_ASSERT(sizeof(rgPX4FMUV1Firmware) == sizeof(rgPX4FMUV2Firmware) && sizeof(rgPX4FMUV1Firmware) == sizeof(rgPX4FlowFirmware)); + static const DownloadLocationByFirmwareType_t rg3DRRadioFirmware[] = { + { PX4StableFirmware, "http://firmware.diydrones.com/SiK/stable/radio~hm_trp.ihx" }, + }; + static const size_t crg3DRRadioFirmware = sizeof(rg3DRRadioFirmware) / sizeof(rg3DRRadioFirmware[0]); + + // Select the firmware set based on board type - const char** prgFirmware; - switch (_boardID) { - case _boardIDPX4FMUV1: + const DownloadLocationByFirmwareType_t* prgFirmware; + size_t crgFirmware; + + switch (_bootloaderBoardID) { + case Bootloader::boardIDPX4FMUV1: prgFirmware = rgPX4FMUV1Firmware; + crgFirmware = crgPX4FMUV1Firmware; break; - case _boardIDPX4Flow: + case Bootloader::boardIDPX4Flow: prgFirmware = rgPX4FlowFirmware; + crgFirmware = crgPX4FlowFirmware; break; - case _boardIDPX4FMUV2: + case Bootloader::boardIDPX4FMUV2: prgFirmware = rgPX4FMUV2Firmware; + crgFirmware = crgPX4FMUV2Firmware; break; - - case _boardIDAeroCore: + + case Bootloader::boardIDAeroCore: prgFirmware = rgAeroCoreFirmware; + crgFirmware = crgAeroCoreFirmware; break; - + + case Bootloader::boardID3DRRadio: + prgFirmware = rg3DRRadioFirmware; + crgFirmware = crg3DRRadioFirmware; + break; + default: prgFirmware = NULL; break; } - - if (prgFirmware == NULL && _firmwareType != CustomFirmware) { - QGCMessageBox::critical(tr("Firmware Upgrade"), tr("Attemping to flash an unknown board type, you must select 'Custom firmware file'")); - _cancel(); - return; + + if (prgFirmware == NULL && firmwareType != PX4CustomFirmware) { + _errorCancel("Attempting to flash an unknown board type, you must select 'Custom firmware file'"); + return; } - if (_firmwareType == CustomFirmware) { + if (firmwareType == PX4CustomFirmware) { _firmwareFilename = QGCFileDialog::getOpenFileName(NULL, // Parent to main window - tr("Select Firmware File"), // Dialog Caption + "Select Firmware File", // Dialog Caption QStandardPaths::writableLocation(QStandardPaths::DocumentsLocation), // Initial directory - tr("Firmware Files (*.px4 *.bin)")); // File filter + "Firmware Files (*.px4 *.bin *.ihx)"); // File filter } else { - _firmwareFilename = prgFirmware[_firmwareType]; + bool found = false; + + for (size_t i=0; ifirmwareType == firmwareType) { + found = true; + break; + } + prgFirmware++; + } + + if (found) { + _firmwareFilename = prgFirmware->downloadLocation; + } else { + _errorCancel("Unable to find specified firmware download location"); + return; + } } if (_firmwareFilename.isEmpty()) { - _cancel(); + _errorCancel("No firmware file selected"); } else { _downloadFirmware(); } @@ -234,8 +267,8 @@ void FirmwareUpgradeController::_downloadFirmware(void) { Q_ASSERT(!_firmwareFilename.isEmpty()); - _appendStatusLog(tr("Downloading firmware...")); - _appendStatusLog(tr(" From: %1").arg(_firmwareFilename)); + _appendStatusLog("Downloading firmware..."); + _appendStatusLog(QString(" From: %1").arg(_firmwareFilename)); // Split out filename from path QString firmwareFilename = QFileInfo(_firmwareFilename).fileName(); @@ -246,13 +279,12 @@ void FirmwareUpgradeController::_downloadFirmware(void) if (downloadFile.isEmpty()) { downloadFile = QStandardPaths::writableLocation(QStandardPaths::DownloadLocation); if (downloadFile.isEmpty()) { - _appendStatusLog(tr("Unabled to find writable download location. Tried downloads and temp directory.")); - _cancel(); + _errorCancel("Unabled to find writable download location. Tried downloads and temp directory."); return; } } Q_ASSERT(!downloadFile.isEmpty()); - downloadFile += "/" + firmwareFilename; + downloadFile += "/" + firmwareFilename; QUrl firmwareUrl; if (_firmwareFilename.startsWith("http:")) { @@ -290,7 +322,7 @@ void FirmwareUpgradeController::_downloadProgress(qint64 curr, qint64 total) /// @brief Called when the firmware download completes. void FirmwareUpgradeController::_downloadFinished(void) { - _appendStatusLog(tr("Download complete")); + _appendStatusLog("Download complete"); QNetworkReply* reply = qobject_cast(QObject::sender()); Q_ASSERT(reply); @@ -313,286 +345,72 @@ void FirmwareUpgradeController::_downloadFinished(void) // Store downloaded file in download location QFile file(downloadFilename); if (!file.open(QIODevice::WriteOnly)) { - _appendStatusLog(tr("Could not save downloaded file to %1. Error: %2").arg(downloadFilename).arg(file.errorString())); - _cancel(); + _errorCancel(QString("Could not save downloaded file to %1. Error: %2").arg(downloadFilename).arg(file.errorString())); return; } file.write(reply->readAll()); file.close(); + FirmwareImage* image = new FirmwareImage(this); + connect(image, &FirmwareImage::statusMessage, this, &FirmwareUpgradeController::_status); + connect(image, &FirmwareImage::errorMessage, this, &FirmwareUpgradeController::_error); - if (downloadFilename.endsWith(".px4")) { - // We need to collect information from the .px4 file as well as pull the binary image out to a seperate file. - - QFile px4File(downloadFilename); - if (!px4File.open(QIODevice::ReadOnly | QIODevice::Text)) { - _appendStatusLog(tr("Unable to open firmware file %1, error: %2").arg(downloadFilename).arg(px4File.errorString())); - return; - } - - QByteArray bytes = px4File.readAll(); - px4File.close(); - QJsonDocument doc = QJsonDocument::fromJson(bytes); - - if (doc.isNull()) { - _appendStatusLog(tr("Supplied file is not a valid JSON document")); - _cancel(); - return; - } - - QJsonObject px4Json = doc.object(); - - // Make sure the keys we need are available - static const char* rgJsonKeys[] = { "board_id", "image_size", "description", "git_identity" }; - for (size_t i=0; i(static_cast(0xFF))); - } - - // Store decompressed image file in same location as original download file - QDir downloadDir = QFileInfo(downloadFilename).dir(); - QString decompressFilename = downloadDir.filePath("PX4FlashUpgrade.bin"); - - QFile decompressFile(decompressFilename); - if (!decompressFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - _appendStatusLog(tr("Unable to open decompressed file %1 for writing, error: %2").arg(decompressFilename).arg(decompressFile.errorString())); - _cancel(); - return; - } - - qint64 bytesWritten = decompressFile.write(decompressedBytes); - if (bytesWritten != decompressedBytes.count()) { - _appendStatusLog(tr("Write failed for decompressed image file, error: %1").arg(decompressFile.errorString())); - _cancel(); - return; - } - decompressFile.close(); - - _firmwareFilename = decompressFilename; - } else if (downloadFilename.endsWith(".bin")) { - uint32_t firmwareBoardID = 0; - - // Take some educated guesses on board id based on firmware build system file name conventions - - if (downloadFilename.toLower().contains("px4fmu-v1")) { - firmwareBoardID = _boardIDPX4FMUV2; - } else if (downloadFilename.toLower().contains("px4flow")) { - firmwareBoardID = _boardIDPX4Flow; - } else if (downloadFilename.toLower().contains("px4fmu-v1")) { - firmwareBoardID = _boardIDPX4FMUV1; - } else if (downloadFilename.toLower().contains("aerocore")) { - firmwareBoardID = _boardIDAeroCore; - } - - if (firmwareBoardID != 0 && firmwareBoardID != _boardID) { - _appendStatusLog(tr("Downloaded firmware board id does not match hardware board id: %1 != %2").arg(firmwareBoardID).arg(_boardID)); - _cancel(); - return; - } - - _firmwareFilename = downloadFilename; - - QFile binFile(_firmwareFilename); - if (!binFile.open(QIODevice::ReadOnly)) { - _appendStatusLog(tr("Unabled to open firmware file %1, %2").arg(_firmwareFilename).arg(binFile.errorString())); - _cancel(); - return; - } - _imageSize = (uint32_t)binFile.size(); - binFile.close(); - } else { - // Standard firmware builds (stable/continuous/...) are always .bin or .px4. Select file dialog for custom - // firmware filters to .bin and .px4. So we should never get a file that ends in anything else. - Q_ASSERT(false); + if (!image->load(downloadFilename, _bootloaderBoardID)) { + _errorCancel("Image load failed"); + return; } - if (_imageSize > _boardFlashSize) { - _appendStatusLog(tr("Image size of %1 is too large for board flash size %2").arg(_imageSize).arg(_boardFlashSize)); - _cancel(); + // We can't proceed unless we have the bootloader + if (!_bootloaderFound) { + _errorCancel(QString("Bootloader not found").arg(_imageSize).arg(_bootloaderBoardFlashSize)); return; } - - _erase(); -} - -/// Decompress a set of bytes stored in a Json document. -bool FirmwareUpgradeController::_decompressJsonValue(const QJsonObject& jsonObject, ///< JSON object - const QByteArray& jsonDocBytes, ///< Raw bytes of JSON document - const QString& sizeKey, ///< key which holds byte size - const QString& bytesKey, ///< key which holds compress bytes - QByteArray& decompressedBytes) ///< Returned decompressed bytes -{ - // Validate decompressed size key - if (!jsonObject.contains(sizeKey)) { - _appendStatusLog(QString("Firmware file missing %1 key").arg(sizeKey)); - return false; - } - int decompressedSize = jsonObject.value(QString(sizeKey)).toInt(); - if (decompressedSize == 0) { - _appendStatusLog(QString("Firmware file has invalid decompressed size for %1").arg(sizeKey)); - return false; - } - // XXX Qt's JSON string handling is terribly broken, strings - // with some length (18K / 25K) are just weirdly cut. - // The code below works around this by manually 'parsing' - // for the image string. Since its compressed / checksummed - // this should be fine. - - QStringList parts = QString(jsonDocBytes).split(QString("\"%1\": \"").arg(bytesKey)); - if (parts.count() == 1) { - _appendStatusLog(QString("Could not find compressed bytes for %1 in Firmware file").arg(bytesKey)); - return false; - } - parts = parts.last().split("\""); - if (parts.count() == 1) { - _appendStatusLog(QString("Incorrectly formed compressed bytes section for %1 in Firmware file").arg(bytesKey)); - return false; - } - - // Store decompressed size as first four bytes. This is required by qUncompress routine. - QByteArray raw; - raw.append((unsigned char)((decompressedSize >> 24) & 0xFF)); - raw.append((unsigned char)((decompressedSize >> 16) & 0xFF)); - raw.append((unsigned char)((decompressedSize >> 8) & 0xFF)); - raw.append((unsigned char)((decompressedSize >> 0) & 0xFF)); - - QByteArray raw64 = parts.first().toUtf8(); - raw.append(QByteArray::fromBase64(raw64)); - decompressedBytes = qUncompress(raw); - - if (decompressedBytes.count() == 0) { - _appendStatusLog(QString("Firmware file has 0 length %1").arg(bytesKey)); - return false; - } - if (decompressedBytes.count() != decompressedSize) { - _appendStatusLog(QString("Size for decompressed %1 does not match stored size: Expected(%1) Actual(%2)").arg(decompressedSize).arg(decompressedBytes.count())); - return false; + if (_bootloaderBoardFlashSize != 0 && _imageSize > _bootloaderBoardFlashSize) { + _errorCancel(QString("Image size of %1 is too large for board flash size %2").arg(_imageSize).arg(_bootloaderBoardFlashSize)); + return; } - - _appendStatusLog(QString("Succesfully decompressed %1").arg(bytesKey)); - return true; + + _threadController->flash(image); } /// @brief Called when an error occurs during download void FirmwareUpgradeController::_downloadError(QNetworkReply::NetworkError code) { + QString errorMsg; + if (code == QNetworkReply::OperationCanceledError) { - _appendStatusLog(tr("Download cancelled")); + errorMsg = "Download cancelled"; } else { - _appendStatusLog(tr("Error during download. Error: %1").arg(code)); + errorMsg = QString("Error during download. Error: %1").arg(code); } - _cancel(); + _errorCancel(errorMsg); } -/// @brief Erase the board -void FirmwareUpgradeController::_erase(void) +/// @brief Signals completion of one of the specified bootloader commands. Moves the state machine to the +/// appropriate next step. +void FirmwareUpgradeController::_flashComplete(void) { - _appendStatusLog(tr("Erasing previous firmware...")); - - // We set up our own progress bar for erase since the erase command does not provide one - _eraseTickCount = 0; - _eraseTimer.start(_eraseTickMsec); + delete _image; + _image = NULL; - // Erase command - _threadController->erase(); + _appendStatusLog("Upgrade complete", true); + _appendStatusLog("------------------------------------------", false); + emit flashComplete(); } -/// @brief Signals completion of one of the specified bootloader commands. Moves the state machine to the -/// appropriate next step. -void FirmwareUpgradeController::_complete(const int command) +void FirmwareUpgradeController::_error(const QString& errorString) { - if (command == PX4FirmwareUpgradeThreadWorker::commandProgram) { - _appendStatusLog(tr("Verifying board programming...")); - _threadController->verify(_firmwareFilename); - } else if (command == PX4FirmwareUpgradeThreadWorker::commandVerify) { - _appendStatusLog(tr("Upgrade complete")); - QGCMessageBox::information(tr("Firmware Upgrade"), tr("Upgrade completed succesfully")); - _cancel(); - } else if (command == PX4FirmwareUpgradeThreadWorker::commandErase) { - _eraseTimer.stop(); - _appendStatusLog(tr("Flashing new firmware to board...")); - _threadController->program(_firmwareFilename); - } else if (command == PX4FirmwareUpgradeThreadWorker::commandCancel) { - // FIXME: This is no longer needed, no Cancel - if (_searchingForBoard) { - _appendStatusLog(tr("Board not found")); - _cancel(); - } else { - _appendStatusLog(tr("Bootloader not found")); - _cancel(); - } - } else { - Q_ASSERT(false); - } + delete _image; + _image = NULL; + + _errorCancel(QString("Error: %1").arg(errorString)); } -/// @brief Signals that an error has occured with the specified bootloader commands. Moves the state machine -/// to the appropriate error state. -void FirmwareUpgradeController::_error(const int command, const QString errorString) +void FirmwareUpgradeController::_status(const QString& statusString) { - Q_UNUSED(command); - - _appendStatusLog(tr("Error: %1").arg(errorString)); - _cancel(); + _appendStatusLog(statusString); } /// @brief Updates the progress bar from long running bootloader commands @@ -604,13 +422,6 @@ void FirmwareUpgradeController::_updateProgress(int curr, int total) } } -/// @brief Resets the state machine back to the beginning -void FirmwareUpgradeController::_restart(void) -{ - // FIXME: NYI - //_setupState(upgradeStateBegin); -} - /// @brief Moves the progress bar ahead on tick while erasing the board void FirmwareUpgradeController::_eraseProgressTick(void) { @@ -618,33 +429,54 @@ void FirmwareUpgradeController::_eraseProgressTick(void) _progressBar->setProperty("value", (float)(_eraseTickCount*_eraseTickMsec) / (float)_eraseTotalMsec); } -void FirmwareUpgradeController::doFirmwareUpgrade(void) -{ - Q_ASSERT(_upgradeButton); - _upgradeButton->setEnabled(false); - - _findBoard(); -} - /// Appends the specified text to the status log area in the ui -void FirmwareUpgradeController::_appendStatusLog(const QString& text) +void FirmwareUpgradeController::_appendStatusLog(const QString& text, bool critical) { Q_ASSERT(_statusLog); QVariant returnedValue; - QVariant varText = text; + QVariant varText; + + if (critical) { + varText = QString("%1").arg(text); + } else { + varText = text; + } + QMetaObject::invokeMethod(_statusLog, "append", Q_RETURN_ARG(QVariant, returnedValue), Q_ARG(QVariant, varText)); } -bool FirmwareUpgradeController::activeQGCConnections(void) +bool FirmwareUpgradeController::qgcConnections(void) { return LinkManager::instance()->anyConnectedLinks(); } -bool FirmwareUpgradeController::pluggedInBoard(void) +void FirmwareUpgradeController::_linkDisconnected(LinkInterface* link) +{ + Q_UNUSED(link); + emit qgcConnectionsChanged(qgcConnections()); +} + +void FirmwareUpgradeController::_errorCancel(const QString& msg) +{ + _appendStatusLog(msg, false); + _appendStatusLog("Upgrade cancelled", true); + _appendStatusLog("------------------------------------------", false); + emit error(); + cancel(); +} + +void FirmwareUpgradeController::_eraseStarted(void) +{ + // We set up our own progress bar for erase since the erase command does not provide one + _eraseTickCount = 0; + _eraseTimer.start(_eraseTickMsec); +} + +void FirmwareUpgradeController::_eraseComplete(void) { - return _threadController->pluggedInBoard(); + _eraseTimer.stop(); } diff --git a/src/VehicleSetup/FirmwareUpgradeController.h b/src/VehicleSetup/FirmwareUpgradeController.h index 039aa2e27..6065ddafb 100644 --- a/src/VehicleSetup/FirmwareUpgradeController.h +++ b/src/VehicleSetup/FirmwareUpgradeController.h @@ -28,6 +28,8 @@ #define FirmwareUpgradeController_H #include "PX4FirmwareUpgradeThread.h" +#include "LinkManager.h" +#include "FirmwareImage.h" #include #include @@ -51,37 +53,46 @@ public: /// Supported firmware types. If you modify these you will need to update the qml file as well. typedef enum { - StableFirmware, - BetaFirmware, - DeveloperFirmware, - CustomFirmware + PX4StableFirmware, + PX4BetaFirmware, + PX4DeveloperFirmware, + PX4CustomFirmware, + ApmArduCopterQuadFirmware, + ApmArduCopterX8Firmware, + ApmArduCopterHexaFirmware, + ApmArduCopterOctoFirmware, + ApmArduCopterYFirmware, + ApmArduCopterY6Firmware, + ApmArduCopterHeliFirmware, + ApmArduPlaneFirmware, + ApmRoverFirmware, } FirmwareType_t; Q_ENUMS(FirmwareType_t) - /// Firmare type to load - Q_PROPERTY(FirmwareType_t firmwareType READ firmwareType WRITE setFirmwareType) + Q_PROPERTY(QString boardPort READ boardPort NOTIFY boardFound) + Q_PROPERTY(QString boardDescription READ boardDescription NOTIFY boardFound) + Q_PROPERTY(QString boardType MEMBER _foundBoardType NOTIFY boardFound) - /// Upgrade push button in UI - Q_PROPERTY(QQuickItem* upgradeButton READ upgradeButton WRITE setUpgradeButton) - /// TextArea for log output Q_PROPERTY(QQuickItem* statusLog READ statusLog WRITE setStatusLog) /// Progress bar for you know what Q_PROPERTY(QQuickItem* progressBar READ progressBar WRITE setProgressBar) + + /// Returns true if there are active QGC connections + Q_PROPERTY(bool qgcConnections READ qgcConnections NOTIFY qgcConnectionsChanged) - Q_INVOKABLE bool activeQGCConnections(void); - Q_INVOKABLE bool pluggedInBoard(void); + /// Starts searching for boards on the background thread + Q_INVOKABLE void startBoardSearch(void); - /// Begins the firware upgrade process - Q_INVOKABLE void doFirmwareUpgrade(void); - - FirmwareType_t firmwareType(void) { return _firmwareType; } - void setFirmwareType(FirmwareType_t firmwareType) { _firmwareType = firmwareType; } + /// Cancels whatever state the upgrade worker thread is in + Q_INVOKABLE void cancel(void); - QQuickItem* upgradeButton(void) { return _upgradeButton; } - void setUpgradeButton(QQuickItem* upgradeButton) { _upgradeButton = upgradeButton; } + /// Called when the firmware type has been selected by the user to continue the flash process. + Q_INVOKABLE void flash(FirmwareType_t firmwareType); + + // Property accessors QQuickItem* progressBar(void) { return _progressBar; } void setProgressBar(QQuickItem* progressBar) { _progressBar = progressBar; } @@ -89,48 +100,61 @@ public: QQuickItem* statusLog(void) { return _statusLog; } void setStatusLog(QQuickItem* statusLog) { _statusLog = statusLog; } + bool qgcConnections(void); + + QString boardPort(void) { return _foundBoardInfo.portName(); } + QString boardDescription(void) { return _foundBoardInfo.description(); } + signals: - void showMessage(const QString& title, const QString& message); + void boardFound(void); + void noBoardFound(void); + void boardGone(void); + void flashComplete(void); + void flashCancelled(void); + void qgcConnectionsChanged(bool connections); + void error(void); private slots: void _downloadProgress(qint64 curr, qint64 total); void _downloadFinished(void); void _downloadError(QNetworkReply::NetworkError code); - void _foundBoard(bool firstTry, const QString portname, QString portDescription); + void _foundBoard(bool firstAttempt, const QSerialPortInfo& portInfo, int type); + void _noBoardFound(void); + void _boardGone(); void _foundBootloader(int bootloaderVersion, int boardID, int flashSize); - void _error(const int command, const QString errorString); + void _error(const QString& errorString); + void _status(const QString& statusString); void _bootloaderSyncFailed(void); - void _findTimeout(void); - void _complete(const int command); + void _flashComplete(void); void _updateProgress(int curr, int total); - void _restart(void); + void _eraseStarted(void); + void _eraseComplete(void); void _eraseProgressTick(void); + void _linkDisconnected(LinkInterface* link); private: - void _findBoard(void); - void _findBootloader(void); - void _cancel(void); - void _getFirmwareFile(void); + void _getFirmwareFile(FirmwareType_t firmwareType); void _downloadFirmware(void); - void _erase(void); - void _appendStatusLog(const QString& text); - bool _decompressJsonValue(const QJsonObject& jsonObject, - const QByteArray& jsonDocBytes, - const QString& sizeKey, - const QString& bytesKey, - QByteArray& decompressedBytes); + void _appendStatusLog(const QString& text, bool critical = false); + void _errorCancel(const QString& msg); + + typedef struct { + FirmwareType_t firmwareType; + const char* downloadLocation; + } DownloadLocationByFirmwareType_t; QString _portName; QString _portDescription; - uint32_t _bootloaderVersion; - - static const int _boardIDPX4FMUV1 = 5; ///< Board ID for PX4 V1 board - static const int _boardIDPX4FMUV2 = 9; ///< Board ID for PX4 V2 board - static const int _boardIDPX4Flow = 6; ///< Board ID for PX4 Flow board - static const int _boardIDAeroCore = 98; ///< Board ID for Gumstix AeroCore board - uint32_t _boardID; ///< Board ID - uint32_t _boardFlashSize; ///< Flash size in bytes of board + /// Information which comes back from the bootloader + bool _bootloaderFound; ///< true: we have received the foundBootloader signals + uint32_t _bootloaderVersion; ///< Bootloader version + uint32_t _bootloaderBoardID; ///< Board ID + uint32_t _bootloaderBoardFlashSize; ///< Flash size in bytes of board + + bool _startFlashWhenBootloaderFound; + FirmwareType_t _startFlashWhenBootloaderFoundFirmwareType; + uint32_t _imageSize; ///< Image size of firmware being flashed QPixmap _boardIcon; ///< Icon used to display image of board @@ -151,12 +175,15 @@ private: static const int _findBoardTimeoutMsec = 30000; ///< Amount of time for user to plug in USB static const int _findBootloaderTimeoutMsec = 5000; ///< Amount time to look for bootloader - FirmwareType_t _firmwareType; ///< Firmware type to load - QQuickItem* _upgradeButton; ///< Upgrade button in ui QQuickItem* _statusLog; ///< Status log TextArea Qml control QQuickItem* _progressBar; bool _searchingForBoard; ///< true: searching for board, false: search for bootloader + + QSerialPortInfo _foundBoardInfo; + QString _foundBoardType; + + FirmwareImage* _image; }; #endif diff --git a/src/VehicleSetup/PX4Bootloader.h b/src/VehicleSetup/PX4Bootloader.h deleted file mode 100644 index dcf35d5c9..000000000 --- a/src/VehicleSetup/PX4Bootloader.h +++ /dev/null @@ -1,161 +0,0 @@ -/*===================================================================== - - 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 . - - ======================================================================*/ - -/// @file -/// @brief PX4 Bootloader Utility routines -/// @author Don Gagne - -#ifndef PX4Bootloader_H -#define PX4Bootloader_H - -#include "qextserialport.h" - -#include - -/// @brief This class is used to communicate with the Bootloader. -class PX4Bootloader : public QObject -{ - Q_OBJECT - -public: - explicit PX4Bootloader(QObject *parent = 0); - - /// @brief Returns the error message associated with the last failed call to one of the bootloader - /// utility routine below. - QString errorString(void) { return _errorString; } - - /// @brief Write a byte to the port - /// @param port Port to write to - /// @param data Bytes to write - /// @param maxSize Number of bytes to write - /// @return true: success - bool write(QextSerialPort* port, const uint8_t* data, qint64 maxSize); - bool write(QextSerialPort* port, const uint8_t byte); - - /// @brief Read a set of bytes from the port - /// @param data Read bytes into this buffer - /// @param maxSize Number of bytes to read - /// @param readTimeout Msecs to wait for bytes to become available on port - bool read(QextSerialPort* port, uint8_t* data, qint64 maxSize, int readTimeout = _readTimout); - - /// @brief Read a PROTO_SYNC command response from the bootloader - /// @param responseTimeout Msecs to wait for response bytes to become available on port - bool getCommandResponse(QextSerialPort* port, const int responseTimeout = _responseTimeout); - - /// @brief Send a PROTO_GET_DEVICE command to retrieve a value from the bootloader - /// @param param Value to retrieve using INFO_BOARD_* enums - /// @param value Returned value - bool getBoardInfo(QextSerialPort* port, uint8_t param, uint32_t& value); - - /// @brief Send a command to the bootloader - /// @param cmd Command to send using PROTO_* enums - /// @return true: Command sent and valid sync response returned - bool sendCommand(QextSerialPort* port, uint8_t cmd, int responseTimeout = _responseTimeout); - - /// @brief Program the board with the specified firmware - bool program(QextSerialPort* port, const QString& firmwareFilename); - - /// @brief Verify the board flash. How it works depend on bootloader rev - /// Rev 2: Read the flash back and compare it against the firmware file - /// Rev 3: Compare CRCs for flash and file - bool verify(QextSerialPort* port, const QString firmwareFilename); - - /// @brief Read a PROTO_SYNC response from the bootloader - /// @return true: Valid sync response was received - bool sync(QextSerialPort* port); - - /// @brief Retrieve a set of board info from the bootloader - /// @param bootloaderVersion Returned INFO_BL_REV - /// @param boardID Returned INFO_BOARD_ID - /// @param flashSize Returned INFO_FLASH_SIZE - bool getBoardInfo(QextSerialPort* port, uint32_t& bootloaderVersion, uint32_t& boardID, uint32_t& flashSize); - - /// @brief Opens a port to the bootloader - bool open(QextSerialPort* port, const QString portName); - - /// @brief Sends a PROTO_REBOOT command to the bootloader - bool sendBootloaderReboot(QextSerialPort* port); - - /// @brief Sends a PROTO_ERASE command to the bootlader - bool erase(QextSerialPort* port); - -signals: - /// @brief Signals progress indicator for long running bootloader utility routines - void updateProgramProgress(int curr, int total); - -private: - enum { - // protocol bytes - PROTO_INSYNC = 0x12, ///< 'in sync' byte sent before status - PROTO_EOC = 0x20, ///< end of command - - // Reply bytes - PROTO_OK = 0x10, ///< INSYNC/OK - 'ok' response - PROTO_FAILED = 0x11, ///< INSYNC/FAILED - 'fail' response - PROTO_INVALID = 0x13, ///< INSYNC/INVALID - 'invalid' response for bad commands - - // Command bytes - PROTO_GET_SYNC = 0x21, ///< NOP for re-establishing sync - PROTO_GET_DEVICE = 0x22, ///< get device ID bytes - PROTO_CHIP_ERASE = 0x23, ///< erase program area and reset program address - PROTO_PROG_MULTI = 0x27, ///< write bytes at program address and increment - PROTO_GET_CRC = 0x29, ///< compute & return a CRC - PROTO_BOOT = 0x30, ///< boot the application - - // Command bytes - Rev 2 boootloader only - PROTO_CHIP_VERIFY = 0x24, ///< begin verify mode - PROTO_READ_MULTI = 0x28, ///< read bytes at programm address and increment - - INFO_BL_REV = 1, ///< bootloader protocol revision - BL_REV_MIN = 2, ///< Minimum supported bootlader protocol - BL_REV_MAX = 4, ///< Maximum supported bootloader protocol - INFO_BOARD_ID = 2, ///< board type - INFO_BOARD_REV = 3, ///< board revision - INFO_FLASH_SIZE = 4, ///< max firmware size in bytes - - PROG_MULTI_MAX = 32, ///< write size for PROTO_PROG_MULTI, must be multiple of 4 - READ_MULTI_MAX = 32 ///< read size for PROTO_READ_MULTI, must be multiple of 4 - }; - - bool _findBootloader(void); - bool _downloadFirmware(void); - bool _bootloaderVerifyRev2(QextSerialPort* port, const QString firmwareFilename); - bool _bootloaderVerifyRev3(QextSerialPort* port); - - uint32_t _boardID; ///< board id for currently connected board - uint32_t _boardFlashSize; ///< flash size for currently connected board - uint32_t _imageCRC; ///< CRC for image in currently selected firmware file - uint32_t _bootloaderVersion; ///< Bootloader version - - QString _firmwareFilename; ///< Currently selected firmware file to flash - - QString _errorString; ///< Last error - - static const int _eraseTimeout = 20000; ///< Msecs to wait for response from erase command - static const int _rebootTimeout = 10000; ///< Msecs to wait for reboot command to cause serial port to disconnect - static const int _verifyTimeout = 5000; ///< Msecs to wait for response to PROTO_GET_CRC command - static const int _readTimout = 2000; ///< Msecs to wait for read bytes to become avilable - static const int _responseTimeout = 2000; ///< Msecs to wait for command response bytes -}; - -#endif // PX4FirmwareUpgrade_H diff --git a/src/VehicleSetup/PX4FirmwareUpgradeThread.cc b/src/VehicleSetup/PX4FirmwareUpgradeThread.cc index 642c115db..4513105db 100644 --- a/src/VehicleSetup/PX4FirmwareUpgradeThread.cc +++ b/src/VehicleSetup/PX4FirmwareUpgradeThread.cc @@ -26,20 +26,30 @@ /// @author Don Gagne #include "PX4FirmwareUpgradeThread.h" -#include "PX4Bootloader.h" +#include "Bootloader.h" +#include "QGCLoggingCategory.h" +#include "QGC.h" #include #include #include +#include -PX4FirmwareUpgradeThreadWorker::PX4FirmwareUpgradeThreadWorker(QObject* parent) : - QObject(parent), +PX4FirmwareUpgradeThreadWorker::PX4FirmwareUpgradeThreadWorker(PX4FirmwareUpgradeThreadController* controller) : + _controller(controller), _bootloader(NULL), _bootloaderPort(NULL), - _timerTimeout(NULL), - _timerRetry(NULL) + _timerRetry(NULL), + _foundBoard(false), + _findBoardFirstAttempt(true) { + Q_ASSERT(_controller); + connect(_controller, &PX4FirmwareUpgradeThreadController::_initThreadWorker, this, &PX4FirmwareUpgradeThreadWorker::_init); + connect(_controller, &PX4FirmwareUpgradeThreadController::_startFindBoardLoopOnThread, this, &PX4FirmwareUpgradeThreadWorker::_startFindBoardLoop); + connect(_controller, &PX4FirmwareUpgradeThreadController::_flashOnThread, this, &PX4FirmwareUpgradeThreadWorker::_flash); + connect(_controller, &PX4FirmwareUpgradeThreadController::_rebootOnThread, this, &PX4FirmwareUpgradeThreadWorker::_reboot); + connect(_controller, &PX4FirmwareUpgradeThreadController::_cancel, this, &PX4FirmwareUpgradeThreadWorker::_cancel); } PX4FirmwareUpgradeThreadWorker::~PX4FirmwareUpgradeThreadWorker() @@ -52,77 +62,112 @@ PX4FirmwareUpgradeThreadWorker::~PX4FirmwareUpgradeThreadWorker() /// @brief Initializes the PX4FirmwareUpgradeThreadWorker with the various child objects which must be created /// on the worker thread. -void PX4FirmwareUpgradeThreadWorker::init(void) +void PX4FirmwareUpgradeThreadWorker::_init(void) { // We create the timers here so that they are on the right thread - Q_ASSERT(_timerTimeout == NULL); - _timerTimeout = new QTimer(this); - Q_CHECK_PTR(_timerTimeout); - connect(_timerTimeout, &QTimer::timeout, this, &PX4FirmwareUpgradeThreadWorker::timeout); - _timerTimeout->setSingleShot(true); - Q_ASSERT(_timerRetry == NULL); _timerRetry = new QTimer(this); Q_CHECK_PTR(_timerRetry); _timerRetry->setSingleShot(true); _timerRetry->setInterval(_retryTimeout); + connect(_timerRetry, &QTimer::timeout, this, &PX4FirmwareUpgradeThreadWorker::_findBoardOnce); Q_ASSERT(_bootloader == NULL); - _bootloader = new PX4Bootloader(this); - connect(_bootloader, &PX4Bootloader::updateProgramProgress, this, &PX4FirmwareUpgradeThreadWorker::_updateProgramProgress); + _bootloader = new Bootloader(this); + connect(_bootloader, &Bootloader::updateProgress, this, &PX4FirmwareUpgradeThreadWorker::_updateProgress); } -void PX4FirmwareUpgradeThreadWorker::findBoard(int msecTimeout) +void PX4FirmwareUpgradeThreadWorker::_cancel(void) { + if (_bootloaderPort) { + _bootloaderPort->close(); + _bootloaderPort->deleteLater(); + _bootloaderPort = NULL; + } +} + +void PX4FirmwareUpgradeThreadWorker::_startFindBoardLoop(void) +{ + _foundBoard = false; _findBoardFirstAttempt = true; - connect(_timerRetry, &QTimer::timeout, this, &PX4FirmwareUpgradeThreadWorker::_findBoardOnce); - _timerTimeout->start(msecTimeout); - _elapsed.start(); _findBoardOnce(); } void PX4FirmwareUpgradeThreadWorker::_findBoardOnce(void) { - qDebug() << "_findBoardOnce"; + qCDebug(FirmwareUpgradeLog) << "_findBoardOnce"; - QString portName; - QString portDescription; + QSerialPortInfo portInfo; + PX4FirmwareUpgradeFoundBoardType_t boardType; - foreach (QSerialPortInfo info, QSerialPortInfo::availablePorts()) { - if (!info.portName().isEmpty() && (info.description().contains("PX4") || info.vendorIdentifier() == 9900 /* 3DR */)) { - - qDebug() << "Found Board:"; - qDebug() << "\tport name:" << info.portName(); - qDebug() << "\tdescription:" << info.description(); - qDebug() << "\tsystem location:" << info.systemLocation(); - qDebug() << "\tvendor ID:" << info.vendorIdentifier(); - qDebug() << "\tproduct ID:" << info.productIdentifier(); - - portName = info.systemLocation(); - portDescription = info.description(); - - _closeFind(); - emit foundBoard(_findBoardFirstAttempt, portName, portDescription); - return; + if (_findBoardFromPorts(portInfo, boardType)) { + if (!_foundBoard) { + _foundBoard = true; + _foundBoardPortInfo = portInfo; + emit foundBoard(_findBoardFirstAttempt, portInfo, boardType); + if (!_findBoardFirstAttempt) { + if (boardType == FoundBoard3drRadio) { + _3drRadioForceBootloader(portInfo); + return; + } else { + _findBootloader(portInfo, false /* radio mode */, true /* errorOnNotFound */); + return; + } + } + } + } else { + if (_foundBoard) { + _foundBoard = false; + qCDebug(FirmwareUpgradeLog) << "Board gone"; + emit boardGone(); + } else if (_findBoardFirstAttempt) { + emit noBoardFound(); } } _findBoardFirstAttempt = false; - - emit updateProgress(_elapsed.elapsed(), _timerTimeout->interval()); _timerRetry->start(); } -bool PX4FirmwareUpgradeThreadController::pluggedInBoard(void) +bool PX4FirmwareUpgradeThreadWorker::_findBoardFromPorts(QSerialPortInfo& portInfo, PX4FirmwareUpgradeFoundBoardType_t& type) { - qDebug() << "pluggedInBoard"; - - QString portName; - QString portDescription; + bool found = false; foreach (QSerialPortInfo info, QSerialPortInfo::availablePorts()) { - if (!info.portName().isEmpty() && (info.description().contains("PX4") || info.vendorIdentifier() == 9900 /* 3DR */)) { +#if 0 + qCDebug(FirmwareUpgradeLog) << "Serial Port --------------"; + qCDebug(FirmwareUpgradeLog) << "\tport name:" << info.portName(); + qCDebug(FirmwareUpgradeLog) << "\tdescription:" << info.description(); + qCDebug(FirmwareUpgradeLog) << "\tsystem location:" << info.systemLocation(); + qCDebug(FirmwareUpgradeLog) << "\tvendor ID:" << info.vendorIdentifier(); + qCDebug(FirmwareUpgradeLog) << "\tproduct ID:" << info.productIdentifier(); +#endif + + if (!info.portName().isEmpty()) { + if (info.vendorIdentifier() == _px4VendorId) { + if (info.productIdentifier() == _pixhawkFMUV2ProductId) { + qCDebug(FirmwareUpgradeLog) << "Found PX4 FMU V2"; + type = FoundBoardPX4FMUV2; + found = true; + } else if (info.productIdentifier() == _pixhawkFMUV1ProductId) { + qCDebug(FirmwareUpgradeLog) << "Found PX4 FMU V1"; + type = FoundBoardPX4FMUV2; + found = true; + } else if (info.productIdentifier() == _flowProductId) { + qCDebug(FirmwareUpgradeLog) << "Found PX4 Flow"; + type = FoundBoardPX4Flow; + found = true; + } + } else if (info.vendorIdentifier() == _3drRadioVendorId && info.productIdentifier() == _3drRadioProductId) { + qCDebug(FirmwareUpgradeLog) << "Found 3DR Radio"; + type = FoundBoard3drRadio; + found = true; + } + } + + if (found) { + portInfo = info; return true; } } @@ -130,144 +175,197 @@ bool PX4FirmwareUpgradeThreadController::pluggedInBoard(void) return false; } -void PX4FirmwareUpgradeThreadWorker::findBootloader(const QString portName, int msecTimeout) +void PX4FirmwareUpgradeThreadWorker::_3drRadioForceBootloader(const QSerialPortInfo& portInfo) { - Q_UNUSED(msecTimeout); + // First make sure we can't get the bootloader + + if (_findBootloader(portInfo, true /* radio Mode */, false /* errorOnNotFound */)) { + return; + } + + // Couldn't find the bootloader. We'll need to reboot the radio into bootloader. + + QSerialPort port(portInfo); - // Once the port shows up, we only try to connect to the bootloader a single time - _portName = portName; - _findBootloaderOnce(); + port.setBaudRate(QSerialPort::Baud57600); + + emit status("Putting radio into command mode"); + + // Wait a little while for the USB port to initialize. 3DR Radio boot is really slow. + for (int i=0; i<12; i++) { + if (port.open(QIODevice::ReadWrite)) { + break; + } else { + QGC::SLEEP::msleep(250); + } + } + + if (!port.isOpen()) { + emit error(QString("Unable to open port: %1 error: %2").arg(portInfo.systemLocation()).arg(port.errorString())); + return; + } + + // Put radio into command mode + port.write("+++", 3); + if (!port.waitForReadyRead(1500)) { + emit error("Unable to put radio into command mode"); + return; + } + QByteArray bytes = port.readAll(); + if (!bytes.contains("OK")) { + emit error("Unable to put radio into command mode"); + return; + } + + emit status("Rebooting radio to bootloader"); + + port.write("AT&UPDATE\r\n"); + if (!port.waitForBytesWritten(1500)) { + emit error("Unable to reboot radio"); + return; + } + QGC::SLEEP::msleep(2000); + port.close(); + + // The bootloader should be waiting for us now + + _findBootloader(portInfo, true /* radio mode */, true /* errorOnNotFound */); } -void PX4FirmwareUpgradeThreadWorker::_findBootloaderOnce(void) +bool PX4FirmwareUpgradeThreadWorker::_findBootloader(const QSerialPortInfo& portInfo, bool radioMode, bool errorOnNotFound) { - qDebug() << "_findBootloaderOnce"; + qCDebug(FirmwareUpgradeLog) << "_findBootloader"; - uint32_t bootloaderVersion, boardID, flashSize; + uint32_t bootloaderVersion = 0; + uint32_t boardID; + uint32_t flashSize = 0; + _bootloaderPort = new QextSerialPort(QextSerialPort::Polling); Q_CHECK_PTR(_bootloaderPort); - if (_bootloader->open(_bootloaderPort, _portName)) { - if (_bootloader->sync(_bootloaderPort)) { - if (_bootloader->getBoardInfo(_bootloaderPort, bootloaderVersion, boardID, flashSize)) { - _closeFind(); - qDebug() << "Found bootloader"; - emit foundBootloader(bootloaderVersion, boardID, flashSize); - return; - } + // Wait a little while for the USB port to initialize. + for (int i=0; i<10; i++) { + if (_bootloader->open(_bootloaderPort, portInfo.systemLocation())) { + break; + } else { + QGC::SLEEP::msleep(100); + } + } + + QGC::SLEEP::msleep(2000); + + if (_bootloader->sync(_bootloaderPort)) { + bool success; + + if (radioMode) { + success = _bootloader->get3DRRadioBoardId(_bootloaderPort, boardID); + } else { + success = _bootloader->getPX4BoardInfo(_bootloaderPort, bootloaderVersion, boardID, flashSize); + } + if (success) { + qCDebug(FirmwareUpgradeLog) << "Found bootloader"; + emit foundBootloader(bootloaderVersion, boardID, flashSize); + return true; } } - _closeFind(); _bootloaderPort->close(); _bootloaderPort->deleteLater(); _bootloaderPort = NULL; - qDebug() << "Bootloader error:" << _bootloader->errorString(); - emit error(commandBootloader, _bootloader->errorString()); -} - -void PX4FirmwareUpgradeThreadWorker::_closeFind(void) -{ - emit updateProgress(100, 100); - disconnect(_timerRetry, SIGNAL(timeout()), 0, 0); - _timerRetry->stop(); - _timerTimeout->stop(); -} - -void PX4FirmwareUpgradeThreadWorker::cancelFind(void) -{ - _closeFind(); - emit complete(commandCancel); -} - -void PX4FirmwareUpgradeThreadWorker::timeout(void) -{ - qDebug() << "Find timeout"; - _closeFind(); - emit findTimeout(); + qCDebug(FirmwareUpgradeLog) << "Bootloader error:" << _bootloader->errorString(); + if (errorOnNotFound) { + emit error(_bootloader->errorString()); + } + + return false; } -void PX4FirmwareUpgradeThreadWorker::sendBootloaderReboot(void) +void PX4FirmwareUpgradeThreadWorker::_reboot(void) { if (_bootloaderPort) { if (_bootloaderPort->isOpen()) { - _bootloader->sendBootloaderReboot(_bootloaderPort); + _bootloader->reboot(_bootloaderPort); } _bootloaderPort->deleteLater(); _bootloaderPort = NULL; } } -void PX4FirmwareUpgradeThreadWorker::program(const QString firmwareFilename) -{ - qDebug() << "Program"; - if (!_bootloader->program(_bootloaderPort, firmwareFilename)) { - _bootloaderPort->deleteLater(); - _bootloaderPort = NULL; - qDebug() << "Program failed:" << _bootloader->errorString(); - emit error(commandProgram, _bootloader->errorString()); - } else { - qDebug() << "Program complete"; - emit complete(commandProgram); - } -} - -void PX4FirmwareUpgradeThreadWorker::verify(const QString firmwareFilename) +void PX4FirmwareUpgradeThreadWorker::_flash(void) { - qDebug() << "Verify"; - if (!_bootloader->verify(_bootloaderPort, firmwareFilename)) { - qDebug() << "Verify failed:" << _bootloader->errorString(); - emit error(commandVerify, _bootloader->errorString()); - } else { - qDebug() << "Verify complete"; - emit complete(commandVerify); + qCDebug(FirmwareUpgradeLog) << "PX4FirmwareUpgradeThreadWorker::_flash"; + + if (_erase()) { + emit status("Programming new version..."); + + if (_bootloader->program(_bootloaderPort, _controller->image())) { + qCDebug(FirmwareUpgradeLog) << "Program complete"; + emit status("Program complete"); + } else { + _bootloaderPort->deleteLater(); + _bootloaderPort = NULL; + qCDebug(FirmwareUpgradeLog) << "Program failed:" << _bootloader->errorString(); + emit error(_bootloader->errorString()); + } + + emit status("Verifying program..."); + + if (_bootloader->verify(_bootloaderPort, _controller->image())) { + qCDebug(FirmwareUpgradeLog) << "Verify complete"; + emit status("Verify complete"); + } else { + qCDebug(FirmwareUpgradeLog) << "Verify failed:" << _bootloader->errorString(); + emit error(_bootloader->errorString()); + } } - _bootloaderPort->deleteLater(); - _bootloaderPort = NULL; + + emit _reboot(); + + emit flashComplete(); } -void PX4FirmwareUpgradeThreadWorker::erase(void) +bool PX4FirmwareUpgradeThreadWorker::_erase(void) { - qDebug() << "Erase"; - if (!_bootloader->erase(_bootloaderPort)) { - _bootloaderPort->deleteLater(); - _bootloaderPort = NULL; - qDebug() << "Erase failed:" << _bootloader->errorString(); - emit error(commandErase, _bootloader->errorString()); + qCDebug(FirmwareUpgradeLog) << "PX4FirmwareUpgradeThreadWorker::_erase"; + + emit eraseStarted(); + emit status("Erasing previous program..."); + + if (_bootloader->erase(_bootloaderPort)) { + qCDebug(FirmwareUpgradeLog) << "Erase complete"; + emit status("Erase complete"); + emit eraseComplete(); + return true; } else { - qDebug() << "Erase complete"; - emit complete(commandErase); + qCDebug(FirmwareUpgradeLog) << "Erase failed:" << _bootloader->errorString(); + emit error(_bootloader->errorString()); + return false; } } PX4FirmwareUpgradeThreadController::PX4FirmwareUpgradeThreadController(QObject* parent) : QObject(parent) { - _worker = new PX4FirmwareUpgradeThreadWorker(); + _worker = new PX4FirmwareUpgradeThreadWorker(this); Q_CHECK_PTR(_worker); _workerThread = new QThread(this); Q_CHECK_PTR(_workerThread); _worker->moveToThread(_workerThread); - connect(_worker, &PX4FirmwareUpgradeThreadWorker::foundBoard, this, &PX4FirmwareUpgradeThreadController::_foundBoard); - connect(_worker, &PX4FirmwareUpgradeThreadWorker::foundBootloader, this, &PX4FirmwareUpgradeThreadController::_foundBootloader); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::updateProgress, this, &PX4FirmwareUpgradeThreadController::_updateProgress); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::foundBoard, this, &PX4FirmwareUpgradeThreadController::_foundBoard); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::noBoardFound, this, &PX4FirmwareUpgradeThreadController::_noBoardFound); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::boardGone, this, &PX4FirmwareUpgradeThreadController::_boardGone); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::foundBootloader, this, &PX4FirmwareUpgradeThreadController::_foundBootloader); connect(_worker, &PX4FirmwareUpgradeThreadWorker::bootloaderSyncFailed, this, &PX4FirmwareUpgradeThreadController::_bootloaderSyncFailed); - connect(_worker, &PX4FirmwareUpgradeThreadWorker::error, this, &PX4FirmwareUpgradeThreadController::_error); - connect(_worker, &PX4FirmwareUpgradeThreadWorker::complete, this, &PX4FirmwareUpgradeThreadController::_complete); - connect(_worker, &PX4FirmwareUpgradeThreadWorker::findTimeout, this, &PX4FirmwareUpgradeThreadController::_findTimeout); - connect(_worker, &PX4FirmwareUpgradeThreadWorker::updateProgress, this, &PX4FirmwareUpgradeThreadController::_updateProgress); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::error, this, &PX4FirmwareUpgradeThreadController::_error); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::status, this, &PX4FirmwareUpgradeThreadController::_status); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::eraseStarted, this, &PX4FirmwareUpgradeThreadController::_eraseStarted); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::eraseComplete, this, &PX4FirmwareUpgradeThreadController::_eraseComplete); + connect(_worker, &PX4FirmwareUpgradeThreadWorker::flashComplete, this, &PX4FirmwareUpgradeThreadController::_flashComplete); - connect(this, &PX4FirmwareUpgradeThreadController::_initThreadWorker, _worker, &PX4FirmwareUpgradeThreadWorker::init); - connect(this, &PX4FirmwareUpgradeThreadController::_findBoardOnThread, _worker, &PX4FirmwareUpgradeThreadWorker::findBoard); - connect(this, &PX4FirmwareUpgradeThreadController::_findBootloaderOnThread, _worker, &PX4FirmwareUpgradeThreadWorker::findBootloader); - connect(this, &PX4FirmwareUpgradeThreadController::_sendBootloaderRebootOnThread, _worker, &PX4FirmwareUpgradeThreadWorker::sendBootloaderReboot); - connect(this, &PX4FirmwareUpgradeThreadController::_programOnThread, _worker, &PX4FirmwareUpgradeThreadWorker::program); - connect(this, &PX4FirmwareUpgradeThreadController::_verifyOnThread, _worker, &PX4FirmwareUpgradeThreadWorker::verify); - connect(this, &PX4FirmwareUpgradeThreadController::_eraseOnThread, _worker, &PX4FirmwareUpgradeThreadWorker::erase); - connect(this, &PX4FirmwareUpgradeThreadController::_cancelFindOnThread, _worker, &PX4FirmwareUpgradeThreadWorker::cancelFind); - _workerThread->start(); emit _initThreadWorker(); @@ -277,36 +375,24 @@ PX4FirmwareUpgradeThreadController::~PX4FirmwareUpgradeThreadController() { _workerThread->quit(); _workerThread->wait(); + + delete _workerThread; } -void PX4FirmwareUpgradeThreadController::findBoard(int msecTimeout) -{ - qDebug() << "PX4FirmwareUpgradeThreadController::findBoard"; - emit _findBoardOnThread(msecTimeout); -} - -void PX4FirmwareUpgradeThreadController::findBootloader(const QString& portName, int msecTimeout) -{ - qDebug() << "PX4FirmwareUpgradeThreadController::findBootloader"; - emit _findBootloaderOnThread(portName, msecTimeout); -} - -void PX4FirmwareUpgradeThreadController::_foundBoard(bool firstTry, const QString portName, QString portDescription) +void PX4FirmwareUpgradeThreadController::startFindBoardLoop(void) { - emit foundBoard(firstTry, portName, portDescription); + qCDebug(FirmwareUpgradeLog) << "PX4FirmwareUpgradeThreadController::findBoard"; + emit _startFindBoardLoopOnThread(); } -void PX4FirmwareUpgradeThreadController::_foundBootloader(int bootloaderVersion, int boardID, int flashSize) +void PX4FirmwareUpgradeThreadController::cancel(void) { - emit foundBootloader(bootloaderVersion, boardID, flashSize); -} - -void PX4FirmwareUpgradeThreadController::_bootloaderSyncFailed(void) -{ - emit bootloaderSyncFailed(); + qCDebug(FirmwareUpgradeLog) << "PX4FirmwareUpgradeThreadController::cancel"; + emit _cancel(); } -void PX4FirmwareUpgradeThreadController::_findTimeout(void) +void PX4FirmwareUpgradeThreadController::flash(const FirmwareImage* image) { - emit findTimeout(); + _image = image; + emit _flashOnThread(); } diff --git a/src/VehicleSetup/PX4FirmwareUpgradeThread.h b/src/VehicleSetup/PX4FirmwareUpgradeThread.h index ff6723594..afbe5990d 100644 --- a/src/VehicleSetup/PX4FirmwareUpgradeThread.h +++ b/src/VehicleSetup/PX4FirmwareUpgradeThread.h @@ -28,16 +28,27 @@ #ifndef PX4FirmwareUpgradeThread_H #define PX4FirmwareUpgradeThread_H +#include "Bootloader.h" +#include "FirmwareImage.h" + #include #include #include #include +#include #include "qextserialport.h" #include -#include "PX4Bootloader.h" +typedef enum { + FoundBoardPX4FMUV1, + FoundBoardPX4FMUV2, + FoundBoardPX4Flow, + FoundBoard3drRadio +} PX4FirmwareUpgradeFoundBoardType_t; + +class PX4FirmwareUpgradeThreadController; /// @brief Used to run bootloader commands on a seperate thread. These routines are mainly meant to to be called /// internally by the PX4FirmwareUpgradeThreadController. Clients should call the various public methods @@ -47,52 +58,59 @@ class PX4FirmwareUpgradeThreadWorker : public QObject Q_OBJECT public: - PX4FirmwareUpgradeThreadWorker(QObject* parent = NULL); + PX4FirmwareUpgradeThreadWorker(PX4FirmwareUpgradeThreadController* controller); ~PX4FirmwareUpgradeThreadWorker(); - enum { - commandBootloader, - commandProgram, - commandVerify, - commandErase, - commandCancel - }; - -public slots: - void init(void); - void findBoard(int msecTimeout); - void findBootloader(const QString portName, int msecTimeout); - void timeout(void); - void cancelFind(void); - void sendBootloaderReboot(void); - void program(const QString firmwareFilename); - void verify(const QString firmwareFilename); - void erase(void); - signals: - void foundBoard(bool firstTry, const QString portname, QString portDescription); + void updateProgress(int curr, int total); + void foundBoard(bool firstAttempt, const QSerialPortInfo& portInfo, int type); + void noBoardFound(void); + void boardGone(void); void foundBootloader(int bootloaderVersion, int boardID, int flashSize); void bootloaderSyncFailed(void); - void error(const int command, const QString errorString); - void complete(const int command); - void findTimeout(void); - void updateProgress(int curr, int total); + void error(const QString& errorString); + void status(const QString& statusText); + void eraseStarted(void); + void eraseComplete(void); + void flashComplete(void); private slots: + void _init(void); + void _startFindBoardLoop(void); + void _reboot(void); + void _flash(void); void _findBoardOnce(void); - void _findBootloaderOnce(void); - void _updateProgramProgress(int curr, int total) { emit updateProgress(curr, total); } - void _closeFind(void); + void _updateProgress(int curr, int total) { emit updateProgress(curr, total); } + void _cancel(void); private: - PX4Bootloader* _bootloader; + bool _findBoardFromPorts(QSerialPortInfo& portInfo, PX4FirmwareUpgradeFoundBoardType_t& type); + bool _findBootloader(const QSerialPortInfo& portInfo, bool radioMode, bool errorOnNotFound); + void _3drRadioForceBootloader(const QSerialPortInfo& portInfo); + bool _erase(void); + + PX4FirmwareUpgradeThreadController* _controller; + + Bootloader* _bootloader; QextSerialPort* _bootloaderPort; - QTimer* _timerTimeout; QTimer* _timerRetry; QTime _elapsed; - QString _portName; static const int _retryTimeout = 1000; - bool _findBoardFirstAttempt; + + bool _foundBoard; ///< true: board is currently connected + bool _findBoardFirstAttempt; ///< true: this is our first try looking for a board + QSerialPortInfo _foundBoardPortInfo; ///< port info for found board + + // Serial port info for supported devices + + static const int _px4VendorId = 9900; + + static const int _pixhawkFMUV2ProductId = 17; + static const int _pixhawkFMUV1ProductId = 16; + static const int _flowProductId = 21; + + static const int _3drRadioVendorId = 1027; + static const int _3drRadioProductId = 24597; }; /// @brief Provides methods to interact with the bootloader. The commands themselves are signalled @@ -105,82 +123,74 @@ public: PX4FirmwareUpgradeThreadController(QObject* parent = NULL); ~PX4FirmwareUpgradeThreadController(void); - /// Returns true is a board is currently connected via USB - bool pluggedInBoard(void); - - /// @brief Begins the process of searching for a PX4 board connected to any serial port. - /// @param msecTimeout Numbers of msecs to continue looking for a board to become available. - void findBoard(int msecTimeout); - - /// @brief Begins the process of attempting to communicate with the bootloader on the specified port. - /// @param portName Name of port to attempt a bootloader connection on. - /// @param msecTimeout Number of msecs to continue to wait for a bootloader to appear on the port. - void findBootloader(const QString& portName, int msecTimeout); + /// @brief Begins the process of searching for a supported board connected to any serial port. This will + /// continue until cancelFind is called. Signals foundBoard and boardGone as boards come and go. + void startFindBoardLoop(void); - /// @brief Cancel an in progress findBoard or FindBootloader - void cancelFind(void) { emit _cancelFindOnThread(); } + void cancel(void); /// @brief Sends a reboot command to the bootloader - void sendBootloaderReboot(void) { emit _sendBootloaderRebootOnThread(); } + void reboot(void) { emit _rebootOnThread(); } - /// @brief Flash the specified firmware onto the board - void program(const QString firmwareFilename) { emit _programOnThread(firmwareFilename); } + void flash(const FirmwareImage* image); - /// @brief Verify the board flash with respect to the specified firmware image - void verify(const QString firmwareFilename) { emit _verifyOnThread(firmwareFilename); } + const FirmwareImage* image(void) { return _image; } - /// @brief Send and erase command to the bootloader - void erase(void) { emit _eraseOnThread(); } - signals: - /// @brief Emitted by the findBoard process when it finds the board. - /// @param firstTry true: board found on first attempt - /// @param portName Port that board is on - /// @param portDescription User friendly port description - void foundBoard(bool firstTry, const QString portname, QString portDescription); + /// @brief Emitted by the find board process when it finds a board. + void foundBoard(bool firstAttempt, const QSerialPortInfo &portInfo, int type); + + void noBoardFound(void); + + /// @brief Emitted by the find board process when a board it previously reported as found disappears. + void boardGone(void); /// @brief Emitted by the findBootloader process when has a connection to the bootloader void foundBootloader(int bootloaderVersion, int boardID, int flashSize); /// @brief Emitted by the bootloader commands when an error occurs. - /// @param errorCommand Command which caused the error, using PX4FirmwareUpgradeThreadWorker command* enum values - void error(const int errorCommand, const QString errorString); + void error(const QString& errorString); + + void status(const QString& status); /// @brief Signalled when the findBootloader process connects to the port, but cannot sync to the /// bootloader. void bootloaderSyncFailed(void); - /// @brief Signalled when the findBoard or findBootloader process times out before success - void findTimeout(void); + void eraseStarted(void); + void eraseComplete(void); - /// @brief Signalled by the bootloader commands other than find* that they have complete successfully. - /// @param command Command which completed, using PX4FirmwareUpgradeThreadWorker command* enum values - void complete(const int command); + void flashComplete(void); /// @brief Signalled to update progress for long running bootloader commands void updateProgress(int curr, int total); + // Internal signals to communicate with thread worker void _initThreadWorker(void); - void _findBoardOnThread(int msecTimeout); - void _findBootloaderOnThread(const QString& portName, int msecTimeout); - void _sendBootloaderRebootOnThread(void); - void _programOnThread(const QString firmwareFilename); - void _verifyOnThread(const QString firmwareFilename); - void _eraseOnThread(void); - void _cancelFindOnThread(void); + void _startFindBoardLoopOnThread(void); + void _rebootOnThread(void); + void _flashOnThread(void); + void _cancel(void); private slots: - void _foundBoard(bool firstTry, const QString portname, QString portDescription); - void _foundBootloader(int bootloaderVersion, int boardID, int flashSize); - void _bootloaderSyncFailed(void); - void _error(const int errorCommand, const QString errorString) { emit error(errorCommand, errorString); } - void _complete(const int command) { emit complete(command); } - void _findTimeout(void); - void _updateProgress(int curr, int total) { emit updateProgress(curr, total); } + void _foundBoard(bool firstAttempt, const QSerialPortInfo& portInfo, int type) { emit foundBoard(firstAttempt, portInfo, type); } + void _noBoardFound(void) { emit noBoardFound(); } + void _boardGone(void) { emit boardGone(); } + void _foundBootloader(int bootloaderVersion, int boardID, int flashSize) { emit foundBootloader(bootloaderVersion, boardID, flashSize); } + void _bootloaderSyncFailed(void) { emit bootloaderSyncFailed(); } + void _error(const QString& errorString) { emit error(errorString); } + void _status(const QString& statusText) { emit status(statusText); } + void _eraseStarted(void) { emit eraseStarted(); } + void _eraseComplete(void) { emit eraseComplete(); } + void _flashComplete(void) { emit flashComplete(); } private: + void _updateProgress(int curr, int total) { emit updateProgress(curr, total); } + PX4FirmwareUpgradeThreadWorker* _worker; QThread* _workerThread; ///< Thread which PX4FirmwareUpgradeThreadWorker runs on + + const FirmwareImage* _image; }; #endif diff --git a/src/main.cc b/src/main.cc index 585a46c7e..8c097575c 100644 --- a/src/main.cc +++ b/src/main.cc @@ -30,6 +30,7 @@ This file is part of the QGROUNDCONTROL project #include #include +#include #include "QGCApplication.h" #include "MainWindow.h" @@ -49,6 +50,7 @@ This file is part of the QGROUNDCONTROL project #undef main #endif +Q_DECLARE_METATYPE(QSerialPortInfo) #ifdef Q_OS_WIN @@ -107,6 +109,8 @@ int main(int argc, char *argv[]) qRegisterMetaType(); #endif qRegisterMetaType(); + qRegisterMetaType(); + // We statically link to the google QtLocation plugin #ifdef Q_OS_WIN -- 2.22.0