From 23688feb38c57a5a70dea44fe07e20ff21b518e8 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Thu, 13 Feb 2020 17:30:43 -0800 Subject: [PATCH 01/16] Improved driver monitoring model --- models/dmonitoring_model_q.dlc | Bin 186615 -> 186615 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/models/dmonitoring_model_q.dlc b/models/dmonitoring_model_q.dlc index 0aee22a47c593371ae5539b3bdf3e5caeeadaad4..59104e18e865ffdfb9b4fb2a421e29560a23035a 100644 GIT binary patch literal 186615 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 z
    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

    Cf-&D1)%pW~Osl=>B4vm80#FJKr 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

    mbax*v-=ACW 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 }*|!x} zzj32>t(J7MyS`Q*4#$(sm@wX|*RLn;@6>(oK`?1oMt$p56%|X0X{F}K*Th78H>yuZm>qLz|+=y5z$eR$jttBMj7A`=J0v&yK2>C7kQ{zq`jGU`97HQu!K#5`HPMMskDmkc%?Y?$KCO;XpLIR&C&xPBEqHEQgM^~%`l^bx@Y^1>0CppDuJ<*mTwn}#ng+26gSRmal^(L zcB+o2s3VOZncugqz)fd|tvJ#2#fl%zHpZhQv@@epGErDH%vF`?Qrb}=y3L^U+uNfb|U98XiMoaa`Y%)v2HaLH@Ra1^`v zaUOjyhQ*+u$3e;SV&A9)DF&uO_(tb~$V;rWRtgEe1q65;WYx=@V#%%+N`6_v8?@pe z^PvS=sgh5Il2TA9FppjtLt^>3Bf9TrMYE6}Nr=ZOK`pBIp_LJ@%NLxQ?bxNfGZa}v zksX_P0zPn3)l_UP*6gB&W!4p9cb;9!6itv$uidniARvqvn7Ht{svVWGa%5{sP%si# zb%(4n?uqN!TIqJI1q9ksCn;w`hLxjFc2X)i#airE3i%Qqfnr%f(R53if2$nZNu{25 zssR3Ny_N&4l_*pzzEa2XmXoj!i>qb072%!o^o?>bk*tTcdNyh#X8E>WCbXwSdl7`? zA6Vhf0>ddK<(^)28OqoPPt$~Ei@~2H@*fZA9XxOGd=58O|4fgvsMTR zYx=$tC1c|RDy?hQ)TR~&^Mvw}Sud-kQO#%O ziiM~=t=z5}sTL9f(durFnK#PHh+QpuB?G9>W4*(wR!22&-=wz%&sA)KE;o%B_E%Fi z?yskDMT^|j^t9my+EqWk$_1Rl--$S9imEO7K} z#K$2D^isp97B!-yc5yhcUFUXeR0-17{U9RjlflOU9ABQFAL3 z6OlI(I{9%oOYQQv9NS$CiGT%23AV*bV!|y!dd)Mk+l7j)l(QPHhZkcn6IUW(3DnT12LNVer3S*^ONUPCEF-!HTnlsFiQ1vmxrV+sivLb?9iHbGN$&8X^`1!J` z+a;53c)l`JDhF;!r){3$JE;v_L3;H{)d*}Gk_jNI6{C``2a1=)6(#2vtHp|wFDXVg z1fNsVn$A?@)7S+rL zZj!TKbKWFmXQV+jGV@rhvKL!MP;Zn?N3$E{iMLA?HPfwJ&Tn{ulT|eD+C&(5ejG-A z&G<$#WsEy`ww1KB<*K&(AIvMhR+}NV?dR<2>bIOm;@Tg4-$|=?Za7Ng`;ldP!QWL& zRZst>GU`lLv#Dx6s+C>h5vqY?%~jHT%?xXW^!~3o*KbXw72>b|ms4`xp_uq>kPlaG z2KM&N>yDED27#!2saV_~Oqp*Kf-ov1E&;|jhpgAEWZu)=ujcL)ZJU^|6+vs%s@52B ztuXRDd{8@d_0d{*^A*K0N25l`C)!>6#*LhpDaoimtdSgwYvHxohCQ{SsEiQ;S#fl2 z#16fCiKE4~k1Aw3M-lLM=D$5+Seamg42UUmh}vY#L;)Dz;uDs274mS|u!j zz@S{HlerspHPW^P@(IrE}bxV>Oa<97u)L#f`5 zr|nX3XPW1knz0!vdAnN9lG|D7yUi}Kj4JyLSaljDmLC-h<(2&)N_^FG+;ZN7G>sg-HLR3p(pyDOOWlG3-b~x0 z(RXg$z&|jGyNIM$oJol)7}e5LJ}4J{@V85rR=#J{6`ZK=r1yX?%haq&=}xJfey3vR zN~Q1Iz7a;obn!dOH-HH%EB~sNyir*laf!#cZ+|yT^OZ{GMCLzzTlxAOx3CxfD48T{ z<>p<>T{eElOsZ-q9`S2 ze6nob8yl%qij7-82-MVQSdM!)m#$V`5g8iF6R36V#JiEzl%;hKDm<1$c{R1@+EB5? zaEh^8Bx_Yo3toCpeNd_CH!I_}Ow${x-JefgwXQ_YO-DCp^25%#RV&mF+^+=QTb2Ug zXll-NioW-Q&`Pbf?ewjdP} z$SM$=(LtOrP$H~jScj;4S}h#aNSX~h#%zE;SM{T@IMCcT^{EmT(^n(cRY!`GGa~Cr z-ERDLLX6R=c|nzghf^Ija^>2{pj@H%zkG1^*~h>9>`R~f#XtPI)M|X{>K9)A^oyT) z=`%0>)>n-47eD#h&fov$@6CDBVyI=c< zzx}tC?|Cb=ty^D>_U-;Jg)0M&f|6b{r`=9;S|MKsA@pJ$BUv5e{ z1a%gb*Z;EG;aC4^wZp5Q|4;t^svUmy-;`ebm1>7;-~Ms`vp@b9fBQ?H`|*dFR6AT= z{+nutmDhgm(W{?+bx`f_>Sw%H%hjloSa~ROT&feqfeY6&&(E@wlaAM%(MHKJV>{%x zN|NI9CM7PY%s7@mWhN72xhzS?U&#DXGU*XYlM%U#@0Kbv=^u@yljTTP!?EdftPs`H z2QeZ4%*)E-ftLpJ6XQveyh&6zCbV7!IMPvvyeWnOSJ%Rdg_hw?PR7x=GnN6h7Qw?h zTxpEBm&PjroN;ulOco3Pa%+aIPi7T;f_Smxk`+qQ&~WNu*&w)4BC1{D63k<2RqK

    a)Tr%Hmgbjftgqp%f`2X6uBPOtK!y)NE9v7zm?dGGF&?1N25_tn;fUs zBgEpiRUc(sHs!C!s?6K4>YYj815yvfNbsr9qP}E>&<5LYt)?|5H zNvl=csq#^9+?iHp2o1$itx>YaO3s+>OjgR3OphpIi6(I#hpb5m7R4PkxAy-ub|zq! z-DR2Y&OAbLfQqWAQ4C;L?D=?J2EWUNDNU(2*MqPL~`&IZcwrsNeZB{!*YzzoA(QxRPj8g`*S6Bkw{36Jbu86%laiU|1R!A7cU>Fm-h>Kvix9cd4dOD_ zJ=gbhJSJE!BF}O>MRlzM%p5jwsL4&$_wHx%dR~$^X~31bB_A}CN|%@%nw@~T>REYfIk1=#!FuWF>qe66 zY8!7c3c737R*@sNCF^3Oay$8a!}=R-pS~U?lYzL2MNmXq2ra@O0N;@fezK zP{Np1r710^eHO{K5-|-8JxOpmRoc4X3!#v?o*@hEB5vDp!J=LV$`GtQFS1)AVo%7K zx>g!$Yo(NC8&VwjaV2izQjx;gT6R@C^;u+1p{No$OCu$>3>SVNv|B5RZHMl9DynIw zMPL@oMt+r~l-ycU-y+*UF9u=Az{e@lYt#&!N#LKfQf7r2G~Z#*Zsc!LXvEq{IkvS;yVl%dI}>FQJ|orS z&6%;#dxA`1Q9+LFJ&cYS>y#ateKcO4oiHwRT|Dhfzz(GWAUjsRgs)Y1a9m^dt~pk*4YYAcXQeqJ+5Lu@9#Zl&d>saug_FuR>% zrRlo?Ry@zlgEb6+AUjdZ<)CtXEh~LjYwD34sD@p(^$MV#Y$d6lf%8{goGGqhZitw& zHnhUb6d@;euu_|Z-oOI>d3e2?I8yv}+i=mSL)EfcMFI^Hd2yIZ)Gmy>hB}~=m?*NF zf)!bCOgSe@itFDAOCt`o8X zh~6F+sv7CrE~;J?upDeX3^RuX0|Q`MWPf-~MuI{%C{@R1vU+(~5FK^L%G^kaLMKcE zCb+Xx`lTNDZc(JZ5tWK#nr>#;M4r$mjyD*^u+&9M$~D_Sytj4iGR;KOpkkoWVOIsP zKqkjo?xD*G&}@lP#IP0=WPuuH-~^AW^NLc;no^|^?j%w4B2SA0fpssFz^#qA9fvK; zGeTlFYf(W+Y`ZYrg+wm?uMK>g-}Fo)>)Z1^;}Gs5?dmnVW1O*R9&vU zbwV42p~oyE6Vsc6KP!!bbrc;-LYb6GZaPp$Z^uaXo~XK&o66WJT5g7-m|A|j)CsR@ z=31LALVioG)I)ZP@Cj&WhCSRXaL;W051xk(6xq)fnBk%kkpAdM9pUO^m>Ba!9O zZibfH5FO3T>;jvklKP(J=S*PeG8hS8mMQ^nrRIcrqS*h_{iT3Za|9;MqzFinQGz5}7V$3c%1LKXOaO zLg6lC?wlyY%R8&?=#SImL?a&U4 zKrIZ(&sqq`rJ2_0hiKrS^HPN>Q6%~mcr-U%UyMp2Y9~fwHkzsGKv5YIQ<8JYnxzVIL5Qx?GBGWNb&1EeIrcJVTlH6g)Yl@mXl7uo zVWJk3CJ%2KuIZ;`9$26R zG;SCx7oZut0UQ*S4hA&E_fyMm8@AihoIDAv$i~GTL}t0_98x`knC(Zg;H8dJ23`uR z=5vnj1YvG6C@GdvN&p`|L<1)mVy2t8XDE-f#AoC&1C%l#q!fraex765alvM3P5Yt; z`4Z8DlNWQVEO~*f2dR;^^vWv}ESO>2Y06~;P|w)VR$GLHwAJ#fhKYk9OE(PDYgeJ{ z%MA#~c4@>=l|r-O;T6qeRziY&UxZ!(w_?G=H$7B|CPJyA_<16gnaq?5m>hTwWg|~A zE6l?zA&j*YFUaz+E$c#LB~>%>%v51@hbJkrm+1)_#(y2)KA zy`q6tPY$cVC>-^!4JWab*mWLGB-yIA0w3OY!_-QmG`t4lO|GUb3NKPhoTieTMKM6D z^kq?OxJl}H1@xvMGh9KOhIjzmS=88YS}?;hMGB+dG&pRFTj?li9?LbI6s36>g{Bi$ z8ipn{*R>)tg?3yTI4lIakz(|Zn9lx*xdK-t4g*)G*j7*Co%X2`+|UdS&GX-D+xV^U zlxHwJS`=isiyROBD2PJ+v|D&$8{{qeewGADu=uFw0`f+6CauaxIpuYZ#>pcV=&r<2F(IN~&kD1&TlU9RIrOx7^e( zUDsfJvqUp5_Hh3 zf$AI5jNrPrP&HCS_dd`TUhlYuAxW(@%#tYXUXPjJU_}@taAZ+LhgC66s24G zAzn<&F1Q2(*$nW~0v0B2SUH+SWJ}yU_R+ioGprOpDwvos8`Moj6$q@vQE&l+M%BNwl3mtD8YB31gKF^KAVCp z5eD#l2d?A`5jW5Di^7CxFom?m>eIC-1}$lZo3vfED9HgKl#E!)Hlc#diuc9w(q=ml zaR&x*l;aU`bhnaa?y85I#?>9LTKhy1xR!Fl%suslqcCXLB?I@kmxkfbfPH=6C)8Aw zFx)oA&d)6FTw=%t{VQ7*pr{Q}wY^tyrxL2Gtp`FsWl07}?_lojv4mpUva- z%<;4eZ=BF}=@AHP)!I30BW&^T)8*^#7siOgzq>P}H zG!%0^5)o31mYK$(SIKQ}&9CpHlF*tULt+Ii@T@5YuI|a6)-wH4aVz}hSi#%}%doUp zDAXJlbCE{4znx98Utj}tliJwjc^jO)O{o;?W!q9yCO?0e36Yb_fE7nhYg>p*Ds;m; zYOdNPq-KdzMPcFB?!ywIg7x6InQep&U$W`M5EQzKxkXV!XQLee=X@(liHj$+3{5ij z)y(&fx#97$t|o&JIh{HvGOaC-D5L?@l2)n7m9GJ06`xQIz_Ia4kz{n=Kmo%jukG1il91A)XWxP4TSAunZFyJ9_At@U*rKis)Zj(L?J?KJIH9* zfJC#Ytzg78rwl9feTfBwi!DepFn5|Jf}*y-z6$fRQ3&`igrI6$Yck}wkrs{ z*nmP4QjwvHxK7JM`EFqX0@_KnV+6a2V7RE3IvS|~TuW@W52BVB#y)L85!JC0V?ZLl z8hjc{GJY})v}nClf6|%=>d*7M&?K`RSQL}VmKah{8T4SO#APXUB4Ru8IBK>^M;E3C zN^(SQ2m?Ke+f9UZ&C4XoE;nH3AnU4-U=UXgN_W>5R2$I70z_=mp(I%gMj2~G>I!9+ zF$y@x;S&+yz@C{^#?34;LFO9nm#h*CjIkAim*!LxqEMxoXG*37P7HK6wuHn$U2KJ% zcWRb;r7K~KS6`v#1M!GoLUiTQC1qJF8Yz5XHHz>QCrPrUzH-P4~nhYOaloiB+jk4VN+F^z9{NM zJ#q@MM*tFF_EdLH)Vef&oy| zc8IQ>rI~415f+iO8ALI~bY8Neq^4%4RLmJcmCUA6BirtWpd<1Ek~wTs*4rhuv}(H) z=?sBIJPY_A1*=C=qHDo%GV3HkNE6Hi;NcObXm(&X?|=xg&D?DpiFLw-#}8n$PQ+xF ziki`0$_>|-K8_8~XUF)bZ8E3RYS+VWM5zoB=F)_eSBAGq5x?LCE1w~c`amf_!-@b2 zlVDl{E}S5ooy5%-R@`G;S6*fzAHp<7gmT}M3fE$dLEHnL(-6NiE-z0DQj~`hrmYQI zg8ZB%Z4L6fjCCRQbO9_$r-HBAG#KxvhUq-{2HKS>kXD685v?3h0B0;~O+s1^>i6)P?C zo6Z8~Qo`Yga7!U255~>J&)}BY6n;`8ZHrMTwIiA)L|@Yl+*2y07M^}&TKYG<5)uV% zNw(oUlPz3!aQ0dWONyST3l#vc!|?^HH(Shk?fpyOlPTy#UKUpr)4HUOF)B#h0OzyX91 zQi0@rZPp#(O`&MSGbYm+%e}Q|dL`w;kicwtoOg<7uTxLsD^vXtazFDR z4vY-nswGDWy^KH;hNlX=B=rsyR=)**GgIADY2SQK8DFf95#;|Y@ zMsUaqQUUrzCP6-h5a343CJbxLHW-2^01e;_m1xHVWtSBwGQi+$;j_r4D9XbybyGEz z4OfgdX*#Gu9R4O7qEBEYAjF>Zq9~N@3Vu5b$=#?p6cV6)rmCe-yJ}YHwE@Up;5$r6 zIC5XrPWrGSP=aP^>#jSSIQL`8fnRI~3@>gO1|g^QnJ7WODtwo3%SjlPn3<`A!OoVi z+ZDIZaNC-Uf4!ARX`HtuH^n&PDI&?MsRE1*mk#QJ!dz|Zp(K$WblK61L@be#cmZFX0wP|bwLk*ixGMQg8={#W~LT4L_+o*z`=ZlC3mHr!xewb)<8j;a;%s zW$*)KrGXYeIuDEyFjy_&Z^u%i6 zS17-_t5b?o-h$y}8a7E;#tDphcxdTICi0W_5Op{w%Ac$*@!?}4xLVJ&L(~b+At*&B zZTEw|ze*db?Xk=~5DeVv*`!VZ1>U3s6PI;3Zxa1&x+;R8h2w$hDF~p6B{C^x;3gGI zpw2TDs~WU`l`$^VOr=kwpR7n;I|L=Q2p=xtNT~uS5!&c#-J#Uh(W{*xrU{ALg_eYa zg3CikuwlY5-)FLmsM5AoG&0vybvq5<5pi~HJ33sUON1v|L>2^2_$oMSLj!+;{1no9F>TwnA}B(yC$X6yiFTYjPDikXQh#wCof z-KN7^C43^Mxp`Yvs}g58y(~d3DHW-zMUdlmOUnUf39^qTh-sUS1kk0qv1A4ah>EV# zk(g})1~x6N3aP~iL3-FW%}vMKj9l7fTsP1vw;kpx;#z8~nOSgR1=}-y613`(994~; zdBZc73;?cZtta>6Q;4~tE1r@>6=@Xy)v-T951i&MmadoPOd3*L0s9o4;2wK45vmn=#(?m!Z&?86HDIu8-Ev3w=h8+l0Jjgty!fg#QErXBeU=qDTLJnL?g%bZa z!VKZkye-q5*PN#}atw#3M@GX~CHx9ISs(!)*XIw`!GrQ)7|y7ds%r4}H((jvu+ zmr_*h7MiMCCgEB^pA=k7j{m}w!+6~S2~kE>V0&z4xF`h3ZwrG;3vMrue9W)(h3b?% zkYNlUL={D_9+CEQ&23sF9uNOE@pfHz5{+G3c}j&`lXxmq&Tj=0El=86OSY91I)Z{J zsHq$q0kb>F+ggcs=4+&@vI*Cdh6RRHjuRAFEq-KRC#t*x3wBO9qyP~$`nVhHs@006 zFoD-HbwPF;B^YRpz-hw4rZP;1s?m4`<$+_rX3Te3hs zA0r;vmLe2LUlO7^0>Erok43 zJ`xb=60t=GAyi11WVs42T+n4MWlc}9t3Yd}75Talj6lv1b_y})87_Jj=@Cz;sjN&e zX+w)Kb9r!osOeN*DVHN9HQq%HqQs}9CAL9n1Pi1-1yphZiNUKcpVSmKTu=KjW+pyu z(Jp|`BjB=A@P*A|E(i%;mgA;pdDR#Myy?Ct32?mlh;J#d1D_9kV_ob!AgsUzTb=-? zz>XQth6OP~ic)cXsH9jAzY;FHIIbY^GwiC=)R7s-A?OpiT1;bEOLaQ^Qk)|}R0f%z zrXaS+R8#I1BEH<_{?dnvuQ`fT{77Y(DI)o>YC-W5B~ndEb+Vg~3L+Vv!3NR5H+}bLU00j159Sf z?|59SY%8R5E8@{3Vpd~f*W8Tghrx6!td}yA%W)sV{oom{mo1y6=b`pBAzdhG6#7hv z{7&Ubu(S!aj^Un+f~}DCn?6*C0bB@`%?9w+$qYlbK;j0KB)OD!aU$MOHy=#g&E^6SFX&c4iS$+_rjHqhP@ssE^bMuI z2A)@{Ls9cV!o6W^;?8V?7qDEJO)-_C*Z>6ie#`2#+`Me0g7a6m$$s{rRvUTL$6M}l z9)8%5JNBj{RdmS--exATdPN`mf?3$jh(^=~38E<|u1zaD&3VMQNT`kAK~bSDh)iuN zZWt0Eu*O1b#5#?d7SHUc=`6+G>Uo+ic?u_7lldzY!hD-5uXEcz0F#I zR)ncw+EEg2WXHBGML*@ zBWq(gFtbccl!`w7+{5zk&?_J|;iXt_tT57`8Wtt?6Qoy%J{#yZQBeqjQbVT-h3=F^ zOP9%1PO88xrg?5l34}6@d%$6wrUtSO0-?f^#}Y*=KbEJp7}X|C+pb?ViI**&(V%S^ zEmyMB1PM`+(SU7cXRCx+BPf@uw28^X7n{r}OF;iEVu441VF+OU>y%@0W3w%aI8F!z zf!HQ#;R~fHFN+OFEunysVi168B$}KA z3)-8w#!wH#*w_MC*8Y>4j;zo|bU*{bkmRa6`O6Gv3u19A!@@~VmJ7Zzp#fQVNtn?o z4IFZdO@v4r2?_DRU&?b`2 z30L{gnv~d8yJc?T0-B@e&ebjZ3_;wDAzguFu;gvD9ud(c;Ne2h8s=h~RF<2F^E~0e z#4n=*;^0+qC2z=jp0((A4=7r3()tBfWQ}qd4*}1BYn63XOtGP(05auWA&E*5Qtbw5 z0n32oT5Yv!2USrt=@TOTkn~0RF%lh*2#=`|)09*qsp1B%Yl=$hof7E)IPKKe=A<&U zt5jElh=0_;j)94_Mk+MnAc8?zw%}OWjtzTZ$4d=cqvwiDK$W3Es|GNVf!ucVk0K~D zbSU7i_hBA36~ZWxV`8FZ9i(zcD200ARMC=N(}D#8G=k1Q5u33bAJ zmm{NU=bG54s2vH5CbSKxw`i#N;97|y@th1yW@^TIQA3zPd~2Y&44=O2w=oyTIEm#D z;uAkj7T~k6C>a@1g+>$;j0htfHuF>w1A2)Jo;s3dYTSTkrSK@5d4b|97Ojvf-=U__ z$lH3AVnYSeL`c=CT2fBtA|wa19!U!bN)BaNq=uL@*P@gsvZ-Vm7Qa)2N9s#jy4!fg z#Q-ywg59Ph?-ee~4L2stc2kr!xg<>GK2HF%@Vmw|UiyeU8%CzcJXv9*e(nIFmo({T zSQ(N^Ou6MvAfnyM)sRR@7Y1_(gN;(+qLKkvwM~ths-iuFY9ualax+hAp^tV;)=FB- zpj(?s6lOtVB{it|H+TdLbatR5IBA6BY&XKxOTO^g{r6lkxpwMRhu&6qJ6!nqi&~xy?da;p zU57uf({b?mFW>vb6AwNSe@VFa?qB-C+;2VcrFi#q@z)AJQ@(WHudlhk{0DNdxcJB0 zYrp2VWbN$ne{=B8U)(=?eCg_`i;oy}H^kc?zqIw#QzzeY*|(lLdEoLNzUurZJwEl= z$#Ywt|JX_QyKlR8>gXTdR`)`DCy$-{=AqyEv_l`d{70TT^wZido;vioEzdu+^Q*LB z{L9*Fztcni&E9=4JbKT4NA6x)S-kh@T_Afef8|qMAWvQ9b>H``EuZbFmp|11Pmk^Y z$LW7LF!lJA?$u}S>bLqp%)R>#UAXsLN6T%uUH7{8y!FPH-}K&to!2(ogQh&QU%2@V zKl%07&A;Ql@4Dsk_ujm_dzE>^J8wE<0DN@BihC-*N5K=#SrVZagmi zh3D^k(bPuuj_F8Kev;?Xnvd*LUpow9p>Sx@7ep7xl} zC!V+Ok>_3b0dZ+&Lz?iq3U=+UFgt?Q4?-MMJJdwy}+TU=UNTRk@0Y#m>0 zE;kR&&a5m>FC00#q|D84H0&b_cg)?h?VFDuYplO%Zf50%rIk7R z$n??s=I7_`m|kjGvvcag#^U_iTJ!kbGfUIcEB7oPT|7G9oY|P0Tbx~9Rc2?J_MxTe z8R6){;_UmESJw|O&CMK{onJDJeQ^2s^z8hyys$8{HoLSkzcjnBczl|(yLD}Kefjw8 z+}fudAk(&EzU%;M_WlK7^T=F!#Jh2^PE?>+ZkcloPs z{rkD==6+&rZETla0mY*~7=yjvjgE`pUui`D@nRbk(5`UOjizoSRu& zfBpL6QGPC7J$v}ps}KIbi{|=^hwPgU&ovfT7xRso@14Hl4IArAD|gNEn2#SnwthqL z@~e-({2KK~j~x41^=qrM(Pb;{+QQ-W#UFd%Q#T)e+iUjUxx4;>`);}Ku4zUkJl6Q0 z_uP8L^xDkY_4?}C$`#9vOPA$?Yp=NUuFGy)IAk{tA3Jf$k(Gs?KDIRbz4GC=UUBiB z3un#wo3)t-=a-Mnu0%^mUVZ&tZ#*(Tzxu&D*Vd+A*O`l7cjUGjnZu2gpx z*FSX0f;E5pyfAyg$;E39e*a5f`3`&avY$D`!^n=#EG{&fZ+V4y-Ny8)_-<$6@Vzgc zKXz^QwjY|m?6P}qziZ~oBg)LW{HB$Aj?8b&&0c2D-E#L&yyS=8Urk?g`1Y-h<&|Tz zhgT23_|13SymtKX>f2iz8*{H*TtDQ#_I1bJeCG$=__NDz4DGW^a{1`n&+LEUihf|~ zH~l}U7tHT}?!}j0_=1Zrx#$Id{`Y_MXMeRD7wfOz^VYN5zTgjk{LKD_(Yt2q#h-j< zz3t)0FKu72@B8PD99_HnNbA`m)~lad!B0iU>t6cpTR+<(er@rV$M#?R7utcTH}4VZ zICAyV7V-ApecE2a*4fll0^QS(Bi3iTJQax!fFh-=P2#VlgDC#3eCprwU&CZo~8-)+$|CFUg@`Y?SiBDh58`*SMzj6c^Ok2~G*V03cOd3ZP(?2HHf?ZK$i zAM=R!k50y6w>Rz&CZFvO2jl*z-x&=a4%x|YI6l$ojQLXO;SOK=yJ;5IDy_^)bCBYz20c};o)$nH({@X{-iq?4n8{= zj>e;ty~%hu>G!(*LA}@hsMqU0GVXP_1kR;1=yki@L7!{**kIC|@LG4$8TI>Vv|qSmp?a% zJN@oGlmEe|M>_}l2M6PS?&Uvz?{)pZo=CsPS6FxYSN7j~=jg!w-Sw&AD?0z1!ymq@ z_s9)PlfOUSJ1`y$MnBej?Qr;uzdIU@hW9-@ujUDN-g?x@%4 zy|Z_)x3fE%e)$L9I(hUX_x;+K8$3SHx&GSz589pO=Xdsu|7g-59y~TWa{Y~?-S_ve zQ#D6TTE}e0p`bJsxy_cJjnzIPMJJ-@m=n+3B4+ zuz0Zho8$OTM}6+c!S39V{=2qD#?<7}-j@c0!BK0Z9rAns*6iLfW;AZ>zrEA_xi2tP z`V0NR&vEAle>~WkbdAwyGJN3lCnmk_;`sAju5WVtF4tPUT!Wao{{v`PQaC;GR~El<9FXmC%T)4IN&4hNt9ICo`qJRFQir$>{st9Rcw z_C2*MIdNu9^DA-CwJZ zH2>U7_@kX}Z?Ju;*X#AS&-8bCK{wd$Z1+z++VAgl2HQKGPn_=j*61@V*gqv3Y{(cYlHv%|i&`ycjs^VFcT+1ozV9i6H_*7LSI zEbGpxZnxJt)nTG`cLt;0eb?muF{&Vme3&T@=cHgVdbf?4b3>IflZ&$y{>jVDj@^Y7*4EpT9*XNBt=yL@htG93TS`7J} z-9LGxv$ONaPYvI>-TRx~=8r92-~ILN)t|4&x_kBT1NROOZuXWg8|>fyGosg#>EXk- zS*QNwzV6!@)7~5UZ#y;k#oy?k>h=$hcCz|>zgmycXq!bm+t0bMpWgWk#;ntS+u+Jx zZ+EcqvO_=Be|Y9A+C4oc>l^y|B*)~*RLP$p6)6YZ}o<^cki`!-nZ5% zex&zCrshuP{hjUUBii=g-O<0JLm<6*u*JV4?%VbUvhF9lOz^WybM7VI^Vt5s`ABtO z>b38DU%iB1^UWpvPycWCUk+aK@^ef0!K`;?|4sjQ?=@4OxoKQ4tk&a~p1fe+OU|yz zd*_ZV%sxv^^_uh6@XMdM{j)9N_KoS8{hvMl{RgIQ{lHi2WBusU7V)=Xl+I)_8v7)) z$?j-49uL2K`o9p6M`Lp|nT*rP?ekrEd}@+k%vR&^S56QA!{qSpc(*h0#$RTO-QDpu z;mYtczc4z+9tg^NCga~8jmE>#X+A)f8=f7ah4+8{WbN)al6JS>f@I_aKXNpp2Hl^H8?+d-r!tx-Ups-bkzT5p4orJi};?4 zcgr8EN9Q>a+;}pX47SI^)8l`boH{)k|J9h}KRG?p#?#}mH~HziA38Sq)VNsSrO_mu zjP{%zoz{4M{MFIv;qG6L-fkGDKXzt#3u7{BPc9gaezQMfr}?K({~@~@pB={Mkn4 zi1(_;_P^I?9GLpgH{4sF%3Ggn-bg%G!Tk!&UjDzqZh8J6cqd#4TK5j`sOSH>yZ>Jw z{;GeMcgK$+$@<|QH{A7iKOOAe=6pu z;L*qK*^#+@-)kS+|0^e6bZ|=fyARc;^B=!CZ=P1N|K8vJ;k8r!FaOUcv8}!L$&&r- zpxkzBdVcZP;d9>!viI^!uX;94;^{=ilOH&rDS6AEtJhAwa7{aRn5T$}`ny3c;JZPz zYtPN!dJ=P@B=UUk)6(;^oy(71^<34)Gs}?YJf|CT@!F}c#?p6sPFs8Tojc~!meeyw z=yg{;cZ9j_ytkh7%)a>D$F7|^^NnNQ>6zWXci)o}^tn&%^;i8n>-)O%-+RuJJMdp` zzIN)DtoMAUCs*x#db#v$_wo%_J?HYe?wRjA=V^WA7ldo4(oc!s>1mBG*!R7Q>uazU ztC!ukxNzS+YcKpkzJ2CLF2C%mXJw+EKKH+Q5?4JZD17Gh{MJXFyB0ie7@qc_XC|(1 zz4h+-{2quhM+e)zN-5B+N$$ZsWFef!^Y-{AAby%$`#cV_P`H|)LO z_2-u7p0CfXzQ=gkPs+Pb{&~Tki|b|k3a>JoS?o&;GgPhUY!+bx-d5sgtdL wJ-79@e(pTE&HwtHbMNrE=hrW_fA`$)_3m!D;k%w!zjgtC>+S2)+Oy~X1I3R85dZ)H literal 186615 zcma&N2Y4mdbtc%9EH4dvJsit{=8V>~EsnezOQL8i(V|3=3Y3_dvDpzoBgdC>u2=Oc z=Xw=h&IuW4pwZpv2G9T+`32x1H99a;!Ln!Um3L;nJDMV8QcT)!fDfteN%!8m=bZbW zgQJgs^0&TlC!WqU;f+2?;ZKWBdxtHU+il$Hn%n#o9{Ss*O4#Y@eb*G zKU*~Y<~F^tP5*@!OF}v z+V0hujE()8*0zUEeop;WYcd+Xq)KXOHh%8(z0JKUTcfF8ePA&4wY9!OXn5#(y{YZq zK9#4pQI%!r()2&n{*CspeB~Q0-}rj_*IL`!9{BnLEnj}%>;n&c?SThczS92HuYCFb zvwz$A)%Mobd%OBo6^*T3eGi>H`MKWazO${(Uux|#f9|jE{oI%T`m2Ba_51(&%U`|! zbEi+MgpEd3cvitFMs2U4}9_dhIj1v!n=?ESM6vx`hm6n53g^3 z^WC-nl)BdM2z`5zpWOY#JFEQe&z*)(9Ql;`q~V^1-}|-o58i(lc*os$-2K~kAH6$q zN5h{td|KP4?=uBKQvtzyFRAs-E|o@nP=2`FQR>Zup(%*48hnUmsNapBn!3 z1IDS+KV;sg@BZ8e%=_C9EBF<`d*(gx0rNi0{gsdBzPsUfRnt`E?=$dMtRFG(fe#z_ zJ9mHj0|tKe!zzA7@ScHx^8o`t%>A{G=l(YXA9%lkzheD}fq(O11Apu8-}!)nU;nU* zUlF`#;NO10zz=hOrimKXKRpw0-T#RP zKTgRbABku0Q~OKv`rK#yKS@4(U$NtB52>^L{r6`3Pj_s8`p!??^{Kn>xc82~`o#U8 z`1&0W-|^TTM~}Sp$?wzFQ{ zscuxNtJTf=T6M2p+1%W#mdo>%jmqY1<@Va%js5c8^*xnhuQt24TG`$%FV5UrzWU0> z%HCnQT&r%b&R1@gm-kj*-#ov1vr<{z-M)Faacgz`V1IX`da%7*+1T2>zJI;4zO`4a zY*wq~&E2i_%I^B%c4cd!wzgN^u5a$t>pQo$E0t@vYFn$TGkcpeTlMAbo$_jBtNhmX z^49Ica`pPw#^%~;d1K?+_Ima9mD+Y?cWZlVzgk|nzFpoeudd%X+}PgODlb*l=ew1i zHPyD=y|t~)>cU2CcTIix>h9*j%3AsG(!uB$l>*d;&&Gp6V_43;C)ynnF)ynScE9)!!*VnGE>{M=V zZ>hw!joM~;QDv%bZk6}y%NrX92lci3)=qh6bH2PaU#;)e${V{ktJ@ppmF?QudGH#m&9V&FY(1uhnX=sN1%kyK#N}puAPBs#{!N-&>sCQZ-d> z?(J1~m-lwc3%Azx*RQYK*tu1`xU#;!TYmkZw({C~b!ERcU9Vg%S8v~ZdHS|0ai&&T z-Y%DCUptr^scqFO^R?T%)#@ud3%k{o!+PalqjtDfJ-Bw`;&ypy_h9>=x>niWT3#=| zxn8fowsd`CW&Pap;Y?-wC)4%CpIxi&E^f{*zg1tZY_I*-@xATM{mrfQgUYpkxKVkz zK2zOTI^3;pt?%qqHkKB5t{uL5>#g05t)J9Z>($)c=FaZRuN}TxFYnjaYvrE|FV|kX zwpm%ec4fIbUtg+BRBA8p)NgN<2d1m#dgapP+jFn%%)fD?Ub(IA!OJ&yZtqlY7~bz^ht#`@~!#%^_OXQy0Ut*mUVZf{m^Z0}v!8LRH^ zZC?9DdE>@cRd-Wv#k1JyTg-Q@>t)t-QUo`dWRtURy0MU%kD3YrFpH%0hknMooRKt842A_3MY* zwT;`gowrYM{rdjS%GT!X)t}cZOKWR~Z>+pttE|>sw2=U#Yx$aeuqIzCXWF ztFKmX-MD)0<;~5_3!8`P37Vw0<=elw^2Uu?ZTp}+dwz9i?Z)l(om*R#o0Wy-Yvt_X<(;2jzh3_5{ac6g zGxOsM`}OI)pKQFjvs{_4zcqdHaI}6fySlr7qrS0}T-~f>miA{?e!4olU!Iz}xV3$& zJaf3b{IktmcKg58))&`mg_ZfuH!If%|NHj(O0~Liu)KEl$My2f=-lgv*Vfa+*Duy@ zZB@5-XP0KLS0@Ua_1Cycd3$B$|M|!H<@NJhJNvgM52v?&`o@)^Tk5IYte5vL-CC{w zx1X*q&(79fxwd;)U0;~rufKU^X=O!y7kBqwU%D}?I=YpuU)EmUotXdeZv7{>DsLX_ z?>xP^GB^5*?FzWLym9{Z8_V0*s+C%OskX9OuKt&m<*k+FYggA)f3sg(s{K?w+_&q8 zn`;Lf`?Z^^TT9E=*2?RZ!~OO8_V(uL!NS5uWqpQi|POW^f zv8Ot~&7Gyq%J|joTD`nnTi@JR+gn+lIIQe!9c)gmR?G7@s_W(I-qstcQ+#c+T&d08 zyuLA$xW2i)cUV1m{nEO6nz!c{whksXZXRC0@%q-r*2@0X^c&l^%j@O6y}4_Zn?K&V zF!{@s)%n!=#w-80c=P(^z~%VP*2ejIdG;S})vxbt?_ONn8Na=Eursqbx${4&%PR}p z7dE0V&+Z(Q{$>Bo&Dzz<;a+9&=ZCee%d5-(yt7|fojKeXt1z=`)n9DAS^H&q_m!Kw zi$7mm+OACh^yTTgG;(osoV!xJwKn|P$iZ&?o(pShIW-imE$*LRTVH>5QT18t3;VCs z_g;H-e{ri?-rCt+TVDV1!P@I@EG^gn$KtJ*-`d!|{`ae!2TKdrHV@aX*KbsAFI`_f z*m>nAdho{nwfeQgTN~B& z>ej-|joSXk{MMyc*2>Y!(*E{aE9#uCuh$oEE*w-g%LlugJF9BUQs2L&H8u3q*0*;y z)cCl4?UmjA&FiZ>8~c^Y`tEvd{mshu#>UQGZEJIBO?8}8uhy3rmg<$|t>rf>8++Gk zuiX5}>PEFZv$noFv%OH>oZqXhZC=>l+1Xj%Tz!3O`qICwR@X*Wc5bTV2M7CmZ-?E= z!Yey!q*+_us4u>DxUpVd*;!kw&Aq&LYieTP`qkxY*DlZ9DBqf0*nEAdvNpSV^I&IT zZKw9u*1j5u_G^`u%;NR>Yd3H0yjp%)jbfWWzIIhrymC;RQN>hl*SB_8_O7m0ZtX5? z{$y)mYyYS9<+bgD!`tgOk*V3+OOrG6E4!<=-dwx5Y~Pr!Cf6!^m)3W7tE>C3ZQnfD z-m0#?zVX`L<*Un;>+99p=C$=}ZN_iI)z#gl`c|#FvsvBPtJkj9>ebb2TRR&o+godujorn{`tZ*d zw-@UFu(ed(UY)C7e(SZ3Q8!~gFc`j!!T_A{N=|KXwgzW2@e+sSOl!sl%b z4Zj93?h@ZiS?{|4lMj9*ReaxckiBG`f&bw@BP2^;0Lrl@-bk_`z(0u>9M)b{EUD6 z;rq_t{d;exu7B~q)YX6Aou9nxlkcRiciex+`%~ASe6}I?Sv8mbRgA_VQ&tM8^$c_fnnPlWfCLKVrh0vIkf{C~-+SOAi^cK@B_nl07n9hUb^N`>@ zmA7Z&K`+a=Y4VYjzGN**4+B&bebEOfE(GD0tk#PoVK_2E(T2oJK`A0HAUY4gPKshu zZ}*ktR4j9pL&HPJ1Rxb-!@hu4m?AR+J=2LPk`SXOg0d!*5#tdjbZK!Ihz#=qrOh3C zY9a>&fWS-w%Q*9FhDwIrDf!GOVE`qCL;X;Yw0MI~ToXGr8pH=9G83>}G*ceG6%S^z z@e+ooVq|i(hf3=OXR?@B8v{vuN)CXXL!RK8f$$B{1XARalkgj9EJlGDt>h|VZWxu2 zc_$};{DwQh4{D8xjAHi4=`ip4Da>B^@*!$-WVwJl-0X}Mv$>Y8;dyf6Z}ajAR_Vj0 zIwZ`KcZSX|X(y92R5St4un`ADI485N9 z$ZXo-R=n+vNsUYBu%+WkC>C;$h>YFEDknSx9y57rC|OcQB(b3J$n*RlXJMpavRepJ ziNS=Ipoa8BR!WEMkTVM6{)m5K62G3K-7J)o3z_7U5%~cpK+)2qB=C}zA;lPhbR|e7 z?+f`&k?CYAy3;ntyh3txr0-w4a}MA{zt@hra}Pe+LG@l{I5Zj<8^9nNve7?8@M$F_ z_(VL<1~J$jpPqrZPH(g?!Fw}7JX*ku-Q=kVla&p=q6uMw5QZl3fgMc=yabKsDO%}* z({P*^i%!!9%w4EQJ(Gr5C(sBN?~ORa6L? z4Y!Dv(-GPbbouC;@wkN}-91Uy2;<_!0q~j|gTkkah24n>y9|NL5fYKD1J(gMExKSf z5vCB$OTo+@O{tG@aJnO)RwIV5t1Zerz zfIKl~Nd?fvDU0VqU<&duO-^e`zlb~Z>$KdD3)GNN~{t+yol#zt9yayTbFe$O>oN^ls(E2D5}*|ysM%ovY1O>c0g&Y7}` zS%`cy+#R@B>>;ROaBkWMgcT+Pd7beRrR?y85u_oc8jR)1-acHwOOBAv)kFJ!82I7n zv)9f>t5K6zlUk6=^DjK&`vLR(*rXhEihtKNkc#plZ>K&=aWWH}$i|qIcOfVt6BgbmvKBhw zMC_s!!Pxfyx1Iq>Eg$(K2C;9eH;=>wvC6w=_nGFi!u=+Bje7T%S9ydJb5}F zq9P?up<%m-O$-i9XS8V|X@l||7Xy&U$n7QBrRhRu*lr9McNO*0tkV*+mPmUNLL%V; zn6XVI)2Rs*qb7a9jGH1wQ-sDx9d-?cg*=RoVnf`83@gOU1_f ziHt3~4M^05udUJ1z?^6mV?oMvr8Mo4xcoXjkp%Sy7kr|mwWUAFU4yZrGi3~z zE-vP>JdYr3UwR0X;hC$La?u^dGO2VbHKrg_p6{9c!(OmX1uU1(6=U#B;t|k3(cLv1 z;nFL9G>|oun88!HI(pPrpMC|&of}LDgL=_QkDqo%UJTi#;Wv0>sPmg1&e`WlJQK6f zV}y&?u{hCivqK5=r%ppjq6AEnQLWWKXY=|{-*7PMVSJfLWuA9kG?#!tKi1Pe92n1E zi%DFPhtrgs>K&WNEr3z~;Kb|lID!|~2xDy6OI4wIa9lsFDR)9F}a zADv#O_-wo=Yq_+v>hfV8Nb7)DF$r@ZdsaYo5}z5f@_6W?5SerI7@25VNMF%(bcK6t zdN2A?E>r~jXhi!1D2xj}CLLBzoPi@yuP+usU@JrC>H6Zl`&GR&^oPEg(`Y_N95JE- z!$qRKPOAk@X1HU~fv!kQ9QB11czV2eTJW2sc;GuaBXuD=3Hs9)q;cnP*5!)Dj>fTL z^y3dl6@i1pkzg#oh_ps>DSVQhvjS)^y#S2I^*&0|gwJyq=nQ3=0rh&8A#Km~&mr!& zVqMWk4oz_OLW<%jb~u$Cv*a(jj>cj>(@Ex>;iQdb#}GjVT9~wRRy(0z6vQVuOp1AA zIpc~C4vs4%pK8Ap3}q96Nzy?FW(METpSb8W751lwAXfl-WBBr5a-ulHMco0cNXG`fG#Lomi0}j{h_QH3 zy6Np1?$kk&l9_z*@pH{>76?Jly%9+Ifj9`ZdMAA8QbL2r$@tdmeh(I={M|@wP{`!d zu_%mndx!aN=|Y!VP7g$=v=~2=bjnb!*Q3pJrp+f&7t_NCr)gWRtu>Ps(2$UoWG+g0 za6d^;hhyGSI-+=NdDYcU<4fnvgOgz+<)p0|DgTAxfyDH5rYD@T=VGvh^TyI=TBcZGipAATIf*$zcRZ`` zXIL?%NqI{IXBYS`SS-L^LkTX@0OLD4I~JGFa6Eb9O53#l1&>EcJu~r^A#{FVR;N7; zWs9fBqnIWgK5E%sFAf4mK2G!eEbQrs8e|9ARJHzo43kt053X(LWBXmYQMGcG= z6Z>Rw+O07;OKCmV(JT)%X8A#7a7ci!SZr`7gzEF-{9{iQ@N^>YHU``L(SM1=BUB=r zSU;*+$bmg2lQd$nCzS|>_V?u~9zco4Ce`eJ>WfbaU8<9eB3T<4ka!U>Z0% z`HY(xer`U|nHfX2{veq%p#Pzv!6jV)&*;n^r8vm9Pu$4l*pJ zzAZxV+&4IUvKX(XB$rRd(b-tkKOBnC)POW@X2QIWynO6ZZxkai&!DsOQv39%(+$WY zJ!4*2=R8ja^Zm+EdkPsH${0&-XCR{=wa&!GHG0X+NF^(kAv6Y4EC)$M02v9Uie2FX z5b{2i>xCAlP7Ni(#`fHxiAY9u-90W}!RKkhA|7S{V>hE+7FTNR7V``=Yha!XtsakLvc7JH4!+ z^MebK;8F1DDWU&pI6Ga;kJ7xy$+O{c?x=KrOrD1t(}g6kaV-%EdY(W2bdbHA1D%G_A8eifg!^flIlMSoQ&H;!edWoB0oHuKAA$T zU0#N~td%dEr_#_!u&V?w^-qk99f0)g-?jVLWPfG{J9&XK)3W{2mGH1Jel8nfr`Y6Z z_>4b=tQwvghBa>7$%ahsWPD*p^-;zlFnvasxx#@Je3};v1wv2xMi}?cFC@Y?M_e~K zebG8z@asnc=J9?R!gNN+GaM)kaiPU5eywO6u1C_n}x!<4t%)zqFpZNVNpSdpo{Gt1>KlVSU^8fC_=dn8){^$c8HSczV+y%T1yS>w{bN{Cv{1}|; zk>AuN^gaOW*yi1HpE)=1#KZUfLNoC;1b6-FyAa$z{q|k&;9sA(_Y;40s*gf9ILU<_evcSBbZga@q4o52x!B4sZVwaYFTFQ1Yc#t4p z@S0paj6CfjtdNw1Y?2uZa>EhIpR}OVOoUJRa>0}y#@yH#>BOAwFr7_?x~w{Kz(Uiy zhy>BdcwjOf1OeTAk)UNx4udo;`d}b~)0bEmBqyw?UY(uI2;@Liic#J;Dd>TY;VTF> z3flX;;xHSK!;kyp$wafLCLhD_X%7+$DmJ$-Ng<*(iuok}$f&7cP}(s&&dbTD|G8+6 z)dgd$9tQPr_WAf(;ta_NKr7^@op2E8#NGC2_&A*hyf_@uAPOV)qfxEFZ_1|ZqCOs; z_4Q6u(U=HCMIKh78= zBLYUcpp7AjhnOhlP?Q`8MuoO zYzm*3#emPd)zdOgF_h$Rex zK~VI^xG3HNZ{+AU(S^rCE^C27IccXg6D>@6+JoR(Vp!J)g`K8i0ufrB%@IP;#}I34 zZpNWONVwTONw7~5xCb~(gp4t}m3G8Kgrjx>lJaptLjtZ`riYTlK_3zEqGv2UFn^&t z71tU?J$e>F>9e@lE<2Pklu~~Q3nU%K$ndB$W~K0aq|;+}a(lklZc5hyjBRasdl;H&4X4Q$$bCw`l@w&SJ^GgJ1Q_fh;5P&mo z7I-?@=PoIZAd>I}2ml(;S)%DM5Tpgz;~Flp@hKI#)k=ah;GJ8w1oBC*1x9#QVi_cm ziscb35R>Jx25wJlPCCcgAA|M|zJ&c_JLL{um#$jR5uR$z8p9>On zF)wxD8lJj6(6 zFzhNtKoSiHb-_kE2=$nv!)&f25|GqHKG4D8etUA#&)I{?NubZdxQ$LR?U90-vtU3) zMd4*FsSyQ8?nR?4vDzCny5(G;FK$72ufU&+N8A^TQ3US`Q-mjLx48*Mqb))yNbzTM zCe}u-XMz?gmFJykma%8MAPwUQjA!X7w+uuQ2!?i!3|SSuJ7PSg1Q?P<#23gEX}}o0 z{}hUQ9GKa5F@5~36F6fHx1?l!2f`+pNnNi3s>u{35J|XrRu|R_%$ZZ66a%!QgcFv< z(H^r3TMtnrg`7l;d|WT+6V@a@8|qcl0Sf7JF>*i{cJ<>44Ooo^WY$IIkmHyL?Wd*dZ;3 z7}oK4)Q2E&YbYRXf#U8E>tSg92n%`|4G3sauR7+@DB#ExK?A6x z^;%u8B?mW}y@(JHBZ5Unl_#U__c8m%^{fz{efIlkH^9dw~fiq2dRlf8|P*PA)cWm zY%~IS0o|~yWOX2X$w^K)(Re;%=4`#4Qb!X#^sRm_-lKi?X=5C=4gj&RAvvdg(uo_i zOp~9`DQEj&R%@b@r|=XIQ(@Ci4-*8OR8Tn2T!G>)j3rWWtHsJE=Pi`U3)$yjcfoXa z%p9=0D5$qJ?EJnx!#ibNr!C-rRB>t$b9mCEiyxmDOo9X2K|eyA3i=&pFNbz6v?s2* zwSd`ATVoDLz>F9=Ej)!YR8v1tE)J-hM^Y9n%CP`u)e!!(peyJjjd@2YKIHAg7#`@b zF(Ozf045Hz4;Kb}3H3$PYNuGZgC?b^XVzQJM<&e7C8^(p;$zu@PQZIs`QfLNAfeB7 zVlQ@L#a1D5A_vpx2@@}rh@8WLanh{Buw)9ojHiNyO9(VbhD?-zgnS*DG;L*e5|506b_)GLuq+MjO$5_Z7j_J^@_^YmJ-%dsd;+6&&GFAS*`2fL&na`yW}jL+L;F=Sdo zHVCo*%`u;e)c~ixtz+b$zhrYkC%6Dlj>w2ziE6S@Of&&TS0BuHZ8l_bGSoc{L zPMj8~ak5vv$gqo$(}%gN`A+tlJK};oauSC4DQueF@^$+dkw2gZPT44P(C!Pzo=#6$ zO~C_3&Jvu@lbd+C(U2zi->8$X{Q|oh4y#$Mko!RlM-UKm~j|9HviE`w_^9s#JkO`6vqQ8 zczV>to-gp^W3JDN&%Q45%l4d?X)Mb^5w(&U6Hmf&QScdk72p0~SNK z1MR_MqE&eYViiV^=>CY}by#~ix0{AeksT4onJ0$KJ{XVqg6Ca;9t)>}?cNAxBu0{m z#UN=!FUkm)<9NKtQ{ja!beH&=7%7zp9V13OHSc*(hfgPJRI3Y%n4DIO2R!W6J-04!zgkgm^%{ z0@Klc5sl(q7+}E=78xzcVpa!8FvdjGGeH}B)A(j98`0&^fk3z7i9BadheW7FMywh> zj6eaqT2D}9+0o;SI9yOfw1rP$NY3k(V3LfUkKmZiV@3R(VAysn?sg1P@=dRU_8IM2 z5)UK_SlI6( z3IieV)Al+KQ2qYWS65Ik~(y> z92g0(I=4vnJ6kx;Y%_qkRU5Kfq6x~`^4QT9#mku>Zk_>p+P|B}onifz#9*%1>2SFl zgZY&g^bv1#%Gktv^VCVNNxk+GfW&iJzQgYbA<;&#*B5YfA~vrZb@y4#IxE{e67i8& zfWFz7O)I4A42GH0c}>EHI>RH%x&CI-9*dv`fc4_cxyKznu&swSJ0dQTR8B(HETVXy zO%S~DBaCQQnx72QFNL%bra(~e(c?Ow+C6|quvpwekU|@q^ah)(Za3t^JBPH5q$`k* zplV@-3$r;C;ADt38&b1QkW3GC#$iB5K|O3j7AN`~5RCdm4?P)=$$Z?>nus)eTFn`P zwvv6U#zJtSiDvqJK9-}UPU&m}b-dsU7yZ~teUm*^EYRAbTu?(P@nT+1o||;qG3pFy zSI8zaOpuPyq}&&D4WxS0zDBErU5Tln2x;kaBcvHi5|#kYxuWNiF~O(hbyrWe2Y3hR zLuaDBNy^aKJrI|SB+Mv=$#WXm94 z&>@Yn7yPCm&PoBfH9q3$r#aFxtz)==1rwkN_ZYzPIV_hngMg^S<3nyQt9Fj9vMCpe zi3!GNOzG8>BEn3?*k#HN#JYz(vB78@q@5;hC<)q!E+Hd|B$@qIP;1GX1%r(ohiH%8 z9+E_yO-7tTm*UyvwURnzF4i*DeAV5;Y)fe4v-W+y!pZuO$Vb2dsQR(>?tu0|J z3K9u}2f`q&h43JL4V)kBEAHZh3>j+?%v2(Vc+`j?t9R&C0jR%}5EV8NKOdl?G|6yD4^UvwF=y10u%NN9)61Nk1cM`L2*K@Z2bpMM^9mq--Q zguTZKh60sPdw@Jq7q&1ifch7dZ*q^E611#hwGxCB_VkIYCoFo$GD7UU>_kzxAj}5O zVgrRCFNxb{raN`G{jBInF)hIuI;^f&SnP$vdIzuYqNGokMnLmPOs&lWltIzE9Q@St zZf9}>p8{UgvcYuoFi7;-`eFnaIY3>;7T7eP$uv8??G`%a$SPKBB9ybXqVZ?EFOq$7 z2lIqM`C5+`30P(SS&tk*X22-yio*Ku3bu*DI0ZL)ui1fP+K_EYIswXt-hs3Tir%hh z7uVs!36rt8Khf{8Sp26`82}*&4CPqqSaa7EvDIke)MPNSHr}0bC!!=*GG@$x1EZ5( zpBD{qCtk9PY!9Fv%|6Yz8HY!8cyKpUJVuUZ{NL@5hm1e!fmqjQF{RE+2YZ=DA2)4| z^f?O=o2|<;G-XW#c+c6!i<7|<)JZv@^_y@iI3#$IXe1ixO2zQda6qp&DCsD~`YyU# zWIhrJMxZpBfL*wS3a64$E2Q?^xn8t8t;51;fE5F>C7MHRK(J-fV2MV|(gZ9KO8}t^ zM#z{1J6R*e_a|jM?02iF80PWdj~NI}Sn(BeA%|aQ_rjsBl!yvu6dV^_RGQSk_@hg1 zw8!7{qCwA{y264@a8lN3yV5x??}U(Ss5KJ^d5P8xZ>CUO%h2qAB>8CTQDxAEiV7~e z+`K#dXeg198mC%lmi60HksiuR2vHRAG3_?Bh87F}d5vF&`uhBI7Lh!G^pmVeI1$gh zNAYluWUwV0=ExA_>4#miLB+h7M}rF95Oh+>m#itB=y-JT80bgHj>kPLA8~dEO$(Mu zG~$eU_;&XYLZd0L-F#lqS_=_w%|Tq7NLj?m%=a-gr?6g^TSm20ArN9BUI3=L^e(qc zmoN?l%t|~luC`ZV;Vg)GTMPOwxEna_S7TOGuX^F|#^k~XKNB2i8Pz63{x*dohh2lN z-W0=8!YTW3U<{1qhe)>9{Y&i-7U6O@TZpmtuSm9ZpxI#u!wkvH zRDl)JC}a&@AmWL6Elne28->QY^8*2w0OZuB(KOF-(>A}&j>FGmj$CNn>WKmQtQrnc zduzvc;*`yqNW`5&1Z!h`NgxztY4)Ogwu{8`0Y<&_UIY-3;CP#?I(WhXW4sqJX#Epv z7Ge$2%JGTOa45sN836%N<*b>MjWElF)gtO3qjuzEx~vYr2{FS4rbEHQ(@9|vH>C)1 z3iq(#eo8y;h^P&0C{hH>0gSGLf z|9RG?f@wMSC2%e%S)1Y^D3&z40W$=Vdb(hQ>lsw*%qI&PpW#XuhNMQVG z(bc9Tt-d4t?qJa5{NHAt*!6nXqOuM2ua+JCd;@va^>0Jm`R*7}m$5=TJTxlAn!rVyISe zW31KbbK`#AhcpSol@ZBKu@3SW)5{i#kcVX>=X1tkuYBT+ zKWd-QqmHy29}G+dS*$(A1cly0z~ecd(AvMDu_MZ9)@X$S-HGoTc!u_NxdYa8$aS@C zvWPij%`Xu)0WyN4s!Qn^9U}Ul@gVL$~Ca#}h-Kc@KEwn8R>JrDb7~V`oK~8InaXe0SU-g?t6^>!qX+0Rnjx&f1 zn8Bpooi^i9C&&x50xE?>n9~u)s5zMp@@AydA@s%#Bx6Mg{Aj?^Mf$OL&MTPH8T@IW z&%p?IM&pdPDNsbq03oxt?W*sE^D~^KBM>hRCG;>#p2OND4+h80W~a^CZ;<@8ewWti z_htNclY_A)twZ6MEf;L3a9fX4n4~->1}{(a1(RNg%DD~Cbj5#QO_)n)=38-USS^Rq zk}v8CeV!U1ld0@k1PI2gt35;KFm~j@z9EhM+zV=pc=wn~34=B@{)YXYz7vW(Kq|Ip z{R%W_>GcY2%8*H?lZQiGoauHdezatmbJCiOA9DG6HUy1&`zJ4o9AcNXn*7=S>#Lkjm$tzg%Hz&Yu(5lnY0 zP|z7wz-$;wPJ39PTNv%=aDsFUXSxIgMa}qlUn_pm?s>xN<)hk|JvgpnA50vRLUB=s zV2LeWG9rlIn5Jz79Pjm#DWwPRdZP?l^uy^O2$qt76pDJ84hN}jt))l|dE#-e*&PA< zCteDA`Us7i?_~P;zf;-**rN^x_sJ3d2XTgT0y;o+f&KF@cqVH{^Br1u}IBtq3G^{tpxR`>a+pg~>C{2gk3cFHK?nTbsCIY&MF`Maq z($VpCsPS7vPEtQ|Vb88C56C{&#CZ@;z^((&kqk|HJ9WpY0l+Pw0uS`&9O}PXNIsm= z8iany73>fAQI8NzKq%H3wko{Kry zupH5uuwj=RQ@F2B2d@Yose*tD8-xCrDDi!s+)1psyW5K=G? zh_oeVNhNaW2*ju@Hn($4wNzp{$f)$EeUCVyp*#WF?h5o;PztvffEC% zW6_1qsWuqA>csrbna7W|q*WB%MS6oG=9ub_0|A9M@T2i>lP+r{3xuUYiU6(Qz@wq1 zSiX48-_i4&@j2@d@IvFLKk$rcTy*G6-t0O!B zaB@zSbIv)PIyrZr&f%t+d+#JppeT}}D3S`6pk2W*WWzQL7z^ye9}Isn7Pf%}pu@)xX~NeSXjL7fwHwK8(W;RMaOzJ0Kq28Yc4 zD2TnwE*!!>C;!o zK1}*QY}4`)WcIZ8#c}Rq>is7Vj=wpidw&1DXAjNz(Jbl>kCzX-A+$t3`SD*I_76;~ zJu##W=i+-mO2*Zaas2IP(t{7^QS^MFU%VH*_U(`9j&=3^llS+F=LyrG@fq?Kl;f1WZy}IPBP>k1ii?~>csJb&e{*|OoH&xo6Vma zN7ndvEAKv6zS;TU@Yx60pz%pNyqs)2eRy`hm@OW4d)ln0%*R7*rg9&iVp0ElT_7;% z=O4bu4g0h}KX0cmXQ%S)@smgRQR7bJdPx71g=`JL$23psQKSrh{OOpQUuenO8t#Rk zG$%h$9y}cL_VTcI37uQCGu6+#3UR3|HqYJV{-^KgGpu(!{B-vAV|%(kB|h=l`I7j( z4*P@e7~+7kxS}~tO0c`5t~zk~^zZk@&paHW+j?_kZxY&8&Lkgnnf z`~POR($CROpLrQS_tNWwTQB_YKOX$QwLSi?KO?VR`oEJ`zj*E6I^0iztDlQ+`ngm1 z-~3)pKAWpe|4xQxs2MuMg04E!rc=HvNba!2s(V_MZSaoe)NGD2 zYLlY z8xoq=VpSwrKipC`2w}+Ipp(2zk~4;5^Okz-8>#ep7|A;mLH zG4YtqW|{UV%zFpjW*-`z`0~(CfF`4d>6@*^`8ILFA&5~gDvhMPoKK?Q9m|Pqeq?T@Aj$VvuQX!+v+0U!&}1HA8UDI+CF@Aw!}xCHC|NNW!ndy9IYue z%|ROaUEh*yj#V8`u?L0r?4739VkX0j>7dT@wxGp07jk3Gk=nq6=1~{#_)m32VcYYT zX-)?4=?8KGNS~e>aS>nr#(*u}2xZ+_(8d+HH+(a$p7}8-y$ci`9esE7V6m@MzjWdl z564%+uz&Jk85$cc_euiC!)sv(CDaeKhU}3hI*h(ytu;L+fU7k zc4RvvEz)PuKrd~}56+Jr_pVO*ZHdamo9ic4T=u0$=EyBX;$UX45Ru8o?8p}`cSr8F zc_}TwIZhgI0xhx1=}T41k0QSu{@HjLE%bshMz)6uJwpuY%)HvA2bav?cqVH!=9<%l zR(WJ40ER`-c7aLSbKhfdKj?_z>G=Kq^EO8{3H)Ge=g5gZP88iof$g`qk1Z@Fmrh)# zp6Qq4WzVb{A7!7lm(cY42xs;h`h)S7@z)*SJ&-In=v{UXl%_Q1y6PC!dxGu3y2Udl z4suVZxU5v>tYmfndd^ zIB(iJt(Xl7MLZNnp1mftCtAnDgM|C_FkxutE>Ki^DnyvO!YN_}8!(}4Gs35(170Sb< zMMnGW?Id%NtN`j{1G*_ZK=&2-%Z{@J{9 zU%7}M*MN)fUK}nNLk~@9X2||YG@lJYP>(XLSewAyk*zSAzR!s#MZ)XXwF6L?&)oNb z0Fzq}maLe0ySJo<_{bQ~7ud=0ydg}$o-dXsi3lI$a{g2ft5fRA+Wvd%1cr$mYA3QD zkJ}x2f7g~OUK`;OE4r_I$PEr~v z-~jv>!Ib+jG2xLDM;!mJz)db{cAquN;@o`U5;E&{7Jc;sbBrYWN>h{0k>| z;!q8WcG!lFY{BR!q7PHba=8{}v9U0yX-BM;?ZRf86uo!m{oWMWGcf=wF~go3+5yRQ zpa!XgGH+6nE;d&+pj*()e#Ut+FyNU)&!4C`7VjrtJ6@}_r*5N3xA*h+2Na0Nabv~} z2}9(AidaqNRwflUn>-k79m$b#@Qs^>KXht~MtgMZJv1@2{&q-_+pI27r^e3AgD`%j zI(`I>LRa6)&1auFRku5hq*JZKFO8>~AI)_BY*=p0+t0hg!BK74+f1|)H+hsFS}O_} zsh;XqPb15Al&&!rdxr`1TPM?$JWyvZEqf>%adv;xsqno?$35$>HJTSoLpLviqpLww zcJ$Dh33p;-D)r3?iYCYd@!l?8)Sp2*opQdsl{~ke=e@Vv)3hh|gAS~AdJFqfeD}Zp z-u~3|RG%=Hi9SBxJ5XXD&BvSS49u2~C2?G=^Bn-K$$4Nyy%(p`3 zQmEr2l<`0OFoXnF*p*ryfuC3o%sN~>r-1F|6U}{Sg_X_|3RL1^T^MU(9CG>Qc%>$_ zIysl_99%+ELe+%#t^Eex?6L{Xh_>%SgVB{b=$?LzvwWCAjqR;T?gxXtQLD#|=#%PD zXgSaAXlfu-Xh)l-V+N9&6!DYNg6(M(ClqcuL|;98(JN4rcsY{sj>qBbH|m$qu|ugP z)a8={WpAQ%>LlcZ+h%BcfbEkPlJU}$u? z=(0tn%9zE7A=0~#o;7-5;9NqhrSTlJZ2fqBF}Gxku17~#5>q5!6oesBtToHJ(Fvk% zyPn{m(4vWeYL`a`zd|Giaw=(+Ft zoaaxC(f7M&gLYhu?77Wz!*t(m_i%ia1XnBz8h-@Oqa3739}#HNO(Wu@4cMr-2z)+&(Bqt310asU zVK;e{bpPaeWNyyA4y$L~7$}<-pUf(1eJId@VUeV>!+f!l12l1~eO$DeX~^;;R6@Iy zdpw~Ab4}_cek}q{R(8hHVZlKB)SUL9FNuJ)mw(W0K=P<$-Wl}Hk{6yb*!QTLJ@%av zC2zT*!F^}|`ZwZ=g8Gif_>E45PYr61TlcOyPqv?O=zHMT@$+pYSYO^n_oT^LLsAISL)7KjxnUaSG;oe*0e~F)(pLLy{6HQ+?qK2eP8!TbUXZ-x-PLAP37@)9cMy|6 z%NjpsB4#-3iTn+waVmTF79PsJ@x7B9g?;o7w!rCZ|LKR5_lu?!o_b!2%XsbX(5&emtt#NIB3v9u{h|{yUN9w>CJLE{GJ~mIrlZg&qvPm(*c$bt z#7i-Sk~d#4UmG_m(fonjI^bub$+zB#50gpm6zoLVJaF-=^zNg(&%La>hg%CKi~5YBYY8EAa0(}C zqc=M{UKHM+y~mCpEH!)Q^+xC(85v!79Kg4e7q1tHd1Y9aAxCWpZI+?;qajBpsS}Ll zsqq%Sv=0KDliH3dn#xGQZNVAIb*pK($0O4F*hpn=8t$Sj=o3r-i z0S_IX^@wTQK;u05Wc4*IYeAtwy9vGK(fSwDtt?;4*EpMuek6V$KvTMQQK{WAHiUxT25k6QPFL@qSmd z;6S2>>LV}~2Ueo@g^s{j{&vmNPufT$?icQB?lB(Xc}WOK&k|+zg(PxDj7vdZYJT8c zT0rpa4ww15gLOgyJ)y*Y&_E=4r!p#)38KP;q0pR>WosgC*)m5ldQ-Oc{HePL;mCHQ zgqAiU*RlnsX`&{(@W#AiU<03&Tqj_KlH&{ghf>!8OeE04ArZj`jNtM#3_4~xwyRA? zRvn3;6kNj4DX#Pr^=PN&uU#D=w)?Q%3-qe5%MsAERyUjY>$4MXtAm!+%x{qnH&|s9 z=JIjjJ!LT1pXR=5;ZpBbN+M{;*c&lNawOzXP=0!g7|p+E1E`2B~u_9Nre*Lp>AB7y7eP3*&v6jx8wa3&Yw|e^RWR z%%$|23Xt>(vAZz4gwSro5HAi=()0(qvn?^fz1&HJ%^=%<-A#}|chKm9o(S4_(D-U2 zs=RABiwD&Ha=8-?36prt_b!wEL%{dFWU@?7{Y12I5{45nlLRPw*qmev6cz}&rgZcm zAULt#ZmTny7o?&}!L#tR=_70$CDZOt5)59Gvn4;N&V9z2PogONEH0P+o9X*ZerZ}^}>cKWJ&%o;*&zR{O= zRsGR-UxR@##a_zqJlJZ3XP1|I9U6=UqvY&-x~N_5`;}9_nkZ1@Fqk%j9=1q9kMK6f zP5dC%@B!y>RawUk63NT3Q|cL{4{>fJFnuK^qEj104|TF5hrYQW3V6{_#dbM@su`^6 z=%Tc6>++|ufVd+JUFro@1loqidh3GN3Z4g`lEI5zmlX`vz^(%M=#gI~jMzox0S$5` z?szmtsU`#2vk^Y%@n5>~&fXVV*_rb)grQb)g!J*vPZkXQWX9#@ni52C;@9y!0tkWxa701o4Z7*i^}bG*?&5w9T|y#t7eC)-A|R0+%)yHJ9mUX{{}HW zqt@!FR&Uvj=In3#?^ccnq-qh+;3!qMdnTh+zB-G#IO}r{pz#(@S*qFF(9&*4x^*^f z)sW1I)Vt(5`MnNq2bXZf);+8p#g+Nxdjezok1y*>Ts(m;zWuRwSh)0)VcVCa6KQ}t zcwc~;z8HB84>IXwUeaa=WEHh1u`@cZuiy)-GseXEJ+n74KXkn};E_C{j?!QMaf>;U zKwC4@?6GUeO6{nXpn|C~y~`dl)*h$GUR9N{UYq5^H17(~IiXLP z$tXBMvza^UeraA(1ecDp?PMH-&S6%f@MM9mw`J_UPE0-=QQQcg+Yb8#e^%%8ZXN^t z>E5ACqpzR)-u(6CXf)ao0l$M92w;=?l-49Uo*a&Yh=PW6ICn6v8fq{*bh-*J8eDEs zRf<~Aoef)dU+-}KAf8HML$}3V`>rrnm5jInxMX$W-B2b~YMayqJh2yQp z(OG%7rR8Y&&K)PlKRESIw6Sam)^03_So@^`;UxF);6%MPEBNmmAkB6_QA0Ob-R^9( z6(cqEC7g|@kt`q!>=HJ+`{kVx5nMjZ`h^vPA{O?AdnLq(ZpovgS0#{YgA2B73Vmh? z?e9BjBbm3gJpq!Dhe#DaY%)FPa4CQbHNCiU2RJiSlGY19T9N!`5`HZX@pF}Lqt?-N zGX1^qhryQgs^6N&m_ZK^do;ClW^=BO6cbNh;r)>!RNZk(rGtrCygKVt#H{1FmWn4c zuVh9Cxk}mh!yv}pT(g39tQy+XDw1udW$w~aZ2==-b*89*qZ?ul+saEPvB|SNzc%>R zbmk8c$UH%7%BP%e^bK2L-zE|GNTn@>h5>=)EWyH5%eP3=pp;nZNh7+d@ljjDPHBt?bScuK zA-~I;TatRZ`iUi-Ucl?KeR5ITb!LjtYcCy<`8^XJY~}guUWa3J&z<0=M%TfHBK>M z8$6%r7T1k=KIw?Mf%ncU(Kn3Xbl#{RzSkMKzdKZyN2BvxVZly}sQXDOrxfcZ!!tKM zJW56dSrX$OtxA5^+zZsBaB!Hc0cg)odFDHsRg(!j@B@0GYp^9sCUGR{oh=jI7caXb zzGL@-@pwiIJCm(pXZii%@Zs#qgQNHJlyg!YBx~@m@dYy}+%OHI;U0@~Fm@QYZR9WW zAAOiTm!Ac2g0g7$2H5%2<)g!+;y&k0&XmqOn(kq$CrK4EYOCbvq#IuK#)4B19-JqZ zR-%q#IiNI1WPdyAUDQ1YybP=FFeQ^37#is5Q3?ZRId&i&K2Ks7g-2!MygpcXP-2sx zxPr}(JXxBs&#iin&0;=2;~v67640?fvbq3$#vU0B^1z>XeUtdq0=oe?fF6h4F=X~( z2P4|Hr_cI>Vkh(i*?k)us&*+}J@jpX))%4X2akbMzSl2_NCN_?GNgNE+`BjVj6)8C zq|iP0XPqV%y#AzP;#b%@THl4(PjMUR05fbwv(48B8WQg z@41r5kNPAJS*lk!@VzG$Cl0Ix-?7GJI+=2(yGrGdk|cYZ(~zr$XNfY^f5#E%}(|K*~4a!5(b&am?2X*59H9zWh4XS(IZ zu!{(hq#X2(PYI{k51p~n7egpaW0a!vI!H?fK_jSu)>2Jw*?|iH3DbUQf(sk8-DyU; zvxoJjFr%Hft4_2K_2{JQn&(0yc*LT4Y>8TH5ZF<{<~kLJmN zFH@|HU2P49ePuKVNJ7|P_v^=Nr!xe^Q7(S6Ye3>ci4F_e(<`pG`=^EwSz5Dj;98w! z%Ox1dQpAO~T2v_;&e zl-@_WAj$&C3|(nU`a;hppLM?;`Jqkx0u9V)FMLSSyJyKeaDg>u4E;T$idPmu{(ku0n!&q z3=Ym{wcFcP2RtHJd)%AtS9H2V2Bs$VN?KdDq-L)J5WaLu%E7Fo)f5^{kLAOF?az~S ze45o%ooT!Eu)rhqNYm=~ld-hpdr5Z(y7qRQ;+8*>CehwZ6;oi4SV2I(k6x5+@=a zNBW@>FQNtW32Q1`wf17`PN?s(k7FTOD$Hc7uCw#!wFsCpp($Pbi|FKmN_O!SHP5T|zSgIk}Qbz}J-Nh;G0;XIaC4oFrf2YaZj zg<3Cbk)m-yPf2egsgxOtm1Rhib}(S#g%i-*h74LkOHyQ7wNQo$FD1!bT-i}Xz=Va? zgXsa>eB6}?VSJc4WpRAeu@?BKe~?hf^UL522SZqCC@d$BoPS~o=)(x_E4_|VR`%b2 zI{D5Keff`5TTYSyyPaAllXPF@%QWvZPqJDYIhM3b7VVs#Vn!loS>8>%DBzWCT?7HF z0Z}fY>=kjG)IoA6)v>0AxK)!UkipHi3O2R=q!0Mw>+y_Y(p!GR3@Lg>@K&kcN{W~U zX$r<*pkF4qfZzOnq|mm>s|n&Pj5FcYr6!#pPX@9O1Gdqjx;ISiBdP^g^}zQXyX12tt_1 z;))EmCX_=CBF=JM@ePqTY?-et2LlmopI+9@4Ci(hzJxfp`N`~1T z4v5A!E@TB{JR!1ce3R4F!P{t8vAd8PYlKgIs?FZ_fW8GIz&?gl$ zM(UYGhUv6`mye7f!?ub+I=6IH**W^)_ZHmQP4#(tq4h14Kk4y8FnWG@2yIRa#m1j? z*7{u|G0`avTNP^GDtFpfSCw#hD#WuEpB9z*C&i^GYiR!7CJyK`HO z+;To?%#2V%U0&czD=^}>1QM7B8sAF>N8(%yq1Qsd2ULuouwHn6`?Gy;EhZD7DI>ZIup5vRB+U12qPZbi_NrkhR1(u4GTU zew)IRg&=_?ra#V~Z4OqGh~^T$idF~wzU{uH@;kz(&qNb`K7ij)y#UJz+e?~DeSHct zzq@wQpO9#?(H4N?k-PWHfti8AtY(waBICdXZQGrz??YopN+8ZciTyG z{4OnDx-|Lf#)|_p;q#H~qu6xI@~CExbdF=Q;+`B|Lg~!tN-)gy7|J@Z#?)6 znEF3{^sjTTyubIIzdq_Of92V)fBFmW4KM!1m-nir8{6^Gk5A&uZ~ylnzWS5T|JMJs z{|BG@Z~x=%8!!9|_+NgOd-eB!{Ke?+{Lf72 z%Fp(w`#Jcj`_F#)ho9RyKf3k8TfbWR41RhcfuH_$>eWB}cYoy<|D7aw-n;peA75<$ zoqub$y`ODp_csAtKiO68>%VmEXNOw(+jqeGxwH6#ul(5`es29DIg8W7Z+vzZ|IyD7 ztpA7Pt^f09{MXAr&wu^R;eYws-+s)WqF_I_(caIU$A9vRvmbu06Mg>n3-Q;LbY(Ce20wpAmAmodY=@?bUue* zeLoeFJe}DLk+uHOqGLXirdgxu#Dm@ zhGY$xc-0X?8RLg47o#kK0-Ft54Gg{LLe2cjAz$#hLK?|b4&HvsrH^16x0kqj$7Jeb zC&36ZWa(|19%7OV-(0=hZ!Cc8ko7SYtU9_cSp62))86u3-^SNG#Ae~G91!C4dLn&s z=3#24;3NT||g6 zQEI*3LH{1eVVDrY?+{FjZbpD9%UX%4m@-YcDT>hR4P#ZP_EfQOs53+u-$y2S3GL?! zAcdse*AF{IVj%-0c#r%w_|6_mmnJT+1^_2%Pqr2DEXyrj!yuI+v!Gt zT@%7|RM;GHH&>%NyO}B<6m`p8=U`NaaxU2{Ei36YDWdCu1+_Mk6{hY!cynUn;9Vk7 z@N4j0Bt^E!;f7(#gG)jy5yX1bt0}}Td<KyBao z-q(Qj?bh}Ty$^&>`D2x4Xpyc$7ni zIplbv4btslwX&DL*Z(^9VYR;dimGPQNtj9boZ|d9_a1L#N|{PxO)_8pg`MZMoEDdC ztdJwz-}oIlM|&HE^sgsO*TWuRGvy=YA{NF8W2NoFk>Mv+gx_~07@WsRtJK7+hB zmIGxVTyVN&*ObO;H9VH$No3!b!&U6Q0lqsSQngO4*kA-@|N7g9%Umk+ObHHNQ@hAe zgas6!8YHhFw1lp_ak$S{wzXx+xz!aWgsA~4**YLwdx!wqHKtnXPpw>>-9h)P4N45A z_HDV-+!1Cj)%@$^W+Yd4W*KB$RZP2zWhKBTy%~}V(iaTJqx=dpF1(N2zS*_*;8JZ3 z$ZwdIPA!{opb|I1_HN67?vy<8mDl*LOV)0@BBMf6MFu2SgZlOS<_(a@?HF~E-mTt- z$|z9YHR_U=XK!v8Xwe~u7fFP7D6S>kzUDYy7oc^zJK}qMcT>V&T3ax3#=faxIxDp|O za$fhJeFsPDa59P0jk=qercXB2{G#Il3|KC(I&TrT;4Z;t*YUf|4ulykxPqc@i9tF^ z(NfD`H;Jyo(@>@LaS{+#5)x@#<|i6Jt~9w=Or>pT^)=0wR@OQnD>A!5fz9(mrP7TQ zl$G(`n#tUpL%BJ8SKWmgO;poxHWAI5jYck|w)sMGH5-}*B2od(CKnCOKpLGCilbK( z>JCD8UfH$Ssn53ebFN%)QPJ;xFg%$H<&$CGMrcTk$j~abZS6> zT;AruBW5#?HieBAe2^D!5nMyG`c=wNv|9TWsn|pjq}JVoy&|ITG>|=jU2Ru)VMg8Q z`0BOP*C{GfFia3}xLE=kG=E3DG~00?;gvhM;T8Bs8?`a2Rf6(@5?1Q@l*-_=|DcOt z_f~oBRoQ*LT9fu#oSMSmyeEX>qwR;gVZNi>!SjpvH5^Hr52)JjH!P-|-p|l_nZG3f z!`62OttSc5;!vTvLt}&y#Pd5 zqjCRg_f#Xae^ggA<-PABXukc0K!@30mwV2Y%draY)b>ta7*pkP_E~H#Hnv+NWM%k1ZU1*}1Hl{CZp$}=D7DkHpFGwm($Wh%s99BEi9^UrzazBCgX zLL|sCgCu|s0kGXW^u;ow?idGIi`jT%b7N{u+|}Zp>W-Mt5U2LN4-yXjOa>+MT0s zA#d2Vl8T=vooN}d!-mM-Z@r(2tZOGjX!opIOY}> z9L(K9*37+5Pe~SvNZu%yiB<9-@N@c32bMmp+7j8gQ@aiwT*(Fx15N8ZFbEu+7A6^* z%!YWWU~%6j$m+9}yWFV)So`e|?SU`7bivZQ)A64+q0I%?5BS`M;8v!JM(8Wm<#tY5 zWvf7O!++&NLct7Yv&>h_7HY_3v9`MPPqoLkx6!ELyJ}TDo^Vfcm{Og=Y8p4A6|y+^ zmubP9o!C^b~RW)dbjEt0n`SVoVa9s*%@dX5;oAhkwO?Ia}gc1pC%VtT? zE3c4rpUWnd1dz{5MkLGiokE0M-GvC-1EJ37l}3I??nXD#=FYloZdxPMgq+n`KIvdrHF90RwULiz^K_b z1?-jk5Wc*-Nv;94hL6BySHYV(R)=ZtX{mrFEdM;zI*oB;%KBq84x)H>Du#~3bpQEF{o*kGEGB^ceBPYGexkAMkirqq+AD#Acblqk{gmhQsM#D zN;WvifEq^VB7n&SvgB((09ZF=JQ+NTP3o7~nAk(ZxTVi;lyDW+=TtxQtw zhateGOfW|&G)Osh8oyN&Avg~nYABj@Vg0YL8VHJ=5|ThdNTCg1OLJ92!ja9^1BDo( zHZQyecu_0tiKELsZ}TwO5$XjQ+2Xo7gYH}k;(2}JR=fI=2rhmP@XPnStfdj=gt*Op zBJJpqoTbWLVlAFYsT1k)#yRKBT(W;_%N-b|Y&(=@Y^GIv2 zh{;z=6{Zc>p)%=E&QShZ8!#MlTbD`^MVmP!yDRYI=0&y2_NA1SV%_?&%dtur+?D*9x zQx<)P4oLFOi}hLM1}(LvKj>;VZ-Mrpxq7Ez?$k~2RuZyZwtEqQ+Z zI3i*yY!k}d1UA${eyzDt+_=^R%5xx_syO8gQxYnW2x&KVWsvvIFXXK*+)ef!Hua~<+s_y#WlvMO${w1>G+VLdm|>7G`5YFO|f(W{j2C+ z6H1B`xKc$NdcXRbg(Z^NT^^EBFwAAD<{FZ7?`P|~ny^;27`_(}rQ|k7P#JH)rvpBQ zSLFSw(nykL;u?fDs)$|Rs=NtEhpD|}h@1*YspV8QVT5kBcI^#ym#ikM#eqI=GL1&q z>TM!E<+q@kR6S^mQN1}18hZZMY+K~%wRN#2fh>^8kz}4LF*Ola_}Lq#ME_wgQr5e= zP(xD$r+u8JUrz#6bqcs_HJq{2OPrRLhII|S%A^Ge+A`LdM(#!mRT85O^Cp{9h)OnV zq!QIET`VOw6ueKbw|-P-QnFDkx;D_Xhuacgs0(;xJru5+R5J7FeP7I~8+Q^2g9kd? zwIZWrKy+(&wakh6tQ9rW*PL8!W-`D%`$j`9<~GSRkS}KTCMcMHbAs6}h5@yDraBO> zD=3pMRFlpyEveXJLZ4$RwZ7CQ?{BNEVKsx+wlC?dZWnqnG^9pQThHCfJ}NJ~J5XCK z$aDpzeH>X3@>X*W?agzA{X(>xFxKTpwzLUEayK=emW6O51#ImQg}7NR*Z#W(#uUmT z)e>;NlWV)nG%EqtAg1Rn z&o3qabb)tJQQFxd_xOCw355O{acJFOG#59{*0Z*j^p`elP_Afmpi~eTva&qnivO^D zpAytY08(zycuz=bmwWKo_bkWh=yrBT-2tcs|G#%#0ha^M#5CG*~P zqD5O9S9?39+9bOR8va`&I$?`{Q^{1?4wvhKMU3to?W|IKBO7c?o6>7vP_7}BJ($|c z@nzw+!&mk+;imZco9I_)=i2*stMf;tZ*l2diP*_{c%{*H6-H7rzi)1KBck*soqZ)W zzAGm4j8ZZpNx=@ZMaHGG^|c%oBtT==FOPc^X~0Gq<6o!MQ?&n(xC?Xghm zRxz7*Ea6&5f_GDe`wVe5 zryFLlm9E^D3Hl3%|3x*(lfwdmqi46QuoN(z_ji+?yQgwE3T@23Uu3Gm*?og8kzY)0QhQ}No1HIWEjvoF9H?^RSCU^qt3JI4ReU9`x6;}^ zU4OHfhrzT@5N4BX+3eGNT`J*3i@_J0l|~tB8KeSMAJ^l0@IMUZg@Z208~MSl zVD)!ir|4R12U_XOu~8Yzuw$#V~ribhi zw=X>y?c|?^nTsO|jY#(lH#%1RXeBy?$$K>Ntyl-mfyx4%-NDgn1GyqW+Js*|o@+^oAVOle^-JIP#S>M?lG^0yoN{t!ksJ#A8R&3P8U|ZZp@^ zTPthqR*fIzv!cfeo_nqTM%Alx5cahYRPU5AZ!$klko=UgM^_rn_wvu|-Bfu!<)zI1 z9P8}iwwNj zO%jCt9Rn?;*Gnn8_-c|Ck}e^&hW1vDRA|Gg+^2diLQTu5n!sE{Lxqa&thKjQ6_FPY ziLL*Uijt|sR_T`|7JEB4Hc&KryILw0rH!q{0o9Kl$h}&rK)(4pj1GXrL!7*XM$;XH zDs{42U^3O(!+eFFwya@uJ6B0ep+OZ23k9aqYIp96LfbbAM7mv4=r>`E80u@VzOhqX zXF(=K?lQ>*(qL6VShnDlr+Z^!dRGrh)%{uGOBXX}4j4WsK6jx_7CbFNY>_nSraLKmwg=ux6AWg zyxcgyd#`1x;_9E~_2OC{Xd)(OW$GYcAxwFNPqsTX>4w6=B<1CU6jOYwF|~I}WEm0k zd)-M;7~UL~$$Ii_gQHkzIHi%BB;&Ps=%#5jeYt!+?R`xn%Q z$61tV`XKKSfNW{2a_uEpgT;rE1TG zUu2t1(l=B#-(vSO%%B|KBv?V;96#t2N1gJ*=%>=<-7k$WXoTTij?;R{je%SYcR@tU3InXN0qN|QpnL3yN z2poXJrM*pWcVEtvbLyV%o}Lafou_+xrsL$CXI{>4<88W}aR3oX(=wGxC0j+i9H_F( z_Wv^dJD>=HlC3KJ^ZwY{+3A|u_lw`}=W`23bg!YRcsx}QNhL$%_rK}A20K_Nyp}Eo z{0ZGR9(kiU0VuWO^ViIkc4dgl9=Z}xKmiE-eO2O$@pvpcUkOk$)K-x=(j?YmZy?#l z#4@t&UJasgKjbYhN2)8xe18MZCECR7Ye6_q>Kh$I1eZ+8i^v2^25hb>nV|r&o(-nR z+|nB64@f7lc7K2Kb`*-tf+w6Wo~F@eHl0e<0UTSd{QVC1Zj4%qA_xLxZ8H#S|` zR$bEZIe%{+1;MbmH|?h5BEOj=t`a>YTY{lAiK6)5qXUK4O<_aS zGF8KrW(tO7q&hm51Yv&{%GYdfH7Uooa7!ICQz@KKrFp_8nn9gWzcbq1GaytsET9CE_%s^frWGhO992tE7&##C*;f*1dR~V`A7a zG32PkMs7png|v@_c&=PpF5a1&^w!g%y%;OGJ1iBe1^gj-^G06&@)r`|+UQlcov%dW zSQ(^}C!0-QoF^cx@x3f}dv#;ptRqAMPze|(?hE-dn?igc)g;P#nG8_e`XfhL{$9JF zJZ5}BaQU^2&^W#*;yjbW1xrjp@$D={6cr97UAIFi&oEtmqqO<*)v!ZhsBc99Dcjh9 z!f&8U_*%r3rQPbM+jrcXxlBuT9k|2katd$P^HDxVQcASa1Z}A6ZqbCs1s~xLe5bU& z*;lC8K~mr{-K4u#h9zh}(k? zqQyXZF0tid>^!PQz#%zr3KXuay30D-aFz2({5(YQm=sKfL&h-8&c(Y`*_ruztauAr z3qVX5zY+_QK~Lok&6^4ZaMY8E;_2v}05cJm`I@rcMXz~73)vySDGb$gCAjWd6&BOC z7oZ#^6H+;byY)M$nrzn-NJYY#bjuN_!o}ZpM7&g#q0>@@yBULg{dzkR_av(6IQjP1 zlSuQb$DMbtQUXkF-`JLvYj66q=^*w$Tr2~OJXBqG1=832e}E&kXo?2Mwe&kZ^e2I2 z(BoUA958kYCB9R^7K_o4x6ds6K2E;jil?wiF}R){dunPf<=^(nhU8N#Pfhd?{h4~L zoDx5rb4j`N<8UYS?SxWFoSrdxd%Wy7h_&QmU98_b&aWRNgV3_NOeI11E`pH@>@Jcc z>T#npMOeZ`4f7!Y@SieQqJ*v>2~b*VIp~eoD0nIChDj`@Q~oLL5oXZlmCyyq;uPIU zY_+gSyx!b5?`+ykc&;^A-UDIOesD8=ytEU$@#_9}p00C+Ml!Lgm9^`Q)2T*zV}htf z{qP}~t%cJYa@0%~w@T2_dxPL5h{?fh@%FqNEp1T#&uTtAQ4C{m<|ViD>(@fzWz36r z7N`@QT=e?k;Lh{cVLh{v^ibdq%=IJ#XF=x`bLHc3vQ&wj5;I z?`?4?;|pJ3JCW+r7>X@**as`_nCwt;_-XvzuYP?coKxcss}|087~(uIU(4UDHjylc zPy)vY&c)cg1BYGfuL##x7~HQ0it8x*3Kv(sH-8WeuVf-E>K*c`GNuwByo850Z=*eF zjni(m{k~QtVXv&EZqUu(r6ZrL-ps9r9zM^P&B+~VdBb^wGoLtaL-rG0E#A7~&|lfW z`ip+he0zWKYLEXRg}pdji7b3GNDwHuqiqqG%|hIGlFf^W6+6~o828Xy^Le$ql!6tv zZ?Tq{^llHKx$(E=ZrlFQN&}2Y`S`b!{PG|%zxCc+BMUD3B6F!%mJX|-xm%Hpd_B=l z?H26q!gA<$zA6-vWZ4U}<`{PL&3dzu9{@4iC)wY2|=riZa=8d##WwnYyTm5%$cY=;I^!HSudg-=KlxhUNBS;lj zvw*!_hZ<{6se*grvm=3G5K%&J!mEg*U8KXU4;b!$ANJO-%4i%d5U>5$7u(;-Zu;ps zgcqZRhWM_b+}|ddOljx4jkY_G%}@?&J_Z*zJ+A;xa44|m#Yk{6>3VtYTYoo|CsITT zoX~GMi?@Ce<@&!~{`Wt9;rMS}zwu)8_Q{t~us``#6b$-@#@_$)kJ&%}(ck#d@BQEJ zmEV7NasN;M@t^d|Pk%6I&i>g~M!$Id|H=MmpT6)f&70R=-1wdU>dV&?zsBDB?n{m& z!T*xI^~>PX-~3hV>(|mvf5G1Ra#V>#S;xByt*VtRH{Oj4P|L`x^TYvF(zY1Kv^3ttToSlQ=zw!w0 z{QlDCFF-$p=UxnN{i81*;s5wc<>GIc|2BcEf}Mh!%*fjKzS@x0rV7*;A1v?qPkfJD z0}(+<>=e)>PPF*p@ubU->C+oDkYVpV>D6-igGkr>CsesTwxr%(f1D|t431wjqjz`J z-Bb<&3OZT%DF#eQl*GNUJZR+)stV)R22_)69IA4OY$*ZI#LHZcZr9R{?(wWJLCU4! zenYg-Y@ty!grke+;9O2*9Hsm3m+DWtF*%90)}%`{RvJIrSHW-;YISnj_<`M;9tkjV zP*9_ArBml7odl;o%4&s}qfHEQmRjAdHkfGXy=?bc9;^sR%&yc;+5;W_tfe(~YLWf? zT!%<`dlg8oiw~$u-D)(!z%O=``mBF;vpgvi?H1(}X6azD?aS#OJQel+acS`3v-P$V z*xMhcbj3ieqj-zY6s!Ti+l=>$@ggDS?0mJrW1~r_(W(RSK+zmd^g0i$rTLMr2?Oy7 z09p-!nM~f(5oS2s>2%%YS*ZcV zn!NYmF(DT$n`Y|yQ2nxW371zoz&B>?8pVNXz$UEss`pcdS7@F-QfpSO-_L<+Z|30W z{X}}A8QMXLi-5;CJuB=tnT}C6@GdRag07wE!P|Ew{-^^KvQ;%2F@E}%&?2+>z7Toq zlif~MFWxpPj{PiDC`*7D`@^f#nj|(c0{)cAq`hka($2n zb&0y%YfC-k=(4*mPHHehccQfdcQ7qj&I41e&$5pju+cB|4=QXIi1sR|ZdlV#^emLi z)`+{indj-}RG*6OiCRXk_H_IYymssU(V@Tq7DI9L_5}EbYW7|^(B>xwi4^#ucFMK+ zs&u&*95d6LKF%{ixjihN{*dY9dKpQe!fGpK0rZvBO3H$kkr+kH2w8Jhy)AjJ4*{k=UMzL*lja~;P8ljzuyi2OR8ht1 z>Ab(4D<3F?evccS6@fLhoUc8$TZicBW3ei1RVwZDe#buJyW@fEpzK4ojg`APyQLN7 z)I(7EWvb6E)~8W?MWr{aURYo$jgZC!Ll`J!v17 z4_ZTJT4*pzF|yA!l(b3JXXB^kiOJd87=ziWniq@UXwly~}g z-fq?#(>8dfoxG}6)Z3L22#`{CwSonk>BdvF%N9(G?ez3d zRKJl-l@f|gnK?56_MI#WkQ+Zaqn68;_u7^HQn6*4n0f4eGd9faHMNf2)Ycouo|zS& zg?HcToTaNXRwrw0=7yltp_@Ei(+39?0GC0PK>)-kI?9UDWiHAkLN4*|?!$>2vwos3 z2aFwMPB`Y<%@Q;f-m+aWuI{RdHD~hJGJ4-TsTFw=VDG9^mlP(%p(VH=p6Z~@4B!|Q zsG_(&9Q6KBBE>vmcE<8_ur_rGw`1DZrT3^JTO(@~r2=<4M_6dIh#=>KY*O!hSfo19w}kjhT8lC{TC;-cNyf!eClS*Q`CG|;2)OC@ z$?BvbNpW0V&l3;E+}%%}*T{fM{iKmI>_BHKa`l{Pu>;jvO6Hg+$Ga#_Ood8PEjNL# z1Q_u49v&ElY_s+Kd$VXT`O)2n8k6{b|DNz%ZFh%{JktGA08CzYSw<}Ei}#+Eg-3Y> z|6pWAhHbog?~(d>QQEs`JpOsuNqsu|2W4*8NN?}w-1b|8{lVLm_U^bP>$=@#FN_~a z0qLUq;90BJoX2+(tWtMHS&urxfTRVSCGX~h(=*$eIcmjWCrm4yQU^2@CxZ}YM2lUI zn%ni!y#dxW#`zQKho?t$>&!l{T#$E@g@V{OnLAp&G++lgTOgw`vDn~B`tI)8U{W8B ztY<|)b3Qno>W6IwpVo>e+ds=3&w?DeP8`Po?`} z*#wxPvdW0Mc~~wsl#U*KU@G0-icEu}#^aBB(#)3YuCuAzK0YGo9GwE~39iks+f3*y*}f=vt$ZT8TrVLJ2KU;#PKh6~FoPvPDb=CwS+KQJLX; zH)OwD2F5azK4>Z9=lkEOQhCM5+`U))Q2pQ|=KIwV+seb}cdeZQ;ALe?U~FotH%g`&<~Vqfy>Dm75UX!OZ&xP0%Uf^f1^FSjSv-bohUZ-dsXnLo%sliF>*yPwa+ zn`0ui6Lz08M1}wi!I65zsnue9&Kk!v#z9>vlzRO~ns;(q1m0}1h=X~eQ!>hS3yB`& zQ-_tPR{mOhP-pZ}#yWnqUse#M*su$S`h+$Gfd`7d3~5)YXRW=1CU7YUK;Nc4YjkY6 zPjIb*)vNX!W3C#zt=iG}QC5$0K*|VYTMybB4^(s1+9}RjKNhRie$F)!U}$(O>A;m+ z-u-X}#0bYit4Y}%yRVV&q75V4-|dNqBdORhyH&eYNsl-NNG^`p7OU0_sgUf|yGpkt z>?2*nxJP28L+pSSs}NT^ZcS2W{N~Q7{3N|`SbAGdRVV7`yxwl*wf;^g%XJFRn?O?Z2lu;?^hN6E3$1wGJT*~C88mC*qkbAtnTBUw zrd_Ulyh~h;YVk^a^w2&SRN4FI1!3nwqrGz;)2MoL`tb=USo^EuUfHf-O^&U>YL*Hh zwW8AAohTn&4lTUH&|*JMw5{~!`INPJ(W2FYGA$8FDBUUrpG0@3sx9h8tWvS^O*vmw zdZ$F)nvHs+eqF4PhEc&Q4at6x1O$jER@{x;OSE~hu4R4BgHD?}ZUFd1rVJLg@?OS# zC|~Ro&HYZTu8?goBOH&T)7i0-Zed@jOm1D04o2u%?7(AcmJ*yX(sw(DbY<|a*!<^F zRLLAycd}qo7N&!G)5!@Z^|n7+RU5f#{vx__zWX-dm&8t5F}**@70T4zTuGsTT=lF~ zdj70ft{fD)cQf?hEn#%uW{xR2lDM0xtNCWD*trYcFq_TF^uz*Qk+}78PcfPAW{&Dv z5(V_d$P8!|E96+!rIxnUXf&eg#i@%CaOtpmb{Aa9T88O)z&$gCT6@UGYI^!63$s(a zImu6o>!TUqEMKOqUC=16r^%$nSxQI)H}0ARS3@;!;DUvm=4{scmLU!YtD*Gg#$)y1 z?)nUvg6vYsPd4-2OkjwKQf^jM)sw?Vqt+;2?PWQ2_I(ZerahTxe`>{Lz0YQS^4?6b z3YMXX6VWUcOrGWR87d77Q$G;1TG%Mbnz?6@m1(xI4{PARS;vaCfE91HpX*mmd%$99 z1$n@Waut*^l?U=k7n$|C&D$+;TGIKvD9Oq|+{cC_|A3EAY$Lr7HaKi}de~HfJ-gSW zcd62(${5|>C;gI2$E@Zn%sgo`R#O4qT0zHag~bl=ugb1aM!j4-WaVll9{ z=g^bk2*jF%Rj4)wKt}U$T+civ?ec(6cRDOb*QHbXqD+2hg4LWhD0CJVfJkAjP&y!~ zA}~d~U5w8D`#~>=+hcNK0NL%mK2K13Av!5Fhx)*@r(J-{lAzH=E%g=W+<2*4+E0B1;CO@Thn)?yd;Fut-3C9qD5>vt_5gX>(%8nu zZbA!DS_$Lc+BXHdP#wB@c@0A!7<)|J%8qJ}tWOPYCv&jV(q`raBjjvlPtI}jMR8OI zq(J$kZ}A|uQOp+!ySu#iLAli}x3pLXAglJ?5}NtLN$GKu5e78lP?$s&EefY)a52{V8-9wa4cbB&Z)rNsU zY`1pC1qa3jci%F1txKBBTP1{O*Oj)KtX1=-3?a)qQ^ee0M{?jn<`7hCs^! zrlwgXS$h0T7*L&bX_n%7=CbSCQieZ0kwi7ucz9m0->VLvd3L&E{2k^nSKW6^jF0mt z&jIDUG=+;bztYjF?!vKA1Lli&2;r7AnHjWIG%hF2Qt2V)?bgPC?3#b3Wx~P~`+&Zy zmzzVeTe~|njsRq}CU+vrq= zRkRe&8r{OXwh1Itby*;wBbo$j4&`W~DQTm-o8QGc=LbCd)7;Na1_uv5>kKsg)X@0M zIFPA?NB8Nc4~*(Cr-gfbIts+@Rq0$*lIpm&D_EHtS7D-LxGfJ(OQ3;nFV@&LC0fco zqZS+es5ZzY6x`0v>aps`;{Ro)IUd-b zH=ig57+8+BSTfyI_iFXqfQx2G{Z711nbIK8PLvg6+%V!+rj)NiG6~o|<$RlI=SWU3 z30;O97oOGM$7=q^eW6>kkN*(sVi?#_qJ8V1x<^k{TFq8qrf#UsBG59e?%<}{e%3A{ zLa|T;Gf?}r=BJWS%Z|G!`xg87>7_&o_t3(zFtMoQl4C?7&%+ z07uY#%a>#E(1TeG#8-@r0NQUOfj--{168&&v)g8c$&38nd!JjE(;R3)49;v;I|9u1 zy(*9ykrlPT(T}hpLtMSCF z6zo>BnQ2g9omji|uu)W0i$s6hL*GSow*9Er90E=DKHpr@9yTvC?S1{A`IG%Uf_QtU z@fdxCn09U#?M0OFp!iYIEWZ-lh{*rlvSahB87dVBS&%CsoqVLiPXwW{T^UF+qmmNMWB+C=h1 zr*5UC=unRpPKsDF6YV_Gs(X9=PH!?Ro%BBWOzl^h-G{aH25>BxpWTnkJXch{>U~_8 z^#_oQ5hY}hxo0uEy?0BIz9y%%{@{VoA9dP?wqDRj0P1!GovYS~mMsl?&53&E1zauJ zo*4SkL*c|Qrw@Gtt2n5G53>HW-EU6B`|9{D3!yrxv-3hJIozA(=x5cgIcZ<3i$#EA z>=av_QC`>jr^HTOeO?5gLaPGyiFI(|nF|%Vs#1DHe0*GLRffdgB?}bs_G#Nh&sDz& zDFCr>R_ipVHxD?XUhSv~F>Bm4HAAw=+UcP*6!(aAkiuw|G%@#%t+yyv(^Q~F;5oi4 zg1N>~$?4Nh=lNT^P5wv2UFqm(Hb5`mbL`vo-mr~u`TY+l0Lr`BJ`?U--X9+2xqrZ` zo!#9he?0EK1?SiardfS({zJ3+?t4mk@QAs~3AA1hJhcen^3W!XGe803&GIE}w7PwG zpp|9^lZMU^U>&tPJ{uQ|p#lhyhh?O_udP};x5Tr9cPruc$lTR|&9*qs-zwazgaNIo z&{&S|o~XqkOzW+_1WLIdG?~+0LoxtIbwAVVHJIJ3UC?XIPGvSYZZL0mc=Xt1X%>_L zNL56?c*OPcN_F>uA8!vqhq^Lo3im(PcG)~%s8dZ#=(E@`J&`YV+3KDt;-&qa3M7_t z)p7%{vEL!#=|TBJ{or6NZ!H(<4itWX~k^;)$R6Y>*Qs{r#It?p2Qby91W^%9ejlRE<{QLy%_-EjjI;r!l3 z{j^ZN$Q->NKkj|5yv!Saw9}pS!E$~3Wa!8u3&~jQ8^*!Oeksp`Q}h0#jwFhE-1T?H zd(~5(*Ttf(c8YCvoT>FYg`BhMJ@a0Pf-~Ff#HZ}Ka72eU|M*IEDKILqgcj0$W zDv#==WBVBZKS`gCnw;?SV(q+T>!rGQutSLeqjHc+a`(<2d(ELFpE1Yvda_wIrUhU* zlz?BtQ=vI95Kp+!X=mVb928_KGtCG}tu|GXf;Q&GqRyB0^PE?+Oxvv1J2ahZ7Ld-u zFk6wNirqX;+bO)GQ>b0c)%}>|h0CsXcn3xv7PaL@9i=dhj^_h zi&D2DkhNJ7t$e^6vwGGja2MClT@Sj&1~eI1qtLdUZOf*SpUI8Zm~ZNZ+CDY()y{16 z+{jK7^hfG*X;=^6HwK+bvSNy2(~{5Z@?QRvetydA4{D9vN#z>fxACL#0D00{1NX}v zfH*AiYNoZFGB8Eur^;wp>OeuI*dNr|KhEvn2aY*mz*DXJ6=e6Umnv0DhIwzIeO$wa zqY2kJvQ9c&DSXOTot)kEXqEN`#3xWqQeL6>jIj%K9T&}!~J zV7?4$d9a8qiF8BTukmA`1K2V5FDfU#V_E8)Jm()0pJ~gp=UeP)HQCrI(9}xk(;nYE z)eDViC-Xz%iC(PlwhyixiWd!3-W8kuTz|1@U}dH!=(W<5ysVHzzR+NsC)pxkL|23% zTak%yU1FLNFiVUog2?S8N+KMchD(+;wCm|3-Cx^DC!Wso{7A#nwRiVQ8L=|3CfML$ zpV=`>htILY`?GPeDoc&BQ7zPkOIjc5$eG=*4ksBq5Aa#-s|YpWUu}p77PNap{rFU) z4NEEm`cRG%Laa3@R7>Y&ORLvMI$xXBirmuRSfLxbZCCa&YSa=DxNh7(?cG0mi!sfg z8?9Zt)+)-)J+WD8TxoTV_6(t#=nQ!=Rwy^_RHVmBht-w5yx;oZN&R&1ayaTw_W#K8 zJ^ZuLG1B0`%|*-@?@%?vtMD@3luyV)T|X%qO$#uDYWzg!XrnfllRo5SD=PO*n^dE- z$(~8;Z>w7A+2uPj3@H0Q1b1Ff>qKVw@nhm18A06gw2+YR@WoaEXhqn{fn-gZmektw z;(MiT?5HQIqd2EYAL%2CK2k3XyGl!!1F6<<)r;j)#=(`f{fb6uwNQlvUWVRPV(y`<8Gn}Kjfm33NX)Lq~hu{Ny%fJReNBz%l48v>0UQcnC(pT z-bvzKrT39mBU)(i?Bf3TPn|-`k|*tzUhIPrST^PG0jd0yc{`+4A;N*4+{7;rHhXLg&N zXt_&t$EmEpS)TSzlv?PXQ5ZtyJlJQ)&z03@{K?(CdD0q5<#Vf?-D!Zm=s@2KcmZdW z%H8ASd+qi`HxCJ<-fDKqoe@&}`_;oePxa@Mwo$|EM!KHs^r!X?ThqIOHEU%$E4u^J z%HGV!RZeRMy}3Scn*yYMBd2X?)T{yw8k*V~(Z;~6-#?f=?CqUg!e_Bmp?FfjC-K8` z{ZRO4`DzFIL{`!qp_Z7EZoHe`2}bCD0ZBRh_9?0l$}_OVZ&@19jZCGzNoT(@fN?jC z;aHb5)5VM0)>?3N+1vu>SQ3SORMWNSn_Mc}=*AE6>pHL=I0EXY(VDHf(V;gQ*% zO$!gjv2K4-IJs9FQb;$Z9)4*RF6d>xS=>3jJSj`4R_Wwvs+nr+bUOJN5o8ZD!(?Dw z+lRMli~Q(N-8-4Wdw^YLloaloc*^(YS4cy z+DDJx|Lg<7_yDv_XUYk`!|`zpKLYV|B9BWASd2BL%gMKh4!}rH*UQ6+(c16D+ak{n z3wL2)7R|K)Jd6K#m~!!jsC);_~ay;4)@jX$reRX_;T8CRsHUhH4U z8YS&K<;90C?xNFX?>^9aUqG_JDs!Ta@$_CL7v_s^TNXeJRqOTF#>vCbxO8++}B<$O)p_>=KZU;D-Drw{+fPhQv%tJh!rc=@$2U;kfz z@%7qo{0Cp+tN`ljFX&spOk4d&{|3y|uYy>=mU_DNH-7c6{FMmQv*q>AUr7I1Ztlfa z;eYxv0`)(G2-N={fvfzl5xBnl(t2FlqG{rn1g^jScmCGj`Mcl#kFWgtoBgW{u3x?6 zuRO=?Z@vH73&}Bfj(_iq`(Hjs?JuKP|7Hf)tG}KD`{e`tC;#e)^K0+7jrPI!=bLr@jlaZF- zJ1IZysqG~_y9&1f&^OGP|!~eH5DBYAYJh|D{iBEWSw=dNqVe-aBTyo zO9Adx_v>GU)5Ve(CJjKAxDobYta~MzHHRikr#QU6u@qwp@!v&mI0JV!Qoq%RpGTO; zM(}2kxQTUL;i$i;dKDyAY(*T&wQVRZz>ZcW;D-Q^)W7KBcKHxx`dJD}D@fLygQ98A zW=*Oef0G4N~GNoyWWZbUCu=s3GxdoAKpnVLK|v{KfDa zI8V69^d=Ml>^2}Aap0Pt=9XS&eTbjBMMXi5iG`Ptdc-3ZW6^fH&NyDBw<$^dj#deU z*Aggzhh-_&5zCl?jV%a-TkJ434;V`kzMv#^K)msX!q4$gDk&iN5tO3i04zn~x8$%h zSt4WZ046b!0vGunky8q*MttMDYPpgwd-HDOw^>lj8Ox!?c-QB>vJGuZ2`3Xr?X7jl z7o0~C(C#2$A+r?$6_QiuB8CuToH1-K6f3S}Q{}b5Vj%Cc8&P^4Lu5Bqa8a*UL5mhZ zq1V=`aYP1GoW<=M;0`O`@E4FdS#)fz`g=SOf5OT?RJOI;QIXag`E59wKRP@>-$y`qUW8j zI?}?j7=0~H2Nw`>4r|sz(Z~crS(uL)nRpuYgjR2RXS6$+b4#J*JdC32@peW(V3I=E zOF=rFAiUBRVBx#TDE7^8rte5)bD6b9%*v+RIG#M9fsAXB3`ctewd5^<#p*Zttz_C4S-Pb#8>YFjU0DRw$^|@xVZKDP8G^hd&E?r>Ob(OL zB;8V!gaT!9X*mwyR)r>(S#)^=l2nswFC`N6OF-4l#vLmqz9M-RerHbAfh zQ;K+!i3GNm%vs6Ab{PvhQ{e~!loGTs7Yosa>(OPsrQ=uLu>9a%wu>Ql!HhYv4*>)} z%H0frhtH-ku^9v2u}~_~huXBH!W%B8=}|X<^)e8R3mG@WW`l9Wg>2-m>nx2VnOx|Y zOl`;A+mQtT`L8e3c!%>A<0GozR6VEpBd=X=&_qm%8^Kl*TFutUP!Pk{w^I!)%jBbI zDHfiWQXImNF*qA5#gPOP^Ti;VLbEY}bVjmhdK1S}BH=8A*>KC7@VIZWn+hF(;CVcp z1hbKVIOZH#UdlQtq=Ci-Y>8fQq(Y^|dJv8VQk8AeWqzxe*8?mLKJ67>3aPr-HD@Q7 zu#DL2xzoVL`qrxJo6zD$2vnt(-LI|d=S!&od)=lYZnjJ_#l;Y|c0aJipzGCyv7>GzlPehK z9|j2QPRJuUfnbrbi&?_H?#aiKBq`_cT{#7zWVF<2w23vY5uloRab!n)9nY;d5?f3R zK@tcP@w$`YTyVCjf&@|^eJ6AnCV8-)CTwSh3_^)u+KU3#6Y1=@LJ@w^H;DUERzz3@ z?oF)N^#Tc0c-P4W2bVI%~=fdG|axJ-uETJ9BtG0V) zCE=)V8cf%J;tGQxEhh#~2MvK#^6thQ>TI~OFBb{dT2^poVEEbwm_umI6&Cj_CRs@F zB90iIRi9&Ni}voXZu*R@2Z`jq7I_ng(@Q)hRX1*v(3{FP%DKeSwP>b}-JlVh?5z!a z;L611j^F;B+p%CabaTO_Z^>KH?aJCU_x0e`YcE$ufH^q32H~3&mvKgmy>ue6#?{3@ z3M?&dSIF|~qg+%^#zlz5BSP{-qi-)eQ7ntgL| z4v(}WZ>}}?_0`D6<|es3Uu582_gsMY-3o%GeRu5@{R8$+;Fsj8Xiu$UYd{N#n%I5UKCTt?n}&Kw0P+Y);A-P z7(mw&isG-0c5pn(=Mf2VEcZ*qffs&FEsj;P!wo z&iQRAP2Pbs|3zvewi13@BGC5rvg!^^c#-fYBTLb@764rLwX<2tFL#G?8P=h2&Zq$V zMyTS4qv3Eg@N6wekwTKk>2<$jDZ0h3YvTHa;$MXJk7m2Qx#G?ip3~oRn0mTDJ;Qru&L^cB2W8v_h7lte`V2V~7+E9H;O#{?|OzZ#m4pkW*5s1%>gW>O$z64}VXJ_K}4uXv)~t=yLa zm0OY6PAU&$-sN@93$NrWNiQDo22uc;7=0v@4ww$+Vxk#JVKVAW!}=tHQ*7KuH?wS` z3(`Hg9?hkjIk=*hE^REy4Nh-53j=kBTTkM*YUib{z~W{NLn*g}9O%am<^3DApph;lqk z#@EwGK+Hf(u|OnMBVZAakTmeDdJK2q1j5%dzHGpaxzYe~;CDeW9$s>-0Hg1XWGePj z)UiT10KbIuIy}@$lmfiHL|6=LI0@5mt;(KI@Om)hUEGL4 z0hh-spVjt!!e@l;v!);&6?5ZrVA?Z zNQ~wnu;;oHL-M6NPf9;Bi3GluWWl>azg6}Q9yj~OYYDr?7IRRfjYbuo)2Tuz_QEl zVYar(IOTn1ov$+(i}D^K{8a$3#x}gMO?QmWP+NuTTP)>F1h*+S*yvLc$0rm|i0ymTC zYyhlG640X1V$qU-N}9-|JYE2u32jlih}kR%$$CCb#M4wPdld=sC2)vc)HDoBvoT)| z!>17lbeiAoAt&qRGf<>3?|TDUkJC@s`ax9$_3xtr3A#ra3iYL~bQE;tMp>+=PNf14^9Z6hCIei<> zR7i6~Js$s6mpjMY+K4Hkl@vkY!s@gP!4)lV#TgES6G%7~LjfAm?S%=LB`GJs)xL%i z9Gc=2l+UL@f)@{EgV7L?jYmD97!^mi897V(;J~7z34Hxj=?p79HNON!G6D615@LfuLR`6g=kUGbHL$kxiD;zHj z;d3GUc8cJ_RKk-=I_Ao^REuI4j65&72(ZA*q_dZoO;-$qPdN%fx=XR zi$PmSe1q^T(a9v{%>ceg09xN%f+aRa=Co9FGZfe)i6kPw!Jx$Tl)Q8Y&NC6V6)sCb_bjibL_OOX%e^VAC(8&14^UX1#we$mfAC z)a&tuU8}XkRo~ZEyt|<}*Hyw1O?`Wj;c1`|*-lODwB7UF zgv?FUnXt)pG~U6N^jN6k|K01SYxC>z6p{(njd*AsUX?MPP~*&ad}nh@bU+Y|`Glo# z)o)@xQecBb5O|Zbn$H)M|;eh@ZY8ie5TJnt7HBcdEi5C-V!*kL`1dy5aLlDcTWdtZmp#B% z6&DXj$0dqhm(fB54MkL>Ln@(IsGq8b0b@a9;RJ`LVlbA(QLr#{uCoaj9YV4&34Nmi z+^>F&pi)B($6~H{mWTuwuOSqhiKK)k1}4RO5OU>eW|sDD=eyk6CaCH9oM{>f?ReIl zXZbf@x#rFyuON}iH*@Rr>zWjzLov_o8Vk0Nfd+uP5DJzltR!^B@_XOcE`<|76-6*UO;Y!P_GnV5R6~& z(~@)}g|Eq1Vo|+SD>GqaixmG~&fYslvMoLA;~zF)`5())MH7-AA(>dx)=FM3ce(rc z!oE0fa!=>n)m_zmI=dv0|d}# zxaMjp3xZ`}fEsl7t$Xh|Up(hKzi%_C(a4-8&Ug5m$DrC{<&ewUa)OXi4c2@CBXMo# zcXm#k02atU-08^Wyic;m^)=zHZe1>W#8)M`OYCLc`rZlj59MM3+xviX*D7EcmO$gT zT@FjRr53Pc9~}tU_xWo}d{9dx(!T$pdS^H4{zsnuL-x_TqIK!|osT)2VGoDT16iSD zH+|1^&z-%+3JbyV=Od%svMEw6Z|d_aCnmmSSAD#xIFWIr0{*>CzVgI;ytKK-bN!y= zwwt%K!WGJo_Ksv?Wsq%}@J!%P)iQg1k9$98mqs4(#+K9)XZKy12zXe-T~t;((i#pw z;IsBYZ>3eaa@QDUuXv94KHhu8SvoQAus;m5d2zPuu1L>E6zW^@pjNZZ-M50)$p&pe z)Dr*TZGpwikxK5}vvZdC8iO&W7cLo%mL0_he$FR9(1`Bc54^?Saf)w)P6}Jx{fOmW zTI`T&A02%1-$?9v7E36H?%mdhw-1&#QU_kHn00HJ$9jL|n7fIrYsBXJC)`yz>j(Ea zM?8R#-q*$BoQJoL=Bw5uQEj!-ls&pgXQBt_E zvkGr*e{^g+*k$qm_M=w7;)kmVW=k9nJdIjzY@Xj2)ZrayFd$@}YfA^^R1xWfg25 zxTSKL$tN`Ks}79dh!IEqZpg}YLtKTLXNvJGqLavx{$D^w&@Qj2=Wa;lzHNnH7t1|3 z(wS2dpkm*;%l4am(e)~T^xGe>4n@{u@j7(q*|nS~GhlIfAMm`lw}neFo>MJQ9EtOMZBNc56zAaf|^QB7r_NxAvFOcnStr>N) zw-0Js>6;t{U~cWLNkstZm|=sK^{elh4z}fHjZ9;S26zIH)|d|01$$n$~emc#UX3^;X|=CdvaeR0o@>VTRiWHn6q?eXH^-Lyv^cD4-Va-PsHonjrUgf zK(~~W?grbiEx)^@^g2#9S>mPDw?1?z*7vqHj^nEqzw6;H>$bq4N7#nDTSovU_{rwx zZO6{8YGXyXX;7QD)$gm^taT263O@iSRY`i~&JO!`9(@przs1|vOLq^%1`*F5|1j`-k}aNHwZ>66z8jaZcuMHV&wD7k&)?ZNx_kd)?%K|MA9tVi$v%LL za6l>Nq0A{$Xbe(;X4xbSgY~oVu7!If-BhWp4}?A?f0HXc0zFM^Y~I1Wr3!W+3@Ct2;C`_|)6z#c(fxo3EkIo^~VnYf~TuugUS zQPv~4V{(HIzb3<;Y}deY9xcliyLUF$6n9o+$7{Ps83W8dUge8<2e*U)`JrhY;=Z#j zFd6sWx_kU+jm_Oz=D&IP`)2ds-)5hDU#d8{vm^$)9h#+sTZaMwCR}@;v-RQD9m&=S zKyz`lpsB;jrURt?k(~#3?YnH53G|0h@wNn(Bi{1S#)nHf@xk)3l(V`aJ9?x$I9NI6 z0g+nTxGxYI*ZEfV!6xk6UHYzZ_x6GDKRQz0)-SmgJlXC&Ib;Fo85LT1S*&1Lj3S{3Gz3s++4s3>-7-sfXb0;(N=^VA-sCAdgc%)a}-oABL;yz@(f5h4JC|Bo?LOEXuc#0mr-YigoWSzm{kt}34F)>V14D=f)lZ1pKJ4p4M#RkV*5b6B9JU^?y|Vs z9+7lO&EC^+-xJFO0u|e`D`hEIBISW>Q^r%SE$=FpL@URjJs{i6X72?!Jil1Jv%I;# zf4F|gWgS1bZ9arRqxusz*JJ>Yc8B9Tg(3g|Nj#9HaX>$UWvTecF4rCbB)Up=yamKQ^zJT4t!66#6ntN>3SvDW`$WVC z(gFJHDp&WmA08S`?h3>r{vF=#2ev(l@?_7#m&iz zjlCY0NH+Fm%g3fQ{^1^If$iQ9a1;jztfjT}?L)=MZOtK@hk)e++re$Sc<*RK{1NYP z#j>JY2DTOuvwzQJ-nlOZOJYLK;rknVyZa^1$pgU=OK+9l7x9HB_f9O!>^07zOaLFN zKNJ9NFvQo~1HJE-w?5(@n7IHX4OTn363~2VSG_OQ3pd#u4&SN;ZLv7J3eb(??LE+H zgnz)>g_ris-=DQEl zERhosMn8D3E-=fuw|-anF2};Y>pl2^99hq(B(cqvkEMLIFxn0(tP&7?+y1^sn`3`} zSE#=GzIc7(cMi67!VRVQ@CPFH&WT{}x5V|g*c?y!@W@(HYwp}T7J{ec=^t)-HT;!l zDk;l!dU8xKaP%%1-HH18C-#O&;onyKF63A9OgJcaFk`M`68B zW4Qat!7|1NkY0%ugr1;x1jz0rng|0`Q?5(rRa^T&y?;WkNmIZvvgC)hT6MD3I zxF-3*dsh3=CmPm!Iy>*d8xexNKjSUTl zXEy1#Z6KgEE8Z1 zUf&90_du7k zZHI`jTjJ$p$3EqQs#C`bIsK~8VT8lBfB5m;b@;=BW$5^ke`6JF2|qBJ*jbNlk8d#B zg}*LPY^w8j_MiuQ~bj$t>XdU$wGt_kQ@IPA9$Wt$HahGz1hpb{VRjjQf`kvPQ?9EbI5JooP5 z_a&n3_Yb(AI5)S7(MLaczeu_w;jOue!L_AoBZG3S@j-0u(1BB zq)YR?-@JI^pa0D8-Z%ckKm7c4N!QC?Fu0tKY;J>XI)3Pk+eIUfC;XSnkU#S));O=u z+wKOfe|7zjeW8mt@*B(VedBKXe|vrXul^E)>pQ>1;QD#t(|`Jl(yw2tZu)Bm*U$g_ zZ&Y2q25Y_1@_=fmo8S1mudAK@JsDiz|9{tB{pw#cxc>TfzbLr+{=c>0s`Od=>J8nW z{N{(>h%*1@>qq$Jziz(x?^ke@(x4(;2!h$1s_}c$}SX3 zmn`}$tujG&Njl??dQ(=Nr8W>(T~M@~(AcYo2cbcukCvlWa`n)i8JWZ*z4{N;Nrzo# z2_(JFaUu;P?UAbsRiI!Li^mASgXtN{O29`icYIw%shDv^KF@#>;81-RH9V!%rU6BzVpsur#aT4S$`)OBk<_U^KKR7NvFW^?gG)yx zswvot-e5{>b`aj>>^>n+ey+^8w11|Buej#bydAm21igan4v1;sfibC1*n-}qUy`k5 z_N+?3B3E(7916Y99nlye^}}cgYDA+=S2LC{q+7XY!t)OrwzcG7H57{2jn%5cJ8Y!W zT0a>7iX2i-9eRGy+m2h5+ID2K;JiqL5wR;3Gg|$fQe-0tLjl(<(3Rd7lSl%_c+@sl zXJU}}WiAx0#9O|ESFLbw!j_=6!VT&Yuu+y4lA)?smrmPl1-Nd%PGa>Uqdc{q}Jo>W&y%fpowV&6gZR`|7`~29FEYYN=oHmg=E5p?V4&!*u#2y3kR0 zOW{v*2I?gu)$L_MNa4yo{RO0BrlGmB4ydA_sx zDwK`i9Qmc7Bhtq*t~(Mm@rOlqX(vAP`S#k9iGax3@&UY_nAG;*Y}B6jMSNBy9oG3x zo*oWPO;~D`nB1uKydMY}vs-PWbz;_N-Ily|(s(gF?zPJD*y`X-_q)A_`lMw`A-A&EM3iAA(#ily9{eUIry&e5=kY-D{=i)ABlfs7daUC7sV zl}bvlOmjb)OU0ptKBnh60Zs?4e!nAyUF`#3`K2v4yA(z=NGLDVW@Lk=%UyO=NGS1f zymQcX1)APgpfdFN^VryKPp8WnpN|uuY&kSd6G!@EMc47 zC4(%UdSpEr3?)rP*p-d4QqA{aCv^9+;`{a=TD}$cX)W%6-yVm>9k(Fe&6%{3j-iep zCbGK9l2Ul#iNHA3ZZ306EmnIlZwfa#{(w~O%ND%4KbwHuh`Fa*(1P^54aK@Kb zMG>!YxT1!|VYk7RaJGwSp~so>ZkVeMZ(ALIsBr>N2)_V5oOLEVR8{^o2Pgge1DCmE zDM(yJe#97agJZhER6gtu+TAgy!wjd}5Dn_cAE8bx}!ZErmalctVcByPZ#8_1YTXBg|Y8|_k z;+*0gOD<0G{)ceyXa9xRO0hlQ=uN=U-E9v?)!b??t9|-kKFeqQLsQXGBV6VcCMa<9 z#09aj=L?PzyVv0+%6exg>65z+@wkI!;8qh}fm_~kTeN*@n$1_*O+W0ixowGv7qLby za2V0+eRk03!x}tygaMDkQ47SOTsmO3lv4?4=8n{>6>~S?u6k1)Z`oNYIla2RwF5tG zx{QfTBo#Yu?39Cui|<{4^jmY!k| zb|u2)fR7)~i|TvUq6@IOKZ}OLl%-M!(pIM;fpNs&%IeJ>*xjxb8@bex!=6B}h*~T% zbt?T0ypN<(^%G~=3tE`Mx<*fde)*Wq}%>=5>hO9Ela!Shm)z z`+L4FQZ}^Sh^*B{Ad`iE{^9oAbVeK=r0NXg@tq=jxr>iJ3R zapHwE`W?hy$ykp&P+H$CP8@05B0Dg-3;w_~>un&AEt@LWB9&0@WnMP9Xssljv8djg zNcE$}Uguv-+~JKs^R~TRN7J4tmTcWbAWOtOts{?p(Z|GxNm13K8x^DNfU1E{?2bP= zSN*xq!V2*LDvyl1gCl$1qK1?Uv9362M+?k;kKbb-1t$$%8;@l7 zzEC3;cTJS^Mj(ziWJ{JZ7IjFhJr28*j=as9s~8jhRtkaHNNTU<-z|beXm5)|fDh%k zzi-Q|RsUkFjXI*%Sme^q)|f2eU|*q)24SmH?}j)Ivy1JrX*bKA&rfr!nvZ;b+4-+Fsq>}igEODwr&C0fHkiB<-5gUv0{#cV2NNtM9~;ikR=l@InElA#4;6Vh6$uXCar zT@0?IKm)a!!Db3a%)ST`bwfp4POXeOtVT)R_7C)CBCSGRLc$IdiVvcw|1TTLQNLI9 z`lIcuUXn3%vcsGu^TU!4k0i{VjxpAi=XcE3@80IvcxDc$|A73Ua)AqHM22Vj{u@g3ySC^`naQyR-C40=x${m)Qu_} z7gZ_*o`%c@aPq09B*4rMIqs6lbGCuFa&&K%7ar*ya(6boUq}f9P9vO$5^qb*E}y|u z|6d<%eeOakgKX}5)lebl)5IOVQKtN)q>W@itC2z}(36}yTS&-iOM?TT7P(75v0F=4 zCqG9nI9LBHm0M`_zbdgxBav+5U8uwu;OZo!oY&pFV(6vCNm9b%R+f6o|>o6xmhF(h% z@UM|T%pD*5Q;2)&1S!s*mOauI{9JZL?+lBju8!W5LLb*fEvcfmAOt$MFTY{7ePUAC z+A^hlO6mXwG{K5%-68OsPTpMfOT5Y&z}uEMS~D0^gJjnbH!130g7fAqPdDQ$LYYjZ&fW+K^XZFM zz9wjq>)x=|cEBc-dTmepCwNv8cQUo>pxciqE1`6=5WU?k2cx~ZDv`3JiEbA1rV-*XAw=&j*-rS6Y4o8zK*L%=GjyKf5f z%dnPG7t&fsvKbX70;WJX*ei!5A+l!;MWUHOyQE2Q!tvO+s6NprqA_1WpKY24mR!r1 zGeZUcb|Eo1?%B^Obvc2j+Lq*v(^fppGXTLeHRP0CzEvF$u#o%OP@-I+;@%TGq}$VK z>yb{uqL+1(6(5=}vHkVjLyaj02aUFIJRQN5SoYU5+0sCqg=(Z4nHQP5zbdxJ{B8tL z|8u4ZAWQT-A-6vbC`Wr1Z_*h~XX6#Mi;En+kAIXtkQF2^R2g>OZI{LE>C92tv@O8n zS;w^Nx(puA`*CLsD*9TYu6Izb8igkXBtL38lTi4~q)nu|@q8!a);Fr3R|BZ3xQYY9 zT-4-s2hbNbH+JYeZu#^fZYALkf@d)KJ+6NyG{HqmQAw*)(Ka-8KS#qEaE>*f_hous z&BO+XsJ4T$Pv@}4{ek-EJQwP4wc2PkER9*;jmU!OtV!oDTGR2gbDZ=-L7O3BbrE?g z*Y89SIQV0IIc(<$az#Ym%SIFJ5-f};Q?8LAS=e_00%$TF2qf7$5t4L6Zehrg4sK;^ z8eaf%c%@Biq@2_wZQ^iQF7Z|6NA@y6ka3HAGr5y+_=6ANv9N}&y7?C8L zU8YdXMq`b3@QGER|3Fq)t9JFpg4b)e89;rUrwA9~5qCQPmnL~6mulvzlCx?n1LT?^ zm2#{?-8Oq5b5?wa$L9$-;Y>k?xa!3);&jD&0Wa6%gNly;#&dtp$rf#{MzM|}Q8@Ah zt^y!%V&q8rV#%=0mCh94KsX(WHOsYzzuPHAPP~1Vt?#aAb})gKnv}O^Dzn963K9s?eAjHNl1Bwic|?3kOP{5B7Mo`~DiBBAm)#wKCY)l2yYH8R{ti-r2KU zN1acJnH-Ok5@#IMv_=n><1f9a%V|ntRcSFFbivi8dmu(WRacFEJ6Xs~+X0WmU@8}4 z@=C2*NhLkjq9b9fM%czikZQ|JjfL5lZos8{IqNA{>2jBqc0tpsZ4|5v=KB%_htLO5ncQnmCpSRE2qjD1x&+rSF&a4Z=I)5CnQZJ7)K zla)gb(riRzmmk?{uoD@#{ z5ktH8ovt28<*`umS;*@dqsbPI#0pa=I~}zE+Ms>y8xn6Gi6aVWWXqGx{P%gtlgfp2VV(2I zpF|rllh<7~p@Y0Hoc(Efm<9Rp*lrlw07%YXulSq|$d^^`!6vi91qDuEw#S&v`$}0$ zxYAc#WQ3BFa5380GsV*J>tUXv3)7Uv>i2BRErC6Qu@DRgD;iHG47iQaFEeK3SW^Ip z1968!=1K%AF*s30HmnK1Yd`MiH?LYuDL;>hfmfq{esynfxND5IV-A)(m z)48)tsf1bbsmqsYMhy^Ot^gTk(^JS#t+_U4aOPQx1mT?Kiet0niFBek zgsS>xSy1%B{s59OB7pK-*T__bDrB}4A;3KhI*aZQ=vkHxofcZh84v?BcwSA>5DS&e z5wVKA0R%Q}vVYJGq`aMAw}Mpq4PQo& zWDEmEB}5bq9xfJfGe=$f%8zo;l{Wxp- zEMBpB`oVZVTA{#^FFv?F^r3A#;Ns4ug(oU1c1xJ8I{KM4=r`zUX}m zEDcPn4o&uFwM$z^@`}}Q6wPkx>&sdjwK#N^(vzUZmeRTP_O2V$tH%odkTi}RtcUSxwv$Z^AdVR3c&pAbl&<`$3=;@@nFmd8m$s4U^Dn=O=T>XShZq zqR32a7wzV$3>L{LwlZG7yH*Net)Z?sRXN)RL$)KW$F55Wf1{}Fbdhs7UWA@&6M1K8 zB6FI9{ZJ74mnJya3xxGKvk}JY0oQjg!%1UCsX`EAsCnX#>}uiA-G;!Jv4tG@hToe= zl#s40Us^;8ol*>v3mAZnf8tqG7M0wSw4>v#xYTQ!L=v&Z4Fk737ZV|{6X5H6nM&T? zFZ#d^)>xOI+~KIDFw*NNrqoT; z&QjVJN-6<=JXntD(}s}We_YCW#*^Q5rUH2zqlDo&67@CW@-&nc7n1Rcq#6$@U0&R8 z$rhnlq*BUs)P6UG8YB+!5(7ENw(gHTY??&~IhApW3#?FAt zUTq}Ts$N-D-jV8kku-n`r;(P)?yA@-%}&g9Wrh0oY|vu~JxuS zthZ#Iw+oxlst90Vdtiw*Hi(sCdULgo1tZ>dtR3)&Wy)fxm}i+v0AU*%)btI#CLGFj zqjA3{*NMlnCyqY5=@*s?g)Uf@(StO|f!83D#~D}z^qN#m8jHBpvOqfuf&j=?h00Es zD^#ltJQZi>k4{@*bF?mdF4ylyVs2@<))8T@t!yeRiC9{R*o+l+c9l@d=YFN0XtF4-}gDo4jh(X0=g z?@4t4Wzgd2^sIq!)>$%JuQ9A(MDU#3E3`CH60f%g+Qno_mSRH{B)nN&BBhL8=X3gE z*r5t`qb27&RnLkpi`FFSpZW1F+-XuLy>3+)Gq{F~8ywiCvbs9R9UbB0Tmjsk6(+)I znH5&sQmMYu3YNW7b+_E>n^Yson{2Gaj})^lXEj$Lzmv*i?Xsagl%}FcGy@&jB8hNO zTS{kh50e5{jIbx+P6CToz|$Q1?C*Z{fmsxnP*hN3?+29rvyCp}{7to4QcF}oq^yWn z>t80lNU4%F7mS*wJJt}n?}oz3-8dA=*a}g7JZDdLXP}BGm>X7-9;mSQ*cL9M`9j>K^Th6DffsPA4d7pk=Pz@sj$>C)Shxj% zD24E?S>j3hHdf4Ma2OpjP*Lm6$^=01mM0i5l=WEwFa^$ z)#GS8j#YvdZ9U^tdlOA1gpm1M$ZIJeNA5;IUN?a>+YEt)os>IiXt-njN_tREq@od@ z8-#fYKN9kk5MMA}_v*ccl-?aqMo7ER9|Q7hi5L)G`PhD9wsQJKa-o7I2Zw= zIts{|nw^F*jHLz%!5La!+Jc4#1YUfqv|gtLy2HIZYp`Q zF4*sH`;oXV;vXSCYtdQok8B|(@KqO8a0<&dmM1$JW=B<>h`Y%Uu=i%3&~ za+xoZUL&JqF$WIDB#I7Wa-fXSVves3nSNr1BR1ZKyR7uf{Y?xPskS0@k}RPRt1D+r4};?KAp83*<)8;Eu+9DbS3k zQ`9Ei+7rh}8c3SG1D&;^d=|lpE^>GSq@k3qX~(F&+-ucM&bm2iK&s|SwN&lleZMm7 zj^zyt%~pBnfolpn=Q0o4qd`)c9p@8av%r!GQlV(PQt!d_NzF6}R9ZF2QtNkzWIE_- z*OJLh6(AXHK7+d$&P9qYq#SOC1)f*{gy*J^#lv#&;1@@M&9Ew3)+YFwK%Ny5^UPxl z9929kv@1zJlt|+3MzIo(&EbJ88LCvueD4X|^q3vioYR>M3(DSTs8l6c{eY!E6l)Sz z-lS7cdushmm6=>PyzxZThqXodWVq%A-B#O@L-^05?z+RGx~e#gdb0P$Y40r7Rir$# zd@fxn(zZ|`fM?2KNNA5JqUq=HoVsR93F2((zED!|%sgkWL?v%VQOJc7d%mPCr$AJS zhG8pxzLK~|%8DC$v&35;WKNK@DQ4|IsXBa0XHPvg=?*9joEGQ{DM(xk;0jTW*7?@1hvCu_%( zgcTP$EJ*$gcraDEVuk#PZsAwX1NTBLCT~M7!rnkW(6+Ut4xiOjNV#W-$!Q7sl`%b% zX!;v@zt58_?t_L8a!)j;4wZtbfE6rpIqWc^f|4bK;h4N$a>o%_^)OnK%{^4ca-cs` zP*&}F@{LTt0vJ%af)s2sc`^-~ivgP}Wuzd}=8Zif)Ug*qJNUTG-->w6sKsS8^p_XrV`!uJp+XmW-F()aOvepbJF9^L}hf2la zTzP1*j@3eYw*+NDL^C%qKl@6AFiX!Y3P)A88=vj&iyO96SS}&d@txYglfY8;YFgg5a2Mk zqL86xj^G|SRJT|FU)iMVxl6`GT-nik%x7|_{gia=jQex*f?+*LXCRB zpo`i9Hyz4or2s9`pw*oWw+-Hq=2Wrq`wCMc;=Am&KeS?Lu$3_DN5s--+kv5=CA%20d| z>5qnN7;Z4Fnm=5%p?8Wp`PYX0D*E!s0JAaeG^&kBD-vSJ-|NQ3+uK(v||0aX$7nl5v=lFAO^6?w*d%<&X z#=6(fk^k51tA96x>&;)T1N->{{GESuaDM5YzbFZN^WRz$_B*mSuHU%3^?!WujpgsX z`yP0XjsL$TVJJ4p4F{v)GZdw%0dev4^aVcbke8iypTN=aL_?s>P;{JH?GeiUl9sl- zL5|z89&6&n1~^`I2@JL7(}kCWYTbH;S>VXsLhaUAVmVB;Z0PSNO$&R?FRt@AIx z!l=@OLa*q@Q=FNi)7f-EFfYe*`uvhX>9cbTfBF)`CNumAc}9;ZlqRmI=^1f3`RvCd z`is+_yqrDxzi5oc&*$XVG=9p=t_m2@h_}YEHo>4(nFlTJ)6^6@Md;Fq9%58wMw8>k zoS;U}2zq>pjTTQQG`XO0iXbLad@?$xCs*Sc)gO@9Vp2MrjxdHgo1>@1kiuppGwDzS zKF-n;?~ExD^(@+TQKR`eo}cHZi*xeo@+*=&JH4LcmpFbkdwM=TeK{efpJT7Em-LLG zhR-fZl5D>kefHBnLEu09ikveHdD&!sJSL|Yfnpb6fa=4`=>ntZ{;<)T_G&aSznn)I zVoYK2mv;oAK`hzyJm`*8rKu)Pw6g{DD&TdF@bn!WU z)}Ik)r_*P%v;2%6Uy(7V1 zQ<5TQFK;OFv`NgF*YkN9t6_^WK@k&XLUgcGoSY681a%JNpgqFKBm?1izW~Ia8mA`i z0frLvDNRnsldmY^%PF6t(O!9R4|W$JCpN^DNZcLSaIG5_VkR@Pf0pW(HK6O zfUjm4idxi&QTJ@#TQCGZCt3rd|BS(?&j}nOrv!HSW@bd3`jv0WUF~Bq$2SJ|(~aP-o;1QTmG^ zL(is@G5Hb$@`;_#@Y#qsXVA|G@cU=i6x{=cU`j5C0RyJ5*cem5?%j~{PbvCzNBkF#zQE=) z3K(CKI-{RaICzceS>>FZo!`vx#n%M!BjUvs{^b`WG43$WnM)vl%!C@uMuX{Sm`8h) z#gHCQKx9yo#2P3)BPTdMZh?6sa4?B9wV=UZ4+!eDNtjU$9Nc2mtm8OUX2zohNuWdO z2A_0*+PpRcFKKK_JRwI6F~o1si^hVSO@VOrUR&J-&Vbni+I-q9kyNMH8=s~6)MVm@ z2wDrIhamDq1jr>ixWuRlIy##+Cn$Q3PUkc}ef?vMVt|<;#`M^OFRFQGfByD}v0?(z zKh(a%pv0*C$(gr_Q5W5}swi3l<5f$53CBB`YP~m}097w{$H2Z*ZC@Lk+zg&y-jHV) zHYY1r=m4FOG&*cfZ@@fZ^kWJmCzsS~CA-4l7(Dy4 zG4W#Yr!W7>KmDI6f}D}Bt}ffaK+XPT86QsrrFI`Y{T8rmSSwKjVMh)n`$Y_Gj)7$v zc4tpX0%%%y0Per&QRIYt{PmN^gU2|HQO%!TU7+}s9MOxLIYlh+Gn~BWVoxVeo&f>4 z0ylUL#FjjxCR2Jcp=SLb_XxVmoT1k*QS2J;V|}7eG1Te!6de#i4MBhdwB+%{XJ_!) zWJH$yBpxSb^!aF3N`66-!+D8j@D>An`Sq9@(${Ag`2-{I$^3^zZ3d#V#{=L+X0N8i z7eArNpUnQr)%^65!Y&BnGi*wOkJDE`X|p}-G>i8xfx$@P$SNL$&W^gb)=Rm&mwFyIo&Vh}~VwD>h2*+86q>C63Evd(Aa4k=TPE6z;puD7gxN?;DjQ*%}@?-V-Qk| zfD=F)z+~VP;A@CcV?LrzFNr?coiM;XkH_=^Nbz)WHUocCykO?Q{No+Ug`a^4kD6n1 z0z1#n=@EgE{RKV&x5o((x0gZS_83I%=U+0!%g3*1a*m-tyu3t-&rZj{^5Rdhp8+|d zXM-#@$rsunb|DstE876A^+*u@J*hAWhQ>xzYYyUX>YPO9SKwuylK4wtzUl7i6pSq~ zW9FBAAXSS7^_V24ljnu`v<2ko^qQz(K!bq}w}BJyG;kc4)Jc|jGNl`**eOk%p408q z#e~2w{}4Rq^%Td4G=6

    XVNr)A_iMy&#?f14_-OpMl5&XGNf<}$nvb-A?Jfp7&{vQ z>qgL*^oRxt3OaZ~gM8*{iVa8*Hnb`H@|3188sqW!$r-c2A1|JP=b;xM5+|7aYtMFu zqc}0lt!`uw! z6YL4dp@B1S0sxclTB#}^Z-e@0xi*~G6eK{`K$xG*W(+oEFmm{#!EEp;wwTVwS>p178cac?PSA9>MJ~ED z$=sZt6GNaW6gqoJjwy=5u+A%-cy@j{9bJ>;_5AA@c?z5sfe~Mk=ja6F4n&S13$YA3 zV`!>P17}de32Jdlp40siNZoPpA~?fTh|B3IJsMy1rj;28OZ#+d1OgKZgp>vxhn;$S z0HPs1Ue4fJ5RddHcoMe^P#o_9M?i3LFDnPK2-(I9Xvt{WV6I@Iywho_jE8I z&Ku(ih*0MAV+?qd!MF>g6r(V@a|YyNKBar7O<)YD5ilXZzl?}B_BtM7PR}oZ_P+cY zKU>UZHz2QJo{G2L=ul$TX0I*#yKwN|fmgd&1{5q^eL592lLkIL1rLIVFik|}^ zB#8y}8I3;!T6aArI^;DDrXM3OP~xlyVm@Hft0Ryf1IrJb(&#a`7^D_V6PWRS54=>D zVLJ590H0$Rv6zqBObcWQQygp3{WIXPQECR}b@&*&?2Pegx7Pw=h~X4ad=OZ?01E}c z?)J$af;?vi66+~mCtsihL7dH6H2JuV6O-OK)uXY`rvvbOk5TLiJ^|4mIvc!B7bZ0@ z(-_%idSEt55O$4*Uy$d(|4#?Zg2rw}#E?9{nBbGfjGBO#o-@>fxth*UoWY)g9C`?{ zU0_TvdLUt$;2`N3(agnsNTE|=IA2hgy%aMbQ6e=XuYh8Mh^pTK=^{A;B0yY|^D$k+ zK%5K)cX&Z|fLWvlZvrLj zcGdSuk-Va4c?B(d1NO2M_1RjbLQnp$NO@>a~5 zN@bNnkTVb~6tlRwPTOTGuUG{g`d2FN=$X8c+}JhCo7F<53>c+!6A0IAu+=K$jg1(^ zt`r$RD|s#d^39T# zH1iQdG4j_-81RywD&H)lA=Ns79V*lS)=S!!wry-_Rjp7iQ3A?rZdlcw(#NgB7r*>; z0a9x|u05K5+vklM1u^Bh>SrN<6cDf-B}D?aRWKHf#p+59jqd7tO3&5zOWJ-pZe4b_ulyenxh45d_HwsV1tOilDoW)=^#jZ-V{D#?Ht%16w+{#vYb30Wlr6i2Fyg2{Vbp!X@Qq;=rR54fR%~i0x z%vP;hEf+Gm{$K;zmMb)J)`p@`#@X786}AfMnpHZe6tneM?PdG`DzH&E^5iGQ?NlzG zEmu+?QC-g%k>VGjD23XVrEk70<-@AEiLW1Q!ww$=C|{? z(q6H+Sygiz_}=J<=3%}H@js9M8EI=fzND0@HLJ2zEv-i^P*E=cvWl6`*W+buHnsM0 zwy09~(JSlMwS7{1HFuSo5d>$is96omfC|P7)+UMU(N>{EWr@rl>&X|h`)idAN`84t zbA@VUzgVhQk)qnz$~6EeDuJ7rqLr=}V%toc=_Imw4ECt7B1uMp+D$A^jWP=F0Zg#c zRT9`7#5^!pi$zNz`GSNSo9WUTsBe_*^DT?AVcuHJ?(HgtT)Mt*8v05sR_c(8kRYbF z3q~GWSdBqP3I;foT{hj)w(E^OATw60&7=J#K%jR~Lw78Z{ z_mocV{@zZnw3%1x?nY)izX@;5_l*6WZ2~1ZcDe9EGLqCP zQ7fNOe=oYXtQu<6&|k8yvpKI-UZHYjC@QrGcwDKno{Pe?3I0~TBM}oWSBN{MEn~L~ z0Y8YAtEsFGbW?Ut9Z(puNRF@M%+(Ubha!c5ts>Q?D)pAKUR^URcxkiTbo?6hx0-Wz zD4gdtOtWqk3x_!i5U?7BL0PTmly$whL8X(54VB+InOe6_wVh17m@e<_6!h)1Sxtj2 zn-x_rkr3v#th8RmN=j>E877r9;;Uw*>Km3?F`4h0YUK}in^v6?FSR}RXKqKST8(w)gcozuPjCH!!2Dp;mPODMyIWQ*E=kTsa`Hrt3TgNa&O7BsNUrhFrpm! zVZNtjgM723=Zkd;vtS=Ys#vMAK2fB8Q!0+uw|8sRO*3E0)cfYHRVkN?X5HE^7YkYx zfvid|r?0V^aA=TO<#j5;+g7BsiGA0q>)GP&w!Udll%nFivj>P0!AihP7zf8Gq?LAe ziwbL7mbq81@9Mi&wq_k=a>bTbtJF52zujs*%$ZuX&`}RC&GKe(dmD~(!~#;TRE?EW zi3Ek3a*fI@(6OeMbLNVb*4Fc;W+h426MNQP-q3EWT0(cDLD<$fsR*n2g%hkfunUQAaBX3R@&9&Tm6y77GeO6uA* zBSD@2b7d=pEJny1v?QPiek zm|D@wBqI5(La_|)8yl>C09kna4kI@zksS52vcxQ?6iYn0{VavLJ?q?PRQJze>ZaBO z;`K%~PyS~$N@$)ewX&|7ndRHiSF>=Xkc?GVtg5B0Tn6w_tG?Y@@c5FKRm*Wy(jzt!nRpbw_B!W-e1| zRBL2>q-C&LtE5t&F!T+$E9Dxy|)mPM&;bGMYMD~5$J)>TU*ecw_lC5foih-g~h$)j~vU37}%nH1Kz!);|N z9fhrBETz!U4Tb5^mWgTyzy}6HnxY?V8(NJ%iMkeq*eX`7T9FbG$qH49T_wM_S!h&2 z*tHERStZsHtuBnHV&qmK+PNch)yiv(Oxs9R2}%W)D{54qS(Rk*P*t{Vzx0S8d3=QkFwu+f5!8=>2<+iB>!fyfUN@1h2M&=3$C(TeddX)SO?7v|ax|9V? z!o)^h>U%2%(`vCUQ`k^*`OPZT*EGCaXOW|(z<eL11Q z$B23LY{^vXc{ORIbAZ*(Mm|%nn2Nqf243GLXU$t{0GiOYlDW?MfM(I+cBy9S5d*4- zjnuZvnY?&~vYL-tyPKrQ3;AXdBiSdLp(brDZLtV}M=Gp-ly_*3!c#Gl)2~w8&E_aM z6pJxKC`WOz$Yyk_lxD5ax{^|~L@EizDJN@5yTOzqqKUpvzZ{ zYsLTas?I`xLYa#^YwFhae@6^jX1NoSMcu{Z9#k9yPG_sRY%*`vo?2sR-^?gNV_Bcr zst#GyWMH~l8>o8T+@*-HRc{cAjLqU+sjdgGykeO79oG<5d2Sw(#me1x`bMH`L%d-eaI)Q^( zBvPYXX1=yxTDulEE851|*%5(2P35Dt44L8XP*L;jGe{N+DK)pN){5m8K?m4dsu)S5 zpyZONt=#JRt)ig{FE49L>+AzD^J~%a3XY~-!=H8)Wi!9QsE?|(-3^v!a@C$J*pd)c zt;$JhYaN}+j*z~!n;!4wDL8GHP6}t_2gd85m!&G{yiT^ts=ykw84d5jE?ZO_be)J* zOGdy4YFhC^Di>d3Jz3ADa=WWaxnL$Y_o)fyk4!af<~nez!huGSN;NmBJ!0SKjlRKS za{IYT8rn|Xu{9)6ZMKQ_r#c%C+Iz-+^fpVZ=B-S+o=X*$4Qes!oMv3jt};PX&9wzJ z%aX$ejH|q8rKxV^a@Vb!>5Kx_U9;jj>v|5CEfek`Nj;V^S;s9dSaUk#SvDw+ZtQFW zPkPaV&llOxP^)A&ikX8fezun?m@Bc;^TdZ*UN_^$lQ@4tnJX?BQCv5DEj!!N^@WQg+ljvKPqu{pm5==0zxwz7(m(p>`j?;luD|``KlzRCcv<;J zzwp<7_Ct}ked-gBJ@-@paCZO3U-^lx_y6Ow@BEQRe)I>6vL)=l{+2VizVBPk;Ci_6 z>D&Kg>({sJZu;gKTn{h*4|ZM6&fxmc_x#HredIU(^8YPwKK#!)gX{j^eD}BUwxY-W z#z#KmBcT$9v!Yop1ji-~NHOfB)P6&fEX~+yC*~e(oJ>Z$JOwyA%A9f4=iw?|t9m z2jBOl54wG~+p-6?-wcDpao_jfXtaF0YX^2{w}WBF568ig)17oLdJB&2hHdkt|JCMY z_pZA)d9Xd|o>hkmhExV}9&>pCMp z?+hKsANuyh>4$^xbmVo%&Nvu`mq8GYoS-|Mw8H*yr1z#L$G#i*o-^sWLuWX)2d?K0 zZQq{+;a2Z(FbxAIbdI;~`aQeV+3$|F+LPDb8uriJub-XV>ovOGziD2))^6LAv%2l} zc0zw*j|ZMNa9!8&4$JO&!|Mk}{>b&)Zs?p2`#})+2fZL16sJ?KadtTfw$6@D{m}D| z2dfSLba*l9v`$8y(c!e~dIv4{JUBdcw?~zB`+l!?aNao%+HdXOYfMV#e=yqLy>HiT zzma$Sz!|rCTaM$oj??fR=MW7vn+&bfa)Lu<$O+wo=N%7%mR;Qr-FDxx2R+ZVZO1+G zr;VQ5_WXnKXxt6Zf#aNvTmEF|dww|e2Z49o>>Y>UVW&5AcE|1J=yK?G{btv3d+um7 zbiBY1!p^kWKMTLnA2}DnA5PvpINv|N`}Nb^uaq{O@SulON7J!4wkMs@s1^jHuA4h_ zI^)67N%-bqIvx&3ZyflwyBEQE*yz399`}R&qsed-dZTa>+P*g)+e4~{Li(ZK1t)2WSQeiv!nUVq|``={G`uG4efAn#0vfj12NzU@t!TN&2K>kqrr z{c-3uC+@a=bTS-W6fP%Dcj}L)jgiwEwEO)A+U}094$nCWoZ+BjyRKIqjXiA2Kk1CS zjm@6t_jdb3r{lPjX*=lj{YiKS_4@mR*72d&&rEuw4LxY@Uk)#Nqt@|*&~e*A3;Wn` z0;e_#{k>odgY)+~$GvXvFl_hkp0+y!zx(=e>%{91I+xDKJ4gGI2gi*`@KtB(`gYhl zItvfG_L*%P?ZN3r_joc12U^_;x5IIN>YqEKQ4kKp;V|foFZP4*WIDFT9lsyCW8dAw zpX{C+VvWzp;ZA$(hvDyggJHin z?S|pB>V9S1ZHI488hxj_@46u)b3Kg8mVcbf@S;8*OsBzQTxuae7x$Xjli=9z_WiCi zojAp9-Z zq;2=>Ug&tfJ$2lH@47>8+`MOaxmf0>Dl%O%@6co6dbu@w|!tE zh7+8Rhfe5W(K}-d%yHcVdvH8D>epSrV-H*5*dL$y!{HzpH+p^dU>f8+uJ`R`=-amI z`O^#65AE}Q+imsTZi5{|;|=JCPkpX$@3mfWf<32Oai$G#vfZlMw%YOC$>6m0X45G(hhJ%py3K8v{3(j{W|0 z;_ZcJub=JOgGuL7Z}^i&;G9fb)2-$x%ne3i&z-o#uJ3xCemEp9?VJt=o?}nFNjN?R zsLqe4({V6z_N;+F0SSts9R{xN1Q>nK4!xQOdOOZwV;6JqyZ&%5XpK6J{u2}YyLUJa zNB?4kFZk1XpLp`-aNKJJQlV@dNvK&o~R;90t=cJUAGgPujbqje+C)$CIKre*MAt1Y`&gdzZuG zp5qw+-H5ir-fKAbc!-^J?9KoO4*fyB>pHub{G^9RAQO1u4sq^1TU?>jDT9kve=_JF zc2U+~@8zNE*u;#+Uzkw9>Q7Lm(sw`nw$$*y}lC zhd>YvhmEE`@J>!W_d)YCoOCbG!_ocMPo~ppzt=nL;`9CU7C0Q{FUNpz6Q3Uq?j73u zZgB6+@BLryMd@%ndF%e^C=AC3cf+7h4EF()nKkNkVz#VLbSWjor zE>uD%?+u#$ZhPWx4vu@nS|#YaTY;-qtv%d-&!3={FdTXe5;wT8-C)o?YB%pXXWrO5 z?R1aZb*~+yz1|<t58hcZoZb))XTMch9!pI6WJ{8San5zCUV) zp4Scf<4!O1FjbaE4t&oJJK&q$g$dzaSb#Hz$c(#Qr`hdqjfP+yLwUgW$GiRT&<(x5 z?MyNGk?#cq06z?XS{$HP-3r3cwtKyP7dx_tvGJhaed`?WJimL~I2$zW<9~HJJe_t9 zoaydC->qMaok@4#4BeA{C+H3in?cwdUyR4W<+(fB!|cPIk=?pS$lnW2!_c{O3F7DC zdXO^P>BRw_I$N>!PPT_Tc024gJD07ykcRNI-*JPZ{-}BEhLb@z?6oG10O}8)js;eIM@A^P27vEU(@2-G(0wyZc>d7}!p8 z;2nCckz@BqW7oC434CH=cSARvVq}iL@4J`H0Z81#%|idA?IKY)v3DH_aHkz_DwJv9 zp4ma0U_BX))V<5IuTDa*`$oEZd3iKG_Zz*o8w9R51yTGdFb>X?UH51@=x^Aklh;p& zcQ0|cg#YHfu-$FnZ%#3Egd6gAICh!FiF;_{5X77PUO2!|4oACTwbKi#fp@aad{2Ar zp*z_12i*-u-S5^1eJJu^U>~%Jjn1e&0QG?fxKqdW51YNhA$BqbD9_sm-oWk*{XTqd zGHG?kL2ux>N5W&hzIQfq?MCC?sQFgS4Ug;x2cyG&>umeJ)3|tRICY!*;m~bQZMWGE zM-cyhd(_{k6COQ(Qg5`v<2D?t-*gTpXThm=>iU!3F(BypTlv~a4|dryX@Wf#)oh>_h=Y8BYO}8ld0c> z-5m_E|KN3RG6X^for}o{M(4X>?_gT;PsVsKplVO1yS?Fff9!;V1B7Cx_6DtvGurPC z55_$(Yrs$yEI@Vv0q^TWQ2k<%pHBfzbk{b1}klQAuV9`r{Lb=|Hz!HPDAgFiY(J(nZy3#aI* z?ezLS5NA`uoR*{9(9J3piidJksuomT7Ji0veCj}&vyI$U;t@yJg4uu zo3?ES$FQ{ttUU}z$IXy1-H*Wo+}+{WZTZC3LC_dOqKVr<9wvL>-fg*~?s@%q)E{}l zanQWivb~mTO%EG;!$#LX@Q$2zuj_T~0|NLVDe!?aS~reHKq=I^Gacspgvn`dJS7)8 zY&03@gX3EF?#S_a$D|nj;o-gZ)`df44+aflQp4#t2LnPzFsVYwchQ+nPjfEB(yu~H zNJ;E*Zv!7ZCMNZV{cg?e*uAbD2^+rK>GcPDu3c+2`xnl^#oevb&W?BPc{{Li5|Ci< zHTd%Zriy5Z&Jhu>e}WVAcI=@~cy$Kgc36S_gs{IILPc;qDfh$hXxw+(LL_>_&URyn zRk_aI@o+@&vX5ajVbI&|jhu;#27pB{6Mz7P`4m8q9Cvtv{)3=>!ff4lhjxg^w5H@I z#~1#nx_jcQ$B~=nwox zbK2ex?>a3);K75z4%ZHYL(Jp8cXmn{z$U{Pk!Uls!xO?9>D}p}Z*}%u5Dg#(tbjd_ zBxi7PWDmxF;GG@q^q|j^i~F5>ldlEGgMQBU?~yB(1gr_Ms2WFQZuZ=P_o05Xdo<{d z0k(d}bD==aFk%M>q1!vEdz;B|~AkaSc&khd#_R!h0 z9s4W&U|1TRo&@gM1;E-Jy%lsuB=JRHygvCVo2|*oV0iXMyVnZ0 zj`E%M$^GtVzt{h_NBy=xygWL-Kicd}cFu!0>h4#`!aJjjlaAe*w8<>a+O_Zx2V-dU zWoQ5B?@c;~JGdB$%}08`uTJKI^`-mCfEK3a4S`R~YW zO<+9(eyQy-UHjn$5E@{0IKg-rj)GyMEO`NDMh`@A=4If8d+y3$cySpNh7Zm!gNg6G z@%mTWU;TQcY5O}VG6|did}n+U6PB*}nm^6nZxzE0d+Hue5AEHvd(+~Yf{+SD(+(Gil3!lF|d z{-2k><5A-X;m2Xv03~p!L8vrF&4xD`HbTD>_NQ(m+#~W&tEcYx$T>Zsz6Y9*iJ_!1 z?s#DA+QaiB0$5|*=r`^k^xN&T!`7RW+GYy8{SFv$+`z z4s6K4@uWXoqDD%Y=y}q>S#|Vy@B8NF5xW) z{lPKl=p7FS?(xVz#+r#G+fKtdK0hB0-Erp}*!RPr#^4+#`Z(8Vb2GUdIeQBTX4Vh8 z*z+d;QM&8XRcFt>`}@U%@#*E)9vn_O)A51tt7ISCPu}c}`ear#;k(Pd)-IMUMX&Zz zh#MXadPT=Q3_62R(ApTEyZd(c;)DtmoE}Us4W_+t8h9g0d%+|)3P~pSoZ)12dFJ;A z_m8R9jDqb3GTrZw2JJWcXBWK-%AL-?9Dj8QnMUUIv^EY?zPJCo{E_zLSbhmf0-)aH6B|D}&3)bnm-I z-A;cN!}RvSXR;c3Ix){=eP8x2l} zy(z{s8jgAlE9sA>Ujy2_q32PL-kwnB8)I#S{%~qsT%4ZV0}Lk(W(8t*9Ar0-N4w0k zw|BaEhBEtylY4f5f7+n3w_O@HTSq(4yNoKYKLn?j2E#pi;5cqu$tSc@c^;B%n;?FlyZuqC7qq8`{)6-T=NrQdU8wp|bK1OoZQT6d zr{NKE*Cp@r8$=u|rQaHbwnh2-@WkB?&*`O4f*07zVZYaHL9#-j=k8&j^ps*boZR1Q z*Ze-1+LqWz%!PbCNiw`%VQ|psG$=?MPN&C^i|*O#FkLM^1(ZyXNecD?0plbGxsdRwkF?7H@FJY05<-KKNW zom@_j+Y`UCHwk-Cl?~M$_wM!s>Pl1-gVw>lV^%S`{Re8d6G1;>B7_vb#y3fbRy-+OzV$xDCmwuwJ{YPx++ zY0deDeYoC!)0^G*m%sPbt@lShuKe&LKlfkNWTElLzh$BEfBsVojqm-yd;esi@mp5) zzPZr&@bdrfLgT#~tn&TkAN%hA+=a%ocfR{Qf4tE6*FW&5R~p~*fxr08mBw%O_hWzl z|GoOhKK3i0{n3wnyxAZ}A=P`_en+BX?Gk%XiijD~ZJX+*%^KoJ>R()|M8Q zlF3MHeR(;RT3%RLUWzI!E0M)$TEiJ7q+=;HNBvZ?&Xk>XMy1cx+vb3ZomX}ue#4jUD(FKl8 zi6o+n%WMHoM^_eBBMZ?8H`B;!d`Vx4MpKHKTIHMNJ1g^v#XEP>(P%WXw7wjRFC`+0 zR4f@!uB2{cQt9M+e0gngX(_fCORRIn;$m!JITD?pTeyW3t4q+8{_ z*jjY$`OQQBFET#ZGe3yaaza^%LX#6mK&`oIT%|JQcE>x=LH zv!D6shd=(g4}b7anfQA?@V+1TkD1dqZT#W&|ABeU7SrFm|KI%ZBcJ(&Zx?y~a~9Jd zd-q>_>|>w(*rz`E;a~meuRQv>&wTW=2=duq|CLXF^anoho^|Z;z1#2nz;}Gp(!bg3 zkNu}_`icMIU2ps8_s$0V|0B?MZS21D-GBBYAN}NKKgtO%kN(5Yef&Kip|3B!{R98) z5B#Uw{;_X+c*lRtxn8>VYuoSt)$jZ7f8>!j=AIYe`T0L_F6*7Wop-)FvHi|>ed+D* z+J5KzmiX^W+wWv!5l_-&4IYp-*c+_zth&k`SUsEJqot_2YN|$7Qw`0q*ow#QL7rP? zj}SXMG==NsAqibE*o?^*A9i(_s;09;$52f+`RM%76uwY&nldyRW+xCG@r1v{#yHxd zTRP>kJh$koqN{YNDO{(pftP*^ZZ_CcZPE&Ro_T1T?czL{#|B&;!Qz=y9htaEM<|2G zloU>xU?)6BKd>>HM-RD$t;SryquI!7Ag94!9NnR^H%~E5p0zO&e#wp|9+%>GawqaJd|hWIc)XH*j>ye3_Bxy7Wn$TNG_whA z)ihO|S01y>HC0JoM<44sav`hN7j_v=T${*<|veJo4C-u4gjp ziuN;HrRmEH7*@nIo@1LC_E*5>@&)EkTVc9Z*cY#x*=%aNpkSjsImP^#nT)w3cEmjlD-$hN&-OA+9voyBHd+!nvy1C@G(8o|=u7i+(QCJPsFs20Jm_Ybb8%%; z=YUm=L625Ysj6QsV8pUfQcdGoG_j_!d6f^k_3A5fN=6*6RX&Ocy1C4bF|;n{rm%l) zW+ywQo|3WZS4HeJV>55lW|Hf9_9LES+-X$1D(z*oq}*v$3?9M4$*!!f11CD)ncQvV zBDplaY8ag2Fc-&=d5Tii3Jc4rOnODx)e}}^E~jz|b6VA2NNCA5BXZ@2S&XHauPDhEjUr#Fy-AwTm@0^xtaQ0zF9B(zNSkEikZ+zNHo2f7B`aBQx zJg2MIR+m0$WPYBR{=)C%USxBg_2R1b#b+`7@BKaVN)mOyWM)32=r26FrL4Z9Y(DuB z_3>XVCY0NsE?S8z1^w1@#V`EUoK{iYckt4}@iqLPbV<1w*?qPe`7y>X3g z;L3b4cY6sx%qR=b=>LbMzf#LxT{Wz~^S{JDe^dRiWjyx0zPy?<|K{JnapijHmrVVI zUwA3;R7QLD)#viJo_-Nu{il!WFWklfU#e&SSxLM0B0EXv6fK_1f8qHJz3}Sql+4(d zd9=8e{mj!x#(d;yj%|5iE?uhJy!qR=nXU!UF`=&WoR0Z4PXol%s-~nBHv8)JWo**2 zcJyU-j`N&hHLYP%xt)A`UE`6e)%x1ZmXD0&Eq2G7FPEb?F(u=-kMit$wak@<`AWPN zPo@p^j;Xw|oQtg#)5;2>X6_h9DiK|XC`M-OOIj|ordKtM*Lmn#Ciz<&4U*D9vvm4- zY$%o1D>yD7rKJEazELdp-&#OHwxCD=$`-T?_M#}@*$k74YC6yU^m!|$aE<}cq9jZ5 z2nbKBV7eUOfQji!GPjUq_bhfvsA7{S7&(hJGk*{de&`z@JpK!k5?jDQ=^h;7*a0-I zSy>an#Pc|Q2&f~3@Radt2B^Vz*t(DVSBkI1OtuLNN|rc8W}_ZU@sty1Oz4FzTrm>^ z5VJFRUS0#bFnDywF!i)Jjrb~`b)MAFrE@(a06GH<`~-|fT)9}HB65MY8i$PV4fg^@ zxR>#y$if(yn=H>o>erBmr&;NSC+@2o8fS5G76$GqkM*0G6_lDS7};d`R?W<2;RTpeiGk*ValI zZTH&hzyEqgQT{;H|NSSjkDL1Tovd10T4Q*etJXDcbF2YJ8Wf@+p5Jhv~&&E5Ma&fbJrH6%~_L7xSKbcjur(P!f>DBUUiC^a^F#Vufz45fD zMl+woYoP;{{*;g~o{L6{$S0%;FBU_=W94Ul;yV;0oJ1@tXUxCydpDztkKbHdox2su z-MI1TCknT3{QR|NBB@`0^fznDFI_uHX#f1zuD$v1|E{9^Q7rqfz9;>(`E>M?YP$5} zKdZn%u9t_;eOxbdpwg@JOE={aWPK+ped(JOeSsrM7(JRtF+`4yS)e4cDmW&8_=}Cv z8cfe9L&LxC%SZ1d-3|x3om?Ot!_N~g)e>iw?Cyl z|49Xv!#Ks4fH)2xg23Su_zm_5k%N@MTA3dj7IF!n6mn$=$Hgcz7SDl#Xh0ei2cifF zf@UHxV1op-#$(Mq^@lt0rK%Q$F{)78493iN+=+Q9IW;3bCj`#SLL89@Oo6|Fn~1|@ z%sSl8fF>|NpbvhE>OnZ*0VYgW#C;fLW=8ZRE~r^;hRYmw1}-8S28feod5Q(DLQ?{k zG|O*MqY9+(%%l)l7_g>r@Dc6dLcpm|M5r%Hg8xE*I5{y3v`R_WTp&2OM69ufdy<2D-t2;G3alAVnYu ze?y2x79@r0(KE86Lk()mS2@nD!AWsdvKmQ3(3^=0bdG6*Lvz{!cL|YK$W>;rr7JS= zGBkjg*tmSh`+%#brJPe&nYjMuKiDZoAQjs%C@?6hzKv7}vxVWH$I9^CCeBzfAN2ZGR;K=vjFNnrEfk?2VJmA3hVsm}h-v|#Epca-iS z&GI|uiul7P)QlS-5S~c9mGo2O6G&S}gA0;c{BEA-adh=1ISDLpEyb2{g*5ILQ_4Jt zm0!cdIEqnHHXw9^=Ytp_50j!A^eo_su1oa9&$J4rAP2dh_}WY|E=547f!rdlgR=Mw zskrn8R}1zEYXZZhL7EZ^mPazBX?>j;M|+aF@NfoCvtgs$k1--7t}pFD8pIZ679-z3 zbSipgPPi5`o?U?3irERWuZR$$u^E5KV8)1#&Y;F92;Pzt6PCo0j&h2U6=nmQ;yZ4V zkpMk2A^1T?O-4@txRZcDUVac4#_yyT@@#a2iwI$Qm_Os2_#f#Sw~LFDZt@iXF{4m? z3j&EMEP*b<5I!XJ1Oeuph3kVADrSNlr~yr3FN_G>!hPtJ2i6n>X1>8lE)WG|kx|eB z+#>In@e4V@thv=qJoA23<-BP5P#RI#!OMKct_&@l`;6f;2$%m)}wI2MaWN@xQ{ zh(&HDyU{fu_A8m52$B%FnR(SFbHCX zfbYzg2xY<=l}a+3P_ik3sa%$0=5m}2kKre=PrxXhqZp)7d&72dZ=HkVz(5{WPmzEN z!fP=eAI=(Uag?}bCV87TH7rG6T%{g)B}>k_AdaoQ8YTIDQjINZSHYQ-`bE54pVt!+ z>m{UYED?;;zXUHCauQFa zDPMH9niRMQfP>O|`c%~v4@nCz7BnHDS0t;=NbKP8>y!~!l1xn+N{`uOljaT3CcYfaW-L8j zSxX?dl1ZCM@nHi87tScttydVOXoUXE5;fAKL`M|Hz!_l{EEncRpcJQN6!gbo91NN^ zm?2&G2){PgGa1M)zsC;pYRY7l446r0`7u+-By%sXELq9&YAj|Yl7(b~6Ib+jHoFKq zsb!*yx~fyx!kgL<(X$J>*-u{QTrqh$gir4$yLjm zx7Kc^Uf{^Ec;O1CJyA$4Evg($9?dE*Nh!Ugs_SZu>`2SbK|?uH&HTdpvmCspuPmiG zxpmPrZ)%G<&iYoCEqL?1VXhh+;L)%&#HOO7TX_TEguz$|?nJYo(N5#S%3RAy^Sic|y(J(UQocDC=a4Mk3EF zQAfuzv+4>}bifbyo8@{qCPzq8Z%|YYHx*0g@NA9`<4Cy;>d|RaEtk@Js!_|qP&{5IEmL#(@{=ZLM`*D+&tBnq`tg%rnZIQz*+aiB1*BGodWVcfgH!3%h3aT4M<|v*>QC7%O0Lx?#weaBdDusr`#jl&>s1Jra3r4Z^SIU^oR zZ4Boi4hq|lfWzp~024|~B$5e{0E-D|QbYsFv05-*xS2#Vn&Bp)hFl3sqE4FNhFLTc z)+R9wpA~i_XaI6D8-Ptl;uvA*Vb?TEGhl_RO3?=NhS~BqSa=kq3GT=Ma7`r9jew8T88!#6oln%aO0ZYbj3960IN# z={pS|1JU9kg%U;}>;rjdkMD?@+)I2Ceh=OVUUP~G_D|@MxJDmz$uP(|xtw$hh!-M3 zk3yQD1%R3e`7pQQD@k=`U(qHbgeei0MRBO-VYZKS97sg0M`Lo<9ix>9C}UyCj^?Eg zBD+v2iHeL{ND1VLIE$drNT>o+B7ZC59~J8Rn3yus%s37?8{xBctw4^6)SfrXvWHl*o`w z(lA65WJfSUJU5V^N!{5wI=7a$&@lCG$hFrQ;4!LGE~68=>-lbE#Lx%ktCQ1^OACruyeEsw|>Y72~+TA zltR))FWfy#T7*Ih^Fd?e2r>+k1i1wZ<0`Z&lf*fGv?K;3zo!!{7qq|~Xbf5@WjN7* z=m(!<`l9+vk0CgB%-=D8^A<~4PyR|$&2TcsZ-2D<{HOHdRmPexT}weSZkhUGQ!nBb zB^1kgj#!%!Gtd!|GGka6b1HBp#)gU>Ix7<|9ZH@r`V}mboM!f=)Wv6?`3uGK-iy7T zE@gASYFWSV5%sZ87Pb0Izm9P%Z&sfEjmn)*qzk-bmm>mKk`S9Ky1uvrQjq-O=k=Pf zVntu(O(Q5ggGIq7F?*nV*0*7%r0a)%F%yW-B8hw!ZXhC|RDR-LT!ra0bHDTAFC_AJ zepb&vbuIC0zo=`QH$IilXiGc0&wXa)pFdWqJg@7OVrAh4)_bq&`t2>PqAM$GNl0vP zXtb$c)mDpZh@oeo3#?VsYErnQs7U9aed!Vn&vY=$d_<0!By=wjhlXeJ$hXK^jDP93 zHS?ukj1@mKpNajm#Zor*QG)}+(t8{-cRj+&0>X1B7t#~X=|pN?^bN4c6{OA&^~E&LG=4v`}ZL_lQ=w^E9Rx0um zR=gb1c&U=1EkJKquO}DptSIU0*Hek5`Dki&OXbTXD)OJx&m5n5S%VLcjKk0zs; zl^fSyT#dZ)+12ISckZk&EheMO^K04Fr)%lRVsrJ*x|-z2bn;3@Sy?V7Q@8XhS6Meo z-!fvc=t?rX9=r8oRM(c5O!eBe^|fq@eNNXnxQF%A+jFsnWGub3d^1^0MGPytvaHOl zW@FK%xR!v{CRwyxTZ!F>#N+g9rQ_={b!~a+$<=gp;mId*E9+|uhI%c1b8&e!w)SdK zi?2N%U!Q{+#iOaz&8(J4EM*dF9Q74TrP8<86N&Y8EfH5Ew^kBb;!a9U#n+07Xets} zNm|)CO<7-C-dKyKSJ&5~^0SsmE+tnYY1txWW)=-4nOt36yB)utU0>6ZcZ%#iSjk3i zFMTPSh(}&|LdnKsw>RT2X4lfonf2^k)>=PKJ#iSnTW-+>(Rv8+9J?=JEN|zt!CEO600jW*hw;%$);B4Gl@)MW%;^d zWtKA8#A-xOuFk)>ypk=g-&&8aMDJLM>vJ!p=8_AEFJ6tj_`=nC{7Ldad_dA~kc?PB zWRcnqP=L*1@UtY6RF2>$T=H1~ZkprVc0vs!1eXaG@KG+NekIWruVpyW zDmvkJL>C55m=XR*I}!x&T=F3)VMy>I2oW@3E~K`CTWCqbtQ7rdi~IS6VEjRJ2?2aX z6SOD6ir9c&fEmQ4X38hY(J3KForz3W$^vMSR)7@(K66BsMlcepW?%|vf&2-fQ80@n zlI@~Xb^=M&P^K30$k0S9Gj>3QNh($XGR%}LGRW!*dZW}KRWOO|jEv1m=ttlW^&q02 z2jv+vED{OfSoFdgj7%alj`y>G{a6jZB?_QT{*UAji6KL!H`-R&o>QPEmw+uK*c@VT z`j_>)x*j7-dGZ;yZ)jJw;$O|@=P2fW^y8UWR(Vw^eWp+%`BI*K(OlK^8)p8BRra&5OAfW(E6~7e>MWcMekoi|Q692~n zP&A+`98ywjHYY%u8Nmw2Hxagzyw*sVzqt&P&~)-~Ek;#@7X)!=BztQzSrT^6C@xT@ zhtIP&$5>$npBDMU5}ILfI-EpA5dnQ-+Y+D!;^lu*_T(CV#%Scj>|eI*Soyix4_kF; zGa-S+XPI&{k&(rCw7Nj=+|1~&$v~}yj1;;>szsF1bE`#c1PoI-czf0ZZp|cdkQ`Kk z2ILQZM9&YqgsBjYStOBG#cQOOKbFV5LC+YUID(AAxHc1;59m=M*i~dn$}eZAI5V(ZL zb|wj>O4-yf;ZUiZKboF}M#EARxl0xj!xCQ%nm7&hhfoqWH0zDS>>lK2s28 z41woVE?kiJC!{dHyo&a4H_C~nQZ}1m`;&|wYM&8eYx2$kKFT^|8cm2Bqg8QyS?7@i zj>0@8WfmOK3~oAGYovSx-cweEdsCois$gGOPZk`O{FT><%uoh{W_g;c&7!2LdB%n- zpbN<>q%pm)P9nqMuvCi>ij^Z~md{l2X5=5o!-tYamO8Sex?nG#s9C2Unq7u^#_W6H z(abVOnrWChwvLu@H%ntt4+YBHqexls;LSeRv@m)6Pf`Q`99xphab(5^8cAIiZZ8&s zT}n86n4r?X6eWeCOUX{EVd5Wn4fBPdQno{dk{+Q98LBMo@=;O_A^iMBS%>@r-O~}k zFUblJ3fPj~pm8pT!m{H;F6MrVqtFHasiPtv03y>vM;I+rBldt=Kql-2&0`4!NumhT zM^h3EXi&P7lvSsajAc+dkwjYT5G|04FioH~f&p6$NtP*a1~f*PlWE2k0BL575y@=H zz_1PG4?`5`X2A!6Nt+~2&O#D3TaLG+Av}_~XEkl+V(d%BP~^N^nGvbz(KQ3ba4#qpB{5r)G_YEV)Pkc~M!@`%l}q>mh0qvV7MPYyPTr0tt9~FhV?|mb zpe)r3fKwA8*rN&tGmAVFZRs4#;CgySYL_Ht_yBwz1LTm*6hp6aY%p-jznoJKGXs6tc!lJ$j9_i!rl>*c^|G16-(>qQ)3=5QsIf;&Ntw>`=6>IQ}U>gS)E=lo7nyH0IE{aqxJ;DgE1Bmx+#{G?Gh zHNQ9uQO0cV6E#!`t^#r#h&$t^Vs{`2pDpdGpo7j!GVZ_+X^a9mP{t(kwlCoh&_nSF zP1{wKf|P11PqC*A7pW$6V<|&Q7hBKrelR@?^~T7URe%s3GcYMWNudp672A+z1qtvr zoLq`L7@TZ<5zGW(h)n>GltF~3h;F$b8^gLgl2iCwxuZ z54J34E#gZwiBHpMhP73GmN11LpM-(0}EyMI^PMDy$1@4O9L5Wy) z1!g4PF(RSC$SFFMRU>AC_{PWusn7y{38sjb0c>bmoL80TooGF^gke6)Dt=nmwsvw~{$1Iy@I_+c zs;(Ti>#?V5X53r`nitY*N1a=#-$@piv@O?7+sJ+Nd7LWqQ;9F@%16wIZGQf-WTvw4 ziKS0#nO{_odLZZHXeT2Oqd)VMkc*cvzU&f07%yC57WG%j%ThOFLKp9_M#owO2i874 z$Gk>f)MJ`?jbFX;*)&fh%zvIY1!;G9$Ic7C&bvlejW1=D!x!U)(%NS~znoFiwW~_@ z@sG2p{n&4P_7zoo!dS>;zwnBp#;$$g(U;QM$Jxj)6q(?QslnPwLH!iE>n7Li=atSl~K`5+A4NfrcGpuZVgz+v%o85%eRAE5=% zcSem7pKfM+f*p@=LC68J!K7x8G}{Ovu!lnuMHwedFUJ767ELl*fkkMn6r|B3$_A?h zIk_D$g+alC@NtWcC?XaFUzRAiR-R?S>p)hV4L~R42&aOW$xa@aCe6>7EGQ#ELxhs; zZ&HV5CPYqwJ1~>IB@zc`5JUgW7zF^PA65g3$qWN3=s+?WHsF9z65b@ri^z;jf)c@k z_Yd(aUf(C7g2_R0fJV#-7$8gtXc5aqKVY9sqmW}kcxi?gP612+l*Zw}2xsuN2H8qL z@1TM(RNfH=ks&n9eilAxayt5K35vHi%B1ra5mM?^$xN}pnGD=btmE^;8RdIGlH<2& zSFWiEuJQplBv#NkvV&epZV?*>$$5}QoR682-(ZhgMY0ZrpXKkwOG%1k5Q5_zmyQMU zNDbQ_%{A7*dZYb zVO8HW=6O_Qop;*mx3nbLXBnn;^QQdvx74*lIko+)vVFN!$4 zieV15;0LVA5h=Nbii3=p{DaH*jOEJD!YQOvVP$~&OfdN#c`)pmgYuKS_tC5ZEWiZn z(w>k}{32NR|Il?N)g`*C zyHu9WtkgO(Ga@pyjvH6sbMCn!REnyu>TUo=z-Cq?&E`kYj9V_q=k;i$h`8b$KklD+ zKjELd|2HU!;`<@A?fE5|yf0y$Ubwrc8r1T@V6LB_E)ejW2jv@}mv8bJW@MH=FI<{L zNj}Uov{flmD3>I_<#s_AxaVKu^MoAnJ(3ZfHa&pTlLF#oO7_4Q zA81~@N&#mA)u12@OQgy?$~b~P-A(NYsYn@k(iYyNHoLDrATWsBF)eW`OA#Ki7Qv*T zlQ2TO0FPSyX)2V){jPYQHLblfym-n1(%{Kp29YeE^{^zpF%)hpbs_Pz1!Sk-QgI(d zPZ$iFOh`#@Fe?5C%ww`9>AJ9g!ks=0a#Lo-8vyd;$Lkcm4m0ye%4HQ_jVTg=7CGC^^MvRbd0Bz5VXRqR% zK~3zb$Kw1r4@hdrWDjG?s5B#+JPvytUpg%bLEOQ{5#>?O8Br3qhJ&Gkovt)# zOfua=O=I><#GE5$k~s`8LPXB_-mxtV@@~EC;LXFI^17p=QxxvOl~9*Sw?!mpvN}gAM$uh#F8$6 zEwROshfHcy8*&k4BHNGJgS)NX=Jo05&W-ua8{De3WtAPP(`s#PdUtC$UQ94$Ovl5a zKzlWL26nUVwG-Z1tk8RYGVUN%<*-2#A*6OVtY>+hg}=x$K4(vL-Dl-*(u#VbJ7E%4 z#y;=;H01RMkFRcSEMJ{BHQT)JhkB2YqfC}Map^y5z3+tX9aN%(Ge{BB;*Fshxa<@;SN#q{;uWTXM;8LI6b#|1Z5qv- z4KF)%D5bveCshb}5|uOlafWfHE^>|@<=7Gz3@B)SK+ zQ6VynP{v$>jC_#EFad?+lf<5Hgy3RF9n}y66EycI4S7`9>Nx-wy_qLaGL9V45KNT$ z2pUE2DTZMt;{sH*j-}CFGLv#g4Dkff16pcMiaXyJpF#~GbkJ|;u7pKn=VXK&i~(_h z+I)zybtpP)#fi*@rAhjydyY9`$m*Us46VoEOp+6Y#T;Eu7;uQiW7go@uiyp8~?4;fjVf1nUkuypvPAuaZS&h3$M;z$KF1kSs6sfb0 zzcfq!O=GcVm|?m|3UzuyiV;0ok70+zI&Lfn!Owc8v_j(~#N{{=1yQuYJiJGdLD#u| zNq{RUj7tGJK#itYAhby7&bXVjwF7pXMryh$dUP!R#<>?sC^62A6t95XT1XmOM#YCD zpqrdK)pXpPJgy8HVaW0dl(HmCkN7zzXNNsQBcEA6F{qCptn)@%-s|5mDReyU7WKEQ+W@Y~grq)DeHIIJBI~ksFn=l1dMj{LJ zypT$oLBnG!^npUegD9W*ILK5`nn?I6CS=&`p3L7L*4_Kn?8om^k1vgHZ2kPNZx8?Z zf4bZLkAG);>!(e#lvt?#h1jEcuO7ehrPDbl)iXAcG-&*=QNv_`$b5FxJ_eBSw+{6N z2v{QS^d8U5)1vh0<;6E1?&}zgTvqy%HzamGxcTwnbak~EKiK}eTerSh-~afn+uIBO z=49vJKKb?Rt(T3R2acTtzkb>@e>keX{Yy=poxdD64u~d9I(lvXD=Jyr$} zwXw7J#c%Dk9(=y|i+^HY0JogGK9qHRY=r7qlI6XTAExl!pX99N(L#a5Y<~0d9GUH< zDHYd~_OTPRLJpth56}toi1O?#kcZidFm*ew9v6w8`T^wQpoMzIWTuX!!mlz}L@en> z-$Wi9a)0oY0DD*1q6YD4SPPw+T29E9h-KjBqqrmzLFejHOJLkj1Q!I&P$Rik z0zlGaO;RdBR+EF$1+|*$2?Md(Eg-N}!@486#dAy*EG`0H2@~GAHfLqUP7uj0s-+%ya6+Wt`ZqVdP$xp zwax?3ktFHi4lfLLCT8<=;+p^x5lI^3=;47&)K5Bs7-UkH{}lhgov5lUG|@I|goQC4 z7-~Y6IHHRWJqBIS$_Uu9jK{^|W84vHd2&3IxIB?4XwIgvLh<%RI8m46>x~6KWIk!m zbl2UA!%1H0%*CceUi1IT^EIebI+?DPo=W1J2H_^zFGSXJ zywdxtzj|xu)7Sc)pM7@l={w7DI2;}SizJ_RN#fYT8OcdPe3I$ZOo-AfhnuN@WXH{> z)ehJ7$2<#%s;o_vsSF}U&%)gp?%bMJ=|~}0`i)FL#B2yvDznaTtbg~(=A+g-|Ks27 zzV+V0>|fjh;U0w5qG8zrlIjDqEeNCkOkgs18RK$v7>xKIAHvfJyg(|H$>*^Q!WkV) z3|Kx)k~CDwn~UEX&qhq6=b@=D;A|i%@kv5B!5081qt!lR!3G8>1F_zuDP2)Y2Os9v zH~BcdaqM0Bz69qYkXI$FZG1xz$*_7@)Zcr=0}i%EL0nzQJOnlzkH3!d1YbOi zfYC|MLI$r9F9b*CyvTsPhEslo^o~i-U7Wdi3e_y45&}Z}P2eUn3G4d;*OKlN*O4Kf`l%FpLy*@8{!iJ$32`Wln1 zhXMq>YnTz9cA>X(AiaMsu>3OOGkFn@$gAd5n%^z6>G-+NwRf?t;=cIA|?> ztjW>wu=;fdpyZY^HO-UsAREPS(nF3Cu^N$+#?`W^BRqx{NDP;3)?--#qIAq%XRW< zdr!ww6vGfD!30>%wlJ|ek1{YSRyzB$#nvQ5M@C%%uezj1u~lLU;>1bMY(qRKItSR3 zyz}g-AcgHuwk>%{CNatstC9SoaY1vi?Fd8Q0n5B05)h)7*)Q?|RI@Uaw1M=rAR5Z_ z3bR?@RIAB~Yj(_tdriPymXMkYVMZctmW~PKV}c9ALly(gq!v|5yuo_QgXk88DL|f} zC+?%r$)mE|6CsPg#8)V{BM8j2_RkGEX6)3DVU9`WyjUch_A(*G{>2|E3k+gYNex|R z%jg7U(-+-{bHr!S?F?HHTPjtHc?9n9ru0a8X52seo*0HcVEy?ys{)V}ne7t}E8fb3b=kzz0bLZ%vLjIf(vWY9 zYWX@!=&M|bO1N3x7wvEviHOc!%sg>!#`kU=JhkEG>)r0b?%L+l_0d(wI#;8EOAoEk zesSF0Kkqj4{*xEayOosG5WAT+PWhd5p&Oh9?-Y_3%7D z^C|w=tq-#ahu)GD94in^WX9tUfAo{i;ENx9d;jUvyQ@v-^X2L6q`IZUe`fkqiO@m- zEZbqchtB@STXuYX)4(<&Kp;w10e#eyQKk4?`JbyL-K{1@tn`BV`If`wy;@H+^ooTj zJ@=H5R=>A@eD(hR%ALR7*xp>cKEHCMH&u%{I^36W=&I|~WX~K#aIxQ;seWtAB2@JT z$HKV$>oi+Dzk3|6!|fcqdTaVq zxw6Q~7g~;#nGC8g6Eo>`6CBV{02WOV^}vJozJO1 z`i;8n=|OX}KRG+K+47@${Pg%>e*0!Mzu7)N`Sjg!`*teL zXnZshNx0zU9g1X{ATsGEundNH?F|VK28Y@h7bMZ+Uq(3N9g;KJJo;O{hD)pZZ+#A@ ze#JtO-m3bW#0|EWJ69?TB&shBad(%{;K-@k4WX*u^C>Lffro(wE~9i3|g zrLDaMUPV>o;1e$6jb9P1$lKCNd0Y`vpU^K~csgp9T5@htbF4NNY3I(1Pucn8q+`an zJLG`}rh4;J(lC0ILy9By%g>(6Hn%p{Pfzch&ARn>UcSWWk9O`pd-n0O^R3XPaHhNn znROtX(ZNtVLyMJ44@xh*JPu{x&&7Ajrm^Wlqllev7~Ap|Ym53BM>NhiFga0B@HCca z9`mD*$-d?e6X*V<*@9re9KGmC&`S`#c~Np7+hf;$$R0J zG6aWpyQ$&7`CKc%lvHOT;Vfe%UVDaq0(~AdF;mig;{MWs^s4mDuk<4S^DjSKoZtEB zbL+wMJ?y;r3aJV~^Ign(-p43}AjOX-c|xdMV+%E%Wmp6%%%SgJ8Xwbjnj-jQ*PpWm z#>ovZWhRQ3o(}vKPQjWK2?HHq#;fPnFvy|2k7yaIo{S1HjWfb$LVMZBl9z~+6Z4*b zIxme?ya0WaB;{y%g6YX@5hGI6xY`o+Jt9e&`WUsi=#pRVW@6}Jh{8!p7M4F$0$t{} zvX_}pc^A>Cn02`+1i^e+5vTx!626;5qm{x_Hzqg1*-2|A1!P+ay(MR6Sh&RQ_@qBAT6453g$c988;4<%zI-$ybi zM`3BXQNNRDiy;^(u;m~T7ZhcIk&g*f<1F#6UuAg?lNPZ|vflUPQ4~egqEfP}C`?53 zIv5eWEHAvyi@f(DAEgo6R;1K>Y>oV;Rw+-fTfWyJea3z|=<^Qo4R{8xdBc)*I%fRzt?-2ey9W!aLo**me|?LlRoiFY z3rC6}@Z>-ZS-l>fnZ)KNm|r8Fh>~dy-d1iU@UavDq>|nW{$K|qt08Hom_G?R1WjaQ zFhOc!i{)e-Op#?Ah!seBlh>T=lZPoN=lAE1eBlOnvY`~3s#0&PjRgT1sdCN?9hJ*R zF3>Y-tW{HWBBdy_B%TeijUFb&RV*pP1V_FX90j$|fgIrpF-Tp4K6>q%D4I$>H%HU` z^?j1a1`Z0~%Eg28ArVh*5PnG_l6~^0*%059P-HHB4OH_Vl8jAGIN}%gHR)#jM$d#@ zNxMeBO1_Yc1W_Zto}XE>tj^#WW^$SaXfreqo?$#R@NnHicxojJ&KdweXoT{(8h-|2 z5t9f*>g7VouuXRY$0UgqYB191&NBJsZc>;i4n^I{h8Q@cBsEb!GiGr?$!$r%3a`=7 z&}AY6c#N1>F{qeMB1|SME)tQ#RZWz=h?0+#5pidD@12V!$&oF>hk*&kjWgQ@=wJAf z#8L7=Ne&cJ2JI3mhCUvYfE2Z&>IwhL82yxG*BbJL`ieF^Oax0e66@)qMiC^yiW)8f z86+p>h)ib&1V0fCOf&5I6nPClu`HD4)M7s(X_PfcCOJkb0Pe976zyV5u$4*FYyuQE z7qTE>bR?rrtFgb4#V{zo3M&+8bxX<@XjS%&54AH`!;Or4R3dVU9|1uRbTb%@gL8bQ z9P$Sd1RRO#gc$M6BIx=Wr8eRjv@Ag7G05WawC#N8$m`#e-tliw66c)@Sma zY@khzaYwOcnw^ftXK688_da~jX9^f)jeu$wOXo+);6E- z9|}zmHk6(p4C~bun~Js*@0bcZ9X_)uzD=UOTpu+r@nyN4buIPLa|++Jdr^1!*-Os# z+83w)8papsJ`0{HZ&`nalU(dGGsINY19%$u~5}WFfJ3d#>3?Y;)@X4o+ znMu7jf~@R6wy5jD-O0pREQ8*`7t24{T0H$XZ%(G&_F(hP?vuCHo9(CXyxL_yA8xNK zT_eH%4=nFS2Z z@Wo_*{H2*CD@BjT?dkX*)U13H;*b93>)B3oCO1SIDyH~Cn6g7A8_HtR|)1ADfTZ`Ik`%kI0vf({ciDm#McUJK`hxn zLvdFOJ)Naa0dy~m2jwQ8GmGquTXc@`8kRGQ(+nNIvf*#YL zX&9-erV2@ff{ztH{?*kSL~){ zxFW$IFpcY;$8g5SWj0Zaz2j<{myn-lKtWcPmuENuL9EIa-mW9#z=mMMHQG(8taQgq z{)SETM2`R=3{L5V$qxR5Zli=wc)WTJUGr>Z!aTp+jYb@iVq9GvyPH(Lh&bD9hf7+E zkY+ZeM7nV-d=M}-&%DuXqR zGne!{(AG3OQ1C-INlm5+Rl7kSs+;bu7-R=_?YibCHkqAx>=q09}ibbSYuy z7o(0(viKN72{rG#^fuGrNE87j)Z{tdGQuRTMS|ReFqy7&Gh$+dVb&JEsmP$ZNXimN zEXgBM6yeey571-GX^`5&XrxRyp-Y67A|xF~KyZ&jqM&C}UZPHVsh%86aolj0Fr=82 zSfoHhPxUXz3M5d~h&{LX0T<#G&5D&P4~u({|73~aK@j%I+tM{-@gQP`g~~?ow zYR~)pH-eB(zA-QIfp7VvE=bKnK7g6+1QiTYSq$&l7hTX)(jDHXKk*Opsv4Zf3qOwaPic*c`sB?=s}z-f`Y6mm8 zIk_jgo9RKUH>UK59CdqJ*U!%O4$sdYT+m4_11?Q-+k}%EB6;3 zzx#Ay_u$dht*1+urvKOWYU}*CI-NdR{OXIFU;aDU?6!lK2GhNE_vz8L>R;9NfIR^wE{4O?_v*X|F7P zFunT5;~O8!D&OokcY5t7_2MTVF1!{zGn!{+9b!)AQz zy({;6!?&h)->*L01#nm2zI=5!y;R+Qr?;>JkG3vx9c_ut#uKb#Lgzt%hWXjCsw|NZS3hoc93)$#QF>z%db&u;dn z+k20Dbr(t;b#_*kPp8f9GGFL$L%wl)Z>^ah?5v3um+l<2yKAk>N2k5*`_hqXS9cCJ zmUlnDeY&`Gc{*#|`1#Yp@Z>R)?d&Y_rt9c zAW*r?-9wysTjhbV2dTx*(-J|Aj&RUg8!_!Imj z#zaajiDHjTwmj)h1c#O8aXd4woli{y@S60;U3?f=lKgyX9+MS89)>dOZpBr>Z-mQ= zO9L4X)N`N>@B-2)w_zgLHufiE7so+TqEgqEo~&1Dk>c~3CR4IgO?GN~-ey@xb=Wqa zY5r8B^>!?)gc5)x`x@yLJCd-NQEB3hstQfZc;56?V;uFKYgMJlU|Kd&>q#^6SJ;#x~k9Tv#yvy)jmPk;|;M02SS&P$H|55?#AIZuHI^E z+O)?K_Pi`}S;O`44m)|C3C&M$37=^p_pshE1@ipHhU(I|WOfJcD-;_(>eijauWX&& z-R!Izvl2B=QHV$7m=TUubUj;UG@0&R&j8D``*?f@Ae15`Ytg2X0*BGe_^4bNCP;+| z`4hh;ej&j*nt`C2pBR~tw5NeZ^CpxsI9nNW7M~uuACV*6?sO_2Q4{&tEUK02wbA}Y zj3ZW!*PS`l3_kpv2-2bUji)~iXAk=^XGqQnA7i#=T{~_moAt+ z`Hzu#1g)qBkN?8ZT)VA-xP9s7w-C!Y}2Y$n~AZSsi6)w?t}2(~k{4>_=Yb&H_JSh7*hTnkaG=XV!szDEY=JGr%sNw{&WONbeSsIEF=tNP%R&BbwjY2NqKSyhOm zOOx|cGm9y03Rvl6QY^ml5dY)3c^`5)?#CR=9BO!s|2@0wK* z%SF5xuvSlt^9Oe{PJHSY!QsGXpi6s>STz2KEm0>8XXKDO0w7^20LleL z0Ybr6eavG}J0^e*hFM9&>Y+(2alT>7u`{#QK9z7H)YTuD9BI@e{%K4as{wMggOlb) z(N3)h7Evk#fQxdn1Xhy75E#Lv9~oEU7tI~yBHd59svI2Yz4I7p;yV1qhHu9M7c&?F~7$T-KR*xMqSwiJ9ITV%2$AqMm z%4O+RfF$T*3faSlIRp)}6G7^sA9iqx0rMu~;ZHD?as+fQ@x9oSe zY%}_Lu1l#gvqTb-)(IngXzb4=wLNlvGMby9Cj$>YRS0<#;S?xsG~LOKmRY~#Mu{xf zr@o5nty6JiX2}TimzXY-iJ|G3)JPVBCpyILs3!IoP4$p?_dyf{@YllOZfF09y>ujB zuQ!}M!EiCOj)fT2xzjRx!}_y#s#Z2@Ng?@_L91Tsbw;CKD2T!D6|;p`9KJT0?Cf`C z3kXr$tVdE3qxm&3hA^kdn!z?XzA<2x{?J!@(X-H`H(#b_S>)WhYD&`dXp0b4OIV7< zc$+!^u4Tz|$yK#S5gMaUnHl(3(s6xP&Jt^Bq_OT?s2)bI38!YsBu^p;7=NYerA9TP zFQ=GWjDiTs{Yl3lYw_sg3=+Cn!m>QWrve>L6#CRbYHqKQop{C?>ky;DS}A ziS&(~Nd7Y-fiOY7P%0U_bP+uwUPKuK$tc7tqfYKAcoc{RorOVOC&(#ui|~u15D})t zB1RSsL%l@wizvuIBv;Bp2SMSDXJ+syrn|6W)M)>T%qX9wU0PwW;$;-G z6kCKJOj>B}4li|>DU&)*_(g|2BF~TZBu|g>3@CRm^RF3NLN|jklBljtLlcXkOCn{% zglEJAub5)_T`eW9i!fp*MN3}az^NBUKK(_G;yDu)Y@tSYlSa|YsL><w_w7dTTV3h3o1^;5&+18ca5B5{L9=_w z5|P_q)I*yVOzwX%S^I#f${%Wp{HPOhP$APrN{tB##bw4EN{1!E9V~XfTzEpd{6Npl z))5UBC||D)A1)qLi;MH#X6RJ~=tAQ9ON9ukji z!lAbhEd{?m>18a(K0s5LOk-ChldtGtUctN>(Gv4)# z7--}3Wo>5%gKDD>egIdX9KSSFm^d)eQ=)L0M4(XS7yShIPGaG;{w79GP?qdVpdb!h zQD!5H^9-5ME~?6!WF{pSh@EM!mM+w^5wAr6;spZ^4rA;VoC^W~3!{Th8Ap%`KofuV zlh|N5?bjeWF@qY!zcnBVdk>lrsq%-6x!Qs4rY* zxDYqlrvd%3RreQZ66)+8-V1pShNbKLnOGkPVakDO2FhK-`=-QMe8Pe;3#?AUT)px% zof63u|M9^%8BUHk=FW1VlY7{$(Nw=9x*(V`WlAaUdmi9`UOAZOEF|&@6`0 z>%!+CC(8iB**VC<5N3mL++}z;mk=ZqBv_k4c_2;9z5!tg3?%{5CH&&(rOqTKAO&U; z;}7dIiUsy<;SeK{AWt*EnCxJC&SD}t5>1xI6nm{1ZIFS0sxAi6!bVR+qga92=<`n| z)z`=4?X8XOE1Qf`XKOI)-5HL0?H8lYdH)DFH{CUJb0cN`z4MZE57K86BPP0U@s?&u zhV^@ll;Pq>^xd`qWXyNeCi^Q#K0fI4tN#HB_@rUO^?T$d#iua+Bo@Wc=$j6 z-t5g+v^r>^-4T1-{ky)?zVyL9D;_n$nuG3mYb^M$|o?loLfjXJDe zFs`eM?WB2(r|utFvuD4L;a9Ws!D%Nal|6eNW5`LESdRt^oF8j9axXNU_vEM>)1ZFc z`{-68$k?}e@u+;3f`*DCuFzL*XkEsYj`w3931)!?c%?IZIQ zP$UzKS^wxC0mgFjyRSX(x5+6?Mu)SPls+}47oqHU-ci?(ZLq}5A<@Ckco!j@rAX=} zF=Re4(K3WK0~6q~_&MHcTwkAOAPGg}y26T>7gk;#4GWS~OJ_KeiLR}z@fG*sEYsza zZ2FKnJ6p0*X_vR9Ig^l(>?7SSw7pr9ZR0rc3P&a)BA3}GLTV^otqld(5P4lp^o8X< zm2fq7FB0>IJ}!yRlRWN4iWaLaLUKiJNsETk&aQsOfywHl2Tj>Hz>B^0;n{H3ne>-b z3G;t;Rs_x74=ztq5KGtoY!k0_-p$-Y1Ho*7u`5%J9t5X#OgLpx_%@E) zlRZ!+aBBe&kY>vU3Jryr&Ny!4b!au%9SjekugmSfX)p)FzWqOgwhRkgwr*)F(wjU7 zN-2`@L@#W8oe@tR)Q)eHzeX02;pNf|A2(P0N8`xJ6FNKELABQA>lOszQtHHgH- zU{_rl2o{$bTnJZ^dzzbuOH6ud0c#M(^XlT^%2?z(D@PSOp zi|c_sWuo&CHAS~VFI`S{KfY!rVZ!SShGdEo5=q2e_6QH-;YXKr!#GG@pr4)aw?HDF zm75tKTc3zN^iGpZE5A{)KU3#1KVh;*GE=TIcHKzO2FI8%YRx8zhCC#|=S|=xaT|kH z?5F&_5y*p~WRjn3C7$PO^FazNkv9tsVp)+rbN22+azn%)y^SFa!}z!~6*;t#NfSzq z1B8{on`E^_1~f?0#-8laD~-e+88K!FTyMjj688eZlecla)GSYF4b7)p6x;Cv##gwV~s&LjJKEv(G@4{yyL zS!o=;(Rc%JLQVf*BD3wVjzmy=Ui@&+$#aE$*asU>BAfJp!&*LCvelDI2!UXU^tIik#~b_i?l<-Qg@eaA>&FoD$F1!Iu`NwlTC=RO z4(=oe+nhZ!({dP-V$Op)pV*&mG_uR6@<%;VLzV$`O?4myX3N4{^wXMkNaFQdc5p>k?GH$Hr+3+Z(ADIALe|G z-~9;@_~Q8MgJI{rZNlQnpws>P;}_%SXIJ*5I(6miZW0nNUg*?W=#!8KvRg zMtUQS#5(bw2oco~lJn|ln<#47jYA*+5#k>DHenl$^dXfY<55rfQt%n(;T=(H`jp%~ zaW%7HmMZ`Nea!k-Z&XG_d1Cb}p)slwkjJ4;{6e$Qht|?p&MjHwAo1j=H#gPCj)O?{ zo0um&mK|g7buTkd>3j*e<$;qHH$v`y7?~SDrK=27iYZ~S&yzbspCZ{sZ4{yg9eKxu z+tc7IywMN-TlNP{2{Cz!#RG8hinWlO59Wzjph;QOk%UEpkj#a#<2sUD9l{`B=#O`P z(gtyGBBRh4!vee+yMY)UQtBBOE%Xn#uoa@C^d~IVQXFbAdY%<=iPs#%r(Vl`y(!n1 zLoca96arOcy$d~Duim>dIayhrPe1;6{MqH|>g>l$?_ay}`r_V)A9NnAwyVoSMUKNS zkIzP<*9ZOAe%(*{^jE`C_v{oHk2>-Crpw}l{TfiJN)pS&A|q1 z*L1%cACE?V(dqwz!sBsu{1vgvCXr3mooqI}lcUL*b7|#A>=V+rekYa;8T$^9(~n&( zM5P-(Wg&f*BBV#84Y+#2M*116da(c2o2};EOVi2wxBDNxHUDJm&8yenc>T52{*A5q z>dlWjB(GPue`z`M=x5vA&y;I6&6f^ylT?FFDcR|~Xzey-erPbhd!sqM{ex$d_wLRQKNvh&U0D0<_S>)g z&CYjkHmC1=Za@~0GP!XWA5(I)bpUK7SCFxCi*y0gnl~e5nZx6MxClF1?(q(((w}g} zq@tVh=um^Ni6ZHb$`&%m^7bcJ2hG#zXnlR)Jf^<7+|NF3K5jbeyT`Mtd%L;w_}(M4 z-_xUA#$~Y9tZj{UI3LrlGms|Do&J1h!l)go5YMKx_TkvrXeH!5=$$YDU5Dzdvdc8e4sh$wO(PVK25)jF`M`p~`eBiiQ^lx^H|BFrQL==mIt0{hL_On$n zdo@4dCIuvbtcnFrE;mrx#+(X9QolqU{2JM!#8cHn?_Wvi-o+^{c6x@FcnO!ifhp$6n-OqDuW24~nM z(d5R~9$6qas;uQeyMxL3XuJ~_W|3c|lapiT0+WLJp+h}Vr^n@nP?b}_{O~SEM+cvX zpvzqePK)e|0SpSMm@8m5vk9SI_{fsp$$>9@6#D4kq*^W1LQ~QRXvdtFHO-A5ZyllQ zOqogjC_csNhD06g(UqM{@3jtX3Jf;gwV#z59f$ehRi5jaOe07~tc$;%=DsANgBZg<50cXXrXm&Ai)j@}`!! zFm#EJB+Ei+{tCHAhm;lB^f}&?N$a?ViKY|9_AM`GrP0pVRC2NX1U9>~o5m@9Ww<#Y zyp58D3p^qROyoe|hTx=~g~y33xRa^Sb{ISLlL#odo#nM;&^bMc0OYCEMC%j3f~SBj z#Iul+M-)znhk-JXG_@y3l4|S*xeWIpG+veZHhzZiiMySIXC)2}E)<@l2CA8+ z=b55OLRNV_JumyYq{bl>znI6s5P>neklct1JuAYhUH&ynW`{oQ_?k1S4g2Th_Anye z9jW7CX*mfy?-A1HP1PRmUj-8U_)U3(wcw{9`2uwyyn*uNGLYncI8LU2_v@FPs(HTa zM2>^%U_ANh=u0`o>6549^}fBMo1-mB8>1S3+^N$cWPEAJM<6{ZD}NH)J56r? zNh^r!4=T7iUq;c3mk}+G(sN!CWksg(hKp6(J~2q}^Q+BLGMtm{jq;uzUV;J)`Nx5V z6gPZm289eS+v4Bf@pxG`2+2VLO}=q#udY}^t_NhLS~0prlw7v*Upq$Tukoh-+52`# zQ9jW-uw_e5EvM6-X#2~9!djQcGM|m7)<4I=)WW9dtm-YHhGlYQ=~_d@M57g_5_Sg1 zV#dXOt+D25HF$P1>wiU=*1ChI_SK~|C3MX^sq7qRbn2J+hoBFdGUYKIi2>X$#i&;Z ztw*RqzM?c->JU1lO@E2V1^+L?_p9&xp;b{kx~UC-Z+57>-($@h zJz0qobtqi)>~zxUBq0+sIFdl(9+-w;~0HZb@nGCpTW)*ek;mlb;c90!luljQk^cc$Z;Hn(jmOb5f` z{^?|&)zTr;qxv^~4)YaHNWO{@witsG+lTh51JV&AMAj)8FN2E2c7fG#D)`tgbC4WS zGdZ8S6us38R}-|UuSa18*(yUOPo~Y@ZTo|q>^GAZ1X-Ll%a4w&r`xy9vwYd!>16K+ z(c`tw?6@-g+ENIV(xa>f#P7@lnK+{7s=|_$lQlQDW4e!AdI%g(Ovj#{B(bN`(c7s# z%bWR(&6}3p?CgY`qjbf^c0{st$KJtaT7>U2CL-Z&BF@YpN?ni3#{YG&fq6O5R2 zTV)SGz*t>LEFL*67Jy|`YArywH}fDRq?rWHma(bx1txF@m4}XxWBdf9@i$ma^R(6z zX>`(`+XD!NLu_QtkqYsW5ik{t2x1XcOn^iK7JCj0%!(Kxk=2Jxf=Ri>|47~WB17m~)!H}U%cWlL&C05Bga2>@~7IrEunRKOglL3gO^^+aurm>8&3ViL9)5LIL zmEZUX6>(rN%k39uV_@ z48lm9MiB2X&J$zrKv7H<@+5uap)@4HM6kQOC!Kf{k1xM1d&)(OZ0R_th@c^2Trj){ zSO$f%2EeGHR-v!&%XR}HThZ?5bm9tB_T-Qt`6(GSk255*FLB3w#p4sb;bctPVvFN`q&qY# z%LbEZBNZO&j*BP76~n#!j1H1G-~u_5$##87v=;?H;6(if1>-Z=BxdxTpvyagzU0;U z#WD?(9A9ftBWtGH)0smJ!D;5KYa1}SCPcy#FRVLHawqGpjFbbS8`&x^o_?GZ!Xift z&0XR%5(wJVhwDLxnx*j5ne!|Fk=_KbW%B&ye`9bl#*BemSc-vS^37gnY8{P|*YR`_ ze0?5hslIri`^|DQzH>Vi%!WcwJP^aAnKg{T??F*7WDWJhvp3xkSs^SrgifI5$+;A7 z2~*U=A0*NMCMW1Lq==TAL%yuw9*y@lRm_M(Gb8XJL%Z(!ar4eq7d9V{hxhL3-FWo+ z<>=n6y1vG7B)wObE1P}uk46i+Q$mzAepNF$?O_pBIuSK`6hFz4(LkIBiUZNPcm{}> zi6c$gpbk@_r&LZnPmRWEI`tX&xZbJ)6JbT2KwML=xb!1E7+z_2<5wO&s_VM}@%xq= zWcK2w&*0>sb0zyjIS{%7?xbJ`do!dY*!5!PBGsu#PY02S9EV~W6@4^45lUjpf-`Yo zfH3tAkPsIRc?b%sy|QAIh${Hh1b>|!CZ6bSp%Mhd2a*+R>?!2N%hR2_)oVWg&)JiZaEv zxQ)gGWF|0p;1i>eF6Zu`aIjIA$+IFW+AqZiLUan^@Z_?AAj*iOlD*b7UIWX-6%O80 z&)CH{x?ltj8W!ET)ddm?MTW5h343=r?Q} zXyHieQnD87m&HBC2UVR8HMHPGy>WOpefSugV#QiHKxsPL-Kk!3)=kg%POQ09&o!{G zmo%W`cO`n}9qGm4$ocWLCI8)tBy7wx&mo(=ACa3;j-x3>0b&lwCmv>K9E)DcI!=hK~{&Urne^bsBLMAJ9|Wbcr?qYR@8??2_P!NWI-aT@N_@F+B@$J?~g{;V6|h`9zN_p>kk(lE7Y8% zq+xxlKkFSWIVs#aqVBAJ*y^{P&1pYhE}s=UMaMGdmbC*%sZZz8ffd!zm}(B2?&2i; zA(|E_5pQ+?PIOU5@%aJnOz6~f;M(|4P)iD|$IlXs2vd2R;Ztb;4 zouj(h`O=YZqt-+=t?Ye8!9AEhP`V(dCzt#kcE!9iMzn_U2>)y;Vu5Y6k0kma6Ja*W zP8qB&Wf20kGHC$d6>uv5h;LQ401f9;ITLR^HjAv%rA*jrr)Pe?Xb9PcW0{F{ALj{f znaa^T z+UM3SCaAbb#0wmgEK=$*{^SLsT)fd3JSdOPt-twS*m0)muM+66x4A6N!au2PGU!nd?;rnmd!;h?{T zY_tZg7c|D8F)QQD&oIJov`N|;k9_VWq=^Fn6AhEQv<6X&&%-KXOtt4?tCC=uc*IU7 zZD+2r%RpWjm9VNh=5ZQ5-H+?;2iOsE*~=xPj2#gK2`Q2(^DrXI(rbbDK?veqn&1Ka zoc>sXU|G^k0UsQbSEJaIhdF78w9by94=cu<;?-#^lnp;HIL7rLXa!BQ@~@0I*n&!V6+k zfQ6JXiOo8~Dz`_U`)k6vxQ zsNZ}4_WskKou2*V&F1?mkyrovgT4MIuk3HX{?lted}XD(B&t2`cZX-y=)+-k>Hir$ z)7`)42)uj=sj9=~5&hpM?#^eYa7J7>9qm8{P%gEVh&G=_b^HnDl@~6!bbR;sNA>M@ z>iWO`7tL#bJo)gO;lJ7(T>9ga=1=}`^yb!R_wYO4>yECz*KGanUwr(|Z@+u^wrQJL z_nXsSj7EPjsDJSPRsTRNzC@>Qei1O-Z>~*#N$`$`o%ZoJCOX*qO?@a@d}xarpOjfl z{VAJTrGq$-4&nWHf9se|Gr6pH0`6?tFCT>G%JuX8ggeN&E8o;q3sI_lfU}=fBNb6@3o2B;mhCoayb022h|_^-^2gV{Oa%jgSnkwd>#Ebn0$WH zIQMF90qhyJ0Qg~+kCxDUYK5LY7x34QSi#~d>0>AaIw{qo*Y}(2e>E7t`oZ|#ch=g? z$9E3K_x`Mz{`mU-{`a@G?_K}B_U6-%ZXWMFe6aFAZGRa(`8%@qw}@*U>i_ANvv{id z1N5gMt(ijt2-jpOb!7RnW@)&M_Lm8(VJ5|6?TbfVjU2egqX zT9d6~;<=TqA;_lHBC?!0Ln~yGyo%qL>4_4A%9og$j&ny3B7%G(+E#3kC*V49joR>D zli=rCYbsFzA01O5IDj!Vkwfl1WRwk*VP?r}#BG9f$cFH7Nr^=1vqo*YmTmuYW>kq)>-dl_F)*E~P!wvDDVsK35DbQ1hOHk?rCXt7=` z&-ZO|#r>sv?H?bd-&V~hpIlx=OuW!e8Yr*Qa+E6)5a%iVDFe%Za<%`Cmr2*9FwjM; zAo7YBXNbbt>t8VNE|uTVgGu+D>*L24IqSS1R}XKEAA~fYcfc$zhb)=6H?S!zXeQ z&>~|nvtJig$#BEqfNyDBV_Ck#0gDaAwDGLl z)k2XciGbLgIJ40m_se}m@-p{_ukOfXF_Pk*50J^$8NQDk`z{mJaT{j6FqHg9#Mlez zUsor=pTW#nE*KN;(jj-39GMVbdcgi)Bga;8v2C%(HpEc&v@C0x^hY>u`$=tuV^KxwqZASy z$h-D4Z#9#Bix*)->X^gyv2>lUj!*_DpAsJ$$yiCEZJ>cp1O@e=*tArfdesy$+R57F zo~TD;Jvz?v39CV2Q5Irs=pr?eKp)~u_CzfzoA<4MD&6p_GAFIfqH9?47U)KA5#>~2CsS> zSfN&_lCb{~EstoDG3`@@@FsI>QNfubPt|WIg*el*blZ#oOokckj?TVP>}o-0$q@mH+aO($Arlbwb~MR7lqgfwLaDsXl4k3*AkN(wUDwDJKrjTh%=V#o~^5r9d4o zObkL})j6caP<1>NIp|Gp$*NBPDbS4afv_x~jz%*Dxdv}C1(#VC{7l9aCtZbLm}dP` zc0n-oQW8b+TtO4V2_OVj0?qN|P#a+gypgbQny1eQuYGS^qp~M?zSukVj0m`mJX3;D zRQU}wqH)D`(>AIH@3A@-L-HVVVWvktQU|iTRSYaqMNb3lvsF$?4s>LME(!?wPt4OX z?2GEFZzPj3F~c4hj`yi8f0Obx{=&Y9Yvzc2h!DX?>VTlqg5ZiSl1>9mt~j%I z#_z%0LEwSy-Uia>5V?kBNrj^pO8XO6JO(Z1$pC^fF(vbeF*ra#~Cz4#?8L?i3H9i2Int8`Y9Ziprp4z5UpsOe@jn>IfG%y)U6ABW8 zIIhXv^urx+5Fd%9Ec?(m7-J3a#cJ7!WZ%p z8(F8Y$~O@#n8AmFfbJL8L?*ZZ0Rrerl)Oej;R)A_0m5oIrp%joK?vw2aVEJ)r4Mlt z5u}A)(uaXXz81Mw9p+qA`PxD-uTQzpJQe zacO8LtP2|I7R@)TkhG93W)BWW$2o>++?#kR$?m1UQBse>JnmoUs7DE-J+=S>LdBaD z_~BLvtC1M%z#xyQst-+;%0)e6<`0_b7NKP}ScSL?@#2f&vFnjVW>iGybbE5FN+yAr zt_BX|)e<@(K&00TJ@7GV6v?JuvD!G}XQUkwBT;!xm7{z!mC1fH79=y)R_S>2zbol5 z;#{H}uBvhlWNs*i&?pej#fWE;Se$fAJ)ApLdpn0kM(iQ^f(C%3Y7Db^tAk_M$28sJMN`kw2T`~DRrnoe&pZ^ z+(ksr&PKB{Nno^N1*+!k#Pc!`+o7rVaT@1%dQL1lN2764%g@#0s<*b@DB57_rsIYW zZ~>0A-M&$l5}{JpeE*zLO|c8`p0;9f`M;T*w~mPDcnH3HiT){Src^$%-?toc>i?XM z!G&t(il#K_TH81(;XOS?4=dgKlgGnJ2RF~0JFPx%INgnZIBlLETRj_*-HyjJp8Vp) z$k+F~eXEC+P+2>gK&upN-m`*%>$6&@7H+S@*g&RZqHO)Sb=8{j=)m_$0Viofw!|C} zKP9my-Yme*dpzfcnBf}@9FC%YcD6(DrrjGJ#LA4?ZAV7dwPR3LJa0JMUii%6&Z@SX z+mLkBvCu0AZGST!oryKnNe>(k$b&2iuHLBEQMP0M`QQhqqS^;cv3<1D64I(UHxo(( z=iMWd^lGPmrN}Xz>OC9ogSO#0GJ9Y3el^?~9nI_G_Q5W(tlQ7Kn^vTDClB8Ku+O_$ ze?GdyW3E+Ann7UgiOFgPV#jevpI0WjNVI8os2?B-VoT}caW-034%3r@!x|_f*f^=W zX7Msp#;Obkz4L0f*BKmuX0Gi)r+YRyYOQwcH#XeskM?X3(cSM~-QFATZ(O$D{q9PC z_c8GR^s(&(JT;QRv`p9Vh-i2GG^JZqj!&47F7qkzh7w*UGLIYX^dd`dqYKCbMM)|c zq`(j|zI8B|8l^6fuaf#Da2uDL`m+fE9BvX~GZ`Q+vq7jHcb;y=;HdB7?b0U#7`%(y z!Fcyo7fd*V;8lZDVi{k%geaKd!P%lc8B40|7rf*spSd)6$=&bO>h{>Mm5j? zHsmU95wdy?BZ7i3EzQ8~P(+UrQ@E7rCGH)b>yn~l4`fdD*Iow*mkp&*5;(+<)E2iO z@piyC?jOyI#{?#6IUI@+iJj)q@wl|Z=U@|1mq`R|2B;hHQ(~QywxxG0cET1ePcT5# z!qY11iNFS;Sq)#+A!S+2-+LH8J{ha^>Mi(fP(P*KLhMu_9{RXUK-mEYxJJ;~cVR7ZUmk3fIYS)Z|O`7-V6V(ws~@~SvD z8cbKccP;ApRoY3v^Ip@rIUR}a{N@KHdoOVMO+UQO_u45Baw`!@i~8fHC{NsNSEPO> zm!?6#leZq7=b7o6T#{Eg%OYRVhjI;rQ<|!CaG}Nmo%+f6WF9oSS02u(hCkS^R!&*M`%(JN0pAePj5{GToEr^kn@|dX;22%2Iph&kkB;KbHEu zbdg8Wf!g|#+;JY9ImwV9+dSZEhL$fP@8trZMp7 z)9!(Lu@L>sGt-jEb_jUkoQeaB+NpQoX0XBF_o!OWMeff0-oTQyht!HY z&mGhV^2`s*9b*E2s~3nF};e%;%Y@T7G^2 zx_W<#T#{aiv^|H`v82Q<<3+0^jlGFa;$bm&$%9c2VqZ26Pq7Bp2Qcv8UlsNoW@F{J za1t_}1hpPiO58w5ZP`5vOA>gBF8PKWN&hD5;f&i7f$+tpa#7+xpC@F+B)CUXO*SS9 zGx=eZt~YpUBMXPf(n5M&%mWIrZ6ZxXpbLj!dgNm0zl08ZXp8Uls**&h~bL6 zBd-(viVK#>s9YC=ld&mj&(x;-%bExen)ak*X+8CnXX80ieT&?!s!*L8C5_&MJ*dpX zkkNK9eU4DaS{F^!^l%F4rO72j1e)xC&JN^C28u!e4uJ=G^SJVvV?nv%p=eiZ%4^e5 zyav(rvGNo0lRPk5@#O~DhA0L#<8~d=raLw*iW(-x;33?%60^VFZdF94cbKd zV|$}qUfS8I^v7G~ES`n*vu9q2!-dyCtWp-G`U}4}-a@g-*D0K)lo8Ld1XH>6}yOC{g zKHP7ak3FoO@UchxiPvK_^}yyl>JXz67mv*&ye63!8lPyW8MkP^rRKPFoMUL?^-mL8 zNAs~N%aP6${^`-NDnss(8G!TH#0+e%VySgFdS?Hx5_NSvid~`JL9u7y3T{8J7 z%He9H4W(3ohF$sLXI_OC*_$DOYC4zfd}tGfMl=fI@YYJ_1t|G8Q$Zoy+*afHf@|~*7gsl zJBRJb;nvIH+Um3Y-Sg4xsNJ6&Qi=V}df(c@mBUV}ceK9U?zT3o{$BfN*xEQ+*xD8C z94eh{x4Nyp{@zoisDrIp>!{t@?N0hTD?3*H?9KK!+uQq{+3xpb+F&>A5IPqhdWh&y?xm2?;aj?`nx;B z_GIvQ&>jqy_jlWajn>gJQc@!;RiyyH)j8AEC@(ecWE@TsqpVmp*>`k%d+d zb}!#JJnlWF#E;IZ?ZfWsa?3$~Er_*S%PI7FM}1qz+c2WpAKvDr_4a3~YZLFgj>~x7TBWW$TdXwaXyOYK+_?<-DS$5Y8S zgFi*E03SEmN3yHE=nip@E<}Pk9MOfSt&55u93b$4Tn*L9*RYCuOdU~OaDl!uVH0Jm zYF~ZSWzYNdm80ww<&KNOZ63s?9CxO;;;w0k`S(zX5v{#wZX814vt!QYn&`~+dcDAg| zpbP+sz3A;7CyF=G>o@Bf$5D362td@aT!Y0ITwTNGsW1`%m^TtP4#dfhPfE-XSyQ!~ zDKS!JX#itDJh;>v6*a~9plykOvde6S$D>GgM8{L2isJ@vP;7W69=T0oKdV)gx5hDp zrwu@r$jHhV{=VAva75FQ(o9bhP3}Y-8|;ib0@`f2O5_A19?g5<>hXclE#iPC>~{uJ zDl$_G69#a8EPKgRocj8S zjkZK%qKD!-Yk(_Mrl3s+@ZjwFjFfSL`72ypj3DvoVpbHd;@XlFZ1r7sB~9;RJ^3Kn zGz}rR&5=_HA4ED$;wvdpPvI!aW*$gy(6<1lY>OgvV-Xe_uLIG8fU z7S4;e=C?f_v`*$`Je7Pf+Ixa^nSM6uW#=N z>h}+ioqsZ}zf%w2e~$%Px-@L}d)?vV!z&AWKR){4^3SHf{@}+yKYa1$g|i-~g*Yzh z)L4#uJnQf^z}FK*mDT6FCvLGfunQC&fzll9?f)f%0HZ`ac|Zr>_xiosq7vs*7f$5Ox6(a6T3`k ziYB(lrzvJ})$CcqYirc;T^|hr7eZuQfL1>666PQ_+4q_(b9{biF6oPL^S#&V8$TIe zJ9+r(ot4l2ZEyN}-|gSsdsc0%JV4ovFTY-1_^fr~JJnKPXZoZw==N7^8oJO}G~}3^ z{<)>lJo|pVI5bxx#v+yQCNB$A=K0xA0VPBD0`^($?(6uDP|PF&Sy}(&jh*`PU)JOI zo($gmtK%!ze){T-yP8jMLz`CQ@aZFy=l>vf&a?@W9-gR~It4*t3n~>Z6ozBF5AbKp}QNv8dNF-(=d?zLTV5iW02` zAB;o(D4s)+8Aj|P|CLgMQn?Vbmv{hTvomxf*ve`p{_+zU!Hx^4Q1JF*MjzN`9UVJG zqoRf$lJdvIgRGiwhzLN;y{-lR#C@GWt!|0^*GvOrh z!49|tZ5T}qFtE_(|EKFsgKWF5`##b`!cy6agi3u13*%wq(n|)rOL6(DObu>PNgz@ zNGf0aMfv^DlM+>`(humq!#QW~z4luF^10TnT5rr31MCE|E2_A(8FzXVhsJR3{ z4nt&;f`C}LAWVf6rhrG0u__GTq*?Tyx|D`dI^t0Bk=PZb60-4iF=2v45&j9aCg#^5 zGsP%tqErI4l6OIle6J{0(+imxQnNp~S3L?o`IdQe@m1{$Nnx5%PN zZOF%K!WjaVLzku$({F_Qtay{JKvK%QFhK;lOQLLAQ0R(CDPzA3hQo?+b371@f(ils znvTU-8@JPeXbz4X_JZP~&gvq%z33`rL?vX#5-k=0A)}u#9y$)vg60+DhOh>ni~}!d zO`(Pm8YPWxku9L!atnw`1t4_lt#eVeBLGM*{K7Mn*+IpXOO%(UDGX6FEJRKK-wUOt z5qu>=sIg1BN@l@>&<6|_RfU*vH5#nE3Xi!(?{OQ?SLnT1CFn8(#u6e=U_!IfuMFMaJs$}sl!h4-_wfAaF`r9CG^Y@7K9(6ZJ3`rz}P znBeB%pNLiaTj%MS61N+FqB;5}93pv*{Z`l&hl2%#j4aer0|%GzW%lq2Ujg7q;fc9aD?_JsAI%p;H=5^!PFNDwh&DXKfo z;13@aGvbQ$De0e4+{9iVF*o6&az917IAY%tBRl3T=9~C3Q3pOSLZ_y;tEGk}DgiKE!LcCx5&x3Kr z?$MBH#+azT#!S#qE>WVE$^t;LW}d4efDY!E;K!vG;1NDqV+}=T>1m3M*cVkpa#USD zf_REnXpD?;4aC50fD39sLxBQWt+K&n5R1SD*yK5EN?PnO3tovH6o7>dYIhIf+G2$v z;08gU206->hYF&*wtFvi z?yPRJ2_RAV_Q(%d0;uqrU>EHH-!}=Gsu3BTwTJ&AAB9#?@!ND6tAMr)93ow9F+}u7 z*AtN<8JS7pImMOr9xHiZ6>>frnn6TZ2<{0*I*S4{D35LhzPnKB%MZ{x?V{=3B6qAE z_6RYFQyC^9MIx6&0u>=)Y$=nU;Ci=l-AuFsBW&i2txnzmq_+{$Ic3w^V71k_%C?vR zgh+g`m^+(W1Wi&vZC4AJu$_?Fiik5z*hv_rjgymz-7MH6QG1Or zs>9*Z@ew;er?=(6$;@qIPWvQ4eGT>BkVV?K%nZ`}EQs{hh9xK5YGZwzgTxjaXe*wI zoiFY%Mt#JZ8>TNa-=C0X%eicti+Eqc(;x{f6V^g{gAFmn-)JS^gI%f-;>Rq~L>|B| ztRn+0*|d|+*G{-}Apa8@A2B!SKq+`kn)g0KlBd_tHiz5AZFyXe_HWqYaJFG!gH$@A z*Y0I(xwXD?@&k4(;3=of{mx$+vUQPtw0-vTr}o#8Ki0q~5#u>4HdUCbUcitZCYM5u zFhzl9ftmcGXnKHa1mlJA6(6EUyy1VEp>J^56bRhGj|5D#lCPL+&n8-@?8RHG+{mxN zRc?G$InC3fE6?8bUi`i|sljTt-7v}^`vXU0iG_r8<`1u-Ofm}rAye-j6khz+ za15WSGFntt)Cx$T3iKF;OWp@Xk=^Roi8)`L+`hd%apaF?43WR`Se&ig`*&aY{2!LS z^4}=S!@TvS1yr%=3Fi}O>N!jSGmxyZH76oa8q)-dNeqZ>HRml-;xjq24PA>QdC4gT z#Sd@v4glo0ET3l<55uBvCHDH{wL3@0sqi5p^iL+rpXG02NiT4oT!D6Ey#&_$$H4^Z z6E-i^qxnRruQp3-Bn5q}vQ(mc9xsYM*4H>XK0%87)>AZ|QTV_IAJA|RhjGo%wLVU$FJufE*U!bK-!*Eci(ve6&nDrtuxR z@&>2>thW1PVZQd&<1F4nFTQ^34VGW$S8pyqpWU|5uKC?a18VQQ_yTJVHrCPhWlenB9^E)t-drAEVXq8b4NTBmtnI@U z9FiKmLz8gUU%+7cUIm-e;k`3+?Kzne53GW)Bw9 z=TdMDxe!J0=%PA+kV?@a2J}Ijx@Y8M6lJjIDqpakL}SZzEmK&*7Cv8b2CH;n&U?+G2ljNlipbQ9SHNYV3CM>jyW$;ZH_u+pm-pDR`t0U)xeXHBSQ^T%5S=X*mqZV2 zt0?yiOx!B|Dt|y7kqDZsp5=|+>J$B_0sNDqOTG1h`h*LyW_UE^E~yV@wtN$wO?|ej zJ2Z@!_nbR_u*}b9M|baBxWGdC;|R~?(RGlGA&x{m66h7&7`t-ctnwVv_Y&PsO+X%E1^1Q6CgrXp)Htdd*F|aaaf)b|JkC_Q!flbdN0&~STBr8VNbY-} zY!9l46VqGGnb|N0ni(Q$%7ZWrH9;Qeii#-wAyW*=MjX9ypQ-R#p+QqZDCxaB>@lw= zz>QrZDb8`$G3l5oW0>50OALb_c~#{A&X_$&p%1zhZmuYbt7UOQ4N0`9BY)HkWXz?! z;1)&#H|w_~PTgLZ=w|!9+vqU`5_U1qetq)#-7VWK(NxIb4Buz zt$`1h=TUd5zskWXurET(yNZY?K)^&V%MnQ`QULzwrLLy*LRKn?CU6~Apx^2^{?|)d zuiocUwg16=cwAaoT&^bRcD0ym)F^(b5Pn4K^aS2Lc&xtC#d47P^e^5OAMuOYA_LWe zUs6F`!~XY@kl`S!u}e54<46+-bs#T7rr zdxh`1i641zVSn+ts-<%EgEyaA4`Pv!B7 z)bHwm=}1@z9B8>^57hPh0okG^h+k3iXNk+@29CcB8A8DLeyQC0ctSbai($j|s7%^9 zdswT|zxsHv^e=DHC!RPPHkeIGZ3sG)zPzK(U3b3)qXB?eA_^oPxf2?4wYF z)idjpJw{=u?D>z-4P5=ZJ?kuE0?vg4Yx2d*N`;WLJW)E}wPbmpSeVIQ>$k3fyEivS zH%?yxP8dd99m40>*p1Zb;yddNy9YDNC`q-I^5%Q%&Ew$e@l$`ukPBN{aiBhLe4gB5GFl#b`;R-3^U7=atJe;0epY8`K1rvM z4n)5W6ozd-P6f6{e=H^V7NObEuYzg!a5+fj(>ThbFyfQ&#~dQ1>v6-!xve}Q;jrGqLKj2F(6q(MghfO*h1sLnl(r=i4kV82n>ge zA7)u3(okR0f6Aeg!-4b3OB2f}+l=JN56f#q&VfX}#Y?mNfwuq`6qN)7giUU%2oI?s zN=43wUPqhAXrrWBEKLpxk3#l}av9!M5}#+|-lezbGPR`JNi%VyMhlSVpG^?+_)1py)!}CFn7GE({q3A94d|;6}Bk|;>q-@MjKZ!Z!_jKiHYy6XA-#I$)nbgN|@W26b zUu-XGYzQk;pmJyEU6b(3Y2ia|5Enze^KiPeN8 z9wcUhHF%4tw}!NNV8$L*lqhKknZN=jHqloF`ydxse52GGH)G}sG%BAnnS2D3$e8DC1R4i>MlB-|@?@>wa)PgHT5GXC3 z&D0j02iZz&TZ3Sl8=yogkJq>+yp`%RsF`Vl3;vLNP8kaWP-H`leF^4>Go(=<2X>R1j7&>>V)LzG5|)E0$9*yl7k9QSz1YhxQ6sKFj)wbXaS&r zs_=_vDP*DTN@yu{tzea*EUp2dK?LQl)c=%C+re2D&0zl+G#!Nz)A?GFpfNP6F8#nf z!869OfFh7V$$%o~3J=G~=nwP;v`4M+N#H9+NCC8NK?+qEhI~{wE<;}7n9()xSL5Is zQQ+A;mb#Ls(OfE;q3jw%3G^+fg=DmNK(rlTmRmqgv;zqnh)4}pZ$xWS3<^pWv6KQH zB}1t?i#&mB7TPl2gdp$w@+xV-IKVMB$#J!eN2iI8+a%=+>Ry$q$MZzHs8 zmM%&>?7RUyACst+`K{I*Zb+bXa&nb9kSJ@e1C1~MWv=*3Zb-IBMYJSJe-V|ynYB3F z&kG*S3o?=@DnXVslI|y?NUxuqWVfU_@=}~yhz|%>G0>%>Wm%_<^ul#;avUTIcCW=u zT=qAY7Td3~ly$YfnqJv%ocw9ctPyxXxTpn+i=S0Zjx{w9^9_|B{qUUV>Cd6$-2=DZExdE;B%-HF!m-MP(jbOFQ_7N5H-{c8*6=eKu~$ z@S_eS(YOm2VXpr6b@C?HAJFO(?bvbp`Zj;=_$&bp@7~?y&Kbj>&*H%W`Y(NBySlKw z!LHTJ2({PSORt=+XZQRoq%2O%E8qb`jMswe&!OaYr_swr=oZ#roH?^uKSwEY-N**O&9Wo~u z2x1%Qh{c@8Yx60DM;)Dr!CKVP`2vX`=62Svg8Q2%-b|idJxuJsm^{gH`0dea`K8sX z{9Sx*^*X)tXSdj}ma{6hi_71?$9%JYefm1-lcFp5Ev2BN`FYSY{FTk{^8jvs~f%t&8PM!XV01YcVH#= zSWf;m2Yr9Ud-*;YKTgvLK6A);>LcDWY&Izcq3pk?2NjY(QjgOR8m&s{HSq*LJQzI` zG5CGnVCuJ*ZwMk*+b5{*8F^iX;&K1$&F9dxx3-I0D|?~$+poR#z4f1R-IBHI8jX5o zbD51=1-fsN?_lz7cooeM&3uPP#0te@xgtj0zly8fEoYNmSh^= z01X(xRB{Tey#*NQBXk-W6X<-1Y)94^ttky%rPjyS#wqNg)kP5j|eebL=G?m z;fe@gT})4sch)ouJOKEKPlmM07i;>Fz(a$qOt-#&~c)%%Q-6(c7&25_T}I3DtmzYR47CL6EA*oo3J~Tp3*f zDZq?GBX|IMDNVtf{6*z}cV6)nO{&GuLue@*5%UoFS6a;YG|!Yf1=|oa?p-nfVbF1+ zW=Jq^P#=V^umV&>GO%@pBvMu8>OjGWE3~aI6(X6bV&vx(#t09o(-Bh+WLNYMB!nMk zt`_MMvkk?cjCYu^gA0oJ!YP812LW)G}O3aV=HU*VT{A^XRL@Kao(_aH9*L@iPs zm@^=MLY6qoh=Gq@q6_I_-v91r&w`WF7uV}P-{Z23bEnbq*(>fxNJd|fkS!Cz@ujbJtKE(n6~8 zC5lrW5G=>ytv8>dcFJ(@@<8c?K9!V^`2C`~=UJY@kE;0%U3ADV_=*Cyr<4z8SJw5n z(I6%+JjK3~8-^=5c42et-sSbT&uAov^KL&$g>OB!wWuLGnZ&1ijI$j-ar6RK5BLyD zP!Ou8fEPfJx?<_rHy<&}foG$cs3bOr?umO9^N;ul6hQTl*&>?Yl6P=lvM3qQ%K9ek$OIa1C@47;_{4ihvotlWjoZC42(mLd4K^OjBk^ zBUDYE4kT&PJNQa9Kn{T`me1@X;1$&tref5nve+sT(`svQm?vslVUb{TAF}dikX(=&gmT53%qIxDw>A& z^9W!TfhgaDXekL?1&=h7Nw2YX%?8Ak$cw3wLOBhkDhVLEF8~C;sRtFnSm_FZQz40> zvxzb^Jx{(*tIQ>5p1_reg|nfw9~d1@%PB)5L0c39%Hb#yz<3aZqzR_uJLEusd3+-x zpbosEb7anRX#wZx5?)X!hxH;^9*p@T6BLa;m*{DMrUJ2kn_(CFn#v&rc_2)Wveg<~ zt5#G4UwS<1DX)coE6U>^=_))gDj~pE8d7_EDNZ_OCK%KlT1WQ+n}ng55KW<2L|gP2 zHjZDBYaxn|)(UO03L1b{!9566sRMqLK0)@hPHTdRJ4yY}$7&358(vL%j3R4ZC_*4w zqNaFYBqx)hDk!kxJCP$wK?QI>NKl;yV9Mt`XoYl73+O41e&v5!L&GVRhSOzK_kow= z8T2#-U;}ThpW&9MmdEWQY&ghCR7tS@atMZqDK{(G(0XAM zEv9iu3c2YNTph+kv64IJ&gxgf8jMG$TFcSXf9hvx*8{DPhmeC;I)%urxQnXM6DU5i zPzR9wq)r$PCuUMM{UA+|;Q|hflVNvQGBpt8pbH@r7zhnebg%P-Xd=RqBvA_bTuDK! z1c548M+8(tQB&MSqA+O?a0wl`B(_#LU&l+y>8oW~9 z%QRRa#OiUXM$$xmr@o;53c)$oneDO2OyKl3BN- zy&#{wDz>>?7NIkB;H`Mf7TQ1?T^s;Q>}RBHqo-+!+To}PCNM0bg2nb;Z(cg@v_e%?;~syBmh3jb*Mi0Q7Rz1qHzhL8f;n>SD(vUiT`lW z!gg64+QhUg@XUUaRXf*d62r*a&dh%L);8o|C7c7{`$_iNOQ)MqCl=#9@Jj15i$Mek z1vX0aijJjNUISioXXH?^Il7nBFOmQ}8C9Zk1xb|U6H z*0*4~psU93QTpwQ;T-ipI;(^U4-w}=-3y*9_s$2xqk?T~VF$a+!epo|=;G*T2QHHE z^+4$8Qd{RRJA}gNea_?{U%1KRO@15fApXm8o!&!#-VUXsw<0G0urX{gE}){h2N{qQ zq?;s3HD3FSp^L~==qm$9+Csws`O)jxm5w%FtvK=-XoM*NMGBjimoBKQZFROsa~qGi*7>lM$y0nz$Hw=f6%;BMQW^{KiF=`)=sB%m zNRIhKM<^Td`}z)d!iTXkiHItNdU28ai^6pXH}#k?07IWjkz(=ZmwB!-)ztT@#!6kj z+UGqox{lpwPuTSrHL~=wXE8gSbjy0PgQabMadDINzY79^OL1&&a;< zkX1UP1ri90FQ7fF6r&FrZCZ)p6KWOnNAc#Q0kp>!2VxTBF`K`DYZZ)fL)B=CULZ-C zy_A(&uz#UURrLev;7HOZM0Q{G);t+*5U=#D7ZYkS$nLh;7Psy}@LO7j7gPa}8&By*rmS z$G`T3vszrc?td+h)8ukdy21ePMtno+EW63qOcX=!IQxgsNn)Zd>YqZ#sfKi!4?qWo z+c7r%P#{49$21Wy;*{`8XQ@sPLo=2^vOL?Sf8xja#Tg2dym$#CHSb+8gAae{db|i9 z`NCJy)uqKF7dh$5VuaVewYK8?4-U1NaROFipg_?!zqfu2pM8AnFl}VRSTuAWD_1YRa*fN?bIcc8uiQ)FDI@3L($VS*B%N`IH_w=@aZ8&$euXk| zEFx zPtG`Dn&g@|*O$*W*Y~f!XdPVtcDT9zifcY|4LLs`R%+rmn1>{*({ctj8B^Qe1i~=gl#L^LxVF9I`9Iw5QF9$CgwTpTVpLq zCF9(-ml=qZYl4BnJ2jxfs5jD71dkA5wu+3<0R%)01}74w=V{PQM8I$0Pm~2sfv1Aq zcn;A5ExM4F!=_+kEa0Hq3h-Yg*a#yt3N!H?^dcgIwh`iM+71ik6f(#ULe15ekawaB zB^wDs75*u}Cdw!97pJ6xkZ(|o=TIRHNEZ}O7OL2p?`bOWG#03j08scLrq69~Yt13$ zD~2`^f+98aOLQKVM7{vpMo6l#M)VqHNh76)3M;j;i!Bw9(2158Fpq>~^e}EVSknf% zO4TXYLDy-w0D|!D_~e$>cA{==sp@PN!Hm`WjTG>4V{fFoj|n~NxeV1%>} zI!|vXzGvIwLRrKo$zD)bK?ov2>57a5y`Uj4g}mIRAPR9KA}!Vtd>7@>9Yh@5${ZRL zg~2K+hI2wwHL!~p1rP9lnv7JWd)2Y@8tO<;LO2D`3g@WJ0^uT2I!N>wp-DX{5*0$V zF`D9#0sct;X*(4_D@0`!E)WM2P)acooirAIt^5jhN1G*Ml!*a4868P8&5__r{zD%y zLoFwvk9djJ%4R?z#EA)Orw3)p=m906iAIG=3KJkwGCSo5Gywh};1dNav;xJ(?om%f zNJfA%3ym&-3q2?TZjlbL1f{Nu43!cmL~ei>N-b4KP$+}C7hNQDlcVEOz7#nkcY+q< zR$0A+#Zi@Gc!TtW?`7s#X3<-i0NI;j@jcuHs}lTySn5}l8-=0^c_aI#QX-A)Sa_=M z(L+^33rc7^`=bJ5yr58G$wJ^7*5!p1O(CgThz4NRK$ZHK*3e3Eje?et>q4$_aU^Z% zWkf_~rwER;sEqm{deETgRd9oTr?K*)^s$f*D-cR4(*Xk!ZRI6~O<=27U7ZegY7cxt zGIFJGbul8)?1RF7K_6M8Oj79w3`TgWbcqNUvZm4q2ozNZ387F+Hr_z<_*jD%3ZuD1 zFve@?A;b{%(-TlR!e=}Ll9P7J)ae{B0b9^BDGKP#OTLarq(vNZ5;(%g5*oc{|;PB~dP=>pDeypcJ`P`Bu$OlY2k~ zWyN$B<8wIJT5HL)HCBRZ#v?n3!T`c_pD7{edl@e{>N_CJhtfD^G`gA}MJanx~W+*;>g^)AA^lE);qS*8nKk zvm^{O<0T5>PJ0j-7Jz{0Bp#|;xJVlTV-}czA_qhz?5BNlG>A-k!lS7kS!SI9CIjF* z6h(c;En?kr5GXEfm+i=}2)lH44*)8mOMoaRl0zb>sN~Ki*s@%;^3C+1JUS0nM`EoQ zKQGV?{-BO*jEn3FQhto`DAlRmC*`s-m^e=dr9!N3Kf_wH-~J=f}26B1hhZ|<|am@Obygr(E@-$ zm4z@^6qO>v+1{bZ^%){2Ol0SJ&ffww&)D>WjX+-7-a}OS{&T;_Mz)!~y#t-r`{%#7 zy1&W4cYOP+_!Q=mT<$@5mjB}D?8)`|Pnjuy`8*Gk8gU%BEZ}f|X+Np7*q4D5Bs)9s zyiZZV?WYmdtggD0V}DTC%c!i=*=Z$I`;#K-Ox+bWAe z?p9uZ;w8MrZ5sGtdh^zM&%Vw^Ic(N-wD~3FcRq0R(v=ft@vQC52RYMw^(*;>ONiiM ztUy^#l15J{(Ke+{I1I#+Jxfj3UFx%4Tt$nrPq246+qm;V_)U5=nQZUeIsLbw!u;@a zko?2;(a~F9|D&U|^Mz;6pJpe||Hf+j?FOSuSKE^TyQ;gp(Y9BQj`9~Kozg1*)ZIt3 zZT*Yc(Hr0He|d1cdgaW`**lq$e)bfX_8ht31de&@G^IaqAuIYima{qBVbu@=amkhB zi)@{6^x{9J&FinNlS`W~Fel*XOaCdYS$}@Bys&wkIda>teMTzqLcYAbei>AfFK?-bREYsjT1$+rDW2l$a8Z4L)&CpZjeJDpegE zD{p=9AH3~t@B8lGao_%qr{Dgr+1uas^xJ=|`Q_SYey;R$zxc~fHUIw4e&WynZs$Mw znbI%C?|iKOlii>B4?p>~AMZ4N@!|8OA9?g=-aYvIPyS5z`cF1Gf3N@UpKAZuxv$#E*8qQ~Rk$AN!e~e&UzM zdk@#k&Ec>1f2#S(AFltx&%d+#@SyzjKmQ{?{>foc$N%%AfAjvkAODS8zxkez{;Plg z0qXd|cYfvff9P+0;nB%sUz+^-dmE2F_N5=W@%Ep1`%gUm_V>N}1OL$@kAC)vN8kIA zPwhSN;YS|%!23S(!+-U{54>;Zuf5}Kjh`qU{5F2xZGGzJe(OE?-~Rc3%7fqbw*K4B zzwOgp@z4L}JKyowf2_Dbzuu!?#Rb3ezkK+Iez+)N@}Ir8y!XZOmmd3(M=1M4@B5h# z{C`UN$dCTzb^lLg{z~EhQ~n2Uy!+bfk>7gHum1c0ONGDVyMI~u(cgIP!$0JI=p7#_ z`r$+Gdiq0)*yRxKV6pJL`P>SF#9P>|pU0=cDzt-Wf8hmQaOQiSm&b8vb4YCvv9F@b zE|ABjXZr5kNg^}JUHc|~gE`ZK{V=ow*S50#Fb&wK%H^r47xLgBa(~}DvICP1Amhk4 zm-qIQ>lY79>(KN=_r!7ncln0Hp$02wW!jz_Yk$WqpZ~iHu9qgSUNNojJE?Uov+VH9 zPG{~SjAm|X`(Eanr-2vQo)>$O71;UGBm1_9m`~&{tZ3!#FMTs}kNle#%x#c5*$v0F zIBeb9x4eKGT*t9P(+~U^n`102$6GjV5U~N17o>48w}RaF!u=q~JumTBuDkGW*{&5F z#zCB?caxcoY-0O(6~?L0ZjOHC^Uy!O!X6@a7CX6Tg(L%gCPjPe z#B>hc^z6)WZsw-{LYM^Py!@DEF0SX{^;+c= zbpFdcKlCj#4RR;sP>97cuzc4(ypk-~XVXoB=Vo~v-hlT7ft%Q77$wmG2XEMs>*m>M z?0i3q&BL?AcfTCpUpg=O)*{MXH%+X?(%CraHHR%?7uzU`^1xqnOXyIR?__?2+|D+p zwRV^D_@bZq^i!I^P!l_w-w79oUY5nt&Hc3*U-R5oqJz|)C2<-ZdRx=9ayQNwe(Ek= zHcGXk+;*tBPaWsWz)h_v^?fr8(3`+J2;9(fS0-ML?NcrGE2Wb%D+?S`|D|YWPIKl# zw(#?mEkRQ!uns-n3F3%-L;Zb}+l^5(%W*@;r_gAbIObxW-Y_kD;|A`bZ`tYLGUQl| z+)EC#IL`JD7rEyq+2MZ179oq{WfHyVIV+b9lWy1!OKv$!C7kaEp&e$2Tecz%^Kg+{ z?mT6SHQdQE*&Z&P)ZHhsmfv*q{U}dTCt(kT_;8cEse5p2g-(E7M0PZv$FZ59ZXAf> zMX1Rvj7$zOG_%CEeQYu?6FUi$nC(~--^?>JH0>mbLNCBD7g=a!i5t6|r$ZNe$%0cw zW{Y_kFJ_?TIht{ZqaBLCV!60)ktU(Vgw4n3@So7rI+ zdzOP$CYa%3_9k5ygloq>4A_R4-C)g#o=ifN#akdW-!t7qPG*ZD%*}~ccCuviwq)&Q z90x>qvvF*|ywAX9iwH9^qrhcJg}p@Q(pj1YmK#~IXQDQi6XqrbE_{<_hc0#nPK1#^ zbMn|rY-etnHr8wTn6e-(J+!04(2PUViPB|cp;!wu21t3pJaJstw8GS)6;X8 z+jQf?G<{kaW$xS#!HxhxS}YQ@K8a%6_IOa}<*95U%@4T)P4xUAv68uMnlx+ST4_SX zFfWuojjeg;`aY&$13)$w;(|dL&szkk8_eur9%Di?niZIfg=;$zvoNua#L1DepC?un z1Zy{N9PA6jN`ezBPds)T4IMWBHRH^?5Flva(w@hdw}S>ow?Ggl$zrM&yI_o!*$dy# z*B+f?$4SICc8TSCZWP8jTLdRA!pzJ#UScz@w@kz6COhV)wlvre(?qx%+~8dGqmU-r zap3#cBJZE=r`Ns6xtqtUKab-ie9>F@Nt9vGL3q{ky(GHi&Y$rY9?%4cc`FNa4|Dqk z@WBi2XH4V{LibTKiOuiFkNUY6>|e7pb~5}}xQOm7&vJU$W#{F<+D|w0!1jXB+L-%G z2Ww(rf>kdIhhDtqkq-$xlRII=#-Tq8Q|Cfp zIlN@Q=j8Y4AMaou9^nh4WPTFGsK~;L1^75aG|tCKCDF{x(p7L0crgylb5g2xDZ$>7 zD?Auhr^V#w7d$%m# zig6(G+zxO9u6$1%*}iqW=9Y*8bHH_h-nr4tTdtu3s0eN`jH4u5SYDWTOV71tF<;I0;`gur52EzjW<0wiT+ok2n z90vyDNoE{O731<3bIT4wZ^=ETy-ZwhF#|+wPUwnr6Sql^E#?V3WXCDncc%c;!d=e% z{OEi0Czw}0f`Nz!M%G(B_hE5SjQT)N;T*Ml5T*!Y&0{SVfKeLs%4G4mgYlM!n6aG#Q)cc&mY4dD!9QY)~iW9B9?9>HP9cnmaxJI8k7U_OpF zb4-uNW0iCICYf3Apx{ti7YUss*K$&5uM6$Tvw#l3)9VKhmaxoro}z3se-YzV0~d7w z2YnlUfrTN_#ZeUc44=U6Z$wn{rEG(v3E9GaIX`g2SKag$v}w7vl8gQgKfAq{aZH)% zA6%P%d6`C6QoPfjfvG8&hSy6x_GZsAh%KI5jvS+jVJv>;?B@VnBuUci7Gx6F zvN9D*;E~U6Mr#}`bU*)N*K{Lh9$f)kT`xHFQP#jVJ%{k)xw+|plNQKrbL_Hz=!Njq z#GI#d|7sj~bJITbeaejk*a9tvfJf$CGd3?R>_vRt%DgxHkXj|-Jb1;rioFEfvACT0 z_AGYY8)kZRJA&9;cRkJnO@fULSFl3JMr;#wEF=5YQ z40AR(rbCb%V>d6l^0+g5_6ARgGw9F_(~DQm1}1L)na_@6slPY?rRKE4buX@B^A`61 zZ|(4Hsg;@s-Wj;XwqUCW4(40Vr6+?mL;GeBEc{gxXAnR4>cUAp>pQR&qJ;z&zi=vx zedu*?+2wrcByyjHN|4O}#n9qH9k+pD^>fCvP zEzs}q^+UoR|6rckPDbRH0|-bk&TfeAM?nhnTiA085SVAtESf=(4%3;LCJ~?54tpDU zbCl%3W%P!?YnfzjM6s?zeQcXf0^UL9SVD#;xoMg?S(s7*44ywkw?xUX8sDFrNWu}3 zf!&EZ`c~@48KKJojY`I|6Of$>zjcZn#+5ICKt#i1Li=09?_50Mbup%!6DY zbzzZ2FzpOUxk;WRu6c-Wrqa3REYm}CzF3AaG&WS6;vs{BMSw9PMjHog2FoCY>d`lPDkkJHiUmPq#^DuUzxt)uE;JN(Jq|y-P>rhdbAbJ^RVKz(Q${qkq z${{4I_ZF@kj_a6N2z{qZIrD=1J$B!QNo0pn>O!P&95LF6cEJLqD_MF7?*#SI)QOk8 z%>zf4o}}rL-Y3E_tu%_4VIXK;%wjk;0X?42bs-FyW5ek91OftC$V^z4=eS5*=ugat zq7f0xwV*mU(u|2asW*>;xhF4*zJr0BRe|jUwmn-Up}9y1Zauu2iRE!71~3V)nK`zP z4^3flS?pS-pMC!2ll&6l#B&Mc?N!J`^qn{ec=QWk0RL_T?0B`bAxA5?upi$Dkb-|7 zc9-}OyMP_cZbKZc>zo1`-wsfnBzp<8UIw?edARk65!nxQKbv0(llv~9aZG}M6GkxF zI7T}5_UeBZdf^JDZJRHFy0}2R`|<|7%%5lO<4%6+M|4Z_>>Vdgj%}>R4sl-JCP!jF zhZdQEA0DE!OofA2(mr<$CSjxA&E)ui*u;sRBu$arAQp*8XZi4nI1iSvkl0S4gbG_d zoMR4c!0RTaW#zc4c!@jpypXhj&8Z^fv9df2@Yf_?U_PEZeT$`W6rq;VhJ#On>H!>!!how+cK z*xUCx`UTIHCfmq0@hvbLzA)Vs%14;Da_1{7-}Gb4A%9}$2QFOqU>VP_ia3g~%-|Rx zEI8pZB(;YhNIjr7jNXD0B_a@uzzRv<>@W7w({SdpnBNmeTU^RbvMrha^4!b*RkXO4 zS`KjwrbyiG?bD9bJ`C4k_&@rDek4E$+35z-A&2n{PanGB&CQK4IKorT4{YDGIbLSY z^T`t)f}L4_GTSSUrMRzooPIhdH1zEYVPHk~1C+|T7!$xc-d!)Z-OCnmY`eJllZXx0 z?#!3KK_0>RJ^RmyDBNsmB{LL;HbaW9e8(mBLjSK5Qe>u!N{2oe0h2+P;&&|dcrsrS zz!OoW$7C=A_eDs5{9UwzsYccxhW_WyaJ7Mz+#r4P4A_?5h@4j!Ysw9m92Ik)NR(8T z6W>Ea=mbm=)b| zf9BqB-5Gv|Ow!8t<~i)i@}9p#s?$9_aLt$QA)#zz`4=D+}y|bT-<*)GmoyoH=Ij>pG8+4GEle$B1>x@G+)l6%pvAGIKVH%2$IAiU?z-$ zc9B~(X^LZ_mkWH9R8-+M1kSe)=81_jz_*hTF(YCupM!eI*^(!Pt`Hm0d<-8vIH5?%boh+AxPzo=+=y@f_NVG&It@XG2 zBu0=QIS>nI$G1f0Vg5SAdsFeo? zGm99{TFu}{bb~!VT?SVd5K+sz>ZMnrh~SY!g^qGJSaJ~4VYJQg&@rAgz8OH}4~eD& zcVma^E12H?3~UJ#>vTpM1UW{CR6M?A*qmo-K04wl~f4n2RO(aT_c4SIEr52j#g-! z2%ixGY7RqPSaUit^^gdpnK6mE8GDd9dOlmg8(@sqoV0-tWwG#c;2Av*myGwnd1!|( z9vLJ6X=2V2RR-JDP_CIN{-F&cfpevL)p(%3+ZNgAZVu^d{OUhga}L zo(ed#1q|YFnU`ym&4k60CLk>g`NuO6+mInempmLiqeZ0R$vCEQO#b0ESb8yVXX%MV zO8%bAObUo0nBz1eV|q0rQMAMnlB|SKTG4e21y}-S86xtC2e;S>OvGJ%0dWx{kh91f z@~=l^n{JZLxA!>FrQ^e!OdSK+2sGOm`qMu2YDV6BOW9S*)t%N z`-uP8PvaxpJ}0zASFHoIAHvGT2Y!}#*TU!^@)+y#y?AcY?QWa}%ek|H?orY#z{y=8 zH;Y3eU1D9sxB-6$tRT4+q3W0$gGUSB$^A4U#Io%rRGkDgPl8y|)AaHo0Vl7VrAd58 zUK;)ikS$y$1$Z`zU{Y%_;XmDuB`yzfxeC*)VCK;^lpNuelgymW4jtG7c~C<8ES;OM z?Ae?=I21(Lc$kz2Euv?!5K?tyZ`>v6EGnnrl8jteGAG<}9Men^A{%m=*b!qpkb{Us zOiZf8&d9&u#&|`H=|+%Llr<%0CizZ8qpUS>mCfhooI#&#P6jq43UQR4#Azgc=Ff@p ziCn=N2bd%UpDhCNIt-Sg;pQAnB(X}=Mc#vb;sr4YMq%fCO{9;)5o8eUF|=V)Lqrvc zNr9ojHRtR>JFww9^v+6-WOX>FQ|LP$M?N$2t`i3@7Ox%IYl~#YmK^TdUZa6{GLnGH zB)9UMv^#rRLp_%_7$!8g8QD%)15iU!8Te*{UfO&oWF-)x71t?Mt7rniH8uP07)(SByNyhL6V zs(y<$g9mo9f3Tci3qtb;x%bKs7%d16ZH@{&S>d&pH(qfrIQIM^iz_k`r;!~jP3P2O z&>#=~%d1{;N(SyM748lovmVrb@?ie)jaXyUi63k{k2`{B&QwAi+UZljl5xF=7lLLcf{upj@ z2{Th`?z$E^44#03k-;Mk#HbN5`y3`ha?&9a&7f_>ptfzN=8Tkx#W)m|n&CT%*~!>B zR3hTgM}}1pD)DpXV_)PhK>&DhL_!IrCqJkR6v>MP0SswG2);|Af-K)`Ms5|J0j~KV zt!Ee074|Q_r1}t{^onWvYcz4#lQQ`ue^NzFK0Xa!@oM) zVfwzG{r{Wo@DKmrsTY4`wuAEvf8&jJ|I7d2wcmWtyZ-jWn(gpoe`U5qwD&{)^c~Y; zw!`#YPfxo>rL|M;Ri?FRYj50ns@^`oTkZ9F)1Aq9R5i}mb_T{rr|rE;r`B%N%e{Bj zx;>-4*P4|3hS90C>WyZlGcrm${nq3&gK=e4f3!Q@8}xdmcH`^D(3m{jH(Jw5yVf>Z z-Bz#P?bW9B(ZJaI*zUMGI!Ec}2S&MD?)8jLr#kG_TlHSEKWRVGtoJI$V0Y9T4vmIU zYczIxMrCCDQl;A+w^~Mj*n4=~t&IldR<~n}jbXo9I$v(}hP`UN+uLdPt39LDFxtcE zxIXH)dPcu{zTE5A&UN>?(`t9nYmK|(#-~5r8daLp?r=QnG@Ikeq-zYR>9kxg*QSF` zf7tBRD&2zx2&Y(K#bfF}m$;wbT1>zuzv?gx0XtDR%}Px~Dy8k2sO1nR(4jvhn+x2?4 z)@wJ~RIt-BTAd-CJ!*}*l~#GuY7Xn|{}@?xj8VBa=(n2_E^0SA1EXm)``xZlZB?6% zsnHxX9<5e-+*WJ%OGc+kExP@3zt*MxrEaT65xsW3+ZnXFd_Cwj>BIWqT%}Zbs5~r< znssBcJFE<9t#WhJL&A;W__ur0Ucc9+2BT7|Kkf9&jeeyvuGD+=QLR(y)F0~Z(now( zZ4Y*bC1YxIo8{qTui4w}luM=Vz%WL?U9F*MgWmMxA8QYK!=_OxkNSIU#8O6N?QySd z)Z5iTa}O!jM%~u1U2V5Jol37wQ~HBSyWQSB-)iqQP{ewZme<;y)~MC08ZD#U9d@d{ zW{+RpR=-n5-8;QzWiTqY`pssiVRY!SLA%{-R~zjv?~P8Q*Y2Eeo@@8UT?wZ*DH+X4 zf86YidhKTK;Z_|{*4oWh8!7+*Q@jf%}KZ0Y+_`)=USy| zr_yaV`=e23+-{aeoyw@XvpcN~2S%q~8q|2=pfl;zCXME(SsgU0%|Wl*s7`A2%4A}+ zCe_m3xYw`K%@w26!y2ntPHFetaJO7HDz$N=Kbe$j)%I>_QtKFlVYh5_N39M`tL&X` zw~*qfH|_Mhtx|j3?eY@nn6~R}ETduc#|>=1+cqj4bgI*BAkR*}Umv2=?NNKs?N)aB zy0`ntkdEupu3@#)Zt|cO{f45^jotA8!L>#Y^8tufZ?;;UalPB9S9;w_ztJ9z$4I9$ z=?)+1bjHJ8t-c*8qnmhSTY{+s44?_2{K)6Mbo6!_7x}lf7!A!(H7;v-`0| zwMR>O^=i9g)INyd`khX{WsK{Mey4f9+(9Uv`tGPXuJp#Odb8IoReHODa4WnCamn;2lwK14hOAR_` zf^{O*QMprXwT8n=tyi7wPRAJSs8#08L%@BjUg@9j?^cXjZ%UoUqdlX$(po@>=xqv^DNzEzrz_Ueru<&-I^-JbRaRC3hq(*A1+tWVEq}X~z&i?9%bQL95vxSKAHAzA@Ts)xp8u zM~oVfGr?K3C+$IN&;br>kATYUcC9(=(Y>8v^PvH*+%Reb1Ttl2WAyh{%Z+qK>1 zxYim@cXukyYPZzJd4Y=U%DKv5JZwx$)oHI%>Q%ed5l*-|ZnnpJy-o!qtGBAXTD#mc zCPSQdtz7Bhcxg4g^s-2x3e-Xl1|CAK~6)oW1^TzuRdI8Zxk!VRY#>aIX7_ zK@(G(Ko?rAez!L0cc)EbN*n37;qZ}qrPHi_v~A!E+jxf|e)@d7v;$7lL0D_6JLuMp z7QT;v`1amjdCy&ThZc+Nt#Sdf>qj z?}I1rOzTynPs>os-nmX2RY3OjDK34q)2-73qaGBa-7Dcc+9)C<5P<7HjMcSTjZaPC z5JJ+ONeyNM0K+)ib)?Z46lyqV*Ns6<%7MrEg)xmcdd&&`u-og6nuGpcr`|PMP?<+T zLpWQ1(4ThC*ZRo0+nzQWBgo7Id^W(uW*1?0c<=<4z!d8hh1rb+f(jo)+)mRNZjpUD@IfFplv`$0Ebq`=))&>OP#%8ZCq|NfR=K@7}ZLX z4m#ehb?T!%SmVIx?^Ij09=@d4o$Nvw2dzqT(4xdibzGWaQ1xbir(drR`<)7WY`4+J z4UJmHu+!VAjhkrKpwuYi^uYg0W7@(W){W^DNZlRb`dZydtyQXwQ8PHy?x0`kGzN|7 zxQUk}|GywisV)TX^jqYKWqDjyl|eSl)egZEbUI^7Qu zr%;i>c!K2GAE{OG#;taxTIvsmPhng<-c|tv72%yI*OJhNVGkTrHLB^=28-c53}8#z{X-Ym=Qyxj*hr1|7Wi z&dy$Q7YYp9COjAdhY!^|ld`eb9gOi3opDQ6Ua1)L%5DGJdDOM-4&co!=Q(y2DbbH29qc zyr$MX-yZBChAJKm^QjnRI-oagLTaj2D4l_qL$~|y2fJ`!Mso-|?HOZ4-DpD!#_i7D zryw!qR%;K6*e2c@^gFxL^TR%_d04B0@tDYNg z-Vi=ItrPcl2u;tI&yAqf^=hMEukKF!wQ{)))QB(P_xWa*pcvxP99B9L+&zfjsT#dT zt&R(AHoKD%F&e#4-%&g@n6z50kz~|qqQ$Ky=-saFpu_!Esr68)yE8II-Q7}ynw;CK zmCEQr2fw;=ju(V)gmuKhok6KStyMd__}CH=vw{EaV|#rn34^79#)P=CL@->N497LG zoSm^z8`b-J&7E=!idZX8c8qan)W&(&DAL%Q4(jE}^n4vcJ|NB;SD;$GvO(klGM8)g zMypzyjLUoNQVlR{^~Xq&xTm>0sP-p)z^PRkl$-e9(R5s?^am5<-Ktd^RB&A1p$p3J zzrEd>(V3!M)U%5pql6l4l%n>=m0_t;t(C^DCPpmM zaSVt!ZIuDjI_dxgX!cM?xR2>fM)#`Ys)GSv<1K5~RGH|3j^fgs1PlVYmBRn5s_4_G^+K-rP1JgZ)A*z#3>|71`mxYwR&?n-KqC_urG-1E>xI+ zuF`3rs}JF?Vwy3IyqTAlP;ikg7WRu3P6m+i=GMrcK^ zJ0A5L4O(7qHhPmXfe?_nTdh1q)&(Cwt>eV-fURl0HQt>V z+Mv}2&hWME4xXf1tF6@{64^G2i>D{U%g} z=$at4L$U$<7<{hLL%4+Lylp<#ZH#ISLc`X$J-~h&ooczaJAgxt+x-?)^Y$Q8on~VI`5g31WPkRIa%Z&L-)U4k^}QM~)I{mM zI(^#*i%VErt5gD+p%3k8ABIIn24>M35uYxWNSvH&g!g}CCLe4oV9Xk4lsle zp%GJfXHKlB0Rm^;vgZU}s4Ede@3}}^?BW{hMV2Z~g%IVNE>xpf?1omEQ$huu4*F0$ z(W*yPmz7zQpzzhYIVYe^1uCqEF;!JIQ4OFh)KFwpno|ejt06lNU9+ZDrCH)pfj70X zg{H^KJ~nJh9UKLKB4FxLmxTe8M2kb`tE*Nho6A8B(V&o!EDI$9sG-_6s$-QGREJ<* zfGvZz&C@K~GBpAu5S<*3QUsCaPCoF{1n!YgP+iMq2Lwt&sX0XDIZLc7v!bM%l2d_= z+f~>DH^Ikg3rL`z6%{Cjls`QcWUIp*SM-)aUlXP zs|b`)fY$1`IxAIdz{qV!nPst{+67A0^dOp^sSwsppIIqE2W3^MM^S*<0whor!Bb^6 zcvY2Dl8XU=b1l1u5~}zSvKmaU2ux+v1oEzI35ue4YAGaNi#CMP4MHG$scwfz@DZ%t zByI$YZOI~t~fTaW&4hdMYj;f0mNRCC>sVOC)6hy_ImqOPyVS&8E zj?!U`3Yo3uR}9$?%5Fg-*#c1s4yw%|e%RH3ix5dzNgBbx5^8qIG3Qjzn3HvQh>whp zj%LaZvS{Fd=q=Y_X4^FZ%BIaDQq^@huV7XT8TIkR~rI0fWTo3rN??izT)|o4d^-NZu z3#uj}s|`_shT(RwJnA%^hvF^-UW27#d2lO5hXJE>7<5&lTZEqgevf4ck(ykh z%==riEh{F?-X(Fba1YGDHFkr)4X|hxIBGMHn&#I;17`s!aZephC9v@!R5WB@TIhxI zp%a7rL9MH{N)!f4CY!0_Z+z07=<6 zX9d5<4tG2gS|eKys-Cv9Zbw5@{6O1VCs>&Ew`<}I$PZK{(+gnpCbq9I-xXyc>Ff4T{IwLM1`W@{r9WRDSdU z?1MRl09m$Nfiy1E0k7aeL=AQ^tlJ?LiE6D9VEN&86-W~M2m&E7M1)#LAhfISY=`}* zOCkWj?$y*ts%TNQNc;!~NUe~3KMct3HtSrFl&YY!zI_=L9IB>Pamv_h40jE82+}Sq zKo97lj_YZbYCxaR(3{9~u7)XvDktM#DE2&H4V}~_-=163L2$u5vrXiX)Rv2jL6A18 zWD4S}q&1P(Tod|Jha_-!uitu;$N~8(3rp!k-p-RlFgj6&FOM z2t;GGgrXk>P6c%cny$$*_NPuK9m!!YC<;nFNN$dlCPp=M+`_@LF(M#_z(n*ICJVvM zF(X;j_=ExH)MWI?dZWbh6qy?&EX)z&w(JW)PED6A542G8BvYuCt40Z9LzMU+w9XI; zUyNsz5z_<2JZ#$5hU4h8d$_zTu+T zHGF4jFLAaAR*)i{JYaAsn^lIV)=g3&`-4>q#t96TALb8AA8tYg%Mven9%>9A0q0^9 zU|<>uYAWG`O^C@wnno?NdA}gvU`T^_8`P_ct9dMV(+bdSDo`$Pf?2o`4WU=)Go~Vi zW{7fH2YZ8g^lgr5A(NW8Y-|u#mlSE5i@Yo&$P#fJt<-?^3xUxwH5Xx%{i=B|+9=eU zV9``GZ5a*HQY_WeA$ldPz_wU0yZ2&xiX>SiI4oU}lv|DvNNWHknrNeA8)~&}+*lxF zByCj_y|+|_ie$J@7%@ao#lJ--Lz}}ga%7gS7;4xk%)C;q>j8wiA{v4zVn*P?7dYOk zvOENQoJg93mzL_G=DFCmlFSm|BW{`L3swA5tWa4I0lXr)L2y_f;O~M~SCN}R!=6|& zqLLGc7#wQh1GGikKV0#8IPV4Y>p-oAmT0ne$TI=K6crbzOi(?Za`sTQ)0B@| zndL@cBa2AHP~Jt{BodjWs!)=`dM#AvECF^3L(ovi1JIbHN>$W5=847oVoEF(@SuFM8y6m3;pF)D3Uz?8A&GDJ^7 zY0^ccuR5AOdC7~g4GLeyqgH@xs5fyT^$?3PC>i97_W4h% z2)xJ`h_i-4DTQvhetoA#dZ8r2;OT)>Sb#Dqg^r^y!ZM!iC0QmUXJC?<)N+5rcq$*!71BB^<06h@c) zpexvKGJ39Qm*ooJ3HgBLAsOn0XyMTqyer+ulJxK~SYg=oY$Q>r@}~yoF-wvJpt6sJ zAo9cjDNC$5$UT}^agDMYkqL#cQY_bcnJkV4Sko(Wa0(0vLj=(HL?Dw&1W$rCR}5xF z2MU^IP&TWD>shY=8B_(g!cj_=g;ymgz~M-96yH=Sabnw8vV%Lrd_}qqI>iH43?$h? zYc`wVyb@TZGS927-$toW3w*8ag)ExhPAW7ccDsU=ZTSsjK4utu)Zv&oO%1K0nmx*# zbcJ+9mQe`7VBJU*$i*S-1(*+@@LAe`Z?#_XP0=NkB02jSV1O49L?jA4JUuWgg$N57 zfPDCdC`u;Mn{TN~qlz5Mws54X8Wt21w90@3QC-JjJE0dN7=ZJNFdAS76BQ!Bx8#sI zK|2~!8s!=e3J6}5!BRKNxO+wP$APwjQZ!d37B~pLxLYEb!GX7t03fwKXw;0z4M?O^ z1K|ag&>^e}eYR?H-H*JYWB}zKH0vnys;X#yktZQ#F5%8q-Tg(`NugcHhNWO5>-roq z+IJ-TC<(Ey@{`%5&`fp9O)f2j!M)o7uSGc)l0G^t;$mGdi3lZ1A8plQqt*z>@(q4P z0etMi*g*7`uk%nNELE@?AvHHLljodrfHwj0zKIYCMis63(dlqGhLyK{)2)e5YQ`-KD0Q6Xk$9eGcISa z2ab$>PW_C;^bvaZk)T%*oSs*+U(G|n9UYF<5_^|9LrJ=N{ZbkF( zD`Gg+H74qoI^#De`d}!jgaRfrgXH0Qlb8$fVkh(|mrp~wOL|fB?us4$m}c!m)Q|0K&Z+!4=vbX z6eu}FPlFwbMKwbe=dldwDlEpjfL3OC0_6y#IBMc}Hqam2K2R-qRzRhQ6;_lQ8yeBW z7a%?cniPI^Fquz@^q3=6cTmO)w208NML$(l%!BLjf-j1x>fo-dXllu%4bg0m2? zQJNyWUTP=t5X_2$Oybv(chG-Hwct{ZViw<@BWoi>%+pc$qoo=Z(g$gqEFHOcIP(=# zS1%B^;wswp861LeRw28n#?_P*M7mb4)h6us2_f?=(O_e%K_61Ad%FI0^mO;9boKr*Wk8(5A40yFeVAmCN7 zDrG_Namj5;r^u>4#9gt`*Qgajy_Zx62#Ka#wW&^`Ob7vjlq8E(iX1M{34=+^kIdFVZVand12K=H91B?-El@25 zfmI<+IKIG+F7k^KQC>(KE|eP;&)KPBiubiAU(74pY+1Uujh*C~&L5<+$a3OqHc=2SmoXfe}@+DafWL{)=gR?%ny z_)IY6K@=tuz@P=Fv0DMqDjbV^Owk5fc2V2}7%gizvC(VAQm~Nr*u}PuX6R6y1EpbX z0uWOJ5tlOO2EYc?#~VIw88b>G!Rn#b&ni-=Mo|>eC6cJDtGW%Z?v^c_I!Qt zsFN@Wq@Z%HhEF7LFIreDAYHLKC_5AZlI|OMV5idTNN9)0(qg*EP|b*!CP=f78(|Y1_TQX zD%9u%2QRm7+7QVv|!&Wf2HSg4m3K?nfX| zN}7oUMt$Ym2R%m++{PUxsf7?@Ex#oxesq*ZP zzh^JLYuUKLibLYkx>uYZ^{^QTC?$3U2 z%N@`6g>QU)xo+>R|Ls#3n^%1)ocYP8O5gn9oU^>``42qzgL|%5t#7V8|CeW9@b#%L z$l})N_q=s?@ok8kpWSfa_^FedPdIgQX49oxPCe=I__346t~~YF$)8s_`Elx9t>U{7 zujJUtmmYo736BmpU3UD@Cw%SV_@j?qdFrFjd7Q6dB(0~4PQ8*xzc9Jxf_60D3I?OR zapJc?CO2Jv{97Q$ySejqFJF1OQ!jt>`@cClHFfLRJEp(XzGo{9_~&O{KK1gR$u;5X z$z$JW`K&-n#3{x5YE>S(!us&#vYhGk>#c$MjwA+gLp9wr4JU>*~pM zZ(29GymCP?08`IA_`BPue{{j!$EIS#*Unk<+LcZ;TjTlhiTz!>>55bOd(jD(p02}Z zt^VrKDgL*8djI$r#WEI7_~OfRXRmqm?A2@O@grxiyX)))+n)bWx)|?oHo9SJ7{=Y^ z{L-jDe_#C2w&r}=8g`Oab+K`W-;55# z$r;|1wbS{u-5mAPo*t*2EbhhaUf=fO#z*rk?(}%GH(E@>RwKOq!)e$~>dk(09CyY^ z?Dh}Ldr?0f_2al51q;1&zBA}XaoowJRucDm_eFy^YxH`F7G-J28?<`ET0BmAowOhJ z8%_Vtu9^(;wB1fRy>`1l$D{M+pwnx`NrRV?b~|de+j%nI>$ZA%vcHvN-0ap~ztc=w zjYih%w!29u?MB^x*6VaSt-ROjHG3V44lcHOeLmH0w>z!4+wJyxjc#ucw1$g~xZlhU zBx#!V^TvF$*=}XcUdTgRjbu3JWofJ3&$4bxpXc{SSu4xCy=Jp99A|N3zI9Moy3a4= z07ey5-s078D-hbj(>=g zU7x$*eGm1nXgwIVTDw1Rk#}!D>bJ8bzcat`zI(IezI5cbuWZi0dOW)@+gnJlY$aMa z2yc3InD%d;{^Pj4<=gUC^L#F{#W+`@3HfH`@2+;z)4Mm_@$sy6Z`_^ltxDTh-g$Gh zwekJV#r<@38>V@fEaXARCeRNaf(fC!#ftg`eIe5_?Z#ZfmY_NWM?|$z`qv4au zxZmGA9!%W7ZE1H;9bJ{i*X3FCo>h6!x%q-e4z5~rQPOHJ1g!(7i&$?vzJrfP$DJ4d_SL7`#8*AG{^_an z&uz|3|HFM@5l6P2u!$d=TyxhXVQVEdJ^cB@%SRvm>Hqrd2bMVl@v#p)_QdkxkN?Rs z=QHv>;~#NQ0pH|*Vl(H59(nBRocYE#evcepp5T<2)rSv%=ZPo(^D;;C9(m%AAAXXP z!yZ5U@S(?luzcw7YoB=P$S;4*@hy)Zi(0q8*L!;EKP>)YX8Iq$`$W;4E1x}0SX*`f zYi6d`U2gy;cYO}NwTE#dmXnI_xefP&*Of7OiW6XG`XXlrCC4i^m?6s(&=Qq zyr0a+J+4agelNS1C=h)lw>Rqc`$@Z-b#>xh zGFVEJWU1FnZ_m0(Hzq7}<1A02l$Qo!FYOaP;-r(eyK(U_Zr2}nQ|{1pQN89?NelO|w`*Dka)z6ZIE3$6Co%G|R+a%8LX58;2X}8P8 z{k$L3FMjTJ7=whT@yM7knI`>=hbC#CJH-4UF2WwK@j^_mV_xZVeTQ(&bv>TSYaO0U zEar1bT)fVYeQv?cQa;X~s8KrQ@_)>CyigzztFiL&OS&; zl{1EmPQE6gaq$ismi8Bu?&hN7AL{qsk#r{}zMN%A_fPY*mwlj{e>zE>^SjCQ$s6*m z6TME-yaNZvNvoiQ?fbex#GM&x+$4`WN-LO!R5bd4F(AZ*sA7<2zFIHSb^? zZ%y+zrWczR_0so|Pj=IUB+2sgna+MP!z?NHCyS?|u$@?$O;?4cbySvu)>H4~r zkah5eZ*$=Kh3D>=e(sHFv3Gy`%!TV$uU>cgy4C;m>*ZhXn7(vkO$CiZ=|ecsZ(Y;kGnz5H5Q+PCk8#eIwW z_U+%dw79gmZ~xNrqf1MR(&9dIao?V3ytJ@?G5O5m;?lqD+c#KTv?mt!eRRoMbh>d_t3!q!W+(VQ1vLT$&!rnT%6sj{Lua66betmdYW%yIvggxXdAKhmLTHG^ZgwdFaUUf8peadmlf1=iwvv z1JCl6{KKF5KO9W7eE0|_Fa6t#4;{Jx(1F7Xw-m>|wN^&w-yHn>(^LO9-UfpH`_;wh ztbZx^9~+&!ull1GN5_8tk6s*|hgYq6{jtuTJ}X{z%Gg|T(&eWcnTM{v@ad@^+|!?# zzWftnv6gQ<)5v`6TZfjvaQMf+e`J}o_}Jr1k24Ja?K2F;cQ}aQE1WRHd5*GESoCXOs0`qg(H=M;lG zS4QTM{Yy_zUG;}&?U;V&pMSk*&dVcn0@|Pb+zZ>M|Hmi(?nSi!%sFe`@N!pAlNaB9 z%Fw*=Eb$NL6@ z{Qhy84+q(B<*kKI^U=QBMx()P<1D*-lxBH49OYv^l(zFU%d&Ph z91gR5kPn7^ei-D#j1R|y(RjG9kPnByOVjeCH;A%ikd21vkfxEucog?ZxL`OQ?jI4K z7RC#G?m8G`gCW6lFi2=vHtME1Hy94MdPJj(3)1mImX1cn$BSp>Sw0>O_YLSKP=E#u zv!YXjEYGsyS)-x>{Khke10wY}8H|P_omw3{uE1INQxFEEaS@~G&%l#RFEl8v%^^TE3^?*B-dEacDi=*q!# z@Ci86S>t?Le(rGFL{6`#K6~qUb#r*r-r>yUH!=vf4n}VrT@YP5%pRbp;xHY><6*uw z87~Y5m4!S>#!fPx1&|LO8fV+GbmF2fFc}NqS{kPL1L@#n!!){KobMQI9F-lONnv$*T4Do+{Wo+;m!K?myb@ZyXCxX)0hAAQ^m$2F}`>5MasUC5^HeKyw^PT zcY;iA+OXwxn#74h#fu*}RVn%IukPGF?H{=7SgbfMR1|+V$SVGBkh``YOF~5^q8(uD zk4&C`u}{~QtGAqqZJbO)&e*0mUwmNu^tG?O<(0PS;mI|}o_RuEJZXfk*>Yx>u=A9+ z&e*aC%>CP^|Eql9mA32?lWShgGG}huwOjs`B)#+0_s-b3FFxgMpO!!AztYA%Gt{9=rPG-w(Zb%`bm)>>WOLPVv&g=a2olxSd>i S)!D^stN6FLzG&9O#Qz5rd>9n~ From 1a48e6e6255a8010d2aa1174d46c8fdce624df43 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:08:20 -0800 Subject: [PATCH 02/16] Squashed 'cereal/' changes from ab32956aa..eba4349b9 eba4349b9 put liveLocation in qlogs 6c4735f21 add fixed fingerprintSource 4c50d26c6 GitHub actions (#25) 988783f6a Merge pull request #28 from commaai/power_monitoring b43fd0673 Added offroad power usage git-subtree-dir: cereal git-subtree-split: eba4349b9ac2c15a17f7169e6c24464501308347 --- .github/workflows/tests.yml | 23 +++++++++++++++++++++++ azure-pipelines.yml | 14 -------------- car.capnp | 1 + log.capnp | 1 + service_list.yaml | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 azure-pipelines.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..3b8f6ca8e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,23 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v2 + - name: Build docker image + run: docker build -t cereal . + - name: Unit Tests + run: | + docker run cereal bash -c "scons --test --asan -j$(nproc) && messaging/test_runner" + - name: Test ZMQ + run: | + docker run cereal bash -c "ZMQ=1 python -m unittest discover ." + - name: Test MSGQ + run: | + docker run cereal bash -c "MSGQ=1 python -m unittest discover ." + diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index c14526a5c..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,14 +0,0 @@ -pr: none - -pool: - vmImage: 'ubuntu-16.04' - -steps: -- script: | - set -e - docker build -t cereal . - docker run cereal bash -c "scons --test --asan -j$(nproc) && messaging/test_runner" - docker run cereal bash -c "ZMQ=1 python -m unittest discover ." - docker run cereal bash -c "MSGQ=1 python -m unittest discover ." - - displayName: 'Run Tests' diff --git a/car.capnp b/car.capnp index 745389232..58ff3fb42 100644 --- a/car.capnp +++ b/car.capnp @@ -477,5 +477,6 @@ struct CarParams { enum FingerprintSource { can @0; fw @1; + fixed @2; } } diff --git a/log.capnp b/log.capnp index e01c8977e..46971d81a 100644 --- a/log.capnp +++ b/log.capnp @@ -280,6 +280,7 @@ struct ThermalData { batteryVoltage @16 :Int32; usbOnline @12 :Bool; networkType @22 :NetworkType; + offroadPowerUsage @23 :UInt32; # Power usage since going offroad in uWh fanSpeed @10 :UInt16; started @11 :Bool; diff --git a/service_list.yaml b/service_list.yaml index 7dd1a62e9..5747293ab 100644 --- a/service_list.yaml +++ b/service_list.yaml @@ -31,7 +31,7 @@ carState: [8021, true, 100., 10] # 8022 is reserved for sshd carControl: [8023, true, 100., 10] plan: [8024, true, 20.] -liveLocation: [8025, true, 0.] +liveLocation: [8025, true, 0., 1] gpsLocation: [8026, true, 1., 1] ethernetData: [8027, true, 0.] navUpdate: [8028, true, 0.] From e75b619ae20bd3f7c3683343c1bb76ce34a415b4 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:08:21 -0800 Subject: [PATCH 03/16] Squashed 'panda/' changes from 73a60d5c9..769ade051 769ade051 Harness orientation designation fixes (#435) eb5020b7a Add the ability to communicate with BSM (#433) git-subtree-dir: panda git-subtree-split: 769ade0511483c4ba6e4e9a3b5380ffe50d01f13 --- board/boards/black.h | 12 ++++++------ board/boards/uno.h | 12 ++++++------ board/drivers/harness.h | 31 ++++++++++++------------------- board/safety/safety_toyota.h | 2 +- tests/safety/test_toyota.py | 2 +- 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/board/boards/black.h b/board/boards/black.h index 7165aa6cb..97f322f69 100644 --- a/board/boards/black.h +++ b/board/boards/black.h @@ -99,7 +99,7 @@ void black_set_can_mode(uint8_t mode){ switch (mode) { case CAN_MODE_NORMAL: case CAN_MODE_OBD_CAN2: - if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_NORMAL)) { + if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_FLIPPED)) { // B12,B13: disable OBD mode set_gpio_mode(GPIOB, 12, MODE_INPUT); set_gpio_mode(GPIOB, 13, MODE_INPUT); @@ -198,7 +198,7 @@ void black_init(void) { black_set_can_mode(CAN_MODE_NORMAL); // flip CAN0 and CAN2 if we are flipped - if (car_harness_status == HARNESS_STATUS_NORMAL) { + if (car_harness_status == HARNESS_STATUS_FLIPPED) { can_flip_buses(0, 2); } @@ -210,12 +210,12 @@ const harness_configuration black_harness_config = { .has_harness = true, .GPIO_SBU1 = GPIOC, .GPIO_SBU2 = GPIOC, - .GPIO_relay_normal = GPIOC, - .GPIO_relay_flipped = GPIOC, + .GPIO_relay_SBU1 = GPIOC, + .GPIO_relay_SBU2 = GPIOC, .pin_SBU1 = 0, .pin_SBU2 = 3, - .pin_relay_normal = 10, - .pin_relay_flipped = 11, + .pin_relay_SBU1 = 10, + .pin_relay_SBU2 = 11, .adc_channel_SBU1 = 10, .adc_channel_SBU2 = 13 }; diff --git a/board/boards/uno.h b/board/boards/uno.h index 1545a3f2d..a6a06eff3 100644 --- a/board/boards/uno.h +++ b/board/boards/uno.h @@ -113,7 +113,7 @@ void uno_set_can_mode(uint8_t mode){ switch (mode) { case CAN_MODE_NORMAL: case CAN_MODE_OBD_CAN2: - if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_NORMAL)) { + if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_FLIPPED)) { // B12,B13: disable OBD mode set_gpio_mode(GPIOB, 12, MODE_INPUT); set_gpio_mode(GPIOB, 13, MODE_INPUT); @@ -230,7 +230,7 @@ void uno_init(void) { uno_set_can_mode(CAN_MODE_NORMAL); // flip CAN0 and CAN2 if we are flipped - if (car_harness_status == HARNESS_STATUS_NORMAL) { + if (car_harness_status == HARNESS_STATUS_FLIPPED) { can_flip_buses(0, 2); } @@ -252,12 +252,12 @@ const harness_configuration uno_harness_config = { .has_harness = true, .GPIO_SBU1 = GPIOC, .GPIO_SBU2 = GPIOC, - .GPIO_relay_normal = GPIOC, - .GPIO_relay_flipped = GPIOC, + .GPIO_relay_SBU1 = GPIOC, + .GPIO_relay_SBU2 = GPIOC, .pin_SBU1 = 0, .pin_SBU2 = 3, - .pin_relay_normal = 10, - .pin_relay_flipped = 11, + .pin_relay_SBU1 = 10, + .pin_relay_SBU2 = 11, .adc_channel_SBU1 = 10, .adc_channel_SBU2 = 13 }; diff --git a/board/drivers/harness.h b/board/drivers/harness.h index c996bff0b..4aeb41fe6 100644 --- a/board/drivers/harness.h +++ b/board/drivers/harness.h @@ -10,12 +10,12 @@ struct harness_configuration { const bool has_harness; GPIO_TypeDef *GPIO_SBU1; GPIO_TypeDef *GPIO_SBU2; - GPIO_TypeDef *GPIO_relay_normal; - GPIO_TypeDef *GPIO_relay_flipped; + GPIO_TypeDef *GPIO_relay_SBU1; + GPIO_TypeDef *GPIO_relay_SBU2; uint8_t pin_SBU1; uint8_t pin_SBU2; - uint8_t pin_relay_normal; - uint8_t pin_relay_flipped; + uint8_t pin_relay_SBU1; + uint8_t pin_relay_SBU2; uint8_t adc_channel_SBU1; uint8_t adc_channel_SBU2; }; @@ -30,9 +30,9 @@ void set_intercept_relay(bool intercept) { } if(car_harness_status == HARNESS_STATUS_NORMAL){ - set_gpio_output(current_board->harness_config->GPIO_relay_normal, current_board->harness_config->pin_relay_normal, !intercept); + set_gpio_output(current_board->harness_config->GPIO_relay_SBU2, current_board->harness_config->pin_relay_SBU2, !intercept); } else { - set_gpio_output(current_board->harness_config->GPIO_relay_flipped, current_board->harness_config->pin_relay_flipped, !intercept); + set_gpio_output(current_board->harness_config->GPIO_relay_SBU1, current_board->harness_config->pin_relay_SBU1, !intercept); } } } @@ -41,10 +41,10 @@ bool harness_check_ignition(void) { bool ret = false; switch(car_harness_status){ case HARNESS_STATUS_NORMAL: - ret = !get_gpio_input(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2); + ret = !get_gpio_input(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1); break; case HARNESS_STATUS_FLIPPED: - ret = !get_gpio_input(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1); + ret = !get_gpio_input(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2); break; default: break; @@ -62,11 +62,11 @@ uint8_t harness_detect_orientation(void) { // Detect connection and orientation if((sbu1_voltage < HARNESS_CONNECTED_THRESHOLD) || (sbu2_voltage < HARNESS_CONNECTED_THRESHOLD)){ if (sbu1_voltage < sbu2_voltage) { - // orientation normal - ret = HARNESS_STATUS_NORMAL; - } else { - // orientation flipped + // orientation flipped (PANDA_SBU1->HARNESS_SBU1(relay), PANDA_SBU2->HARNESS_SBU2(ign)) ret = HARNESS_STATUS_FLIPPED; + } else { + // orientation normal (PANDA_SBU2->HARNESS_SBU1(relay), PANDA_SBU1->HARNESS_SBU2(ign)) + ret = HARNESS_STATUS_NORMAL; } } #endif @@ -90,13 +90,6 @@ void harness_init(void) { set_gpio_mode(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1, MODE_INPUT); set_gpio_mode(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2, MODE_INPUT); - // now we have orientation, set pin ignition detection - if(car_harness_status == HARNESS_STATUS_NORMAL){ - set_gpio_mode(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2, MODE_INPUT); - } else { - set_gpio_mode(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1, MODE_INPUT); - } - // keep busses connected by default set_intercept_relay(false); } else { diff --git a/board/safety/safety_toyota.h b/board/safety/safety_toyota.h index 844e387fe..4bf0a5de4 100644 --- a/board/safety/safety_toyota.h +++ b/board/safety/safety_toyota.h @@ -21,7 +21,7 @@ const int TOYOTA_GAS_INTERCEPTOR_THRESHOLD = 475; // ratio between offset and g const AddrBus TOYOTA_TX_MSGS[] = {{0x283, 0}, {0x2E6, 0}, {0x2E7, 0}, {0x33E, 0}, {0x344, 0}, {0x365, 0}, {0x366, 0}, {0x4CB, 0}, // DSU bus 0 {0x128, 1}, {0x141, 1}, {0x160, 1}, {0x161, 1}, {0x470, 1}, // DSU bus 1 {0x2E4, 0}, {0x411, 0}, {0x412, 0}, {0x343, 0}, {0x1D2, 0}, // LKAS + ACC - {0x200, 0}}; // interceptor + {0x200, 0}, {0x750, 0}}; // interceptor + Blindspot monitor AddrCheckStruct toyota_rx_checks[] = { {.addr = {0x260}, .bus = 0, .check_checksum = true, .max_counter = 0U, .expected_timestep = 20000U}, diff --git a/tests/safety/test_toyota.py b/tests/safety/test_toyota.py index 101816245..493fb1f62 100644 --- a/tests/safety/test_toyota.py +++ b/tests/safety/test_toyota.py @@ -21,7 +21,7 @@ INTERCEPTOR_THRESHOLD = 475 TX_MSGS = [[0x283, 0], [0x2E6, 0], [0x2E7, 0], [0x33E, 0], [0x344, 0], [0x365, 0], [0x366, 0], [0x4CB, 0], # DSU bus 0 [0x128, 1], [0x141, 1], [0x160, 1], [0x161, 1], [0x470, 1], # DSU bus 1 [0x2E4, 0], [0x411, 0], [0x412, 0], [0x343, 0], [0x1D2, 0], # LKAS + ACC - [0x200, 0]]; # interceptor + [0x200, 0], [0x750, 0]]; # interceptor + blindspot monitor def twos_comp(val, bits): From 5738bb589675e23235e3653ed7252c157d4b54af Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:08:22 -0800 Subject: [PATCH 04/16] Squashed 'opendbc/' changes from d68f8ff0c..2ae0327ea 2ae0327ea GitHub actions (#217) fbbba94aa GM: use common gear VALs dacd8b460 Chrysler commonize gear VALs d338b9fba Add support for 2019 Civic Sedan Diesel. Split GAS_PEDAL_2 out to each car. (#215) git-subtree-dir: opendbc git-subtree-split: 2ae0327ea25db36227d52acfaae8927a59d4e5a3 --- .github/workflows/tests.yml | 24 ++ azure-pipelines.yml | 19 - chrysler_pacifica_2017_hybrid.dbc | 2 +- generator/honda/_bosch_2018.dbc | 7 - .../honda/honda_accord_lx15t_2018_can.dbc | 7 + generator/honda/honda_accord_s2t_2018_can.dbc | 7 + .../honda_civic_hatchback_ex_2017_can.dbc | 7 + .../honda_civic_sedan_16_diesel_2019_can.dbc | 57 +++ generator/honda/honda_crv_ex_2017_can.dbc | 7 + generator/honda/honda_crv_hybrid_2019_can.dbc | 7 + generator/honda/honda_insight_ex_2019_can.dbc | 7 + gm_global_a_powertrain.dbc | 2 +- honda_accord_lx15t_2018_can_generated.dbc | 14 +- honda_accord_s2t_2018_can_generated.dbc | 14 +- ..._civic_hatchback_ex_2017_can_generated.dbc | 14 +- ...vic_sedan_16_diesel_2019_can_generated.dbc | 351 ++++++++++++++++++ honda_crv_ex_2017_can_generated.dbc | 14 +- honda_crv_hybrid_2019_can_generated.dbc | 14 +- honda_insight_ex_2019_can_generated.dbc | 14 +- 19 files changed, 518 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 azure-pipelines.yml create mode 100644 generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc create mode 100644 honda_civic_sedan_16_diesel_2019_can_generated.dbc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..2141cb026 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v2 + - run: | + set -e + docker build -t opendbc . + name: "Build" + - run: | + docker run opendbc bash -c "python -m unittest discover opendbc" + name: "Unit tests" + - run: | + docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./flake8_opendbc.sh" + docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./pylint_opendbc.sh" + name: "Python linter" + - run: | + docker run opendbc bash -c "cd opendbc/can/tests/; PYTHONPATH=/ ./test_generator.sh" + name: "Generator test" diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 6ac0f9ddd..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,19 +0,0 @@ -pr: none - -pool: - vmImage: 'ubuntu-16.04' -steps: -- script: | - set -e - docker build -t opendbc . - displayName: 'Build' -- script: | - docker run opendbc bash -c "python -m unittest discover opendbc" - displayName: 'Unit tests' -- script: | - docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./flake8_opendbc.sh" - docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./pylint_opendbc.sh" - displayName: 'Python linter' -- script: | - docker run opendbc bash -c "cd opendbc/can/tests/; PYTHONPATH=/ ./test_generator.sh" - displayName: 'Generator test' diff --git a/chrysler_pacifica_2017_hybrid.dbc b/chrysler_pacifica_2017_hybrid.dbc index a19219cbb..80ec2a904 100644 --- a/chrysler_pacifica_2017_hybrid.dbc +++ b/chrysler_pacifica_2017_hybrid.dbc @@ -429,5 +429,5 @@ CM_ SG_ 625 SPEED "zero on non-acc drives"; CM_ SG_ 625 ACCEL_PERHAPS "set to 7767 on non-ACC drives. ACC drive 40k is constant speed, 42k is accelerating"; CM_ SG_ 268 BRAKE_PERHAPS "triggers only on ACC braking"; CM_ SG_ 384 NEW_SIGNAL_1 "set in ACC gas driving. not set in electric human. not sure about gas human driving."; -VAL_ 746 PRNDL 5 "Low" 4 "Drive" 3 "Neutral" 2 "Reverse" 1 "Park" ; +VAL_ 746 PRNDL 5 "L" 4 "D" 3 "N" 2 "R" 1 "P" ; VAL_ 792 TURN_SIGNALS 2 "Right" 1 "Left" ; diff --git a/generator/honda/_bosch_2018.dbc b/generator/honda/_bosch_2018.dbc index 488eed424..bfbfe9b8b 100644 --- a/generator/honda/_bosch_2018.dbc +++ b/generator/honda/_bosch_2018.dbc @@ -54,13 +54,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON diff --git a/generator/honda/honda_accord_lx15t_2018_can.dbc b/generator/honda/honda_accord_lx15t_2018_can.dbc index 9e95f56db..099b1a9e5 100644 --- a/generator/honda/honda_accord_lx15t_2018_can.dbc +++ b/generator/honda/honda_accord_lx15t_2018_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/generator/honda/honda_accord_s2t_2018_can.dbc b/generator/honda/honda_accord_s2t_2018_can.dbc index e0a7ff328..4967d6062 100644 --- a/generator/honda/honda_accord_s2t_2018_can.dbc +++ b/generator/honda/honda_accord_s2t_2018_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/generator/honda/honda_civic_hatchback_ex_2017_can.dbc b/generator/honda/honda_civic_hatchback_ex_2017_can.dbc index 2de149ad5..c085466f5 100644 --- a/generator/honda/honda_civic_hatchback_ex_2017_can.dbc +++ b/generator/honda/honda_civic_hatchback_ex_2017_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc b/generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc new file mode 100644 index 000000000..23a579aa4 --- /dev/null +++ b/generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc @@ -0,0 +1,57 @@ +CM_ "IMPORT _bosch_2018.dbc" + +BO_ 316 GAS_PEDAL_2: 8 XXX + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 419 GEARBOX: 8 XXX + SG_ GEAR_SHIFTER : 24|8@1+ (1,0) [0|255] "" XXX + SG_ GEAR : 32|8@1+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 432 STANDSTILL: 7 VSA + SG_ WHEELS_MOVING : 12|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_1 : 11|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_2 : 9|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" EON + +BO_ 892 CRUISE_PARAMS: 8 PCM + SG_ CRUISE_SPEED_OFFSET : 31|8@0- (0.1,0) [-128|127] "kph" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|15] "" EON + +BO_ 927 RADAR_HUD: 8 RADAR + SG_ ZEROS_BOH : 7|10@0+ (1,0) [0|127] "" BDY + SG_ CMBS_OFF : 12|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH3 : 31|32@0+ (1,0) [0|4294967295] "" XXX + SG_ RESUME_INSTRUCTION : 21|1@0+ (1,0) [0|1] "" XXX + SG_ SET_TO_1 : 13|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH2 : 11|4@0+ (1,0) [0|1] "" XXX + SG_ APPLY_BRAKES_FOR_CANC : 23|1@0+ (1,0) [0|1] "" XXX + SG_ ACC_ALERTS : 20|5@0+ (1,0) [0|1] "" BDY + SG_ SET_TO_0 : 22|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 1029 DOORS_STATUS: 8 BDY + SG_ DOOR_OPEN_FL : 37|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_FR : 38|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RL : 39|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RR : 40|1@0+ (1,0) [0|1] "" EON + SG_ TRUNK_OPEN : 41|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +VAL_ 419 GEAR_SHIFTER 2 "S" 32 "D" 16 "N" 8 "R" 4 "P" ; +VAL_ 419 GEAR 26 "S" 20 "D" 19 "N" 18 "R" 17 "P" ; +VAL_ 545 ECON_ON_2 0 "off" 3 "on" ; +VAL_ 662 CRUISE_BUTTONS 7 "tbd" 6 "tbd" 5 "tbd" 4 "accel_res" 3 "decel_set" 2 "cancel" 1 "main" 0 "none" ; +VAL_ 662 CRUISE_SETTING 3 "distance_adj" 2 "tbd" 1 "lkas_button" 0 "none" ; +VAL_ 806 CMBS_BUTTON 3 "pressed" 0 "released" ; +VAL_ 891 WIPERS 4 "High" 2 "Low" 0 "Off" ; +VAL_ 829 BEEP 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep" ; + +CM_ "CHFFR_METRIC 330 STEER_ANGLE STEER_ANGLE 0.36 180; CHFFR_METRIC 380 ENGINE_RPM ENGINE_RPM 1 0; CHFFR_METRIC 804 ENGINE_TEMPERATURE ENGINE_TEMPERATURE 1 0"; diff --git a/generator/honda/honda_crv_ex_2017_can.dbc b/generator/honda/honda_crv_ex_2017_can.dbc index e8cc456bf..8382a9fdd 100644 --- a/generator/honda/honda_crv_ex_2017_can.dbc +++ b/generator/honda/honda_crv_ex_2017_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/generator/honda/honda_crv_hybrid_2019_can.dbc b/generator/honda/honda_crv_hybrid_2019_can.dbc index 2398526e0..692ab8d01 100644 --- a/generator/honda/honda_crv_hybrid_2019_can.dbc +++ b/generator/honda/honda_crv_hybrid_2019_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/generator/honda/honda_insight_ex_2019_can.dbc b/generator/honda/honda_insight_ex_2019_can.dbc index ff988cf9d..770b3a150 100644 --- a/generator/honda/honda_insight_ex_2019_can.dbc +++ b/generator/honda/honda_insight_ex_2019_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ GEAR : 7|8@0+ (1,0) [0|255] "" EON SG_ GEAR_SHIFTER : 29|6@0+ (1,0) [0|63] "" EON diff --git a/gm_global_a_powertrain.dbc b/gm_global_a_powertrain.dbc index 2e3bd7c85..48611cfc7 100644 --- a/gm_global_a_powertrain.dbc +++ b/gm_global_a_powertrain.dbc @@ -241,7 +241,7 @@ VAL_ 481 LKAButton 1 "Active" 0 "Inactive" ; VAL_ 481 ACCButtons 6 "Cancel" 5 "Main" 3 "Set" 2 "Resume" 1 "None" ; VAL_ 481 DriveModeButton 1 "Active" 0 "Inactive" ; VAL_ 452 CruiseState 4 "Standstill" 3 "Faulted" 1 "Active" 0 "Off" ; -VAL_ 309 PRNDL 3 "Reverse" 2 "Drive" 1 "Neutral" 0 "Park" ; +VAL_ 309 PRNDL 3 "R" 2 "D" 1 "N" 0 "P" ; VAL_ 309 ESPButton 1 "Active" 0 "Inactive" ; VAL_ 384 LKASteeringCmdActive 1 "Active" 0 "Inactive" ; VAL_ 880 ACCLeadCar 1 "Present" 0 "Not Present" ; diff --git a/honda_accord_lx15t_2018_can_generated.dbc b/honda_accord_lx15t_2018_can_generated.dbc index 48c571df1..00a01f6ed 100644 --- a/honda_accord_lx15t_2018_can_generated.dbc +++ b/honda_accord_lx15t_2018_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_accord_lx15t_2018_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/honda_accord_s2t_2018_can_generated.dbc b/honda_accord_s2t_2018_can_generated.dbc index b160dff85..a89d4504f 100644 --- a/honda_accord_s2t_2018_can_generated.dbc +++ b/honda_accord_s2t_2018_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_accord_s2t_2018_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/honda_civic_hatchback_ex_2017_can_generated.dbc b/honda_civic_hatchback_ex_2017_can_generated.dbc index d6c9505d9..9b5091c2f 100644 --- a/honda_civic_hatchback_ex_2017_can_generated.dbc +++ b/honda_civic_hatchback_ex_2017_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_civic_hatchback_ex_2017_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/honda_civic_sedan_16_diesel_2019_can_generated.dbc b/honda_civic_sedan_16_diesel_2019_can_generated.dbc new file mode 100644 index 000000000..c9042135d --- /dev/null +++ b/honda_civic_sedan_16_diesel_2019_can_generated.dbc @@ -0,0 +1,351 @@ +CM_ "AUTOGENERATED FILE, DO NOT EDIT" + + +CM_ "Imported file _bosch_2018.dbc starts here" +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BU_: EBCM EON CAM RADAR PCM EPS VSA SCM BDY XXX EPB + +BO_ 148 KINEMATICS: 8 XXX + SG_ LAT_ACCEL : 7|10@0+ (-0.035,17.92) [-20|20] "m/s2" EON + SG_ LONG_ACCEL : 25|10@0+ (-0.035,17.92) [-20|20] "m/s2" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + +BO_ 228 STEERING_CONTROL: 5 EON + SG_ STEER_TORQUE_REQUEST : 23|1@0+ (1,0) [0|1] "" EPS + SG_ SET_ME_X00 : 22|7@0+ (1,0) [0|127] "" EPS + SG_ SET_ME_X00_2 : 31|8@0+ (1,0) [0|0] "" EPS + SG_ STEER_TORQUE : 7|16@0- (1,0) [-4096|4096] "" EPS + SG_ COUNTER : 37|2@0+ (1,0) [0|3] "" EPS + SG_ CHECKSUM : 35|4@0+ (1,0) [0|15] "" EPS + +BO_ 232 BRAKE_HOLD: 7 XXX + SG_ XMISSION_SPEED : 7|14@0- (1,0) [1|0] "" XXX + SG_ COMPUTER_BRAKE : 39|16@0+ (1,0) [0|0] "" XXX + SG_ COMPUTER_BRAKE_REQUEST : 29|1@0+ (1,0) [0|0] "" XXX + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX + +BO_ 330 STEERING_SENSORS: 8 EPS + SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON + SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON + SG_ STEER_SENSOR_STATUS_1 : 34|1@0+ (1,0) [0|1] "" EON + SG_ STEER_SENSOR_STATUS_2 : 33|1@0+ (1,0) [0|1] "" EON + SG_ STEER_SENSOR_STATUS_3 : 32|1@0+ (1,0) [0|1] "" EON + SG_ STEER_WHEEL_ANGLE : 47|16@0- (-0.1,0) [-500|500] "deg" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 344 ENGINE_DATA: 8 PCM + SG_ XMISSION_SPEED : 7|16@0+ (0.01,0) [0|250] "kph" EON + SG_ ENGINE_RPM : 23|16@0+ (1,0) [0|15000] "rpm" EON + SG_ XMISSION_SPEED2 : 39|16@0+ (0.01,0) [0|250] "kph" EON + SG_ ODOMETER : 55|8@0+ (10,0) [0|2550] "m" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 380 POWERTRAIN_DATA: 8 PCM + SG_ PEDAL_GAS : 7|8@0+ (1,0) [0|255] "" EON + SG_ ENGINE_RPM : 23|16@0+ (1,0) [0|15000] "rpm" EON + SG_ GAS_PRESSED : 39|1@0+ (1,0) [0|1] "" EON + SG_ ACC_STATUS : 38|1@0+ (1,0) [0|1] "" EON + SG_ BOH_17C : 37|5@0+ (1,0) [0|1] "" EON + SG_ BRAKE_SWITCH : 32|1@0+ (1,0) [0|1] "" EON + SG_ BOH2_17C : 47|10@0+ (1,0) [0|1] "" EON + SG_ BRAKE_PRESSED : 53|1@0+ (1,0) [0|1] "" EON + SG_ BOH3_17C : 52|5@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 399 STEER_STATUS: 7 EPS + SG_ STEER_TORQUE_SENSOR : 7|16@0- (-1,0) [-31000|31000] "tbd" EON + SG_ STEER_ANGLE_RATE : 23|16@0- (-0.1,0) [-31000|31000] "deg/s" EON + SG_ STEER_STATUS : 39|4@0+ (1,0) [0|15] "" EON + SG_ STEER_CONTROL_ACTIVE : 35|1@0+ (1,0) [0|1] "" EON + SG_ STEER_CONFIG_INDEX : 43|4@0+ (1,0) [0|15] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" EON + +BO_ 420 VSA_STATUS: 8 VSA + SG_ ESP_DISABLED : 28|1@0+ (1,0) [0|1] "" EON + SG_ USER_BRAKE : 7|16@0+ (0.015625,-1.609375) [0|1000] "" EON + SG_ BRAKE_HOLD_ACTIVE : 46|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_HOLD_ENABLED : 45|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 427 STEER_MOTOR_TORQUE: 3 EPS + SG_ CONFIG_VALID : 7|1@0+ (1,0) [0|1] "" EON + SG_ MOTOR_TORQUE : 1|10@0+ (1,0) [0|256] "" EON + SG_ OUTPUT_DISABLED : 22|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 21|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 19|4@0+ (1,0) [0|15] "" EON + +BO_ 450 EPB_STATUS: 8 EPB + SG_ EPB_ACTIVE : 3|1@0+ (1,0) [0|1] "" EON + SG_ EPB_STATE : 29|2@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 464 WHEEL_SPEEDS: 8 VSA + SG_ WHEEL_SPEED_FL : 7|15@0+ (0.01,0) [0|250] "kph" EON + SG_ WHEEL_SPEED_FR : 8|15@0+ (0.01,0) [0|250] "kph" EON + SG_ WHEEL_SPEED_RL : 25|15@0+ (0.01,0) [0|250] "kph" EON + SG_ WHEEL_SPEED_RR : 42|15@0+ (0.01,0) [0|250] "kph" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + +BO_ 479 ACC_CONTROL: 8 EON + SG_ SET_TO_0 : 20|5@0+ (1,0) [0|1] "" XXX + SG_ CONTROL_ON : 23|3@0+ (1,0) [0|5] "" XXX + SG_ GAS_COMMAND : 7|16@0- (1,0) [0|0] "" XXX + SG_ ACCEL_COMMAND : 31|11@0- (0.01,0) [0|0] "m/s2" XXX + SG_ BRAKE_LIGHTS : 62|1@0+ (1,0) [0|1] "" XXX + SG_ BRAKE_REQUEST : 34|1@0+ (1,0) [0|1] "" XXX + SG_ STANDSTILL : 35|1@0+ (1,0) [0|1] "" XXX + SG_ STANDSTILL_RELEASE : 36|1@0+ (1,0) [0|1] "" XXX + SG_ AEB_STATUS : 33|1@0+ (1,0) [0|1] "" XXX + SG_ AEB_BRAKING : 47|1@0+ (1,0) [0|1] "" XXX + SG_ AEB_PREPARE : 43|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 490 VEHICLE_DYNAMICS: 8 VSA + SG_ LAT_ACCEL : 7|16@0- (0.0015,0) [-20|20] "m/s2" EON + SG_ LONG_ACCEL : 23|16@0- (0.0015,0) [-20|20] "m/s2" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + +BO_ 495 ACC_CONTROL_ON: 8 XXX + SG_ SET_TO_75 : 31|8@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_30 : 39|8@0+ (1,0) [0|255] "" XXX + SG_ ZEROS_BOH : 23|8@0+ (1,0) [0|255] "" XXX + SG_ ZEROS_BOH2 : 47|16@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_FF : 15|8@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_3 : 6|7@0+ (1,0) [0|4095] "" XXX + SG_ CONTROL_ON : 7|1@0+ (1,0) [0|1] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + +BO_ 545 XXX_16: 6 SCM + SG_ ECON_ON : 23|1@0+ (1,0) [0|1] "" XXX + SG_ DRIVE_MODE : 37|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER : 45|2@0+ (1,0) [0|3] "" BDY + SG_ CHECKSUM : 43|4@0+ (1,0) [0|15] "" BDY + +BO_ 597 ROUGH_WHEEL_SPEED: 8 VSA + SG_ WHEEL_SPEED_FL : 7|8@0+ (1,0) [0|255] "mph" EON + SG_ WHEEL_SPEED_FR : 15|8@0+ (1,0) [0|255] "mph" EON + SG_ WHEEL_SPEED_RL : 23|8@0+ (1,0) [0|255] "mph" EON + SG_ WHEEL_SPEED_RR : 31|8@0+ (1,0) [0|255] "mph" EON + SG_ SET_TO_X55 : 39|8@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_X55_2 : 47|8@0+ (1,0) [0|255] "" EON + SG_ LONG_COUNTER : 55|8@0+ (1,0) [0|255] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + + BO_ 662 SCM_BUTTONS: 4 SCM + SG_ CRUISE_BUTTONS : 7|3@0+ (1,0) [0|7] "" EON + SG_ CRUISE_SETTING : 3|2@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 29|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 27|4@0+ (1,0) [0|15] "" EON + +BO_ 773 SEATBELT_STATUS: 7 BDY + SG_ SEATBELT_DRIVER_LAMP : 7|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_PASS_UNLATCHED : 10|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_PASS_LATCHED : 11|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_DRIVER_UNLATCHED : 12|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_DRIVER_LATCHED : 13|1@0+ (1,0) [0|1] "" EON + SG_ PASS_AIRBAG_OFF : 14|1@0+ (1,0) [0|1] "" EON + SG_ PASS_AIRBAG_ON : 15|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|3] "" EON + +BO_ 777 CAR_SPEED: 8 PCM + SG_ ROUGH_CAR_SPEED : 23|8@0+ (1,0) [0|255] "mph" XXX + SG_ CAR_SPEED : 7|16@0+ (0.01,0) [0|65535] "kph" XXX + SG_ ROUGH_CAR_SPEED_3 : 39|16@0+ (0.01,0) [0|65535] "kph" XXX + SG_ ROUGH_CAR_SPEED_2 : 31|8@0+ (1,0) [0|255] "mph" XXX + SG_ LOCK_STATUS : 55|2@0+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 780 ACC_HUD: 8 ADAS + SG_ CRUISE_SPEED : 31|8@0+ (1,0) [0|255] "kph" BDY + SG_ DTC_MODE : 39|1@0+ (1,0) [0|1] "" BDY + SG_ BOH : 38|1@0+ (1,0) [0|1] "" BDY + SG_ FCM_PROBLEM : 34|1@0+ (1,0) [0|1] "" BDY + SG_ RADAR_OBSTRUCTED : 33|1@0+ (1,0) [0|1] "" BDY + SG_ ENABLE_MINI_CAR : 32|1@0+ (1,0) [0|1] "" BDY + SG_ BOH_3 : 43|1@0+ (1,0) [0|3] "" BDY + SG_ BOH_4 : 42|1@0+ (1,0) [0|3] "" BDY + SG_ BOH_5 : 41|1@0+ (1,0) [0|3] "" BDY + SG_ CRUISE_CONTROL_LABEL : 40|1@0+ (1,0) [0|3] "" BDY + SG_ ZEROS_BOH : 7|24@0+ (0.002759506,0) [0|100] "m/s" BDY + SG_ FCM_OFF : 35|1@0+ (1,0) [0|1] "" BDY + SG_ SET_TO_1 : 36|1@0+ (1,0) [0|1] "" XXX + SG_ HUD_DISTANCE : 47|2@0+ (1,0) [0|3] "" BDY + SG_ HUD_LEAD : 45|2@0+ (1,0) [0|3] "" BDY + SG_ ACC_PROBLEM : 37|1@0+ (1,0) [0|1] "" BDY + SG_ ACC_ON : 52|1@0+ (1,0) [0|1] "" XXX + SG_ BOH_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ SET_TO_X1 : 55|1@0+ (1,0) [0|1] "" XXX + SG_ IMPERIAL_UNIT : 54|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 804 CRUISE: 8 PCM + SG_ TRIP_FUEL_CONSUMED : 23|16@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 806 SCM_FEEDBACK: 8 SCM + SG_ DRIVERS_DOOR_OPEN : 17|1@0+ (1,0) [0|1] "" XXX + SG_ MAIN_ON : 28|1@0+ (1,0) [0|1] "" EON + SG_ RIGHT_BLINKER : 27|1@0+ (1,0) [0|1] "" EON + SG_ LEFT_BLINKER : 26|1@0+ (1,0) [0|1] "" EON + SG_ CMBS_STATES : 22|2@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 829 LKAS_HUD: 5 ADAS + SG_ CAM_TEMP_HIGH : 7|1@0+ (1,0) [0|255] "" BDY + SG_ SET_ME_X41 : 6|7@0+ (1,0) [0|127] "" BDY + SG_ BOH : 6|7@0+ (1,0) [0|127] "" BDY + SG_ DASHED_LANES : 14|1@0+ (1,0) [0|1] "" BDY + SG_ DTC : 13|1@0+ (1,0) [0|1] "" BDY + SG_ LKAS_PROBLEM : 12|1@0+ (1,0) [0|1] "" BDY + SG_ LKAS_OFF : 11|1@0+ (1,0) [0|1] "" BDY + SG_ SOLID_LANES : 10|1@0+ (1,0) [0|1] "" BDY + SG_ LDW_RIGHT : 9|1@0+ (1,0) [0|1] "" BDY + SG_ STEERING_REQUIRED : 8|1@0+ (1,0) [0|1] "" BDY + SG_ BOH : 23|2@0+ (1,0) [0|4] "" BDY + SG_ LDW_PROBLEM : 21|1@0+ (1,0) [0|1] "" BDY + SG_ BEEP : 17|2@0+ (1,0) [0|1] "" BDY + SG_ LDW_ON : 28|1@0+ (1,0) [0|1] "" BDY + SG_ LDW_OFF : 27|1@0+ (1,0) [0|1] "" BDY + SG_ CLEAN_WINDSHIELD : 26|1@0+ (1,0) [0|1] "" BDY + SG_ SET_ME_X48 : 31|8@0+ (1,0) [0|255] "" BDY + SG_ COUNTER : 37|2@0+ (1,0) [0|3] "" BDY + SG_ CHECKSUM : 35|4@0+ (1,0) [0|15] "" BDY + +BO_ 862 CAMERA_MESSAGES: 8 CAM + SG_ ZEROS_BOH : 7|50@0+ (1,0) [0|127] "" BDY + SG_ AUTO_HIGHBEAMS_ACTIVE : 53|1@0+ (1,0) [0|1] "" XXX + SG_ HIGHBEAMS_ON : 52|1@0+ (1,0) [0|1] "" XXX + SG_ ZEROS_BOH_2 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 884 STALK_STATUS: 8 XXX + SG_ AUTO_HEADLIGHTS : 46|1@0+ (1,0) [0|1] "" EON + SG_ HIGH_BEAM_HOLD : 47|1@0+ (1,0) [0|1] "" EON + SG_ HIGH_BEAM_FLASH : 45|1@0+ (1,0) [0|1] "" EON + SG_ HEADLIGHTS_ON : 54|1@0+ (1,0) [0|1] "" EON + SG_ WIPER_SWITCH : 53|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + +BO_ 891 STALK_STATUS_2: 8 XXX + SG_ WIPERS : 17|2@0+ (1,0) [0|3] "" EON + SG_ LOW_BEAMS : 35|1@0+ (1,0) [0|1] "" XXX + SG_ HIGH_BEAMS : 34|1@0+ (1,0) [0|1] "" XXX + SG_ PARK_LIGHTS : 36|1@0+ (1,0) [0|1] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + +CM_ SG_ 479 AEB_STATUS "set for the duration of AEB event"; +CM_ SG_ 479 AEB_BRAKING "set when braking is commanded during AEB event"; +CM_ SG_ 479 AEB_PREPARE "set 1s before AEB"; + +VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_speed_lockout" 2 "no_torque_alert_1" 0 "normal" ; + +CM_ "honda_civic_sedan_16_diesel_2019_can.dbc starts here" + + +BO_ 316 GAS_PEDAL_2: 8 XXX + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 419 GEARBOX: 8 XXX + SG_ GEAR_SHIFTER : 24|8@1+ (1,0) [0|255] "" XXX + SG_ GEAR : 32|8@1+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 432 STANDSTILL: 7 VSA + SG_ WHEELS_MOVING : 12|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_1 : 11|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_2 : 9|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" EON + +BO_ 892 CRUISE_PARAMS: 8 PCM + SG_ CRUISE_SPEED_OFFSET : 31|8@0- (0.1,0) [-128|127] "kph" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|15] "" EON + +BO_ 927 RADAR_HUD: 8 RADAR + SG_ ZEROS_BOH : 7|10@0+ (1,0) [0|127] "" BDY + SG_ CMBS_OFF : 12|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH3 : 31|32@0+ (1,0) [0|4294967295] "" XXX + SG_ RESUME_INSTRUCTION : 21|1@0+ (1,0) [0|1] "" XXX + SG_ SET_TO_1 : 13|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH2 : 11|4@0+ (1,0) [0|1] "" XXX + SG_ APPLY_BRAKES_FOR_CANC : 23|1@0+ (1,0) [0|1] "" XXX + SG_ ACC_ALERTS : 20|5@0+ (1,0) [0|1] "" BDY + SG_ SET_TO_0 : 22|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 1029 DOORS_STATUS: 8 BDY + SG_ DOOR_OPEN_FL : 37|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_FR : 38|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RL : 39|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RR : 40|1@0+ (1,0) [0|1] "" EON + SG_ TRUNK_OPEN : 41|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +VAL_ 419 GEAR_SHIFTER 2 "S" 32 "D" 16 "N" 8 "R" 4 "P" ; +VAL_ 419 GEAR 26 "S" 20 "D" 19 "N" 18 "R" 17 "P" ; +VAL_ 545 ECON_ON_2 0 "off" 3 "on" ; +VAL_ 662 CRUISE_BUTTONS 7 "tbd" 6 "tbd" 5 "tbd" 4 "accel_res" 3 "decel_set" 2 "cancel" 1 "main" 0 "none" ; +VAL_ 662 CRUISE_SETTING 3 "distance_adj" 2 "tbd" 1 "lkas_button" 0 "none" ; +VAL_ 806 CMBS_BUTTON 3 "pressed" 0 "released" ; +VAL_ 891 WIPERS 4 "High" 2 "Low" 0 "Off" ; +VAL_ 829 BEEP 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep" ; + +CM_ "CHFFR_METRIC 330 STEER_ANGLE STEER_ANGLE 0.36 180; CHFFR_METRIC 380 ENGINE_RPM ENGINE_RPM 1 0; CHFFR_METRIC 804 ENGINE_TEMPERATURE ENGINE_TEMPERATURE 1 0"; diff --git a/honda_crv_ex_2017_can_generated.dbc b/honda_crv_ex_2017_can_generated.dbc index db11aa3a7..6cdb0c02d 100644 --- a/honda_crv_ex_2017_can_generated.dbc +++ b/honda_crv_ex_2017_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_crv_ex_2017_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/honda_crv_hybrid_2019_can_generated.dbc b/honda_crv_hybrid_2019_can_generated.dbc index ed90b9408..f744c490b 100644 --- a/honda_crv_hybrid_2019_can_generated.dbc +++ b/honda_crv_hybrid_2019_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_crv_hybrid_2019_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/honda_insight_ex_2019_can_generated.dbc b/honda_insight_ex_2019_can_generated.dbc index 775e22ada..f5cc66dcc 100644 --- a/honda_insight_ex_2019_can_generated.dbc +++ b/honda_insight_ex_2019_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_insight_ex_2019_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ GEAR : 7|8@0+ (1,0) [0|255] "" EON SG_ GEAR_SHIFTER : 29|6@0+ (1,0) [0|63] "" EON From d56501a7fbb7ea001c377c39a012e310412b1183 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:08:27 -0800 Subject: [PATCH 05/16] openpilot v0.7.3 release --- .pylintrc | 585 ------------------ .travis.yml | 7 - README.md | 2 + RELEASES.md | 8 + SConstruct | 1 + common/params.py | 2 + selfdrive/camerad/cameras/camera_qcom.c | 57 +- selfdrive/car/car_helpers.py | 5 + selfdrive/car/chrysler/carcontroller.py | 10 +- selfdrive/car/chrysler/carstate.py | 109 ++-- selfdrive/car/chrysler/interface.py | 68 +- selfdrive/car/chrysler/values.py | 5 +- selfdrive/car/fingerprints.py | 32 +- selfdrive/car/ford/carcontroller.py | 14 +- selfdrive/car/ford/carstate.py | 76 +-- selfdrive/car/ford/interface.py | 34 +- selfdrive/car/fw_versions.py | 7 +- selfdrive/car/gm/carstate.py | 43 +- selfdrive/car/gm/gmcan.py | 4 +- selfdrive/car/gm/interface.py | 18 +- selfdrive/car/gm/values.py | 13 +- selfdrive/car/honda/carstate.py | 52 +- selfdrive/car/honda/values.py | 33 +- selfdrive/car/hyundai/carstate.py | 42 +- selfdrive/car/interfaces.py | 32 + selfdrive/car/subaru/carcontroller.py | 2 +- selfdrive/car/subaru/carstate.py | 33 +- selfdrive/car/toyota/carcontroller.py | 6 - selfdrive/car/toyota/carstate.py | 50 +- selfdrive/car/toyota/interface.py | 32 +- selfdrive/car/toyota/values.py | 163 +++-- selfdrive/car/volkswagen/carstate.py | 35 +- selfdrive/common/version.h | 2 +- selfdrive/controls/lib/pathplanner.py | 4 +- selfdrive/debug/live_cpu_and_temp.py | 96 +-- selfdrive/debug/test_fw_query_on_routes.py | 10 +- selfdrive/locationd/kalman/.gitignore | 1 + selfdrive/locationd/kalman/README.md | 52 ++ selfdrive/locationd/kalman/SConscript | 31 + selfdrive/locationd/kalman/__init__.py | 0 .../locationd/kalman/helpers/__init__.py | 191 ++++++ .../locationd/kalman/helpers/chi2_lookup.py | 22 + .../kalman/helpers/chi2_lookup_table.npy | Bin 0 -> 156928 bytes selfdrive/locationd/kalman/helpers/ekf_sym.py | 563 +++++++++++++++++ .../kalman/helpers/feature_handler.py | 158 +++++ .../kalman/helpers/lst_sq_computer.py | 176 ++++++ .../kalman/helpers}/sympy_helpers.py | 51 +- selfdrive/locationd/kalman/models/live_kf.py | 284 +++++++++ .../locationd/kalman/templates/compute_pos.c | 54 ++ selfdrive/locationd/kalman/templates/ekf_c.c | 123 ++++ .../kalman/templates/feature_handler.c | 58 ++ selfdrive/locationd/locationd.py | 202 ++++++ selfdrive/locationd/locationd_yawrate.cc | 2 +- selfdrive/locationd/locationd_yawrate.h | 2 +- selfdrive/loggerd/deleter.py | 10 +- selfdrive/loggerd/loggerd.cc | 5 +- selfdrive/manager.py | 25 +- selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/test_car_models.py | 17 +- selfdrive/test/test_fingerprints.py | 4 + selfdrive/test/test_openpilot.py | 204 +++--- selfdrive/thermald.py | 19 +- selfdrive/updated.py | 5 +- 63 files changed, 2602 insertions(+), 1351 deletions(-) delete mode 100644 .pylintrc delete mode 100644 .travis.yml create mode 100644 selfdrive/locationd/kalman/.gitignore create mode 100644 selfdrive/locationd/kalman/README.md create mode 100644 selfdrive/locationd/kalman/SConscript create mode 100644 selfdrive/locationd/kalman/__init__.py create mode 100644 selfdrive/locationd/kalman/helpers/__init__.py create mode 100644 selfdrive/locationd/kalman/helpers/chi2_lookup.py create mode 100644 selfdrive/locationd/kalman/helpers/chi2_lookup_table.npy create mode 100644 selfdrive/locationd/kalman/helpers/ekf_sym.py create mode 100755 selfdrive/locationd/kalman/helpers/feature_handler.py create mode 100755 selfdrive/locationd/kalman/helpers/lst_sq_computer.py rename {common => selfdrive/locationd/kalman/helpers}/sympy_helpers.py (58%) create mode 100755 selfdrive/locationd/kalman/models/live_kf.py create mode 100644 selfdrive/locationd/kalman/templates/compute_pos.c create mode 100644 selfdrive/locationd/kalman/templates/ekf_c.c create mode 100644 selfdrive/locationd/kalman/templates/feature_handler.c create mode 100755 selfdrive/locationd/locationd.py diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 64a55daf8..000000000 --- a/.pylintrc +++ /dev/null @@ -1,585 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist=scipy - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. -jobs=4 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - locally-enabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - bad-indentation, - line-too-long, - missing-docstring, - multiple-statements, - bad-continuation, - invalid-name, - too-many-arguments, - too-many-locals, - superfluous-parens, - bad-whitespace, - too-many-instance-attributes, - wrong-import-position, - ungrouped-imports, - wrong-import-order, - protected-access, - trailing-whitespace, - too-many-branches, - too-few-public-methods, - too-many-statements, - trailing-newlines, - attribute-defined-outside-init, - too-many-return-statements, - too-many-public-methods, - unused-argument, - old-style-class, - no-init, - len-as-condition, - unneeded-not, - no-self-use, - multiple-imports, - no-else-return, - logging-not-lazy, - fixme, - redefined-outer-name, - unused-variable, - unsubscriptable-object, - expression-not-assigned, - too-many-boolean-expressions, - consider-using-ternary, - invalid-unary-operand-type, - relative-import, - deprecated-lambda - - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=optparse.Values,sys.exit - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members=capnp.* cereal.* pygame.* zmq.* setproctitle.* smbus2.* usb1.* serial.* cv2.* - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules=flask setproctitle usb1 flask.ext.socketio smbus2 usb1.* - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[BASIC] - -# Naming style matching correct argument names -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style -#argument-rgx= - -# Naming style matching correct attribute names -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style -#class-attribute-rgx= - -# Naming style matching correct class names -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming-style -#class-rgx= - -# Naming style matching correct constant names -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Naming style matching correct inline iteration names -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style -#inlinevar-rgx= - -# Naming style matching correct method names -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style -#method-rgx= - -# Naming style matching correct module names -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style -#variable-rgx= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8683d8bee..000000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -sudo: required - -services: - - docker - -script: - - ./run_docker_tests.sh diff --git a/README.md b/README.md index 73bccf297..42770fe80 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Supported Cars | Lexus | ES Hybrid 2019 | All | openpilot | 0mph | 0mph | | Lexus | IS 2017-2019 | All | Stock | 22mph | 0mph | | Lexus | IS Hybrid 2017 | All | Stock | 0mph | 0mph | +| Lexus | NX Hybrid 2018 | All | Stock4| 0mph | 0mph | | Lexus | RX 2016-17 | All | Stock4| 0mph | 0mph | | Lexus | RX 2020 | All | openpilot | 0mph | 0mph | | Lexus | RX Hybrid 2016-19 | All | Stock4| 0mph | 0mph | @@ -113,6 +114,7 @@ Supported Cars | Toyota | Corolla Hybrid 2020 | All | openpilot | 0mph | 0mph | | Toyota | Highlander 2017-19 | All | Stock4| 0mph | 0mph | | Toyota | Highlander Hybrid 2017-19 | All | Stock4| 0mph | 0mph | +| Toyota | Highlander 2020 | All | openpilot | 0mph | 0mph | | Toyota | Prius 2016 | TSS-P | Stock4| 0mph | 0mph | | Toyota | Prius 2017-19 | All | Stock4| 0mph | 0mph | | Toyota | Prius Prime 2017-20 | All | Stock4| 0mph | 0mph | diff --git a/RELEASES.md b/RELEASES.md index b472cf9cd..c0a859003 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,11 @@ +Version 0.7.3 (2020-02-21) +======================== + * Improved autofocus for road facing camera + * Support for 2020 Highlander thanks to che220! + * Support for 2018 Lexus NX 300h thanks to kengggg! + * Speed up ECU firmware query + * Fix bug where manager would sometimes hang after shutting down the car + Version 0.7.2 (2020-02-07) ======================== * ECU firmware version based fingerprinting for Honda & Toyota diff --git a/SConstruct b/SConstruct index 73c603ad5..5944541b5 100644 --- a/SConstruct +++ b/SConstruct @@ -228,5 +228,6 @@ if arch == "aarch64": SConscript(['selfdrive/clocksd/SConscript']) SConscript(['selfdrive/locationd/SConscript']) +SConscript(['selfdrive/locationd/kalman/SConscript']) # TODO: finish cereal, dbcbuilder, MPC diff --git a/common/params.py b/common/params.py index 009e5729d..c623e3dc3 100755 --- a/common/params.py +++ b/common/params.py @@ -81,6 +81,7 @@ keys = { "LiveParameters": [TxType.PERSISTENT], "LongitudinalControl": [TxType.PERSISTENT], "OpenpilotEnabledToggle": [TxType.PERSISTENT], + "LaneChangeEnabled": [TxType.PERSISTENT], "PandaFirmware": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], "PandaFirmwareHex": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], "PandaDongleId": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], @@ -93,6 +94,7 @@ keys = { "TermsVersion": [TxType.PERSISTENT], "TrainingVersion": [TxType.PERSISTENT], "UpdateAvailable": [TxType.CLEAR_ON_MANAGER_START], + "UpdateFailedCount": [TxType.CLEAR_ON_MANAGER_START], "Version": [TxType.PERSISTENT], "Offroad_ChargeDisabled": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], "Offroad_ConnectivityNeeded": [TxType.CLEAR_ON_MANAGER_START], diff --git a/selfdrive/camerad/cameras/camera_qcom.c b/selfdrive/camerad/cameras/camera_qcom.c index c0f10f13b..ddaa51e34 100644 --- a/selfdrive/camerad/cameras/camera_qcom.c +++ b/selfdrive/camerad/cameras/camera_qcom.c @@ -1710,6 +1710,9 @@ static void parse_autofocus(CameraState *s, uint8_t *d) { int good_count = 0; int16_t max_focus = -32767; int avg_focus = 0; + // force to max if not able to determine focus for long + const int patience_cnt = 100; + static int nan_cnt = 0; /*printf("FOCUS: "); for (int i = 0; i < 0x10; i++) { @@ -1717,42 +1720,56 @@ static void parse_autofocus(CameraState *s, uint8_t *d) { }*/ for (int i = 0; i < NUM_FOCUS; i++) { - int doff = i*5+5; - s->confidence[i] = d[doff]; - int16_t focus_t = (d[doff+1] << 3) | (d[doff+2] >> 5); - if (focus_t >= 1024) focus_t = -(2048-focus_t); - s->focus[i] = focus_t; - //printf("%x->%d ", d[doff], focus_t); - if (s->confidence[i] > 0x20) { + int pd_idx = (i+1)*5; + s->confidence[i] = d[pd_idx]; + int16_t focus_delta = d[pd_idx+1]; + if (focus_delta >= 128) focus_delta = - (256 - focus_delta); + s->focus[i] = focus_delta; + + if (s->confidence[i] > 64) { good_count++; max_focus = max(max_focus, s->focus[i]); avg_focus += s->focus[i]; + // printf("%d\n", s->focus[i]); } } - //printf("\n"); - if (good_count < 4) { + if (good_count < 7) { s->focus_err = nan(""); + nan_cnt += 1; + if (nan_cnt > patience_cnt) { + s->focus_err = 16; + nan_cnt = 0; + } return; } avg_focus /= good_count; - // outlier rejection - if (abs(avg_focus - max_focus) > 200) { - s->focus_err = nan(""); - return; + if (abs(avg_focus - max_focus) > 32) { + if (nan_cnt < patience_cnt) { + s->focus_err = nan(""); + nan_cnt += 1; + return; + } else { + s->focus_err = 16; + // s->focus_err = max_focus*8.0; + nan_cnt = 0; + } + } else { + s->focus_err = avg_focus; + nan_cnt = 0; } - - s->focus_err = max_focus*1.0; + // printf("fe=%f\n", s->focus_err); } static void do_autofocus(CameraState *s) { - // params for focus PI controller - const float focus_kp = 0.005; + // params for focus P controller + const float focus_kp = 0.1; float err = s->focus_err; - float offset = 0; + // don't allow big change + err = clamp(err, -16, 16); float sag = (s->last_sag_acc_z/9.8) * 128; const int dac_up = s->device == DEVICE_LP3? 634:456; @@ -1776,6 +1793,7 @@ static void do_autofocus(CameraState *s) { LOGD(debug);*/ actuator_move(s, target); + // printf("ltp=%f, clp=%d\n",s->lens_true_pos,s->cur_lens_pos); } @@ -2165,6 +2183,9 @@ void cameras_run(DualCameraState *s) { } else { uint8_t *d = c->ss[buffer].bufs[buf_idx].addr; if (buffer == 1) { + // FILE *df = fopen("/sdcard/focus_buf","wb"); + // fwrite(d, c->ss[buffer].bufs[buf_idx].len, sizeof(uint8_t), df); + // fclose(df); parse_autofocus(c, d); } c->ss[buffer].qbuf_info[buf_idx].dirty_buf = 1; diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 2d0445004..f1766075e 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -133,6 +133,11 @@ def fingerprint(logcan, sendcan, has_relay): car_fingerprint = list(fw_candidates)[0] source = car.CarParams.FingerprintSource.fw + fixed_fingerprint = os.environ.get('FINGERPRINT', "") + if len(fixed_fingerprint): + car_fingerprint = fixed_fingerprint + source = car.CarParams.FingerprintSource.fixed + cloudlog.warning("fingerprinted %s", car_fingerprint) return car_fingerprint, finger, vin, car_fw, source diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 2bc4e9680..f6b55b9e1 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -35,14 +35,14 @@ class CarController(): # steer torque new_steer = actuators.steer * SteerLimitParams.STEER_MAX apply_steer = apply_toyota_steer_torque_limits(new_steer, self.apply_steer_last, - CS.steer_torque_motor, SteerLimitParams) + CS.out.steeringTorqueEps, SteerLimitParams) self.steer_rate_limited = new_steer != apply_steer - moving_fast = CS.v_ego > CS.CP.minSteerSpeed # for status message - if CS.v_ego > (CS.CP.minSteerSpeed - 0.5): # for command high bit + moving_fast = CS.out.vEgo > CS.CP.minSteerSpeed # for status message + if CS.out.vEgo > (CS.CP.minSteerSpeed - 0.5): # for command high bit self.gone_fast_yet = True elif self.car_fingerprint in (CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020_HYBRID, CAR.JEEP_CHEROKEE_2019): - if CS.v_ego < (CS.CP.minSteerSpeed - 3.0): + if CS.out.vEgo < (CS.CP.minSteerSpeed - 3.0): self.gone_fast_yet = False # < 14.5m/s stock turns off this bit, but fine down to 13.5 lkas_active = moving_fast and enabled @@ -65,7 +65,7 @@ class CarController(): if (self.ccframe % 25 == 0): # 0.25s period if (CS.lkas_car_model != -1): new_msg = create_lkas_hud( - self.packer, CS.gear_shifter, lkas_active, hud_alert, + self.packer, CS.out.gearShifter, lkas_active, hud_alert, self.hud_count, CS.lkas_car_model) can_sends.append(new_msg) self.hud_count += 1 diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 93b84cf7d..bab58b963 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -1,14 +1,9 @@ from cereal import car from opendbc.can.parser import CANParser +from opendbc.can.can_define import CANDefine +from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from selfdrive.car.chrysler.values import DBC, STEER_THRESHOLD -from common.kalman.simple_kalman import KF1D - -GearShifter = car.CarState.GearShifter - -def parse_gear_shifter(can_gear): - return {0x1: GearShifter.park, 0x2: GearShifter.reverse, 0x3: GearShifter.neutral, - 0x4: GearShifter.drive, 0x5: GearShifter.low}.get(can_gear, GearShifter.unknown) - def get_can_parser(CP): @@ -68,83 +63,61 @@ def get_camera_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): - - self.CP = CP - self.left_blinker_on = 0 - self.right_blinker_on = 0 - - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["GEAR"]['PRNDL'] def update(self, cp, cp_cam): - # update prevs, update must run once per loop - self.prev_left_blinker_on = self.left_blinker_on - self.prev_right_blinker_on = self.right_blinker_on + ret = car.CarState.new_message() self.frame_23b = int(cp.vl["WHEEL_BUTTONS"]['COUNTER']) self.frame = int(cp.vl["EPS_STATUS"]['COUNTER']) - self.door_all_closed = not any([cp.vl["DOORS"]['DOOR_OPEN_FL'], - cp.vl["DOORS"]['DOOR_OPEN_FR'], - cp.vl["DOORS"]['DOOR_OPEN_RL'], - cp.vl["DOORS"]['DOOR_OPEN_RR']]) - self.seatbelt = (cp.vl["SEATBELT_STATUS"]['SEATBELT_DRIVER_UNLATCHED'] == 0) + ret.doorOpen = any([cp.vl["DOORS"]['DOOR_OPEN_FL'], + cp.vl["DOORS"]['DOOR_OPEN_FR'], + cp.vl["DOORS"]['DOOR_OPEN_RL'], + cp.vl["DOORS"]['DOOR_OPEN_RR']]) + ret.seatbeltUnlatched = cp.vl["SEATBELT_STATUS"]['SEATBELT_DRIVER_UNLATCHED'] == 1 + + ret.brakePressed = cp.vl["BRAKE_2"]['BRAKE_PRESSED_2'] == 5 # human-only + ret.brake = 0 + ret.brakeLights = ret.brakePressed + ret.gas = cp.vl["ACCEL_GAS_134"]['ACCEL_134'] + ret.gasPressed = ret.gas > 1e-5 - self.brake_pressed = cp.vl["BRAKE_2"]['BRAKE_PRESSED_2'] == 5 # human-only - self.pedal_gas = cp.vl["ACCEL_GAS_134"]['ACCEL_134'] - self.car_gas = self.pedal_gas self.esp_disabled = (cp.vl["TRACTION_BUTTON"]['TRACTION_OFF'] == 1) - self.v_wheel_fl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FL'] - self.v_wheel_rr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RR'] - self.v_wheel_rl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RL'] - self.v_wheel_fr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FR'] - v_wheel = (cp.vl['SPEED_1']['SPEED_LEFT'] + cp.vl['SPEED_1']['SPEED_RIGHT']) / 2. + ret.wheelSpeeds.fl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FL'] + ret.wheelSpeeds.rr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RR'] + ret.wheelSpeeds.rl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RL'] + ret.wheelSpeeds.fr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FR'] + ret.vEgoRaw = (cp.vl['SPEED_1']['SPEED_LEFT'] + cp.vl['SPEED_1']['SPEED_RIGHT']) / 2. + ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) + ret.standstill = not ret.vEgoRaw > 0.001 - # Kalman filter - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] + ret.leftBlinker = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 1 + ret.rightBlinker = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 2 + ret.steeringAngle = cp.vl["STEERING"]['STEER_ANGLE'] + ret.steeringRate = cp.vl["STEERING"]['STEERING_RATE'] + ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl['GEAR']['PRNDL'], None)) - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - self.standstill = not v_wheel > 0.001 + ret.cruiseState.enabled = cp.vl["ACC_2"]['ACC_STATUS_2'] == 7 # ACC is green. + ret.cruiseState.available = ret.cruiseState.enabled # FIXME: for now same as enabled + ret.cruiseState.speed = cp.vl["DASHBOARD"]['ACC_SPEED_CONFIG_KPH'] * CV.KPH_TO_MS - self.angle_steers = cp.vl["STEERING"]['STEER_ANGLE'] - self.angle_steers_rate = cp.vl["STEERING"]['STEERING_RATE'] - self.gear_shifter = parse_gear_shifter(cp.vl['GEAR']['PRNDL']) - self.main_on = cp.vl["ACC_2"]['ACC_STATUS_2'] == 7 # ACC is green. - self.left_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 1 - self.right_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 2 - - self.steer_torque_driver = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] - self.steer_torque_motor = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] - self.steer_override = abs(self.steer_torque_driver) > STEER_THRESHOLD + ret.steeringTorque = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] + ret.steeringTorqueEps = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] + ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD steer_state = cp.vl["EPS_STATUS"]["LKAS_STATE"] - self.steer_error = steer_state == 4 or (steer_state == 0 and self.v_ego > self.CP.minSteerSpeed) + self.steer_error = steer_state == 4 or (steer_state == 0 and ret.vEgo > self.CP.minSteerSpeed) - self.user_brake = 0 - self.brake_lights = self.brake_pressed - self.v_cruise_pcm = cp.vl["DASHBOARD"]['ACC_SPEED_CONFIG_KPH'] - self.pcm_acc_status = self.main_on - - self.generic_toggle = bool(cp.vl["STEERING_LEVERS"]['HIGH_BEAM_FLASH']) + ret.genericToggle = bool(cp.vl["STEERING_LEVERS"]['HIGH_BEAM_FLASH']) self.lkas_counter = cp_cam.vl["LKAS_COMMAND"]['COUNTER'] self.lkas_car_model = cp_cam.vl["LKAS_HUD"]['CAR_MODEL'] self.lkas_status_ok = cp_cam.vl["LKAS_HEARTBIT"]['LKAS_STATUS_OK'] + + return ret diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index f01ad5a09..93220fd12 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -20,6 +20,8 @@ class CarInterface(CarInterfaceBase): self.brake_pressed_prev = False self.cruise_enabled_prev = False self.low_speed_alert = False + self.left_blinker_prev = False + self.right_blinker_prev = False # *** init the major players *** self.CS = CarState(CP) @@ -113,78 +115,33 @@ class CarInterface(CarInterfaceBase): self.cp.update_strings(can_strings) self.cp_cam.update_strings(can_strings) - self.CS.update(self.cp, self.cp_cam) - - # create message - ret = car.CarState.new_message() + ret = self.CS.update(self.cp, self.cp_cam) ret.canValid = self.cp.can_valid and self.cp_cam.can_valid # speeds - ret.vEgo = self.CS.v_ego - ret.vEgoRaw = self.CS.v_ego_raw - ret.aEgo = self.CS.a_ego - ret.yawRate = self.VM.yaw_rate(self.CS.angle_steers * CV.DEG_TO_RAD, self.CS.v_ego) - ret.standstill = self.CS.standstill - ret.wheelSpeeds.fl = self.CS.v_wheel_fl - ret.wheelSpeeds.fr = self.CS.v_wheel_fr - ret.wheelSpeeds.rl = self.CS.v_wheel_rl - ret.wheelSpeeds.rr = self.CS.v_wheel_rr - - # gear shifter - ret.gearShifter = self.CS.gear_shifter - - # gas pedal - ret.gas = self.CS.car_gas - ret.gasPressed = self.CS.pedal_gas > 0 - - # brake pedal - ret.brake = self.CS.user_brake - ret.brakePressed = self.CS.brake_pressed - ret.brakeLights = self.CS.brake_lights - - # steering wheel - ret.steeringAngle = self.CS.angle_steers - ret.steeringRate = self.CS.angle_steers_rate - - ret.steeringTorque = self.CS.steer_torque_driver - ret.steeringPressed = self.CS.steer_override + ret.yawRate = self.VM.yaw_rate(ret.steeringAngle * CV.DEG_TO_RAD, ret.vEgo) ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False - # cruise state - ret.cruiseState.enabled = self.CS.pcm_acc_status # same as main_on - ret.cruiseState.speed = self.CS.v_cruise_pcm * CV.KPH_TO_MS - ret.cruiseState.available = self.CS.main_on - ret.cruiseState.speedOffset = 0. - ret.cruiseState.standstill = False - # TODO: button presses buttonEvents = [] - if self.CS.left_blinker_on != self.CS.prev_left_blinker_on: + if ret.leftBlinker != self.left_blinker_prev: be = car.CarState.ButtonEvent.new_message() be.type = ButtonType.leftBlinker - be.pressed = self.CS.left_blinker_on != 0 + be.pressed = ret.leftBlinker != 0 buttonEvents.append(be) - if self.CS.right_blinker_on != self.CS.prev_right_blinker_on: + if ret.rightBlinker != self.right_blinker_prev: be = car.CarState.ButtonEvent.new_message() be.type = ButtonType.rightBlinker - be.pressed = self.CS.right_blinker_on != 0 + be.pressed = ret.rightBlinker != 0 buttonEvents.append(be) ret.buttonEvents = buttonEvents - ret.leftBlinker = bool(self.CS.left_blinker_on) - ret.rightBlinker = bool(self.CS.right_blinker_on) - ret.doorOpen = not self.CS.door_all_closed - ret.seatbeltUnlatched = not self.CS.seatbelt self.low_speed_alert = (ret.vEgo < self.CP.minSteerSpeed) - ret.genericToggle = self.CS.generic_toggle - #ret.lkasCounter = self.CS.lkas_counter - #ret.lkasCarModel = self.CS.lkas_car_model - # events events = [] if not (ret.gearShifter in (GearShifter.drive, GearShifter.low)): @@ -195,7 +152,7 @@ class CarInterface(CarInterfaceBase): events.append(create_event('seatbeltNotLatched', [ET.NO_ENTRY, ET.SOFT_DISABLE])) if self.CS.esp_disabled: events.append(create_event('espDisabled', [ET.NO_ENTRY, ET.SOFT_DISABLE])) - if not self.CS.main_on: + if not ret.cruiseState.available: events.append(create_event('wrongCarMode', [ET.NO_ENTRY, ET.USER_DISABLE])) if ret.gearShifter == GearShifter.reverse: events.append(create_event('reverseGear', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE])) @@ -220,8 +177,13 @@ class CarInterface(CarInterfaceBase): self.gas_pressed_prev = ret.gasPressed self.brake_pressed_prev = ret.brakePressed self.cruise_enabled_prev = ret.cruiseState.enabled + self.left_blinker_prev = ret.leftBlinker + self.right_blinker_prev = ret.rightBlinker - return ret.as_reader() + # copy back carState packet to CS + self.CS.out = ret.as_reader() + + return self.CS.out # pass in a car.CarControl # to be called @ 100hz diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 68af182d4..1e9cdcc5f 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -53,7 +53,10 @@ FINGERPRINTS = { # Based on 3c7ce223e3571b54|2019-05-11--20-16-14 { 168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 520: 8, 528: 8, 532: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 653: 8, 654: 8, 655: 8, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 799: 8, 800: 8, 804: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 1562: 8, 1570: 8 - } + }, + # Based on "d26bf42deb1910e7|2020-02-13--16-22-31" + {168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 520: 8, 528: 8, 532: 8, 542: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 653: 8, 654: 8, 655: 8, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 799: 8, 800: 8, 804: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 1262: 8, 1284: 8, 1568: 8, 1902: 8, 2015: 8, 2016: 8, 2018: 8, 2023: 8, 2024: 8, 2026: 8, 2031: 8 + }, ], CAR.PACIFICA_2020_HYBRID: [ {168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 515: 7, 516: 7, 517: 7, 518: 7, 520: 8, 524: 8, 526: 6, 528: 8, 532: 8, 542: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 650: 8, 653: 8, 654: 8, 655: 8, 656: 4, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 683: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 738: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 799: 8, 800: 8, 804: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 2015: 8, 2016: 8, 2024: 8}, diff --git a/selfdrive/car/fingerprints.py b/selfdrive/car/fingerprints.py index 4ca9fa093..c2c39f687 100644 --- a/selfdrive/car/fingerprints.py +++ b/selfdrive/car/fingerprints.py @@ -1,11 +1,12 @@ import os from common.basedir import BASEDIR -def get_attr_from_cars(attr): + +def get_attr_from_cars(attr, result=dict): # read all the folders in selfdrive/car and return a dict where: # - keys are all the car models # - values are attr values from all car folders - result = {} + result = result() for car_folder in [x[0] for x in os.walk(BASEDIR + '/selfdrive/car')]: try: @@ -16,8 +17,11 @@ def get_attr_from_cars(attr): else: continue - for f, v in attr_values.items(): - result[f] = v + if isinstance(attr_values, dict): + for f, v in attr_values.items(): + result[f] = v + elif isinstance(attr_values, list): + result += attr_values except (ImportError, IOError): pass @@ -25,20 +29,9 @@ def get_attr_from_cars(attr): return result -def get_fw_versions_list(): - return get_attr_from_cars('FW_VERSIONS') - - -def get_fingerprint_list(): - # read all the folders in selfdrive/car and return a dict where: - # - keys are all the car models for which we have a fingerprint - # - values are lists dicts of messages that constitute the unique - # CAN fingerprint of each car model and all its variants - return get_attr_from_cars('FINGERPRINTS') - - -FW_VERSIONS = get_fw_versions_list() -_FINGERPRINTS = get_fingerprint_list() +FW_VERSIONS = get_attr_from_cars('FW_VERSIONS') +_FINGERPRINTS = get_attr_from_cars('FINGERPRINTS') +IGNORED_FINGERPRINTS = get_attr_from_cars('IGNORED_FINGERPRINTS', list) _DEBUG_ADDRESS = {1880: 8} # reserved for debug purposes @@ -61,6 +54,9 @@ def eliminate_incompatible_cars(msg, candidate_cars): compatible_cars = [] for car_name in candidate_cars: + if car_name in IGNORED_FINGERPRINTS: + continue + car_fingerprints = _FINGERPRINTS[car_name] for fingerprint in car_fingerprints: diff --git a/selfdrive/car/ford/carcontroller.py b/selfdrive/car/ford/carcontroller.py index db574f38a..d708c1358 100644 --- a/selfdrive/car/ford/carcontroller.py +++ b/selfdrive/car/ford/carcontroller.py @@ -33,27 +33,27 @@ class CarController(): if (frame % 3) == 0: - curvature = self.vehicle_model.calc_curvature(actuators.steerAngle*3.1415/180., CS.v_ego) + curvature = self.vehicle_model.calc_curvature(actuators.steerAngle*3.1415/180., CS.out.vEgo) # The use of the toggle below is handy for trying out the various LKAS modes if TOGGLE_DEBUG: - self.lkas_action += int(CS.generic_toggle and not self.generic_toggle_last) + self.lkas_action += int(CS.out.genericToggle and not self.generic_toggle_last) self.lkas_action &= 0xf else: self.lkas_action = 5 # 4 and 5 seem the best. 8 and 9 seem to aggressive and laggy can_sends.append(create_steer_command(self.packer, apply_steer, enabled, - CS.lkas_state, CS.angle_steers, curvature, self.lkas_action)) - self.generic_toggle_last = CS.generic_toggle + CS.lkas_state, CS.out.steeringAngle, curvature, self.lkas_action)) + self.generic_toggle_last = CS.out.genericToggle if (frame % 100) == 0: can_sends.append(make_can_msg(973, b'\x00\x00\x00\x00\x00\x00\x00\x00', 0)) #can_sends.append(make_can_msg(984, b'\x00\x00\x00\x00\x80\x45\x60\x30', 0)) - if (frame % 100) == 0 or (self.enabled_last != enabled) or (self.main_on_last != CS.main_on) or \ + if (frame % 100) == 0 or (self.enabled_last != enabled) or (self.main_on_last != CS.out.cruiseState.available) or \ (self.steer_alert_last != steer_alert): - can_sends.append(create_lkas_ui(self.packer, CS.main_on, enabled, steer_alert)) + can_sends.append(create_lkas_ui(self.packer, CS.out.cruiseState.available, enabled, steer_alert)) if (frame % 200) == 0: can_sends.append(make_can_msg(1875, b'\x80\xb0\x55\x55\x78\x90\x00\x00', 1)) @@ -81,7 +81,7 @@ class CarController(): can_sends.append(make_can_msg(addr, (cnt<<4).to_bytes(1, 'little') + b'\x00\x00\x00\x00\x00\x00\x00', 1)) self.enabled_last = enabled - self.main_on_last = CS.main_on + self.main_on_last = CS.out.cruiseState.available self.steer_alert_last = steer_alert return can_sends diff --git a/selfdrive/car/ford/carstate.py b/selfdrive/car/ford/carstate.py index 7484e064c..ec461ab16 100644 --- a/selfdrive/car/ford/carstate.py +++ b/selfdrive/car/ford/carstate.py @@ -1,8 +1,9 @@ +from cereal import car from opendbc.can.parser import CANParser from common.numpy_fast import mean from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from selfdrive.car.ford.values import DBC -from common.kalman.simple_kalman import KF1D WHEEL_RADIUS = 0.33 @@ -32,57 +33,28 @@ def get_can_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 0) -class CarState(): - def __init__(self, CP): - - self.CP = CP - self.left_blinker_on = 0 - self.right_blinker_on = 0 - - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - +class CarState(CarStateBase): def update(self, cp): - # update prevs, update must run once per loop - self.prev_left_blinker_on = self.left_blinker_on - self.prev_right_blinker_on = self.right_blinker_on - - # calc best v_ego estimate, by averaging two opposite corners - self.v_wheel_fl = cp.vl["WheelSpeed_CG1"]['WhlRr_W_Meas'] * WHEEL_RADIUS - self.v_wheel_fr = cp.vl["WheelSpeed_CG1"]['WhlRl_W_Meas'] * WHEEL_RADIUS - self.v_wheel_rl = cp.vl["WheelSpeed_CG1"]['WhlFr_W_Meas'] * WHEEL_RADIUS - self.v_wheel_rr = cp.vl["WheelSpeed_CG1"]['WhlFl_W_Meas'] * WHEEL_RADIUS - v_wheel = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) - - # Kalman filter - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - self.standstill = not v_wheel > 0.001 - - self.angle_steers = cp.vl["Steering_Wheel_Data_CG1"]['SteWhlRelInit_An_Sns'] - self.v_cruise_pcm = cp.vl["Cruise_Status"]['Set_Speed'] * CV.MPH_TO_MS - self.pcm_acc_status = cp.vl["Cruise_Status"]['Cruise_State'] - self.main_on = cp.vl["Cruise_Status"]['Cruise_State'] != 0 - self.lkas_state = cp.vl["Lane_Keep_Assist_Status"]['LaActAvail_D_Actl'] + ret = car.CarState.new_message() + ret.wheelSpeeds.rr = cp.vl["WheelSpeed_CG1"]['WhlRr_W_Meas'] * WHEEL_RADIUS + ret.wheelSpeeds.rl = cp.vl["WheelSpeed_CG1"]['WhlRl_W_Meas'] * WHEEL_RADIUS + ret.wheelSpeeds.fr = cp.vl["WheelSpeed_CG1"]['WhlFr_W_Meas'] * WHEEL_RADIUS + ret.wheelSpeeds.fl = cp.vl["WheelSpeed_CG1"]['WhlFl_W_Meas'] * WHEEL_RADIUS + ret.vEgoRaw = mean([ret.wheelSpeeds.rr, ret.wheelSpeeds.rl, ret.wheelSpeeds.fr, ret.wheelSpeeds.fl]) + ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) + ret.standstill = not ret.vEgoRaw > 0.001 + ret.steeringAngle = cp.vl["Steering_Wheel_Data_CG1"]['SteWhlRelInit_An_Sns'] + ret.steeringPressed = not cp.vl["Lane_Keep_Assist_Status"]['LaHandsOff_B_Actl'] + ret.cruiseState.speed = cp.vl["Cruise_Status"]['Set_Speed'] * CV.MPH_TO_MS + ret.cruiseState.enabled = not (cp.vl["Cruise_Status"]['Cruise_State'] in [0, 3]) + ret.cruiseState.available = cp.vl["Cruise_Status"]['Cruise_State'] != 0 + ret.gas = cp.vl["EngineData_14"]['ApedPosScal_Pc_Actl'] / 100. + ret.gasPressed = ret.gas > 1e-6 + ret.brakePressed = bool(cp.vl["Cruise_Status"]["Brake_Drv_Appl"]) + ret.brakeLights = bool(cp.vl["BCM_to_HS_Body"]["Brake_Lights"]) + ret.genericToggle = bool(cp.vl["Steering_Buttons"]["Dist_Incr"]) # TODO: we also need raw driver torque, needed for Assisted Lane Change - self.steer_override = not cp.vl["Lane_Keep_Assist_Status"]['LaHandsOff_B_Actl'] + self.lkas_state = cp.vl["Lane_Keep_Assist_Status"]['LaActAvail_D_Actl'] self.steer_error = cp.vl["Lane_Keep_Assist_Status"]['LaActDeny_B_Actl'] - self.user_gas = cp.vl["EngineData_14"]['ApedPosScal_Pc_Actl'] - self.brake_pressed = bool(cp.vl["Cruise_Status"]["Brake_Drv_Appl"]) - self.brake_lights = bool(cp.vl["BCM_to_HS_Body"]["Brake_Lights"]) - self.generic_toggle = bool(cp.vl["Steering_Buttons"]["Dist_Incr"]) + + return ret diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index d9fad4bb5..6b1f9340f 100755 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -106,38 +106,10 @@ class CarInterface(CarInterfaceBase): # ******************* do can recv ******************* self.cp.update_strings(can_strings) - self.CS.update(self.cp) - - # create message - ret = car.CarState.new_message() + ret = self.CS.update(self.cp) ret.canValid = self.cp.can_valid - # speeds - ret.vEgo = self.CS.v_ego - ret.vEgoRaw = self.CS.v_ego_raw - ret.standstill = self.CS.standstill - ret.wheelSpeeds.fl = self.CS.v_wheel_fl - ret.wheelSpeeds.fr = self.CS.v_wheel_fr - ret.wheelSpeeds.rl = self.CS.v_wheel_rl - ret.wheelSpeeds.rr = self.CS.v_wheel_rr - - # steering wheel - ret.steeringAngle = self.CS.angle_steers - ret.steeringPressed = self.CS.steer_override - - # gas pedal - ret.gas = self.CS.user_gas / 100. - ret.gasPressed = self.CS.user_gas > 0.0001 - ret.brakePressed = self.CS.brake_pressed - ret.brakeLights = self.CS.brake_lights - - ret.cruiseState.enabled = not (self.CS.pcm_acc_status in [0, 3]) - ret.cruiseState.speed = self.CS.v_cruise_pcm - ret.cruiseState.available = self.CS.pcm_acc_status != 0 - - ret.genericToggle = self.CS.generic_toggle - # events events = [] @@ -167,7 +139,9 @@ class CarInterface(CarInterfaceBase): self.brake_pressed_prev = ret.brakePressed self.cruise_enabled_prev = ret.cruiseState.enabled - return ret.as_reader() + self.CS.out = ret.as_reader() + + return self.CS.out # pass in a car.CarControl # to be called @ 100hz diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 5a3dacf32..96b7756c2 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -123,9 +123,12 @@ def get_fw_versions(logcan, sendcan, bus, extra=None, timeout=0.1, debug=False, ecu_types[a] = ecu_type if sub_addr is None: - parallel_addrs.append(a) + if a not in parallel_addrs: + parallel_addrs.append(a) else: - addrs.append([a]) + if [a] not in addrs: + addrs.append([a]) + addrs.insert(0, parallel_addrs) fw_versions = {} diff --git a/selfdrive/car/gm/carstate.py b/selfdrive/car/gm/carstate.py index 8a083cb53..f924c66f1 100644 --- a/selfdrive/car/gm/carstate.py +++ b/selfdrive/car/gm/carstate.py @@ -1,9 +1,10 @@ from cereal import car from common.numpy_fast import mean -from common.kalman.simple_kalman import KF1D from selfdrive.config import Conversions as CV +from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser -from selfdrive.car.gm.values import DBC, CAR, parse_gear_shifter, \ +from selfdrive.car.interfaces import CarStateBase +from selfdrive.car.gm.values import DBC, CAR, \ CruiseButtons, is_eps_status_ok, \ STEER_THRESHOLD, SUPERCRUISE_CARS @@ -50,25 +51,11 @@ def get_powertrain_can_parser(CP, canbus): return CANParser(DBC[CP.carFingerprint]['pt'], signals, [], canbus.powertrain) -class CarState(): - def __init__(self, CP, canbus): - self.CP = CP - # initialize can parser - - self.car_fingerprint = CP.carFingerprint - self.cruise_buttons = CruiseButtons.UNPRESS - self.left_blinker_on = False - self.prev_left_blinker_on = False - self.right_blinker_on = False - self.prev_right_blinker_on = False - - # vEgo kalman filter - dt = 0.01 - self.v_ego_kf = KF1D(x0=[[0.], [0.]], - A=[[1., dt], [0., 1.]], - C=[1., 0.], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0. +class CarState(CarStateBase): + def __init__(self, CP): + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["ECMPRDNL"]["PRNDL"] def update(self, pt_cp): self.prev_cruise_buttons = self.cruise_buttons @@ -78,20 +65,12 @@ class CarState(): self.v_wheel_fr = pt_cp.vl["EBCMWheelSpdFront"]['FRWheelSpd'] * CV.KPH_TO_MS self.v_wheel_rl = pt_cp.vl["EBCMWheelSpdRear"]['RLWheelSpd'] * CV.KPH_TO_MS self.v_wheel_rr = pt_cp.vl["EBCMWheelSpdRear"]['RRWheelSpd'] * CV.KPH_TO_MS - v_wheel = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) - - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - + self.v_ego_raw = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) self.standstill = self.v_ego_raw < 0.01 self.angle_steers = pt_cp.vl["PSCMSteeringAngle"]['SteeringWheelAngle'] - self.gear_shifter = parse_gear_shifter(pt_cp.vl["ECMPRDNL"]['PRNDL']) + self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(pt_cp.vl["ECMPRDNL"]['PRNDL'], None)) self.user_brake = pt_cp.vl["EBCMBrakePedalPosition"]['BrakePedalPosition'] self.pedal_gas = pt_cp.vl["AcceleratorPedal"]['AcceleratorPedal'] diff --git a/selfdrive/car/gm/gmcan.py b/selfdrive/car/gm/gmcan.py index 68b129e74..3db0f5b02 100644 --- a/selfdrive/car/gm/gmcan.py +++ b/selfdrive/car/gm/gmcan.py @@ -67,8 +67,8 @@ def create_friction_brake_command(packer, bus, apply_brake, idx, near_stop, at_f else: mode = 0xa - if at_full_stop: - mode = 0xd + if at_full_stop: + mode = 0xd # TODO: this is to have GM bringing the car to complete stop, # but currently it conflicts with OP controls, so turned off. #elif near_stop: diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 93ee18ff2..450fce853 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -29,7 +29,7 @@ class CarInterface(CarInterfaceBase): # *** init the major players *** canbus = CanBus() - self.CS = CarState(CP, canbus) + self.CS = CarState(CP) self.VM = VehicleModel(CP) self.pt_cp = get_powertrain_can_parser(CP, canbus) self.ch_cp_dbc_name = DBC[CP.carFingerprint]['chassis'] @@ -64,6 +64,13 @@ class CarInterface(CarInterfaceBase): ret.openpilotLongitudinalControl = ret.enableCamera tire_stiffness_factor = 0.444 # not optimized yet + # Start with a baseline lateral tuning for all GM vehicles. Override tuning as needed in each model section below. + ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]] + ret.lateralTuning.pid.kf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594 + ret.steerRateCost = 1.0 + ret.steerActuatorDelay = 0.1 # Default delay, not measured yet + if candidate == CAR.VOLT: # supports stop and go, but initial engage must be above 18mph (which include conservatism) ret.minEnableSpeed = 18 * CV.MPH_TO_MS @@ -141,11 +148,6 @@ class CarInterface(CarInterfaceBase): ret.tireStiffnessFront, ret.tireStiffnessRear = scale_tire_stiffness(ret.mass, ret.wheelbase, ret.centerToFront, tire_stiffness_factor=tire_stiffness_factor) - # same tuning for Volt and CT6 for now - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]] - ret.lateralTuning.pid.kf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594 - ret.steerMaxBP = [0.] # m/s ret.steerMaxV = [1.] ret.gasMaxBP = [0.] @@ -163,8 +165,6 @@ class CarInterface(CarInterfaceBase): ret.stoppingControl = True ret.startAccel = 0.8 - ret.steerActuatorDelay = 0.1 # Default delay, not measured yet - ret.steerRateCost = 1.0 ret.steerLimitTimer = 0.4 ret.radarTimeStep = 0.0667 # GM radar runs at 15Hz instead of standard 20Hz ret.steerControlType = car.CarParams.SteerControlType.torque @@ -237,7 +237,7 @@ class CarInterface(CarInterfaceBase): be.pressed = self.CS.right_blinker_on buttonEvents.append(be) - if self.CS.cruise_buttons != self.CS.prev_cruise_buttons: + if self.CS.cruise_buttons != self.CS.prev_cruise_buttons and self.CS.prev_cruise_buttons != CruiseButtons.INIT: be = car.CarState.ButtonEvent.new_message() be.type = ButtonType.unknown if self.CS.cruise_buttons != CruiseButtons.UNPRESS: diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 468deb01d..ccde62ea3 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -14,6 +14,7 @@ class CAR: SUPERCRUISE_CARS = [CAR.CADILLAC_CT6] class CruiseButtons: + INIT = 0 UNPRESS = 1 RES_ACCEL = 2 DECEL_SET = 3 @@ -34,18 +35,6 @@ def is_eps_status_ok(eps_status, car_fingerprint): valid_eps_status += [0, 1] return eps_status in valid_eps_status -def parse_gear_shifter(can_gear): - if can_gear == 0: - return car.CarState.GearShifter.park - elif can_gear == 1: - return car.CarState.GearShifter.neutral - elif can_gear == 2: - return car.CarState.GearShifter.drive - elif can_gear == 3: - return car.CarState.GearShifter.reverse - else: - return car.CarState.GearShifter.unknown - FINGERPRINTS = { # Astra BK MY17, ASCM unplugged CAR.HOLDEN_ASTRA: [{ diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py index 52bea3d7d..f7b8d5779 100644 --- a/selfdrive/car/honda/carstate.py +++ b/selfdrive/car/honda/carstate.py @@ -1,19 +1,11 @@ -from cereal import car from collections import defaultdict from common.numpy_fast import interp -from common.kalman.simple_kalman import KF1D from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, SPEED_FACTOR, HONDA_BOSCH -GearShifter = car.CarState.GearShifter - -def parse_gear_shifter(gear): - return {'P': GearShifter.park, 'R': GearShifter.reverse, 'N': GearShifter.neutral, - 'D': GearShifter.drive, 'S': GearShifter.sport, 'L': GearShifter.low}.get(gear, GearShifter.unknown) - - def calc_cruise_offset(offset, speed): # euristic formula so that speed is controlled to ~ 0.3m/s below pid_speed # constraints to solve for _K0, _K1, _K2 are: @@ -188,38 +180,21 @@ def get_cam_can_parser(CP): bus_cam = 1 if CP.carFingerprint in HONDA_BOSCH and not CP.isPandaBlack else 2 return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, bus_cam) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): - self.CP = CP - self.can_define = CANDefine(DBC[CP.carFingerprint]['pt']) - self.shifter_values = self.can_define.dv["GEARBOX"]["GEAR_SHIFTER"] - self.steer_status_values = defaultdict(lambda: "UNKNOWN", self.can_define.dv["STEER_STATUS"]["STEER_STATUS"]) + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["GEARBOX"]["GEAR_SHIFTER"] + self.steer_status_values = defaultdict(lambda: "UNKNOWN", can_define.dv["STEER_STATUS"]["STEER_STATUS"]) self.user_gas, self.user_gas_pressed = 0., 0 self.brake_switch_prev = 0 self.brake_switch_ts = 0 - - self.cruise_buttons = 0 self.cruise_setting = 0 self.v_cruise_pcm_prev = 0 - self.blinker_on = 0 - - self.left_blinker_on = 0 - self.right_blinker_on = 0 - self.cruise_mode = 0 self.stopped = 0 - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - def update(self, cp, cp_cam): # car params @@ -229,8 +204,6 @@ class CarState(): # update prevs, update must run once per loop self.prev_cruise_buttons = self.cruise_buttons self.prev_cruise_setting = self.cruise_setting - self.prev_blinker_on = self.blinker_on - self.prev_left_blinker_on = self.left_blinker_on self.prev_right_blinker_on = self.right_blinker_on @@ -260,7 +233,6 @@ class CarState(): self.brake_error = cp.vl["STANDSTILL"]['BRAKE_ERROR_1'] or cp.vl["STANDSTILL"]['BRAKE_ERROR_2'] self.esp_disabled = cp.vl["VSA_STATUS"]['ESP_DISABLED'] - # calc best v_ego estimate, by averaging two opposite corners speed_factor = SPEED_FACTOR[self.CP.carFingerprint] self.v_wheel_fl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FL'] * CV.KPH_TO_MS * speed_factor self.v_wheel_fr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FR'] * CV.KPH_TO_MS * speed_factor @@ -270,16 +242,10 @@ class CarState(): # blend in transmission speed at low speed, since it has more low speed accuracy self.v_weight = interp(v_wheel, v_weight_bp, v_weight_v) - speed = (1. - self.v_weight) * cp.vl["ENGINE_DATA"]['XMISSION_SPEED'] * CV.KPH_TO_MS * speed_factor + \ + self.v_ego_raw = (1. - self.v_weight) * cp.vl["ENGINE_DATA"]['XMISSION_SPEED'] * CV.KPH_TO_MS * speed_factor + \ self.v_weight * v_wheel - if abs(speed - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[speed], [0.0]] - - self.v_ego_raw = speed - v_ego_x = self.v_ego_kf.update(speed) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) # this is a hack for the interceptor. This is now only used in the simulation # TODO: Replace tests by toyota so this can go away @@ -310,7 +276,7 @@ class CarState(): self.main_on = cp.vl["SCM_BUTTONS"]['MAIN_ON'] can_gear_shifter = int(cp.vl["GEARBOX"]['GEAR_SHIFTER']) - self.gear_shifter = parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) + self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) self.pedal_gas = cp.vl["POWERTRAIN_DATA"]['PEDAL_GAS'] # crv doesn't include cruise control diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 124e44ccd..7e273e847 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -132,11 +132,12 @@ FW_VERSIONS = { b'37805-6A0-A640\x00\x00', b'37805-6B2-A550\x00\x00', b'37805-6B2-A650\x00\x00', + b'37805-6B2-A660\x00\x00', b'37805-6B2-M520\x00\x00', ], (Ecu.unknown, 0x18da0bf1, None): [b'54008-TVC-A910\x00\x00'], (Ecu.unknown, 0x18da1ef1, None): [b'28102-6B8-A560\x00\x00', b'28102-6B8-M520\x00\x00'], - (Ecu.unknown, 0x18da2bf1, None): [b'46114-TVA-A060\x00\x00'], + (Ecu.unknown, 0x18da2bf1, None): [b'46114-TVA-A060\x00\x00', b'46114-TVA-A080\x00\x00'], (Ecu.unknown, 0x18da28f1, None): [b'57114-TVA-C050\x00\x00'], (Ecu.eps, 0x18da30f1, None): [ b'39990-TVA-A150\x00\x00', @@ -153,7 +154,11 @@ FW_VERSIONS = { b'78109-TVC-M510\x00\x00', ], (Ecu.unknown, 0x18da61f1, None): [b'78209-TVA-A010\x00\x00'], - (Ecu.unknown, 0x18dab0f1, None): [b'36802-TVA-A160\x00\x00', b'36802-TWA-A070\x00\x00'], + (Ecu.unknown, 0x18dab0f1, None): [ + b'36802-TVA-A160\x00\x00', + b'36802-TVA-A170\x00\x00', + b'36802-TWA-A070\x00\x00', + ], (Ecu.unknown, 0x18dab5f1, None): [b'36161-TVA-A060\x00\x00', b'36161-TWA-A070\x00\x00'], (Ecu.unknown, 0x18daeff1, None): [b'38897-TVA-A010\x00\x00'], }, @@ -224,6 +229,7 @@ FW_VERSIONS = { b'37805-5AA-A670\x00\x00', b'37805-5AA-A680\x00\x00', b'37805-5AA-A810\x00\x00', + b'37805-5AA-C820\x00\x00', b'37805-5AA-L660\x00\x00', b'37805-5AJ-A610\x00\x00', b'37805-5BA-A510\x00\x00', @@ -236,6 +242,7 @@ FW_VERSIONS = { b'28101-5CG-A070\x00\x00', b'28101-5CG-A080\x00\x00', b'28101-5CG-A810\x00\x00', + b'28101-5CG-A820\x00\x00', b'28101-5DJ-A040\x00\x00', b'28101-5DJ-A060\x00\x00', b'28101-5DJ-A510\x00\x00', @@ -262,6 +269,7 @@ FW_VERSIONS = { b'78109-TBC-A510\x00\x00', b'78109-TBC-A520\x00\x00', b'78109-TBC-A530\x00\x00', + b'78109-TBC-C530\x00\x00', b'78109-TBH-A530\x00\x00', b'78109-TEG-A310\x00\x00', ], @@ -272,7 +280,6 @@ FW_VERSIONS = { b'36161-TEG-A010\x00\x00', ], (Ecu.unknown, 0x18daeff1, None): [ - b'36161-TBA-A030\x00\x00', b'38897-TBA-A010\x00\x00', b'38897-TBA-A020\x00\x00', ], @@ -285,11 +292,15 @@ FW_VERSIONS = { b'37805-5AN-A830\x00\x00', b'37805-5AN-A930\x00\x00', b'37805-5AN-L940\x00\x00', + b'37805-5AN-LH20\x00\x00', + b'37805-5AN-LJ20\x00\x00', b'37805-5AZ-E850\x00\x00', b'37805-5BB-L640\x00\x00', ], (Ecu.unknown, 0x18da1ef1, None): [ b'28101-5CG-A920\x00\x00', + b'28101-5CG-C220\x00\x00', + b'28101-5CG-C320\x00\x00', b'28101-5CK-A130\x00\x00', b'28101-5CK-A140\x00\x00', b'28101-5CK-A150\x00\x00', @@ -301,6 +312,7 @@ FW_VERSIONS = { b'57114-TBG-A340\x00\x00', b'57114-TGG-A340\x00\x00', b'57114-TGL-G330\x00\x00', + b'57114-TGG-C320\x00\x00', ], (Ecu.eps, 0x18da30f1, None): [ b'39990-TBA-C020\x00\x00', @@ -308,31 +320,41 @@ FW_VERSIONS = { b'39990-TGG-A020\x00\x00', b'39990-TGG-A120\x00\x00', b'39990-TGL-E130\x00\x00', + b'39990-TGG-A020\x00\x00', ], (Ecu.unknown, 0x18da53f1, None): [ b'77959-TBA-A060\x00\x00', b'77959-TGG-A020\x00\x00', b'77959-TGG-G010\x00\x00', + b'77959-TGG-A020\x00\x00', ], (Ecu.unknown, 0x18da60f1, None): [ b'78109-TBA-A910\x00\x00', b'78109-TBC-A740\x00\x00', b'78109-TGG-A210\x00\x00', b'78109-TGG-A310\x00\x00', + b'78109-TGG-A320\x00\x00', b'78109-TGG-A810\x00\x00', + b'78109-TGG-A820\x00\x00', b'78109-TGL-G120\x00\x00', ], (Ecu.unknown, 0x18dab0f1, None): [ b'36802-TBA-A150\x00\x00', b'36802-TGG-A050\x00\x00', b'36802-TGL-G040\x00\x00', + b'36802-TGG-A060\x00\x00', ], (Ecu.unknown, 0x18dab5f1, None): [ b'36161-TBA-A130\x00\x00', b'36161-TGG-A060\x00\x00', b'36161-TGL-G050\x00\x00', + b'36161-TGG-A080\x00\x00', + ], + (Ecu.unknown, 0x18daeff1, None): [ + b'38897-TBA-A110\x00\x00', + b'38897-TBA-A020\x00\x00', + b'38897-TBA-A020\x00\x00', ], - (Ecu.unknown, 0x18daeff1, None): [b'38897-TBA-A110\x00\x00', b'38897-TBA-A020\x00\x00'], }, CAR.CRV_5G: { (Ecu.unknown, 0x18da10f1, None): [ @@ -344,6 +366,7 @@ FW_VERSIONS = { b'37805-5PA-A680\x00\x00', b'37805-5PA-A850\x00\x00', b'37805-5PA-A870\x00\x00', + b'37805-5PA-A880\x00\x00', b'37805-5PA-A890\x00\x00', ], (Ecu.unknown, 0x18da1ef1, None): [ @@ -422,6 +445,7 @@ FW_VERSIONS = { b'28102-5MX-A610\x00\x00', b'28102-5MX-A710\x00\x00', b'28102-5MX-A910\x00\x00', + b'28102-5MX-C001\x00\x00', b'28103-5NZ-A300\x00\x00', ], (Ecu.unknown, 0x18da28f1, None): [b'57114-THR-A040\x00\x00', b'57114-THR-A110\x00\x00'], @@ -436,6 +460,7 @@ FW_VERSIONS = { b'78109-THR-AE40\x00\x00', b'78109-THR-AL10\x00\x00', b'78109-THR-C330\x00\x00', + b'78109-THR-CE20\x00\x00', ], (Ecu.unknown, 0x18da0bf1, None): [b'54008-THR-A020\x00\x00'], }, diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index 7ebb8d70d..5305396f3 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -1,8 +1,8 @@ from cereal import car from selfdrive.car.hyundai.values import DBC, STEER_THRESHOLD +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from selfdrive.config import Conversions as CV -from common.kalman.simple_kalman import KF1D GearShifter = car.CarState.GearShifter @@ -124,27 +124,7 @@ def get_camera_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): - def __init__(self, CP): - - self.CP = CP - - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - self.left_blinker_on = 0 - self.left_blinker_flash = 0 - self.right_blinker_on = 0 - self.right_blinker_flash = 0 +class CarState(CarStateBase): def update(self, cp, cp_cam): # update prevs, update must run once per Loop @@ -162,36 +142,26 @@ class CarState(): self.acc_active = cp.vl["SCC12"]['ACCMode'] != 0 self.pcm_acc_status = int(self.acc_active) - # calc best v_ego estimate, by averaging two opposite corners self.v_wheel_fl = cp.vl["WHL_SPD11"]['WHL_SPD_FL'] * CV.KPH_TO_MS self.v_wheel_fr = cp.vl["WHL_SPD11"]['WHL_SPD_FR'] * CV.KPH_TO_MS self.v_wheel_rl = cp.vl["WHL_SPD11"]['WHL_SPD_RL'] * CV.KPH_TO_MS self.v_wheel_rr = cp.vl["WHL_SPD11"]['WHL_SPD_RR'] * CV.KPH_TO_MS - v_wheel = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. + self.v_ego_raw = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) - self.low_speed_lockout = v_wheel < 1.0 + self.low_speed_lockout = self.v_ego_raw < 1.0 - # Kalman filter, even though Hyundai raw wheel speed is heaviliy filtered by default - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) is_set_speed_in_mph = int(cp.vl["CLU11"]["CF_Clu_SPEED_UNIT"]) speed_conv = CV.MPH_TO_MS if is_set_speed_in_mph else CV.KPH_TO_MS self.cruise_set_speed = cp.vl["SCC11"]['VSetDis'] * speed_conv - self.standstill = not v_wheel > 0.1 + self.standstill = not self.v_ego_raw > 0.1 self.angle_steers = cp.vl["SAS11"]['SAS_Angle'] self.angle_steers_rate = cp.vl["SAS11"]['SAS_Speed'] self.yaw_rate = cp.vl["ESP12"]['YAW_RATE'] self.main_on = True self.left_blinker_on = cp.vl["CGW1"]['CF_Gway_TSigLHSw'] - self.left_blinker_flash = cp.vl["CGW1"]['CF_Gway_TurnSigLh'] self.right_blinker_on = cp.vl["CGW1"]['CF_Gway_TSigRHSw'] - self.right_blinker_flash = cp.vl["CGW1"]['CF_Gway_TurnSigRh'] self.steer_override = abs(cp.vl["MDPS11"]['CR_Mdps_DrvTq']) > STEER_THRESHOLD self.steer_state = cp.vl["MDPS12"]['CF_Mdps_ToiActive'] #0 NOT ACTIVE, 1 ACTIVE self.steer_error = cp.vl["MDPS12"]['CF_Mdps_ToiUnavail'] diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index ca1972a16..e98b2681b 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -1,8 +1,12 @@ import os import time from cereal import car +from common.kalman.simple_kalman import KF1D +from common.realtime import DT_CTRL from selfdrive.car import gen_empty_fingerprint +GearShifter = car.CarState.GearShifter + # generic car and radar interfaces class CarInterfaceBase(): @@ -42,3 +46,31 @@ class RadarInterfaceBase(): time.sleep(self.radar_ts) # radard runs on RI updates return ret + +class CarStateBase: + def __init__(self, CP): + self.CP = CP + self.car_fingerprint = CP.carFingerprint + self.left_blinker_on = 0 + self.right_blinker_on = 0 + self.cruise_buttons = 0 + + # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) + # R = 1e3 + self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], + A=[[1.0, DT_CTRL], [0.0, 1.0]], + C=[1.0, 0.0], + K=[[0.12287673], [0.29666309]]) + + def update_speed_kf(self, v_ego_raw): + if abs(v_ego_raw - self.v_ego_kf.x[0][0]) > 2.0: # Prevent large accelerations when car starts at non zero speed + self.v_ego_kf.x = [[v_ego_raw], [0.0]] + + v_ego_x = self.v_ego_kf.update(v_ego_raw) + return float(v_ego_x[0]), float(v_ego_x[1]) + + @staticmethod + def parse_gear_shifter(gear): + return {'P': GearShifter.park, 'R': GearShifter.reverse, 'N': GearShifter.neutral, + 'E': GearShifter.eco, 'T': GearShifter.manumatic, 'D': GearShifter.drive, + 'S': GearShifter.sport, 'L': GearShifter.low, 'B': GearShifter.brake}.get(gear, GearShifter.unknown) diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index 69f573fef..dce4df810 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -53,7 +53,7 @@ class CarController(): apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.steer_torque_driver, P) self.steer_rate_limited = new_steer != apply_steer - lkas_enabled = enabled and not CS.steer_not_allowed + lkas_enabled = enabled if not lkas_enabled: apply_steer = 0 diff --git a/selfdrive/car/subaru/carstate.py b/selfdrive/car/subaru/carstate.py index 3f0447432..10500ce6f 100644 --- a/selfdrive/car/subaru/carstate.py +++ b/selfdrive/car/subaru/carstate.py @@ -1,6 +1,6 @@ import copy -from common.kalman.simple_kalman import KF1D from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from selfdrive.car.subaru.values import DBC, STEER_THRESHOLD @@ -82,29 +82,12 @@ def get_camera_can_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): + super().__init__(CP) # initialize can parser - self.CP = CP - - self.car_fingerprint = CP.carFingerprint - self.left_blinker_on = False self.left_blinker_cnt = 0 - self.prev_left_blinker_on = False - self.right_blinker_on = False self.right_blinker_cnt = 0 - self.prev_right_blinker_on = False - self.steer_torque_driver = 0 - self.steer_not_allowed = False - self.main_on = False - - # vEgo kalman filter - dt = 0.01 - self.v_ego_kf = KF1D(x0=[[0.], [0.]], - A=[[1., dt], [0., 1.]], - C=[1., 0.], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0. def update(self, cp, cp_cam): @@ -124,16 +107,10 @@ class CarState(): if cp.vl["Dash_State"]['Units'] == 1: self.v_cruise_pcm *= CV.MPH_TO_KPH - v_wheel = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. + self.v_ego_raw = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. # Kalman filter, even though Subaru raw wheel speed is heaviliy filtered by default - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) self.standstill = self.v_ego_raw < 0.01 self.prev_left_blinker_on = self.left_blinker_on diff --git a/selfdrive/car/toyota/carcontroller.py b/selfdrive/car/toyota/carcontroller.py index c2349784b..24c3bfed7 100644 --- a/selfdrive/car/toyota/carcontroller.py +++ b/selfdrive/car/toyota/carcontroller.py @@ -23,12 +23,6 @@ ANGLE_DELTA_BP = [0., 5., 15.] ANGLE_DELTA_V = [5., .8, .15] # windup limit ANGLE_DELTA_VU = [5., 3.5, 0.4] # unwind limit -TARGET_IDS = [0x340, 0x341, 0x342, 0x343, 0x344, 0x345, - 0x363, 0x364, 0x365, 0x370, 0x371, 0x372, - 0x373, 0x374, 0x375, 0x380, 0x381, 0x382, - 0x383] - - def accel_hysteresis(accel, accel_steady, enabled): # for small accel oscillations within ACCEL_HYST_GAP, don't change the accel command diff --git a/selfdrive/car/toyota/carstate.py b/selfdrive/car/toyota/carstate.py index 9ed0fec3f..cb172b591 100644 --- a/selfdrive/car/toyota/carstate.py +++ b/selfdrive/car/toyota/carstate.py @@ -1,17 +1,10 @@ -from cereal import car from common.numpy_fast import mean -from common.kalman.simple_kalman import KF1D from opendbc.can.can_define import CANDefine +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from selfdrive.config import Conversions as CV from selfdrive.car.toyota.values import CAR, DBC, STEER_THRESHOLD, TSS2_CAR, NO_DSU_CAR -GearShifter = car.CarState.GearShifter - -def parse_gear_shifter(gear): - return {'P': GearShifter.park, 'R': GearShifter.reverse, 'N': GearShifter.neutral, - 'D': GearShifter.drive, 'B': GearShifter.brake}.get(gear, GearShifter.unknown) - def get_can_parser(CP): @@ -89,30 +82,14 @@ def get_cam_can_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): - - self.CP = CP - self.can_define = CANDefine(DBC[CP.carFingerprint]['pt']) - self.shifter_values = self.can_define.dv["GEAR_PACKET"]['GEAR'] - self.left_blinker_on = 0 - self.right_blinker_on = 0 + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["GEAR_PACKET"]['GEAR'] self.angle_offset = 0. self.init_angle_offset = False - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - def update(self, cp, cp_cam): # update prevs, update must run once per loop self.prev_left_blinker_on = self.left_blinker_on @@ -127,25 +104,16 @@ class CarState(): self.pedal_gas = (cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS'] + cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS2']) / 2. else: self.pedal_gas = cp.vl["GAS_PEDAL"]['GAS_PEDAL'] - self.car_gas = self.pedal_gas self.esp_disabled = cp.vl["ESP_CONTROL"]['TC_DISABLED'] - # calc best v_ego estimate, by averaging two opposite corners self.v_wheel_fl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FL'] * CV.KPH_TO_MS self.v_wheel_fr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FR'] * CV.KPH_TO_MS self.v_wheel_rl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RL'] * CV.KPH_TO_MS self.v_wheel_rr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RR'] * CV.KPH_TO_MS - v_wheel = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) + self.v_ego_raw = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) - # Kalman filter - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - self.standstill = not v_wheel > 0.001 + self.standstill = not self.v_ego_raw > 0.001 if self.CP.carFingerprint in TSS2_CAR: self.angle_steers = cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] @@ -161,7 +129,7 @@ class CarState(): self.angle_steers = cp.vl["STEER_ANGLE_SENSOR"]['STEER_ANGLE'] + cp.vl["STEER_ANGLE_SENSOR"]['STEER_FRACTION'] self.angle_steers_rate = cp.vl["STEER_ANGLE_SENSOR"]['STEER_RATE'] can_gear = int(cp.vl["GEAR_PACKET"]['GEAR']) - self.gear_shifter = parse_gear_shifter(self.shifter_values.get(can_gear, None)) + self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) if self.CP.carFingerprint == CAR.LEXUS_IS: self.main_on = cp.vl["DSU_CRUISE"]['MAIN_ON'] else: diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index c17782c3f..ff397e442 100755 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -163,6 +163,16 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.1]] ret.lateralTuning.pid.kf = 0.00006 + elif candidate == CAR.HIGHLANDER_TSS2: + stop_and_go = True + ret.safetyParam = 73 + ret.wheelbase = 2.84988 # 112.2 in = 2.84988 m + ret.steerRatio = 16.0 + tire_stiffness_factor = 0.8 + ret.mass = 4700. * CV.LB_TO_KG + STD_CARGO_KG # 4260 + 4-5 people + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.18], [0.015]] # community tuning + ret.lateralTuning.pid.kf = 0.00012 # community tuning + elif candidate in [CAR.HIGHLANDER, CAR.HIGHLANDERH]: stop_and_go = True ret.safetyParam = 73 @@ -192,7 +202,7 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.1]] ret.mass = 3370. * CV.LB_TO_KG + STD_CARGO_KG ret.lateralTuning.pid.kf = 0.00007818594 - + elif candidate == CAR.RAV4H_TSS2: stop_and_go = True ret.safetyParam = 73 @@ -253,6 +263,16 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.05]] ret.lateralTuning.pid.kf = 0.00007 + elif candidate == CAR.LEXUS_NXH: + stop_and_go = True + ret.safetyParam = 73 + ret.wheelbase = 2.66 + ret.steerRatio = 14.7 + tire_stiffness_factor = 0.444 # not optimized yet + ret.mass = 4070 * CV.LB_TO_KG + STD_CARGO_KG + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.1]] + ret.lateralTuning.pid.kf = 0.00006 + ret.steerRateCost = 1. ret.centerToFront = ret.wheelbase * 0.44 @@ -276,11 +296,14 @@ class CarInterface(CarInterfaceBase): ret.brakeMaxV = [1.] ret.enableCamera = is_ecu_disconnected(fingerprint[0], FINGERPRINTS, ECU_FINGERPRINT, candidate, Ecu.fwdCamera) or has_relay + # Detect smartDSU, which intercepts ACC_CMD from the DSU allowing openpilot to send it + smartDsu = 0x2FF in fingerprint[0] # In TSS2 cars the camera does long control ret.enableDsu = is_ecu_disconnected(fingerprint[0], FINGERPRINTS, ECU_FINGERPRINT, candidate, Ecu.dsu) and candidate not in TSS2_CAR ret.enableApgs = False # is_ecu_disconnected(fingerprint[0], FINGERPRINTS, ECU_FINGERPRINT, candidate, Ecu.apgs) ret.enableGasInterceptor = 0x201 in fingerprint[0] - ret.openpilotLongitudinalControl = ret.enableCamera and (ret.enableDsu or candidate in TSS2_CAR) + # if the smartDSU is detected, openpilot can send ACC_CMD (and the smartDSU will block it from the DSU) or not (the DSU is "connected") + ret.openpilotLongitudinalControl = ret.enableCamera and (smartDsu or ret.enableDsu or candidate in TSS2_CAR) cloudlog.warning("ECU Camera Simulated: %r", ret.enableCamera) cloudlog.warning("ECU DSU Simulated: %r", ret.enableDsu) cloudlog.warning("ECU APGS Simulated: %r", ret.enableApgs) @@ -291,7 +314,8 @@ class CarInterface(CarInterfaceBase): ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptor) else 19. * CV.MPH_TO_MS # removing the DSU disables AEB and it's considered a community maintained feature - ret.communityFeature = ret.enableGasInterceptor or ret.enableDsu + # intercepting the DSU is a community feature since it requires unofficial hardware + ret.communityFeature = ret.enableGasInterceptor or ret.enableDsu or smartDsu ret.longitudinalTuning.deadzoneBP = [0., 9.] ret.longitudinalTuning.deadzoneV = [0., .15] @@ -341,7 +365,7 @@ class CarInterface(CarInterfaceBase): ret.gearShifter = self.CS.gear_shifter # gas pedal - ret.gas = self.CS.car_gas + ret.gas = self.CS.pedal_gas if self.CP.enableGasInterceptor: # use interceptor values to disengage on pedal press ret.gasPressed = self.CS.pedal_gas > 15 diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 7fbe60512..6b09b41ab 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -22,6 +22,7 @@ class CAR: CAMRY = "TOYOTA CAMRY 2018" CAMRYH = "TOYOTA CAMRY HYBRID 2018" HIGHLANDER = "TOYOTA HIGHLANDER 2017" + HIGHLANDER_TSS2 = "TOYOTA HIGHLANDER 2020" HIGHLANDERH = "TOYOTA HIGHLANDER HYBRID 2018" AVALON = "TOYOTA AVALON 2016" RAV4_TSS2 = "TOYOTA RAV4 2019" @@ -33,28 +34,29 @@ class CAR: LEXUS_IS = "LEXUS IS300 2018" LEXUS_CTH = "LEXUS CT 200H 2018" RAV4H_TSS2 = "TOYOTA RAV4 HYBRID 2019" + LEXUS_NXH = "LEXUS NX300H 2018" # addr: (ecu, cars, bus, 1/freq*100, vl) STATIC_MSGS = [ - (0x128, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON), 1, 3, b'\xf4\x01\x90\x83\x00\x37'), + (0x128, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON), 1, 3, b'\xf4\x01\x90\x83\x00\x37'), (0x128, Ecu.dsu, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.SIENNA, CAR.LEXUS_CTH), 1, 3, b'\x03\x00\x20\x00\x00\x52'), - (0x141, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 2, b'\x00\x00\x00\x46'), - (0x160, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 7, b'\x00\x00\x08\x12\x01\x31\x9c\x51'), - (0x161, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON, CAR.LEXUS_RX), 1, 7, b'\x00\x1e\x00\x00\x00\x80\x07'), + (0x141, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 2, b'\x00\x00\x00\x46'), + (0x160, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 7, b'\x00\x00\x08\x12\x01\x31\x9c\x51'), + (0x161, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON, CAR.LEXUS_RX), 1, 7, b'\x00\x1e\x00\x00\x00\x80\x07'), (0X161, Ecu.dsu, (CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.SIENNA, CAR.LEXUS_CTH), 1, 7, b'\x00\x1e\x00\xd4\x00\x00\x5b'), - (0x283, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 3, b'\x00\x00\x00\x00\x00\x00\x8c'), + (0x283, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 3, b'\x00\x00\x00\x00\x00\x00\x8c'), (0x2E6, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 3, b'\xff\xf8\x00\x08\x7f\xe0\x00\x4e'), (0x2E7, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 3, b'\xa8\x9c\x31\x9c\x00\x00\x00\x02'), (0x33E, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 20, b'\x0f\xff\x26\x40\x00\x1f\x00'), - (0x344, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 5, b'\x00\x00\x01\x00\x00\x00\x00\x50'), - (0x365, Ecu.dsu, (CAR.PRIUS, CAR.LEXUS_RXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x00\x80\x03\x00\x08'), + (0x344, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 5, b'\x00\x00\x01\x00\x00\x00\x00\x50'), + (0x365, Ecu.dsu, (CAR.PRIUS, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x00\x80\x03\x00\x08'), (0x365, Ecu.dsu, (CAR.RAV4, CAR.RAV4H, CAR.COROLLA, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 20, b'\x00\x00\x00\x80\xfc\x00\x08'), - (0x366, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x4d\x82\x40\x02\x00'), + (0x366, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x4d\x82\x40\x02\x00'), (0x366, Ecu.dsu, (CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 20, b'\x00\x72\x07\xff\x09\xfe\x00'), (0x470, Ecu.dsu, (CAR.PRIUS, CAR.LEXUS_RXH), 1, 100, b'\x00\x00\x02\x7a'), (0x470, Ecu.dsu, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.RAV4H, CAR.SIENNA, CAR.LEXUS_CTH), 1, 100, b'\x00\x00\x01\x79'), - (0x4CB, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 100, b'\x0c\x00\x00\x00\x00\x00\x00\x00'), + (0x4CB, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 100, b'\x0c\x00\x00\x00\x00\x00\x00\x00'), (0x292, Ecu.apgs, (CAR.PRIUS), 0, 3, b'\x00\x00\x00\x00\x00\x00\x00\x9e'), (0x32E, Ecu.apgs, (CAR.PRIUS), 0, 20, b'\x00\x00\x00\x00\x00\x00\x00\x00'), @@ -67,195 +69,206 @@ STATIC_MSGS = [ ECU_FINGERPRINT = { Ecu.fwdCamera: [0x2e4], # steer torque cmd - Ecu.dsu: [0x343], # accel cmd + Ecu.dsu: [0x283], # accel cmd Ecu.apgs: [0x835], # angle cmd } FINGERPRINTS = { CAR.RAV4: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8 }], CAR.RAV4H: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 296: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 548: 8, 550: 8, 552: 4, 560: 7, 562: 4, 581: 5, 608: 8, 610: 5, 643: 7, 705: 8, 713: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1212: 8, 1227: 8, 1228: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 296: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 548: 8, 550: 8, 552: 4, 560: 7, 562: 4, 581: 5, 608: 8, 610: 5, 643: 7, 705: 8, 713: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1212: 8, 1227: 8, 1228: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # Chinese RAV4 { - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 742: 8, 743: 8, 800: 8, 830: 7, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1207: 8, 1227: 8, 1235: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 830: 7, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1207: 8, 1227: 8, 1235: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8 }], CAR.PRIUS: [{ # with ipas - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2,898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2,898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #2019 LE { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # 2020 Prius Prime LE { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 800: 8, 810: 2, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 767:4, 800: 8, 810: 2, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #2020 Prius Prime Limited { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8, 2026: 8, 2027: 8, 2029: 8, 2030: 8, 2031: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8, 2026: 8, 2027: 8, 2029: 8, 2030: 8, 2031: 8 }, #2020 Central Europe Prime { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 800: 8, 810: 2, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 8, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 767:4, 800: 8, 810: 2, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 8, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8 + }, + #2017 German Prius + { + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296:8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8,740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1777: 8, 1779: 8, 1792: 8, 1767:4, 800: 8, 1863:8, 1904: 8, 1912: 8, 1984: 8, 1988: 8, 1990: 8, 1992: 8, 1996:8, 1998: 8, 2002: 8, 2010: 8, 2015: 8, 2016: 8, 2018: 8, 2024: 8, 2026: 8, 2030: 8 }], #Corolla w/ added Pedal Support (512L and 513L) CAR.COROLLA: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 2, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1196: 8, 1227: 8, 1235: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2021: 8, 2022: 8, 2023: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 2, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1196: 8, 1227: 8, 1235: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2021: 8, 2022: 8, 2023: 8, 2024: 8 }], CAR.LEXUS_RX: [{ # 2016 Lexus RX 350 - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }, # 2017 Lexus RX 350 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.LEXUS_RXH: [{ - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513:6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1956: 8, 1960: 8, 1964: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8, 2012: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513:6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1956: 8, 1960: 8, 1964: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8, 2012: 8 }, # RX450HL # TODO: get proper fingerprint in stock mode { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # RX540H 2019 with color hud { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1952: 8, 1960: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1952: 8, 1960: 8, 1990: 8, 1998: 8 }, # 2017 RX 450h { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 744: 8, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1745: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 744: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1745: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.LEXUS_RX_TSS2: [ # 2020 Lexus RX 350 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594:8, 1595: 8, 1600: 8, 1649: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594:8, 1595: 8, 1600: 8, 1649: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.CHR: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 705: 8, 740: 5, 800: 8, 810: 2, 812: 8, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 913: 8, 918: 8, 921: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 705: 8, 740: 5, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 913: 8, 918: 8, 921: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8 }], CAR.CHRH: [{ - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 800: 8, 810: 2, 812: 8, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.CAMRY: [ #XLE and LE { - 36: 8, 37: 8, 119: 6, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 119: 6, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #XSE and SE # TODO: get proper fingerprint in stock mode { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, { # 2019 XSE - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 942: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1792: 8, 1800: 8, 1808: 8, 1816: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1937: 8, 1945: 8, 1953: 8, 1961: 8, 1968: 8, 1976: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 942: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1792: 8, 1767:4, 800: 8, 1808: 8, 1816: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1937: 8, 1945: 8, 1953: 8, 1961: 8, 1968: 8, 1976: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.CAMRYH: [ #SE, LE and LE with Blindspot Monitor { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #SL { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #XLE { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.HIGHLANDER: [{ - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1984: 8, 1988: 8, 1992: 8, 1996: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1984: 8, 1988: 8, 1992: 8, 1996: 8, 1990: 8, 1998: 8 }, # 2019 Highlander XLE { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # 2017 Highlander Limited { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # 2018 Highlander Limited Platinum { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1585: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1988: 8, 1990: 8, 1996: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1585: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1988: 8, 1990: 8, 1996: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + }], + CAR.HIGHLANDER_TSS2: [{ + # 2020 highlander limited + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 355: 5, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1952: 8, 1960: 8, 1990: 8, 1998: 8 }], CAR.HIGHLANDERH: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, { # 2019 Highlander Hybrid Limited Platinum - 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.AVALON: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 905: 8, 911: 1, 916: 2, 921: 8, 933: 6, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 1005: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1206: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1558: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 905: 8, 911: 1, 916: 2, 921: 8, 933: 6, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 1005: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1206: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1558: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.RAV4_TSS2: [ # LE { - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553:8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553:8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # XLE, Limited, and AWD { - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.COROLLA_TSS2: [ # hatch 2019+ and sedan 2020+ { - 36: 8, 37: 8, 114: 5, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1809: 8, 1816: 8, 1817: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1960: 8, 1981: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8 + 36: 8, 37: 8, 114: 5, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1809: 8, 1816: 8, 1817: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1960: 8, 1981: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8 }], CAR.COROLLAH_TSS2: [ # 2019 Taiwan Altis Hybrid { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 800: 8, 810: 2, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 918: 7, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1082: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1745: 8, 1775: 8, 1779: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 767:4, 800: 8, 810: 2, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 918: 7, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1082: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1745: 8, 1775: 8, 1779: 8 }, # 2019 Chinese Levin Hybrid { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 800: 8, 810: 2, 812: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1600: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1600: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8 } ], CAR.LEXUS_ES_TSS2: [{ - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, }], CAR.LEXUS_ESH_TSS2: [ { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 744: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 744: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.SIENNA: [ { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 888: 8, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 918: 7, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 767:4, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 888: 8, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 918: 7, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # XLE AWD 2018 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 767:4, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.LEXUS_IS: [ # IS300 2018 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 400: 6, 426: 6, 452: 8, 464: 8, 466: 8, 467: 5, 544: 4, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1009: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1590: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1648: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 400: 6, 426: 6, 452: 8, 464: 8, 466: 8, 467: 5, 544: 4, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 767:4, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1009: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1590: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1648: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # IS300H 2017 { - 36: 8, 37: 8, 170: 8, 180: 8, 295: 8, 296: 8, 400: 6, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 7, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1009: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 295: 8, 296: 8, 400: 6, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 767:4, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 7, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1009: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.RAV4H_TSS2: [ #Hybrid Limited { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913:8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913:8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, - # German Lounge - { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1792: 8, 1800: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1937: 8, 1945: 8, 1953: 8, 1961: 8, 1968: 8, 1976: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 - }], + ], CAR.LEXUS_CTH: [{ 36: 8, 37: 8, 170: 8, 180: 8, 288: 8, 426: 6, 452: 8, 466: 8, 467: 8, 548: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 810: 2, 832: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 921: 8, 933: 8, 944: 6, 945: 8, 950: 8, 951: 8, 953: 3, 955: 4, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1116: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1190: 8, 1191: 8, 1192: 8, 1227: 8, 1235: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1558: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + }], + CAR.LEXUS_NXH: [{ + 36: 8, 37: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 800: 8, 810: 2, 812: 3, 818: 8, 822: 8, 824: 8, 835: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 889: 8, 891: 8, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 987: 8, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1006: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1195: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1228: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }] } +# Don't use theses fingerprints for fingerprinting, they are still needed for ECU detection +IGNORED_FINGERPRINTS = [CAR.RAV4H_TSS2] + FW_VERSIONS = { CAR.AVALON: { (Ecu.esp, 0x7b0, None): [b'F152607060\x00\x00\x00\x00\x00\x00'], @@ -267,6 +280,7 @@ FW_VERSIONS = { }, CAR.CAMRY: { (Ecu.engine, 0x700, None): [ + b'\x018966306L5200\x00\x00\x00\x00', b'\x018966333P4200\x00\x00\x00\x00', b'\x018966333P4300\x00\x00\x00\x00', b'\x018966333P4400\x00\x00\x00\x00', @@ -325,9 +339,18 @@ FW_VERSIONS = { b'8646F0605000 ', ], }, + CAR.CHR: { + (Ecu.dsu, 0x791, None): [b'8821FF404100 '], + (Ecu.esp, 0x7b0, None): [b'F1526F4122\x00\x00\x00\x00\x00\x00'], + (Ecu.eps, 0x7a1, None): [b'8965B10040\x00\x00\x00\x00\x00\x00'], + (Ecu.engine, 0x7e0, None): [b'\x033F424000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00'], + (Ecu.fwdRadar, 0x750, 0xf): [b'8821FF404100 '], + (Ecu.fwdCamera, 0x750, 0x6d): [b'8646FF404000 '], + }, CAR.COROLLA: { (Ecu.engine, 0x7e0, None): [ b'\x01896630E88000\x00\x00\x00\x00', + b'\x0230ZC2000\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x0230ZC2100\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x0230ZC2200\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x0230ZC2300\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -494,9 +517,11 @@ FW_VERSIONS = { b'\x02896634774100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634774200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634782000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', + b'\x02896634784000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634759300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00', b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701002\x00\x00\x00\x00', + b'\x03896634760100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634760200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634760200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00', b'\x03896634768000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4703001\x00\x00\x00\x00', @@ -551,6 +576,7 @@ FW_VERSIONS = { }, CAR.RAV4: { (Ecu.engine, 0x7e0, None): [ + b'\x02342Q1000\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02342Q1100\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02342Q1300\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02342Q2000\x00\x00\x00\x00\x00\x00\x00\x0054213000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -561,6 +587,7 @@ FW_VERSIONS = { b'8965B42083\x00\x00\x00\x00\x00\x00', ], (Ecu.esp, 0x7b0, None): [ + b'F15260R102\x00\x00\x00\x00\x00\x00', b'F15260R103\x00\x00\x00\x00\x00\x00', b'F152642493\x00\x00\x00\x00\x00\x00', ], @@ -619,9 +646,11 @@ FW_VERSIONS = { b'\x018966342V3100\x00\x00\x00\x00', b'\x018966342X5000\x00\x00\x00\x00', b'\x01896634A05000\x00\x00\x00\x00', + b'\x01896634A19000\x00\x00\x00\x00', b'\x01896634A22000\x00\x00\x00\x00', b'\x01F152642551\x00\x00\x00\x00\x00\x00', b'\x028966342Y8000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00', + b'\x02896634A18000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00', ], (Ecu.esp, 0x7b0, None): [ b'F152606230\x00\x00\x00\x00\x00\x00', @@ -695,9 +724,17 @@ FW_VERSIONS = { (Ecu.fwdCamera, 0x750, 0x6d): [b'8646F5301400\x00\x00\x00\x00'], }, CAR.SIENNA: { - (Ecu.engine, 0x700, None): [b'\x01896630832100\x00\x00\x00\x00'], + (Ecu.engine, 0x700, None): [ + b'\x01896630832100\x00\x00\x00\x00', + b'\x01896630842000\x00\x00\x00\x00', + b'\x01896630851100\x00\x00\x00\x00', + b'\x01896630860000\x00\x00\x00\x00', + ], (Ecu.eps, 0x7a1, None): [b'8965B45070\x00\x00\x00\x00\x00\x00'], - (Ecu.fwdRadar, 0x750, 0xf): [b'8821F4702100\x00\x00\x00\x00'], + (Ecu.fwdRadar, 0x750, 0xf): [ + b'8821F4702100\x00\x00\x00\x00', + b'8821F4702300\x00\x00\x00\x00', + ], (Ecu.fwdCamera, 0x750, 0x6d): [b'8646F0801100\x00\x00\x00\x00'], }, CAR.LEXUS_RXH: { @@ -747,6 +784,7 @@ DBC = { CAR.CAMRY: dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'), CAR.CAMRYH: dbc_dict('toyota_camry_hybrid_2018_pt_generated', 'toyota_adas'), CAR.HIGHLANDER: dbc_dict('toyota_highlander_2017_pt_generated', 'toyota_adas'), + CAR.HIGHLANDER_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.HIGHLANDERH: dbc_dict('toyota_highlander_hybrid_2018_pt_generated', 'toyota_adas'), CAR.AVALON: dbc_dict('toyota_avalon_2017_pt_generated', 'toyota_adas'), CAR.RAV4_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), @@ -758,8 +796,9 @@ DBC = { CAR.LEXUS_IS: dbc_dict('lexus_is_2018_pt_generated', 'toyota_adas'), CAR.LEXUS_CTH: dbc_dict('lexus_ct200h_2018_pt_generated', 'toyota_adas'), CAR.RAV4H_TSS2: dbc_dict('toyota_nodsu_hybrid_pt_generated', 'toyota_tss2_adas'), + CAR.LEXUS_NXH: dbc_dict('lexus_nx300h_2018_pt_generated', 'toyota_adas'), } -NO_DSU_CAR = [CAR.CHR, CAR.CHRH, CAR.CAMRY, CAR.CAMRYH, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2] -TSS2_CAR = [CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2] -NO_STOP_TIMER_CAR = [CAR.RAV4H, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.SIENNA, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2] # no resume button press required +NO_DSU_CAR = [CAR.CHR, CAR.CHRH, CAR.CAMRY, CAR.CAMRYH, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2] +TSS2_CAR = [CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2] +NO_STOP_TIMER_CAR = [CAR.RAV4H, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.SIENNA, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2] # no resume button press required diff --git a/selfdrive/car/volkswagen/carstate.py b/selfdrive/car/volkswagen/carstate.py index 59357edbe..79cd06310 100644 --- a/selfdrive/car/volkswagen/carstate.py +++ b/selfdrive/car/volkswagen/carstate.py @@ -1,13 +1,10 @@ import numpy as np -from cereal import car -from common.kalman.simple_kalman import KF1D from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from opendbc.can.can_define import CANDefine from selfdrive.car.volkswagen.values import DBC, BUTTON_STATES, CarControllerParams -GEAR = car.CarState.GearShifter - def get_mqb_pt_can_parser(CP, canbus): # this function generates lists for signal, messages and initial values signals = [ @@ -99,29 +96,14 @@ def get_mqb_cam_can_parser(CP, canbus): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, canbus.cam) -def parse_gear_shifter(gear): - # Return mapping of gearshift position to selected gear. - return {'P': GEAR.park, 'R': GEAR.reverse, 'N': GEAR.neutral, - 'D': GEAR.drive, 'E': GEAR.eco, 'S': GEAR.sport, 'T': GEAR.manumatic}.get(gear, GEAR.unknown) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP, canbus): - # initialize can parser - self.CP = CP - self.car_fingerprint = CP.carFingerprint - self.can_define = CANDefine(DBC[CP.carFingerprint]['pt']) - - self.shifter_values = self.can_define.dv["Getriebe_11"]['GE_Fahrstufe'] - + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["Getriebe_11"]['GE_Fahrstufe'] self.buttonStates = BUTTON_STATES.copy() - # vEgo Kalman filter - dt = 0.01 - self.v_ego_kf = KF1D(x0=[[0.], [0.]], - A=[[1., dt], [0., 1.]], - C=[1., 0.], - K=[[0.12287673], [0.29666309]]) - def update(self, pt_cp): # Update vehicle speed and acceleration from ABS wheel speeds. self.wheelSpeedFL = pt_cp.vl["ESP_19"]['ESP_VL_Radgeschw_02'] * CV.KPH_TO_MS @@ -130,9 +112,8 @@ class CarState(): self.wheelSpeedRR = pt_cp.vl["ESP_19"]['ESP_HR_Radgeschw_02'] * CV.KPH_TO_MS self.vEgoRaw = float(np.mean([self.wheelSpeedFL, self.wheelSpeedFR, self.wheelSpeedRL, self.wheelSpeedRR])) - v_ego_x = self.v_ego_kf.update(self.vEgoRaw) - self.vEgo = float(v_ego_x[0]) - self.aEgo = float(v_ego_x[1]) + self.vEgo, self.aEgo = self.update_speed_kf(self.vEgoRaw) + self.standstill = self.vEgoRaw < 0.1 # Update steering angle, rate, yaw rate, and driver input torque. VW send @@ -152,7 +133,7 @@ class CarState(): # Update gear and/or clutch position data. can_gear_shifter = int(pt_cp.vl["Getriebe_11"]['GE_Fahrstufe']) - self.gearShifter = parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) + self.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) # Update door and trunk/hatch lid open status. self.doorOpen = any([pt_cp.vl["Gateway_72"]['ZV_FT_offen'], diff --git a/selfdrive/common/version.h b/selfdrive/common/version.h index 86fe8c7c0..74d3ae1ba 100644 --- a/selfdrive/common/version.h +++ b/selfdrive/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.7.2-release" +#define COMMA_VERSION "0.7.3-release" diff --git a/selfdrive/controls/lib/pathplanner.py b/selfdrive/controls/lib/pathplanner.py index de43c0418..83b1190ff 100644 --- a/selfdrive/controls/lib/pathplanner.py +++ b/selfdrive/controls/lib/pathplanner.py @@ -6,6 +6,7 @@ from selfdrive.controls.lib.lateral_mpc import libmpc_py from selfdrive.controls.lib.drive_helpers import MPC_COST_LAT from selfdrive.controls.lib.lane_planner import LanePlanner from selfdrive.config import Conversions as CV +from common.params import Params import cereal.messaging as messaging from cereal import log @@ -53,6 +54,7 @@ class PathPlanner(): self.setup_mpc() self.solution_invalid_cnt = 0 + self.lane_change_enabled = Params().get('LaneChangeEnabled') == b'1' self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none self.lane_change_timer = 0.0 @@ -97,7 +99,7 @@ class PathPlanner(): elif sm['carState'].rightBlinker: self.lane_change_direction = LaneChangeDirection.right - if (not active) or (self.lane_change_timer > LANE_CHANGE_TIME_MAX) or (not one_blinker): + if (not active) or (self.lane_change_timer > LANE_CHANGE_TIME_MAX) or (not one_blinker) or (not self.lane_change_enabled): self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none else: diff --git a/selfdrive/debug/live_cpu_and_temp.py b/selfdrive/debug/live_cpu_and_temp.py index 1f4f2426e..4f324711d 100755 --- a/selfdrive/debug/live_cpu_and_temp.py +++ b/selfdrive/debug/live_cpu_and_temp.py @@ -1,64 +1,72 @@ #!/usr/bin/env python3 +import argparse + import numpy as np from cereal.messaging import SubMaster + def cputime_total(ct): - return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq + return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq def cputime_busy(ct): - return ct.user + ct.nice + ct.system + ct.irq + ct.softirq + return ct.user + ct.nice + ct.system + ct.irq + ct.softirq +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--mem', action='store_true') + args = parser.parse_args() -sm = SubMaster(['thermal', 'procLog']) + sm = SubMaster(['thermal', 'procLog']) -last_temp = 0.0 -last_mem = 0.0 -total_times = [0., 0., 0., 0.] -busy_times = [0., 0., 0.0, 0.] + last_temp = 0.0 + last_mem = 0.0 + total_times = [0., 0., 0., 0.] + busy_times = [0., 0., 0.0, 0.] + + while True: + sm.update() + + if sm.updated['thermal']: + t = sm['thermal'] + last_temp = np.mean([t.cpu0, t.cpu1, t.cpu2, t.cpu3]) / 10. + last_mem = t.memUsedPercent + + if sm.updated['procLog']: + m = sm['procLog'] -while True: - sm.update() + cores = [0., 0., 0., 0.] + total_times_new = [0., 0., 0., 0.] + busy_times_new = [0., 0., 0.0, 0.] - if sm.updated['thermal']: - t = sm['thermal'] - last_temp = np.mean([t.cpu0, t.cpu1, t.cpu2, t.cpu3]) / 10. - last_mem = t.memUsedPercent + for c in m.cpuTimes: + n = c.cpuNum + total_times_new[n] = cputime_total(c) + busy_times_new[n] = cputime_busy(c) - if sm.updated['procLog']: - m = sm['procLog'] + for n in range(4): + t_busy = busy_times_new[n] - busy_times[n] + t_total = total_times_new[n] - total_times[n] + cores[n] = t_busy / t_total - mems = {} - for proc in m.procs: - name = proc.name - if len(proc.cmdline): - name = proc.cmdline[0] - if len(proc.exe): - name = proc.exe + " - " + name - mems[name] = float(proc.memRss) / 1e6 + total_times = total_times_new[:] + busy_times = busy_times_new[:] - cores = [0., 0., 0., 0.] - total_times_new = [0., 0., 0., 0.] - busy_times_new = [0., 0., 0.0, 0.] + print("CPU %.2f%% - RAM: %.2f - Temp %.2f" % (100. * np.mean(cores), last_mem, last_temp)) - for c in m.cpuTimes: - n = c.cpuNum - total_times_new[n] = cputime_total(c) - busy_times_new[n] = cputime_busy(c) - - for n in range(4): - t_busy = busy_times_new[n] - busy_times[n] - t_total = total_times_new[n] - total_times[n] - cores[n] = t_busy / t_total - - total_times = total_times_new[:] - busy_times = busy_times_new[:] - - print() - print("CPU %.2f%% - RAM: %.2f - Temp %.2f" % (100. * np.mean(cores), last_mem, last_temp)) - print("Top memory usage:") - for k, v in sorted(mems.items(), key=lambda item: item[1], reverse=True)[:10]: - print(f"{k.rjust(70)} {v:.2f} MB") + if args.mem: + mems = {} + for proc in m.procs: + name = proc.name + if len(proc.cmdline): + name = proc.cmdline[0] + if len(proc.exe): + name = proc.exe + " - " + name + mems[name] = float(proc.memRss) / 1e6 + print("Top memory usage:") + for k, v in sorted(mems.items(), key=lambda item: item[1], reverse=True)[:10]: + print(f"{k.rjust(70)} {v:.2f} MB") + print() diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index 82bd8299c..5bf8c4a76 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import os import traceback import sys from tqdm import tqdm @@ -13,14 +14,19 @@ from selfdrive.car.honda.values import FINGERPRINTS as HONDA_FINGERPRINTS if __name__ == "__main__": if len(sys.argv) < 2: - print("Usage: ./test_fw_query_on_routes.py ") + print("Usage: ./test_fw_query_on_routes.py /") sys.exit(1) + if os.path.exists(sys.argv[1]): + routes = list(open(sys.argv[1])) + else: + routes = [sys.argv[1]] + wrong = 0 good = 0 dongles = [] - for route in tqdm(list(open(sys.argv[1]))): + for route in tqdm(routes): route = route.rstrip() dongle_id, time = route.split('|') qlog_path = f"cd:/{dongle_id}/{time}/0/qlog.bz2" diff --git a/selfdrive/locationd/kalman/.gitignore b/selfdrive/locationd/kalman/.gitignore new file mode 100644 index 000000000..9ab870da8 --- /dev/null +++ b/selfdrive/locationd/kalman/.gitignore @@ -0,0 +1 @@ +generated/ diff --git a/selfdrive/locationd/kalman/README.md b/selfdrive/locationd/kalman/README.md new file mode 100644 index 000000000..4c4ebd59c --- /dev/null +++ b/selfdrive/locationd/kalman/README.md @@ -0,0 +1,52 @@ +# Kalman filter library + +## Introduction +The kalman filter framework described here is an incredibly powerful tool for any optimization problem, +but particularly for visual odometry, sensor fusion localization or SLAM. It is designed to provide very +accurate results, work online or offline, be fairly computationally efficient, be easy to design filters with in +python. + +## Feature walkthrough + +### Extended Kalman Filter with symbolic Jacobian computation +Most dynamic systems can be described as a Hidden Markov Process. To estimate the state of such a system with noisy +measurements one can use a Recursive Bayesian estimator. For a linear Markov Process a regular linear Kalman filter is optimal. +Unfortunately, a lot of systems are non-linear. Extended Kalman Filters can model systems by linearizing the non-linear +system at every step, this provides a close to optimal estimator when the linearization is good enough. If the linearization +introduces too much noise, one can use an Iterated Extended Kalman Filter, Unscented Kalman Filter or a Particle Filter. For +most applications those estimators are overkill and introduce too much complexity and require a lot of additional compute. + +Conventionally Extended Kalman Filters are implemented by writing the system's dynamic equations and then manually symbolically +calculating the Jacobians for the linearization. For complex systems this is time consuming and very prone to calculation errors. +This library symbolically computes the Jacobians using sympy to simplify the system's definition and remove the possiblity of introducing calculation errors. + +### Error State Kalman Filter +3D localization algorithms ussually also require estimating orientation of an object in 3D. Orientation is generally represented +with euler angles or quaternions. + +Euler angles have several problems, there are mulitple ways to represent the same orientation, +gimbal lock can cause the loss of a degree of freedom and lastly their behaviour is very non-linear when errors are large. +Quaternions with one strictly positive dimension don't suffer from these issues, but have another set of problems. +Quaternions need to be normalized otherwise they will grow unbounded, this is cannot be cleanly enforced in a kalman filter. +Most importantly though a quaternion has 4 dimensions, but only represents 3 degrees of freedom, so there is one redundant dimension. + +Kalman filters are designed to minimize the error of the system's state. It is possible to have a kalman filter where state and the error of the state are represented in a different space. As long as there is an error function that can compute the error based on the true state and estimated state. It is problematic to have redundant dimensions in the error of the kalman filter, but not in the state. A good compromise then, is to use the quaternion to represent the system's attitude state and use euler angles to describe the error in attitude. This library supports and defining an arbitrary error that is in a different space than the state. [Joan Solà](https://arxiv.org/abs/1711.02508) has written a comprehensive description of using ESKFs for robust 3D orientation estimation. + +### Multi-State Constraint Kalman Filter +How do you integrate feature-based visual odometry with a Kalman filter? The problem is that one cannot write an observation equation for 2D feature observations in image space for a localization kalman filter. One needs to give the feature observation a depth so it has a 3D position, then one can write an obvervation equation in the kalman filter. This is possible by tracking the feature across frames and then estimating the depth. However, the solution is not that simple, the depth estimated by tracking the feature across frames depends on the location of the camera at those frames, and thus the state of the kalman filter. This creates a positive feedback loop where the kalman filter wrongly gains confidence in it's position because the feature position updates reinforce it. + +The solution is to use an [MSCKF](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.437.1085&rep=rep1&type=pdf), which this library fully supports. + +### Rauch–Tung–Striebel smoothing +When doing offline estimation with a kalman filter there can be an initialization period where states are badly estimated. +Global estimators don't suffer from this, to make our kalman filter competitive with global optimizers we can run the filter +backwards using an RTS smoother. Those combined with potentially multiple forward and backwards passes of the data should make +performance very close to global optimization. + +### Mahalanobis distance outlier rejector +A lot of measurements do not come from a Gaussian distribution and as such have outliers that do not fit the statistical model +of the Kalman filter. This can cause a lot of performance issues if not dealt with. This library allows the use of a mahalanobis +distance statistical test on the incoming measurements to deal with this. Note that good initialization is critical to prevent +good measurements from being rejected. + + diff --git a/selfdrive/locationd/kalman/SConscript b/selfdrive/locationd/kalman/SConscript new file mode 100644 index 000000000..2a67ad64f --- /dev/null +++ b/selfdrive/locationd/kalman/SConscript @@ -0,0 +1,31 @@ +Import('env') + +templates = Glob('templates/*') +sympy_helpers = "helpers/sympy_helpers.py" +ekf_sym = "helpers/ekf_sym.py" + +to_build = { + 'pos_computer_4': 'helpers/lst_sq_computer.py', + 'pos_computer_5': 'helpers/lst_sq_computer.py', + 'feature_handler_5': 'helpers/feature_handler.py', + 'gnss': 'models/gnss_kf.py', + 'loc_4': 'models/loc_kf.py', + 'live': 'models/live_kf.py', + 'lane': '#xx/pipeline/lib/ekf/lane_kf.py', +} + +found = {} + +for target, command in to_build.items(): + if File(command).exists(): + found[target] = command + +for target, command in found.items(): + target_files = File([f'generated/{target}.cpp', f'generated/{target}.h']) + command_file = File(command) + env.Command(target_files, + [templates, command_file, sympy_helpers, ekf_sym], + command_file.get_abspath()+" "+target + ) + + env.SharedLibrary('generated/' + target, target_files[0]) diff --git a/selfdrive/locationd/kalman/__init__.py b/selfdrive/locationd/kalman/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/selfdrive/locationd/kalman/helpers/__init__.py b/selfdrive/locationd/kalman/helpers/__init__.py new file mode 100644 index 000000000..5ac7fb3f2 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/__init__.py @@ -0,0 +1,191 @@ +import numpy as np +import os +from bisect import bisect +from tqdm import tqdm +from cffi import FFI + +TEMPLATE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'templates')) +GENERATED_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'generated')) + + +def write_code(name, code, header): + if not os.path.exists(GENERATED_DIR): + os.mkdir(GENERATED_DIR) + + open(os.path.join(GENERATED_DIR, f"{name}.cpp"), 'w').write(code) + open(os.path.join(GENERATED_DIR, f"{name}.h"), 'w').write(header) + + +def load_code(name): + shared_fn = os.path.join(GENERATED_DIR, f"lib{name}.so") + header_fn = os.path.join(GENERATED_DIR, f"{name}.h") + header = open(header_fn).read() + + ffi = FFI() + ffi.cdef(header) + return (ffi, ffi.dlopen(shared_fn)) + + +class KalmanError(Exception): + pass + + +class ObservationKind(): + UNKNOWN = 0 + NO_OBSERVATION = 1 + GPS_NED = 2 + ODOMETRIC_SPEED = 3 + PHONE_GYRO = 4 + GPS_VEL = 5 + PSEUDORANGE_GPS = 6 + PSEUDORANGE_RATE_GPS = 7 + SPEED = 8 + NO_ROT = 9 + PHONE_ACCEL = 10 + ORB_POINT = 11 + ECEF_POS = 12 + CAMERA_ODO_TRANSLATION = 13 + CAMERA_ODO_ROTATION = 14 + ORB_FEATURES = 15 + MSCKF_TEST = 16 + FEATURE_TRACK_TEST = 17 + LANE_PT = 18 + IMU_FRAME = 19 + PSEUDORANGE_GLONASS = 20 + PSEUDORANGE_RATE_GLONASS = 21 + PSEUDORANGE = 22 + PSEUDORANGE_RATE = 23 + + names = ['Unknown', + 'No observation', + 'GPS NED', + 'Odometric speed', + 'Phone gyro', + 'GPS velocity', + 'GPS pseudorange', + 'GPS pseudorange rate', + 'Speed', + 'No rotation', + 'Phone acceleration', + 'ORB point', + 'ECEF pos', + 'camera odometric translation', + 'camera odometric rotation', + 'ORB features', + 'MSCKF test', + 'Feature track test', + 'Lane ecef point', + 'imu frame eulers', + 'GLONASS pseudorange', + 'GLONASS pseudorange rate'] + + @classmethod + def to_string(cls, kind): + return cls.names[kind] + + + +SAT_OBS = [ObservationKind.PSEUDORANGE_GPS, + ObservationKind.PSEUDORANGE_RATE_GPS, + ObservationKind.PSEUDORANGE_GLONASS, + ObservationKind.PSEUDORANGE_RATE_GLONASS] + + +def run_car_ekf_offline(kf, observations_by_kind): + from laika.raw_gnss import GNSSMeasurement + observations = [] + # create list of observations with element format: [kind, time, data] + for kind in observations_by_kind: + for t, data in zip(observations_by_kind[kind][0], observations_by_kind[kind][1]): + observations.append([t, kind, data]) + observations.sort(key=lambda obs: obs[0]) + + times, estimates = run_observations_through_filter(kf, observations) + + forward_states = np.stack(e[1] for e in estimates) + forward_covs = np.stack(e[3] for e in estimates) + smoothed_states, smoothed_covs = kf.rts_smooth(estimates) + + observations_dict = {} + # TODO assuming observations and estimates + # are same length may not work with VO + for e in estimates: + t = e[4] + kind = str(int(e[5])) + res = e[6] + z = e[7] + ea = e[8] + if len(z) == 0: + continue + if kind not in observations_dict: + observations_dict[kind] = {} + observations_dict[kind]['t'] = np.array(len(z)*[t]) + observations_dict[kind]['z'] = np.array(z) + observations_dict[kind]['ea'] = np.array(ea) + observations_dict[kind]['residual'] = np.array(res) + else: + observations_dict[kind]['t'] = np.append(observations_dict[kind]['t'], np.array(len(z)*[t])) + observations_dict[kind]['z'] = np.vstack((observations_dict[kind]['z'], np.array(z))) + observations_dict[kind]['ea'] = np.vstack((observations_dict[kind]['ea'], np.array(ea))) + observations_dict[kind]['residual'] = np.vstack((observations_dict[kind]['residual'], np.array(res))) + + # add svIds to gnss data + for kind in map(str, SAT_OBS): + if int(kind) in observations_by_kind and kind in observations_dict: + observations_dict[kind]['svIds'] = np.array([]) + observations_dict[kind]['CNO'] = np.array([]) + observations_dict[kind]['std'] = np.array([]) + for obs in observations_by_kind[int(kind)][1]: + observations_dict[kind]['svIds'] = np.append(observations_dict[kind]['svIds'], + np.array([obs[:,GNSSMeasurement.PRN]])) + observations_dict[kind]['std'] = np.append(observations_dict[kind]['std'], + np.array([obs[:,GNSSMeasurement.PR_STD]])) + return smoothed_states, smoothed_covs, forward_states, forward_covs, times, observations_dict + + +def run_observations_through_filter(kf, observations, filter_time=None): + estimates = [] + + for obs in tqdm(observations): + t = obs[0] + kind = obs[1] + data = obs[2] + estimates.append(kf.predict_and_observe(t, kind, data)) + times = [x[4] for x in estimates] + return times, estimates + + +def save_residuals_plot(obs, save_path, data_name): + import matplotlib.pyplot as plt + import mpld3 # pylint: disable=import-error + fig = plt.figure(figsize=(10,20)) + fig.suptitle('Residuals of ' + data_name, fontsize=24) + n = len(list(obs.keys())) + start_times = [obs[kind]['t'][0] for kind in obs] + start_time = min(start_times) + xlims = [start_time + 3, start_time + 60] + + for i, kind in enumerate(obs): + ax = fig.add_subplot(n, 1, i+1) + ax.set_xlim(xlims) + t = obs[kind]['t'] + res = obs[kind]['residual'] + start_idx = bisect(t, xlims[0]) + if len(res) == start_idx: + continue + ylim = max(np.linalg.norm(res[start_idx:], axis=1)) + ax.set_ylim([-ylim, ylim]) + if int(kind) in SAT_OBS: + svIds = obs[kind]['svIds'] + for svId in set(svIds): + svId_idx = (svIds == svId) + t = obs[kind]['t'][svId_idx] + res = obs[kind]['residual'][svId_idx] + ax.plot(t, res, label='SV ' + str(int(svId))) + ax.legend(loc='right') + else: + ax.plot(t, res) + plt.title('Residual of kind ' + ObservationKind.to_string(int(kind)), fontsize=20) + plt.tight_layout() + os.makedirs(save_path) + mpld3.save_html(fig, save_path + 'residuals_plot.html') diff --git a/selfdrive/locationd/kalman/helpers/chi2_lookup.py b/selfdrive/locationd/kalman/helpers/chi2_lookup.py new file mode 100644 index 000000000..e22cc9729 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/chi2_lookup.py @@ -0,0 +1,22 @@ +import os + +import numpy as np + + +def gen_chi2_ppf_lookup(max_dim=200): + from scipy.stats import chi2 + table = np.zeros((max_dim, 98)) + for dim in range(1, max_dim): + table[dim] = chi2.ppf(np.arange(.01, .99, .01), dim) + + np.save('chi2_lookup_table', table) + + +def chi2_ppf(p, dim): + table = np.load(os.path.dirname(os.path.realpath(__file__)) + '/chi2_lookup_table.npy') + result = np.interp(p, np.arange(.01, .99, .01), table[dim]) + return result + + +if __name__ == "__main__": + gen_chi2_ppf_lookup() diff --git a/selfdrive/locationd/kalman/helpers/chi2_lookup_table.npy b/selfdrive/locationd/kalman/helpers/chi2_lookup_table.npy new file mode 100644 index 0000000000000000000000000000000000000000..6f1bd959c194cbc2f955d8658ac913c088ffcab6 GIT binary patch literal 156928 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�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

    2|Eb$b`${30 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 IZR-&756-ay5C8xG literal 0 HcmV?d00001 diff --git a/selfdrive/locationd/kalman/helpers/ekf_sym.py b/selfdrive/locationd/kalman/helpers/ekf_sym.py new file mode 100644 index 000000000..98d4a87b1 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/ekf_sym.py @@ -0,0 +1,563 @@ +import os +from bisect import bisect_right + +import numpy as np +import sympy as sp +from numpy import dot + +from selfdrive.locationd.kalman.helpers.sympy_helpers import sympy_into_c +from selfdrive.locationd.kalman.helpers import (TEMPLATE_DIR, load_code, + write_code) + +from selfdrive.locationd.kalman.helpers.chi2_lookup import chi2_ppf + + +def solve(a, b): + if a.shape[0] == 1 and a.shape[1] == 1: + return b / a[0][0] + else: + return np.linalg.solve(a, b) + + +def null(H, eps=1e-12): + u, s, vh = np.linalg.svd(H) + padding = max(0, np.shape(H)[1] - np.shape(s)[0]) + null_mask = np.concatenate(((s <= eps), np.ones((padding,), dtype=bool)), axis=0) + null_space = np.compress(null_mask, vh, axis=0) + return np.transpose(null_space) + + +def gen_code(name, f_sym, dt_sym, x_sym, obs_eqs, dim_x, dim_err, eskf_params=None, msckf_params=None, maha_test_kinds=[]): + # optional state transition matrix, H modifier + # and err_function if an error-state kalman filter (ESKF) + # is desired. Best described in "Quaternion kinematics + # for the error-state Kalman filter" by Joan Sola + + if eskf_params: + err_eqs = eskf_params[0] + inv_err_eqs = eskf_params[1] + H_mod_sym = eskf_params[2] + f_err_sym = eskf_params[3] + x_err_sym = eskf_params[4] + else: + nom_x = sp.MatrixSymbol('nom_x', dim_x, 1) + true_x = sp.MatrixSymbol('true_x', dim_x, 1) + delta_x = sp.MatrixSymbol('delta_x', dim_x, 1) + err_function_sym = sp.Matrix(nom_x + delta_x) + inv_err_function_sym = sp.Matrix(true_x - nom_x) + err_eqs = [err_function_sym, nom_x, delta_x] + inv_err_eqs = [inv_err_function_sym, nom_x, true_x] + + H_mod_sym = sp.Matrix(np.eye(dim_x)) + f_err_sym = f_sym + x_err_sym = x_sym + + # This configures the multi-state augmentation + # needed for EKF-SLAM with MSCKF (Mourikis et al 2007) + if msckf_params: + msckf = True + dim_main = msckf_params[0] # size of the main state + dim_augment = msckf_params[1] # size of one augment state chunk + dim_main_err = msckf_params[2] + dim_augment_err = msckf_params[3] + N = msckf_params[4] + feature_track_kinds = msckf_params[5] + assert dim_main + dim_augment * N == dim_x + assert dim_main_err + dim_augment_err * N == dim_err + else: + msckf = False + dim_main = dim_x + dim_augment = 0 + dim_main_err = dim_err + dim_augment_err = 0 + N = 0 + + # linearize with jacobians + F_sym = f_err_sym.jacobian(x_err_sym) + for sym in x_err_sym: + F_sym = F_sym.subs(sym, 0) + for i in range(len(obs_eqs)): + obs_eqs[i].append(obs_eqs[i][0].jacobian(x_sym)) + if msckf and obs_eqs[i][1] in feature_track_kinds: + obs_eqs[i].append(obs_eqs[i][0].jacobian(obs_eqs[i][2])) + else: + obs_eqs[i].append(None) + + # collect sympy functions + sympy_functions = [] + + # error functions + sympy_functions.append(('err_fun', err_eqs[0], [err_eqs[1], err_eqs[2]])) + sympy_functions.append(('inv_err_fun', inv_err_eqs[0], [inv_err_eqs[1], inv_err_eqs[2]])) + + # H modifier for ESKF updates + sympy_functions.append(('H_mod_fun', H_mod_sym, [x_sym])) + + # state propagation function + sympy_functions.append(('f_fun', f_sym, [x_sym, dt_sym])) + sympy_functions.append(('F_fun', F_sym, [x_sym, dt_sym])) + + # observation functions + for h_sym, kind, ea_sym, H_sym, He_sym in obs_eqs: + sympy_functions.append(('h_%d' % kind, h_sym, [x_sym, ea_sym])) + sympy_functions.append(('H_%d' % kind, H_sym, [x_sym, ea_sym])) + if msckf and kind in feature_track_kinds: + sympy_functions.append(('He_%d' % kind, He_sym, [x_sym, ea_sym])) + + # Generate and wrap all th c code + header, code = sympy_into_c(sympy_functions) + extra_header = "#define DIM %d\n" % dim_x + extra_header += "#define EDIM %d\n" % dim_err + extra_header += "#define MEDIM %d\n" % dim_main_err + extra_header += "typedef void (*Hfun)(double *, double *, double *);\n" + + extra_header += "\nvoid predict(double *x, double *P, double *Q, double dt);" + + extra_post = "" + + for h_sym, kind, ea_sym, H_sym, He_sym in obs_eqs: + if msckf and kind in feature_track_kinds: + He_str = 'He_%d' % kind + # ea_dim = ea_sym.shape[0] + else: + He_str = 'NULL' + # ea_dim = 1 # not really dim of ea but makes c function work + maha_thresh = chi2_ppf(0.95, int(h_sym.shape[0])) # mahalanobis distance for outlier detection + maha_test = kind in maha_test_kinds + extra_post += """ + void update_%d(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea) { + update<%d,%d,%d>(in_x, in_P, h_%d, H_%d, %s, in_z, in_R, in_ea, MAHA_THRESH_%d); + } + """ % (kind, h_sym.shape[0], 3, maha_test, kind, kind, He_str, kind) + extra_header += "\nconst static double MAHA_THRESH_%d = %f;" % (kind, maha_thresh) + extra_header += "\nvoid update_%d(double *, double *, double *, double *, double *);" % kind + + code += '\nextern "C"{\n' + extra_header + "\n}\n" + code += "\n" + open(os.path.join(TEMPLATE_DIR, "ekf_c.c")).read() + code += '\nextern "C"{\n' + extra_post + "\n}\n" + header += "\n" + extra_header + + write_code(name, code, header) + + +class EKF_sym(): + def __init__(self, name, Q, x_initial, P_initial, dim_main, dim_main_err, + N=0, dim_augment=0, dim_augment_err=0, maha_test_kinds=[]): + """Generates process function and all observation functions for the kalman filter.""" + self.msckf = N > 0 + self.N = N + self.dim_augment = dim_augment + self.dim_augment_err = dim_augment_err + self.dim_main = dim_main + self.dim_main_err = dim_main_err + + # state + x_initial = x_initial.reshape((-1, 1)) + self.dim_x = x_initial.shape[0] + self.dim_err = P_initial.shape[0] + assert dim_main + dim_augment * N == self.dim_x + assert dim_main_err + dim_augment_err * N == self.dim_err + assert Q.shape == P_initial.shape + + # kinds that should get mahalanobis distance + # tested for outlier rejection + self.maha_test_kinds = maha_test_kinds + + # process noise + self.Q = Q + + # rewind stuff + self.rewind_t = [] + self.rewind_states = [] + self.rewind_obscache = [] + self.init_state(x_initial, P_initial, None) + + ffi, lib = load_code(name) + kinds, self.feature_track_kinds = [], [] + for func in dir(lib): + if func[:2] == 'h_': + kinds.append(int(func[2:])) + if func[:3] == 'He_': + self.feature_track_kinds.append(int(func[3:])) + + # wrap all the sympy functions + def wrap_1lists(name): + func = eval("lib.%s" % name, {"lib": lib}) + + def ret(lst1, out): + func(ffi.cast("double *", lst1.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return ret + + def wrap_2lists(name): + func = eval("lib.%s" % name, {"lib": lib}) + + def ret(lst1, lst2, out): + func(ffi.cast("double *", lst1.ctypes.data), + ffi.cast("double *", lst2.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return ret + + def wrap_1list_1float(name): + func = eval("lib.%s" % name, {"lib": lib}) + + def ret(lst1, fl, out): + func(ffi.cast("double *", lst1.ctypes.data), + ffi.cast("double", fl), + ffi.cast("double *", out.ctypes.data)) + return ret + + self.f = wrap_1list_1float("f_fun") + self.F = wrap_1list_1float("F_fun") + + self.err_function = wrap_2lists("err_fun") + self.inv_err_function = wrap_2lists("inv_err_fun") + self.H_mod = wrap_1lists("H_mod_fun") + + self.hs, self.Hs, self.Hes = {}, {}, {} + for kind in kinds: + self.hs[kind] = wrap_2lists("h_%d" % kind) + self.Hs[kind] = wrap_2lists("H_%d" % kind) + if self.msckf and kind in self.feature_track_kinds: + self.Hes[kind] = wrap_2lists("He_%d" % kind) + + # wrap the C++ predict function + def _predict_blas(x, P, dt): + lib.predict(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", P.ctypes.data), + ffi.cast("double *", self.Q.ctypes.data), + ffi.cast("double", dt)) + return x, P + + # wrap the C++ update function + def fun_wrapper(f, kind): + f = eval("lib.%s" % f, {"lib": lib}) + + def _update_inner_blas(x, P, z, R, extra_args): + f(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", P.ctypes.data), + ffi.cast("double *", z.ctypes.data), + ffi.cast("double *", R.ctypes.data), + ffi.cast("double *", extra_args.ctypes.data)) + if self.msckf and kind in self.feature_track_kinds: + y = z[:-len(extra_args)] + else: + y = z + return x, P, y + return _update_inner_blas + + self._updates = {} + for kind in kinds: + self._updates[kind] = fun_wrapper("update_%d" % kind, kind) + + def _update_blas(x, P, kind, z, R, extra_args=[]): + return self._updates[kind](x, P, z, R, extra_args) + + # assign the functions + self._predict = _predict_blas + # self._predict = self._predict_python + self._update = _update_blas + # self._update = self._update_python + + def init_state(self, state, covs, filter_time): + self.x = np.array(state.reshape((-1, 1))).astype(np.float64) + self.P = np.array(covs).astype(np.float64) + self.filter_time = filter_time + self.augment_times = [0] * self.N + self.rewind_obscache = [] + self.rewind_t = [] + self.rewind_states = [] + + def augment(self): + # TODO this is not a generalized way of doing this and implies that the augmented states + # are simply the first (dim_augment_state) elements of the main state. + assert self.msckf + d1 = self.dim_main + d2 = self.dim_main_err + d3 = self.dim_augment + d4 = self.dim_augment_err + + # push through augmented states + self.x[d1:-d3] = self.x[d1 + d3:] + self.x[-d3:] = self.x[:d3] + assert self.x.shape == (self.dim_x, 1) + + # push through augmented covs + assert self.P.shape == (self.dim_err, self.dim_err) + P_reduced = self.P + P_reduced = np.delete(P_reduced, np.s_[d2:d2 + d4], axis=1) + P_reduced = np.delete(P_reduced, np.s_[d2:d2 + d4], axis=0) + assert P_reduced.shape == (self.dim_err - d4, self.dim_err - d4) + to_mult = np.zeros((self.dim_err, self.dim_err - d4)) + to_mult[:-d4, :] = np.eye(self.dim_err - d4) + to_mult[-d4:, :d4] = np.eye(d4) + self.P = to_mult.dot(P_reduced.dot(to_mult.T)) + self.augment_times = self.augment_times[1:] + self.augment_times.append(self.filter_time) + assert self.P.shape == (self.dim_err, self.dim_err) + + def state(self): + return np.array(self.x).flatten() + + def covs(self): + return self.P + + def rewind(self, t): + # find where we are rewinding to + idx = bisect_right(self.rewind_t, t) + assert self.rewind_t[idx - 1] <= t + assert self.rewind_t[idx] > t # must be true, or rewind wouldn't be called + + # set the state to the time right before that + self.filter_time = self.rewind_t[idx - 1] + self.x[:] = self.rewind_states[idx - 1][0] + self.P[:] = self.rewind_states[idx - 1][1] + + # return the observations we rewound over for fast forwarding + ret = self.rewind_obscache[idx:] + + # throw away the old future + # TODO: is this making a copy? + self.rewind_t = self.rewind_t[:idx] + self.rewind_states = self.rewind_states[:idx] + self.rewind_obscache = self.rewind_obscache[:idx] + + return ret + + def checkpoint(self, obs): + # push to rewinder + self.rewind_t.append(self.filter_time) + self.rewind_states.append((np.copy(self.x), np.copy(self.P))) + self.rewind_obscache.append(obs) + + # only keep a certain number around + REWIND_TO_KEEP = 512 + self.rewind_t = self.rewind_t[-REWIND_TO_KEEP:] + self.rewind_states = self.rewind_states[-REWIND_TO_KEEP:] + self.rewind_obscache = self.rewind_obscache[-REWIND_TO_KEEP:] + + def predict_and_update_batch(self, t, kind, z, R, extra_args=[[]], augment=False): + # TODO handle rewinding at this level" + + # rewind + if self.filter_time is not None and t < self.filter_time: + if len(self.rewind_t) == 0 or t < self.rewind_t[0] or t < self.rewind_t[-1] - 1.0: + print("observation too old at %.3f with filter at %.3f, ignoring" % (t, self.filter_time)) + return None + rewound = self.rewind(t) + else: + rewound = [] + + ret = self._predict_and_update_batch(t, kind, z, R, extra_args, augment) + + # optional fast forward + for r in rewound: + self._predict_and_update_batch(*r) + + return ret + + def _predict_and_update_batch(self, t, kind, z, R, extra_args, augment=False): + """The main kalman filter function + Predicts the state and then updates a batch of observations + + dim_x: dimensionality of the state space + dim_z: dimensionality of the observation and depends on kind + n: number of observations + + Args: + t (float): Time of observation + kind (int): Type of observation + z (vec [n,dim_z]): Measurements + R (mat [n,dim_z, dim_z]): Measurement Noise + extra_args (list, [n]): Values used in H computations + """ + # initialize time + if self.filter_time is None: + self.filter_time = t + + # predict + dt = t - self.filter_time + assert dt >= 0 + self.x, self.P = self._predict(self.x, self.P, dt) + self.filter_time = t + xk_km1, Pk_km1 = np.copy(self.x).flatten(), np.copy(self.P) + + # update batch + y = [] + for i in range(len(z)): + # these are from the user, so we canonicalize them + z_i = np.array(z[i], dtype=np.float64, order='F') + R_i = np.array(R[i], dtype=np.float64, order='F') + extra_args_i = np.array(extra_args[i], dtype=np.float64, order='F') + # update + self.x, self.P, y_i = self._update(self.x, self.P, kind, z_i, R_i, extra_args=extra_args_i) + y.append(y_i) + xk_k, Pk_k = np.copy(self.x).flatten(), np.copy(self.P) + + if augment: + self.augment() + + # checkpoint + self.checkpoint((t, kind, z, R, extra_args)) + + return xk_km1, xk_k, Pk_km1, Pk_k, t, kind, y, z, extra_args + + def _predict_python(self, x, P, dt): + x_new = np.zeros(x.shape, dtype=np.float64) + self.f(x, dt, x_new) + + F = np.zeros(P.shape, dtype=np.float64) + self.F(x, dt, F) + + if not self.msckf: + P = dot(dot(F, P), F.T) + else: + # Update the predicted state covariance: + # Pk+1|k = |F*Pii*FT + Q*dt F*Pij | + # |PijT*FT Pjj | + # Where F is the jacobian of the main state + # predict function, Pii is the main state's + # covariance and Q its process noise. Pij + # is the covariance between the augmented + # states and the main state. + # + d2 = self.dim_main_err # known at compile time + F_curr = F[:d2, :d2] + P[:d2, :d2] = (F_curr.dot(P[:d2, :d2])).dot(F_curr.T) + P[:d2, d2:] = F_curr.dot(P[:d2, d2:]) + P[d2:, :d2] = P[d2:, :d2].dot(F_curr.T) + + P += dt * self.Q + return x_new, P + + def _update_python(self, x, P, kind, z, R, extra_args=[]): + # init vars + z = z.reshape((-1, 1)) + h = np.zeros(z.shape, dtype=np.float64) + H = np.zeros((z.shape[0], self.dim_x), dtype=np.float64) + + # C functions + self.hs[kind](x, extra_args, h) + self.Hs[kind](x, extra_args, H) + + # y is the "loss" + y = z - h + + # *** same above this line *** + + if self.msckf and kind in self.Hes: + # Do some algebraic magic to decorrelate + He = np.zeros((z.shape[0], len(extra_args)), dtype=np.float64) + self.Hes[kind](x, extra_args, He) + + # TODO: Don't call a function here, do projection locally + A = null(He.T) + + y = A.T.dot(y) + H = A.T.dot(H) + R = A.T.dot(R.dot(A)) + + # TODO If nullspace isn't the dimension we want + if A.shape[1] + He.shape[1] != A.shape[0]: + print('Warning: null space projection failed, measurement ignored') + return x, P, np.zeros(A.shape[0] - He.shape[1]) + + # if using eskf + H_mod = np.zeros((x.shape[0], P.shape[0]), dtype=np.float64) + self.H_mod(x, H_mod) + H = H.dot(H_mod) + + # Do mahalobis distance test + # currently just runs on msckf observations + # could run on anything if needed + if self.msckf and kind in self.maha_test_kinds: + a = np.linalg.inv(H.dot(P).dot(H.T) + R) + maha_dist = y.T.dot(a.dot(y)) + if maha_dist > chi2_ppf(0.95, y.shape[0]): + R = 10e16 * R + + # *** same below this line *** + + # Outlier resilient weighting as described in: + # "A Kalman Filter for Robust Outlier Detection - Jo-Anne Ting, ..." + weight = 1 # (1.5)/(1 + np.sum(y**2)/np.sum(R)) + + S = dot(dot(H, P), H.T) + R / weight + K = solve(S, dot(H, P.T)).T + I_KH = np.eye(P.shape[0]) - dot(K, H) + + # update actual state + delta_x = dot(K, y) + P = dot(dot(I_KH, P), I_KH.T) + dot(dot(K, R), K.T) + + # inject observed error into state + x_new = np.zeros(x.shape, dtype=np.float64) + self.err_function(x, delta_x, x_new) + return x_new, P, y.flatten() + + def maha_test(self, x, P, kind, z, R, extra_args=[], maha_thresh=0.95): + # init vars + z = z.reshape((-1, 1)) + h = np.zeros(z.shape, dtype=np.float64) + H = np.zeros((z.shape[0], self.dim_x), dtype=np.float64) + + # C functions + self.hs[kind](x, extra_args, h) + self.Hs[kind](x, extra_args, H) + + # y is the "loss" + y = z - h + + # if using eskf + H_mod = np.zeros((x.shape[0], P.shape[0]), dtype=np.float64) + self.H_mod(x, H_mod) + H = H.dot(H_mod) + + a = np.linalg.inv(H.dot(P).dot(H.T) + R) + maha_dist = y.T.dot(a.dot(y)) + if maha_dist > chi2_ppf(maha_thresh, y.shape[0]): + return False + else: + return True + + def rts_smooth(self, estimates, norm_quats=False): + ''' + Returns rts smoothed results of + kalman filter estimates + + If the kalman state is augmented with + old states only the main state is smoothed + ''' + xk_n = estimates[-1][0] + Pk_n = estimates[-1][2] + Fk_1 = np.zeros(Pk_n.shape, dtype=np.float64) + + states_smoothed = [xk_n] + covs_smoothed = [Pk_n] + for k in range(len(estimates) - 2, -1, -1): + xk1_n = xk_n + if norm_quats: + xk1_n[3:7] /= np.linalg.norm(xk1_n[3:7]) + Pk1_n = Pk_n + + xk1_k, _, Pk1_k, _, t2, _, _, _, _ = estimates[k + 1] + _, xk_k, _, Pk_k, t1, _, _, _, _ = estimates[k] + dt = t2 - t1 + self.F(xk_k, dt, Fk_1) + + d1 = self.dim_main + d2 = self.dim_main_err + Ck = np.linalg.solve(Pk1_k[:d2, :d2], Fk_1[:d2, :d2].dot(Pk_k[:d2, :d2].T)).T + xk_n = xk_k + delta_x = np.zeros((Pk_n.shape[0], 1), dtype=np.float64) + self.inv_err_function(xk1_k, xk1_n, delta_x) + delta_x[:d2] = Ck.dot(delta_x[:d2]) + x_new = np.zeros((xk_n.shape[0], 1), dtype=np.float64) + self.err_function(xk_k, delta_x, x_new) + xk_n[:d1] = x_new[:d1, 0] + Pk_n = Pk_k + Pk_n[:d2, :d2] = Pk_k[:d2, :d2] + Ck.dot(Pk1_n[:d2, :d2] - Pk1_k[:d2, :d2]).dot(Ck.T) + states_smoothed.append(xk_n) + covs_smoothed.append(Pk_n) + + return np.flipud(np.vstack(states_smoothed)), np.stack(covs_smoothed, 0)[::-1] diff --git a/selfdrive/locationd/kalman/helpers/feature_handler.py b/selfdrive/locationd/kalman/helpers/feature_handler.py new file mode 100755 index 000000000..74eb531f5 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/feature_handler.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +import os + +import numpy as np + +import common.transformations.orientation as orient +from selfdrive.locationd.kalman.helpers import (TEMPLATE_DIR, load_code, + write_code) +from selfdrive.locationd.kalman.helpers.sympy_helpers import quat_matrix_l + + +def sane(track): + img_pos = track[1:, 2:4] + diffs_x = abs(img_pos[1:, 0] - img_pos[:-1, 0]) + diffs_y = abs(img_pos[1:, 1] - img_pos[:-1, 1]) + for i in range(1, len(diffs_x)): + if ((diffs_x[i] > 0.05 or diffs_x[i - 1] > 0.05) and + (diffs_x[i] > 2 * diffs_x[i - 1] or + diffs_x[i] < .5 * diffs_x[i - 1])) or \ + ((diffs_y[i] > 0.05 or diffs_y[i - 1] > 0.05) and + (diffs_y[i] > 2 * diffs_y[i - 1] or + diffs_y[i] < .5 * diffs_y[i - 1])): + return False + return True + + +class FeatureHandler(): + name = 'feature_handler' + + @staticmethod + def generate_code(K=5): + # Wrap c code for slow matching + c_header = "\nvoid merge_features(double *tracks, double *features, long long *empty_idxs);" + + c_code = "#include \n" + c_code += "#include \n" + c_code += "#define K %d\n" % K + c_code += "\n" + open(os.path.join(TEMPLATE_DIR, "feature_handler.c")).read() + + filename = f"{FeatureHandler.name}_{K}" + write_code(filename, c_code, c_header) + + def __init__(self, K=5): + self.MAX_TRACKS = 6000 + self.K = K + + # Array of tracks, each track has K 5D features preceded + # by 5 params that inidicate [f_idx, last_idx, updated, complete, valid] + # f_idx: idx of current last feature in track + # idx of of last feature in frame + # bool for whether this track has been update + # bool for whether this track is complete + # bool for whether this track is valid + self.tracks = np.zeros((self.MAX_TRACKS, K + 1, 5)) + self.tracks[:] = np.nan + + name = f"{FeatureHandler.name}_{K}" + ffi, lib = load_code(name) + + def merge_features_c(tracks, features, empty_idxs): + lib.merge_features(ffi.cast("double *", tracks.ctypes.data), + ffi.cast("double *", features.ctypes.data), + ffi.cast("long long *", empty_idxs.ctypes.data)) + + # self.merge_features = self.merge_features_python + self.merge_features = merge_features_c + + def reset(self): + self.tracks[:] = np.nan + + def merge_features_python(self, tracks, features, empty_idxs): + empty_idx = 0 + for f in features: + match_idx = int(f[4]) + if tracks[match_idx, 0, 1] == match_idx and tracks[match_idx, 0, 2] == 0: + tracks[match_idx, 0, 0] += 1 + tracks[match_idx, 0, 1] = f[1] + tracks[match_idx, 0, 2] = 1 + tracks[match_idx, int(tracks[match_idx, 0, 0])] = f + if tracks[match_idx, 0, 0] == self.K: + tracks[match_idx, 0, 3] = 1 + if sane(tracks[match_idx]): + tracks[match_idx, 0, 4] = 1 + else: + if empty_idx == len(empty_idxs): + print('need more empty space') + continue + tracks[empty_idxs[empty_idx], 0, 0] = 1 + tracks[empty_idxs[empty_idx], 0, 1] = f[1] + tracks[empty_idxs[empty_idx], 0, 2] = 1 + tracks[empty_idxs[empty_idx], 1] = f + empty_idx += 1 + + def update_tracks(self, features): + last_idxs = np.copy(self.tracks[:, 0, 1]) + real = np.isfinite(last_idxs) + self.tracks[last_idxs[real].astype(int)] = self.tracks[real] + + mask = np.ones(self.MAX_TRACKS, np.bool) + mask[last_idxs[real].astype(int)] = 0 + empty_idxs = np.arange(self.MAX_TRACKS)[mask] + + self.tracks[empty_idxs] = np.nan + self.tracks[:, 0, 2] = 0 + self.merge_features(self.tracks, features, empty_idxs) + + def handle_features(self, features): + self.update_tracks(features) + valid_idxs = self.tracks[:, 0, 4] == 1 + complete_idxs = self.tracks[:, 0, 3] == 1 + stale_idxs = self.tracks[:, 0, 2] == 0 + valid_tracks = self.tracks[valid_idxs] + self.tracks[complete_idxs] = np.nan + self.tracks[stale_idxs] = np.nan + return valid_tracks[:, 1:, :4].reshape((len(valid_tracks), self.K * 4)) + + +def generate_orient_error_jac(K): + import sympy as sp + from selfdrive.locationd.kalman.helpers.sympy_helpers import quat_rotate + + x_sym = sp.MatrixSymbol('abr', 3, 1) + dtheta = sp.MatrixSymbol('dtheta', 3, 1) + delta_quat = sp.Matrix(np.ones(4)) + delta_quat[1:, :] = sp.Matrix(0.5 * dtheta[0:3, :]) + poses_sym = sp.MatrixSymbol('poses', 7 * K, 1) + img_pos_sym = sp.MatrixSymbol('img_positions', 2 * K, 1) + alpha, beta, rho = x_sym + to_c = sp.Matrix(orient.rot_matrix(-np.pi / 2, -np.pi / 2, 0)) + pos_0 = sp.Matrix(np.array(poses_sym[K * 7 - 7:K * 7 - 4])[:, 0]) + q = quat_matrix_l(poses_sym[K * 7 - 4:K * 7]) * delta_quat + quat_rot = quat_rotate(*q) + rot_g_to_0 = to_c * quat_rot.T + rows = [] + for i in range(K): + pos_i = sp.Matrix(np.array(poses_sym[i * 7:i * 7 + 3])[:, 0]) + q = quat_matrix_l(poses_sym[7 * i + 3:7 * i + 7]) * delta_quat + quat_rot = quat_rotate(*q) + rot_g_to_i = to_c * quat_rot.T + rot_0_to_i = rot_g_to_i * (rot_g_to_0.T) + trans_0_to_i = rot_g_to_i * (pos_0 - pos_i) + funct_vec = rot_0_to_i * sp.Matrix([alpha, beta, 1]) + rho * trans_0_to_i + h1, h2, h3 = funct_vec + rows.append(h1 / h3 - img_pos_sym[i * 2 + 0]) + rows.append(h2 / h3 - img_pos_sym[i * 2 + 1]) + img_pos_residual_sym = sp.Matrix(rows) + + # sympy into c + sympy_functions = [] + sympy_functions.append(('orient_error_jac', img_pos_residual_sym.jacobian(dtheta), [x_sym, poses_sym, img_pos_sym, dtheta])) + + return sympy_functions + + +if __name__ == "__main__": + # TODO: get K from argparse + FeatureHandler.generate_code() diff --git a/selfdrive/locationd/kalman/helpers/lst_sq_computer.py b/selfdrive/locationd/kalman/helpers/lst_sq_computer.py new file mode 100755 index 000000000..669c76f49 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/lst_sq_computer.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +import os +import sys + +import numpy as np +import sympy as sp + +import common.transformations.orientation as orient +from selfdrive.locationd.kalman.helpers import (TEMPLATE_DIR, load_code, + write_code) +from selfdrive.locationd.kalman.helpers.sympy_helpers import (quat_rotate, + sympy_into_c) + + +def generate_residual(K): + x_sym = sp.MatrixSymbol('abr', 3, 1) + poses_sym = sp.MatrixSymbol('poses', 7 * K, 1) + img_pos_sym = sp.MatrixSymbol('img_positions', 2 * K, 1) + alpha, beta, rho = x_sym + to_c = sp.Matrix(orient.rot_matrix(-np.pi / 2, -np.pi / 2, 0)) + pos_0 = sp.Matrix(np.array(poses_sym[K * 7 - 7:K * 7 - 4])[:, 0]) + q = poses_sym[K * 7 - 4:K * 7] + quat_rot = quat_rotate(*q) + rot_g_to_0 = to_c * quat_rot.T + rows = [] + + for i in range(K): + pos_i = sp.Matrix(np.array(poses_sym[i * 7:i * 7 + 3])[:, 0]) + q = poses_sym[7 * i + 3:7 * i + 7] + quat_rot = quat_rotate(*q) + rot_g_to_i = to_c * quat_rot.T + rot_0_to_i = rot_g_to_i * rot_g_to_0.T + trans_0_to_i = rot_g_to_i * (pos_0 - pos_i) + funct_vec = rot_0_to_i * sp.Matrix([alpha, beta, 1]) + rho * trans_0_to_i + h1, h2, h3 = funct_vec + rows.append(h1 / h3 - img_pos_sym[i * 2 + 0]) + rows.append(h2 / h3 - img_pos_sym[i * 2 + 1]) + img_pos_residual_sym = sp.Matrix(rows) + + # sympy into c + sympy_functions = [] + sympy_functions.append(('res_fun', img_pos_residual_sym, [x_sym, poses_sym, img_pos_sym])) + sympy_functions.append(('jac_fun', img_pos_residual_sym.jacobian(x_sym), [x_sym, poses_sym, img_pos_sym])) + + return sympy_functions + + +class LstSqComputer(): + name = 'pos_computer' + + @staticmethod + def generate_code(K=4): + sympy_functions = generate_residual(K) + header, code = sympy_into_c(sympy_functions) + + code += "\n#define KDIM %d\n" % K + code += "\n" + open(os.path.join(TEMPLATE_DIR, "compute_pos.c")).read() + + header += """ + void compute_pos(double *to_c, double *in_poses, double *in_img_positions, double *param, double *pos); + """ + + filename = f"{LstSqComputer.name}_{K}" + write_code(filename, code, header) + + def __init__(self, K=4, MIN_DEPTH=2, MAX_DEPTH=500): + self.to_c = orient.rot_matrix(-np.pi / 2, -np.pi / 2, 0) + self.MAX_DEPTH = MAX_DEPTH + self.MIN_DEPTH = MIN_DEPTH + + name = f"{LstSqComputer.name}_{K}" + ffi, lib = load_code(name) + + # wrap c functions + def residual_jac(x, poses, img_positions): + out = np.zeros(((K * 2, 3)), dtype=np.float64) + lib.jac_fun(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", poses.ctypes.data), + ffi.cast("double *", img_positions.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return out + self.residual_jac = residual_jac + + def residual(x, poses, img_positions): + out = np.zeros((K * 2), dtype=np.float64) + lib.res_fun(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", poses.ctypes.data), + ffi.cast("double *", img_positions.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return out + self.residual = residual + + def compute_pos_c(poses, img_positions): + pos = np.zeros(3, dtype=np.float64) + param = np.zeros(3, dtype=np.float64) + # Can't be a view for the ctype + img_positions = np.copy(img_positions) + lib.compute_pos(ffi.cast("double *", self.to_c.ctypes.data), + ffi.cast("double *", poses.ctypes.data), + ffi.cast("double *", img_positions.ctypes.data), + ffi.cast("double *", param.ctypes.data), + ffi.cast("double *", pos.ctypes.data)) + return pos, param + self.compute_pos_c = compute_pos_c + + def compute_pos(self, poses, img_positions, debug=False): + pos, param = self.compute_pos_c(poses, img_positions) + # pos, param = self.compute_pos_python(poses, img_positions) + + depth = 1 / param[2] + if debug: + # orient_err_jac = self.orient_error_jac(param, poses, img_positions, np.zeros(3)).reshape((-1,2,3)) + jac = self.residual_jac(param, poses, img_positions).reshape((-1, 2, 3)) + res = self.residual(param, poses, img_positions).reshape((-1, 2)) + return pos, param, res, jac # , orient_err_jac + elif (self.MIN_DEPTH < depth < self.MAX_DEPTH): + return pos + else: + return None + + def gauss_newton(self, fun, jac, x, args): + poses, img_positions = args + delta = 1 + counter = 0 + while abs(np.linalg.norm(delta)) > 1e-4 and counter < 30: + delta = np.linalg.pinv(jac(x, poses, img_positions)).dot(fun(x, poses, img_positions)) + x = x - delta + counter += 1 + return [x] + + def compute_pos_python(self, poses, img_positions, check_quality=False): + import scipy.optimize as opt + + # This procedure is also described + # in the MSCKF paper (Mourikis et al. 2007) + x = np.array([img_positions[-1][0], + img_positions[-1][1], 0.1]) + res = opt.leastsq(self.residual, x, Dfun=self.residual_jac, args=(poses, img_positions)) # scipy opt + # res = self.gauss_newton(self.residual, self.residual_jac, x, (poses, img_positions)) # diy gauss_newton + + alpha, beta, rho = res[0] + rot_0_to_g = (orient.rotations_from_quats(poses[-1, 3:])).dot(self.to_c.T) + return (rot_0_to_g.dot(np.array([alpha, beta, 1]))) / rho + poses[-1, :3] + + +# EXPERIMENTAL CODE +def unroll_shutter(img_positions, poses, v, rot_rates, ecef_pos): + # only speed correction for now + t_roll = 0.016 # 16ms rolling shutter? + vroll, vpitch, vyaw = rot_rates + A = 0.5 * np.array([[-1, -vroll, -vpitch, -vyaw], + [vroll, 0, vyaw, -vpitch], + [vpitch, -vyaw, 0, vroll], + [vyaw, vpitch, -vroll, 0]]) + q_dot = A.dot(poses[-1][3:7]) + v = np.append(v, q_dot) + v = np.array([v[0], v[1], v[2], 0, 0, 0, 0]) + current_pose = poses[-1] + v * 0.05 + poses = np.vstack((current_pose, poses)) + dt = -img_positions[:, 1] * t_roll / 0.48 + errs = project(poses, ecef_pos) - project(poses + np.atleast_2d(dt).T.dot(np.atleast_2d(v)), ecef_pos) + return img_positions - errs + + +def project(poses, ecef_pos): + img_positions = np.zeros((len(poses), 2)) + for i, p in enumerate(poses): + cam_frame = orient.rotations_from_quats(p[3:]).T.dot(ecef_pos - p[:3]) + img_positions[i] = np.array([cam_frame[1] / cam_frame[0], cam_frame[2] / cam_frame[0]]) + return img_positions + + +if __name__ == "__main__": + K = int(sys.argv[1].split("_")[-1]) + LstSqComputer.generate_code(K=K) + diff --git a/common/sympy_helpers.py b/selfdrive/locationd/kalman/helpers/sympy_helpers.py similarity index 58% rename from common/sympy_helpers.py rename to selfdrive/locationd/kalman/helpers/sympy_helpers.py index 78688a84c..f8c4f1c5f 100644 --- a/common/sympy_helpers.py +++ b/selfdrive/locationd/kalman/helpers/sympy_helpers.py @@ -2,31 +2,35 @@ import sympy as sp import numpy as np + def cross(x): - ret = sp.Matrix(np.zeros((3,3))) - ret[0,1], ret[0,2] = -x[2], x[1] - ret[1,0], ret[1,2] = x[2], -x[0] - ret[2,0], ret[2,1] = -x[1], x[0] + ret = sp.Matrix(np.zeros((3, 3))) + ret[0, 1], ret[0, 2] = -x[2], x[1] + ret[1, 0], ret[1, 2] = x[2], -x[0] + ret[2, 0], ret[2, 1] = -x[1], x[0] return ret + def euler_rotate(roll, pitch, yaw): # make symbolic rotation matrix from eulers - matrix_roll = sp.Matrix([[1, 0, 0], - [0, sp.cos(roll), -sp.sin(roll)], - [0, sp.sin(roll), sp.cos(roll)]]) - matrix_pitch = sp.Matrix([[sp.cos(pitch), 0, sp.sin(pitch)], - [0, 1, 0], - [-sp.sin(pitch), 0, sp.cos(pitch)]]) - matrix_yaw = sp.Matrix([[sp.cos(yaw), -sp.sin(yaw), 0], - [sp.sin(yaw), sp.cos(yaw), 0], - [0, 0, 1]]) - return matrix_yaw*matrix_pitch*matrix_roll + matrix_roll = sp.Matrix([[1, 0, 0], + [0, sp.cos(roll), -sp.sin(roll)], + [0, sp.sin(roll), sp.cos(roll)]]) + matrix_pitch = sp.Matrix([[sp.cos(pitch), 0, sp.sin(pitch)], + [0, 1, 0], + [-sp.sin(pitch), 0, sp.cos(pitch)]]) + matrix_yaw = sp.Matrix([[sp.cos(yaw), -sp.sin(yaw), 0], + [sp.sin(yaw), sp.cos(yaw), 0], + [0, 0, 1]]) + return matrix_yaw * matrix_pitch * matrix_roll + def quat_rotate(q0, q1, q2, q3): # make symbolic rotation matrix from quat - return sp.Matrix([[q0**2 + q1**2 - q2**2 - q3**2, 2*(q1*q2 + q0*q3), 2*(q1*q3 - q0*q2)], - [2*(q1*q2 - q0*q3), q0**2 - q1**2 + q2**2 - q3**2, 2*(q2*q3 + q0*q1)], - [2*(q1*q3 + q0*q2), 2*(q2*q3 - q0*q1), q0**2 - q1**2 - q2**2 + q3**2]]).T + return sp.Matrix([[q0**2 + q1**2 - q2**2 - q3**2, 2 * (q1 * q2 + q0 * q3), 2 * (q1 * q3 - q0 * q2)], + [2 * (q1 * q2 - q0 * q3), q0**2 - q1**2 + q2**2 - q3**2, 2 * (q2 * q3 + q0 * q1)], + [2 * (q1 * q3 + q0 * q2), 2 * (q2 * q3 - q0 * q1), q0**2 - q1**2 - q2**2 + q3**2]]).T + def quat_matrix_l(p): return sp.Matrix([[p[0], -p[1], -p[2], -p[3]], @@ -34,6 +38,7 @@ def quat_matrix_l(p): [p[2], p[3], p[0], -p[1]], [p[3], -p[2], p[1], p[0]]]) + def quat_matrix_r(p): return sp.Matrix([[p[0], -p[1], -p[2], -p[3]], [p[1], p[0], p[3], -p[2]], @@ -49,10 +54,11 @@ def sympy_into_c(sympy_functions): # argument ordering input to sympy is broken with function with output arguments nargs = [] + # reorder the input arguments for aa in args: if aa is None: - nargs.append(codegen.InputArgument(sp.Symbol('unused'), dimensions=[1,1])) + nargs.append(codegen.InputArgument(sp.Symbol('unused'), dimensions=[1, 1])) continue found = False for a in r.arguments: @@ -62,20 +68,23 @@ def sympy_into_c(sympy_functions): break if not found: # [1,1] is a hack for Matrices - nargs.append(codegen.InputArgument(aa, dimensions=[1,1])) + nargs.append(codegen.InputArgument(aa, dimensions=[1, 1])) + # add the output arguments for a in r.arguments: if type(a) == codegen.OutputArgument: nargs.append(a) - #assert len(r.arguments) == len(args)+1 + # assert len(r.arguments) == len(args)+1 r.arguments = nargs # add routine to list routines.append(r) [(c_name, c_code), (h_name, c_header)] = codegen.get_code_generator('C', 'ekf', 'C99').write(routines, "ekf") + c_header = '\n'.join(x for x in c_header.split("\n") if len(x) > 0 and x[0] != '#') + c_code = '\n'.join(x for x in c_code.split("\n") if len(x) > 0 and x[0] != '#') - c_header = '\n'.join(x for x in c_header.split("\n") if len(x) > 0 and x[0] != '#') + c_code = 'extern "C" {\n#include \n' + c_code + "\n}\n" return c_header, c_code diff --git a/selfdrive/locationd/kalman/models/live_kf.py b/selfdrive/locationd/kalman/models/live_kf.py new file mode 100755 index 000000000..1ee8d74ac --- /dev/null +++ b/selfdrive/locationd/kalman/models/live_kf.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +import numpy as np +import sympy as sp + +from selfdrive.locationd.kalman.helpers import KalmanError, ObservationKind +from selfdrive.locationd.kalman.helpers.ekf_sym import EKF_sym, gen_code +from selfdrive.locationd.kalman.helpers.sympy_helpers import (euler_rotate, + quat_matrix_r, + quat_rotate) +from selfdrive.swaglog import cloudlog + +EARTH_GM = 3.986005e14 # m^3/s^2 (gravitational constant * mass of earth) + + +class States(): + ECEF_POS = slice(0, 3) # x, y and z in ECEF in meters + ECEF_ORIENTATION = slice(3, 7) # quat for pose of phone in ecef + ECEF_VELOCITY = slice(7, 10) # ecef velocity in m/s + ANGULAR_VELOCITY = slice(10, 13) # roll, pitch and yaw rates in device frame in radians/s + GYRO_BIAS = slice(13, 16) # roll, pitch and yaw biases + ODO_SCALE = slice(16, 17) # odometer scale + ACCELERATION = slice(17, 20) # Acceleration in device frame in m/s**2 + IMU_OFFSET = slice(20, 23) # imu offset angles in radians + + # Error-state has different slices because it is an ESKF + ECEF_POS_ERR = slice(0, 3) + ECEF_ORIENTATION_ERR = slice(3, 6) # euler angles for orientation error + ECEF_VELOCITY_ERR = slice(6, 9) + ANGULAR_VELOCITY_ERR = slice(9, 12) + GYRO_BIAS_ERR = slice(12, 15) + ODO_SCALE_ERR = slice(15, 16) + ACCELERATION_ERR = slice(16, 19) + IMU_OFFSET_ERR = slice(19, 22) + + +class LiveKalman(): + name = 'live' + + initial_x = np.array([-2.7e6, 4.2e6, 3.8e6, + 1, 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 1, + 0, 0, 0, + 0, 0, 0]) + + # state covariance + initial_P_diag = np.array([10000**2, 10000**2, 10000**2, + 10**2, 10**2, 10**2, + 10**2, 10**2, 10**2, + 1**2, 1**2, 1**2, + 0.05**2, 0.05**2, 0.05**2, + 0.02**2, + 1**2, 1**2, 1**2, + (0.01)**2, (0.01)**2, (0.01)**2]) + + # process noise + Q = np.diag([0.03**2, 0.03**2, 0.03**2, + 0.0**2, 0.0**2, 0.0**2, + 0.0**2, 0.0**2, 0.0**2, + 0.1**2, 0.1**2, 0.1**2, + (0.005 / 100)**2, (0.005 / 100)**2, (0.005 / 100)**2, + (0.02 / 100)**2, + 3**2, 3**2, 3**2, + (0.05 / 60)**2, (0.05 / 60)**2, (0.05 / 60)**2]) + + @staticmethod + def generate_code(): + name = LiveKalman.name + dim_state = LiveKalman.initial_x.shape[0] + dim_state_err = LiveKalman.initial_P_diag.shape[0] + + state_sym = sp.MatrixSymbol('state', dim_state, 1) + state = sp.Matrix(state_sym) + x, y, z = state[States.ECEF_POS, :] + q = state[States.ECEF_ORIENTATION, :] + v = state[States.ECEF_VELOCITY, :] + vx, vy, vz = v + omega = state[States.ANGULAR_VELOCITY, :] + vroll, vpitch, vyaw = omega + roll_bias, pitch_bias, yaw_bias = state[States.GYRO_BIAS, :] + odo_scale = state[States.ODO_SCALE, :][0,:] + acceleration = state[States.ACCELERATION, :] + imu_angles = state[States.IMU_OFFSET, :] + + dt = sp.Symbol('dt') + + # calibration and attitude rotation matrices + quat_rot = quat_rotate(*q) + + # Got the quat predict equations from here + # A New Quaternion-Based Kalman Filter for + # Real-Time Attitude Estimation Using the Two-Step + # Geometrically-Intuitive Correction Algorithm + A = 0.5 * sp.Matrix([[0, -vroll, -vpitch, -vyaw], + [vroll, 0, vyaw, -vpitch], + [vpitch, -vyaw, 0, vroll], + [vyaw, vpitch, -vroll, 0]]) + q_dot = A * q + + # Time derivative of the state as a function of state + state_dot = sp.Matrix(np.zeros((dim_state, 1))) + state_dot[States.ECEF_POS, :] = v + state_dot[States.ECEF_ORIENTATION, :] = q_dot + state_dot[States.ECEF_VELOCITY, 0] = quat_rot * acceleration + + # Basic descretization, 1st order intergrator + # Can be pretty bad if dt is big + f_sym = state + dt * state_dot + + state_err_sym = sp.MatrixSymbol('state_err', dim_state_err, 1) + state_err = sp.Matrix(state_err_sym) + quat_err = state_err[States.ECEF_ORIENTATION_ERR, :] + v_err = state_err[States.ECEF_VELOCITY_ERR, :] + omega_err = state_err[States.ANGULAR_VELOCITY_ERR, :] + acceleration_err = state_err[States.ACCELERATION_ERR, :] + + # Time derivative of the state error as a function of state error and state + quat_err_matrix = euler_rotate(quat_err[0], quat_err[1], quat_err[2]) + q_err_dot = quat_err_matrix * quat_rot * (omega + omega_err) + state_err_dot = sp.Matrix(np.zeros((dim_state_err, 1))) + state_err_dot[States.ECEF_POS_ERR, :] = v_err + state_err_dot[States.ECEF_ORIENTATION_ERR, :] = q_err_dot + state_err_dot[States.ECEF_VELOCITY_ERR, :] = quat_err_matrix * quat_rot * (acceleration + acceleration_err) + f_err_sym = state_err + dt * state_err_dot + + # Observation matrix modifier + H_mod_sym = sp.Matrix(np.zeros((dim_state, dim_state_err))) + H_mod_sym[States.ECEF_POS, States.ECEF_POS_ERR] = np.eye(States.ECEF_POS.stop - States.ECEF_POS.start) + H_mod_sym[States.ECEF_ORIENTATION, States.ECEF_ORIENTATION_ERR] = 0.5 * quat_matrix_r(state[3:7])[:, 1:] + H_mod_sym[States.ECEF_ORIENTATION.stop:, States.ECEF_ORIENTATION_ERR.stop:] = np.eye(dim_state - States.ECEF_ORIENTATION.stop) + + # these error functions are defined so that say there + # is a nominal x and true x: + # true x = err_function(nominal x, delta x) + # delta x = inv_err_function(nominal x, true x) + nom_x = sp.MatrixSymbol('nom_x', dim_state, 1) + true_x = sp.MatrixSymbol('true_x', dim_state, 1) + delta_x = sp.MatrixSymbol('delta_x', dim_state_err, 1) + + err_function_sym = sp.Matrix(np.zeros((dim_state, 1))) + delta_quat = sp.Matrix(np.ones((4))) + delta_quat[1:, :] = sp.Matrix(0.5 * delta_x[States.ECEF_ORIENTATION_ERR, :]) + err_function_sym[States.ECEF_POS, :] = sp.Matrix(nom_x[States.ECEF_POS, :] + delta_x[States.ECEF_POS_ERR, :]) + err_function_sym[States.ECEF_ORIENTATION, 0] = quat_matrix_r(nom_x[States.ECEF_ORIENTATION, 0]) * delta_quat + err_function_sym[States.ECEF_ORIENTATION.stop:, :] = sp.Matrix(nom_x[States.ECEF_ORIENTATION.stop:, :] + delta_x[States.ECEF_ORIENTATION_ERR.stop:, :]) + + inv_err_function_sym = sp.Matrix(np.zeros((dim_state_err, 1))) + inv_err_function_sym[States.ECEF_POS_ERR, 0] = sp.Matrix(-nom_x[States.ECEF_POS, 0] + true_x[States.ECEF_POS, 0]) + delta_quat = quat_matrix_r(nom_x[States.ECEF_ORIENTATION, 0]).T * true_x[States.ECEF_ORIENTATION, 0] + inv_err_function_sym[States.ECEF_ORIENTATION_ERR, 0] = sp.Matrix(2 * delta_quat[1:]) + inv_err_function_sym[States.ECEF_ORIENTATION_ERR.stop:, 0] = sp.Matrix(-nom_x[States.ECEF_ORIENTATION.stop:, 0] + true_x[States.ECEF_ORIENTATION.stop:, 0]) + + eskf_params = [[err_function_sym, nom_x, delta_x], + [inv_err_function_sym, nom_x, true_x], + H_mod_sym, f_err_sym, state_err_sym] + # + # Observation functions + # + imu_rot = euler_rotate(*imu_angles) + h_gyro_sym = imu_rot * sp.Matrix([vroll + roll_bias, + vpitch + pitch_bias, + vyaw + yaw_bias]) + + pos = sp.Matrix([x, y, z]) + gravity = quat_rot.T * ((EARTH_GM / ((x**2 + y**2 + z**2)**(3.0 / 2.0))) * pos) + h_acc_sym = imu_rot * (gravity + acceleration) + h_phone_rot_sym = sp.Matrix([vroll, vpitch, vyaw]) + + speed = sp.sqrt(vx**2 + vy**2 + vz**2) + h_speed_sym = sp.Matrix([speed * odo_scale]) + + h_pos_sym = sp.Matrix([x, y, z]) + h_imu_frame_sym = sp.Matrix(imu_angles) + + h_relative_motion = sp.Matrix(quat_rot.T * v) + + obs_eqs = [[h_speed_sym, ObservationKind.ODOMETRIC_SPEED, None], + [h_gyro_sym, ObservationKind.PHONE_GYRO, None], + [h_phone_rot_sym, ObservationKind.NO_ROT, None], + [h_acc_sym, ObservationKind.PHONE_ACCEL, None], + [h_pos_sym, ObservationKind.ECEF_POS, None], + [h_relative_motion, ObservationKind.CAMERA_ODO_TRANSLATION, None], + [h_phone_rot_sym, ObservationKind.CAMERA_ODO_ROTATION, None], + [h_imu_frame_sym, ObservationKind.IMU_FRAME, None]] + + gen_code(name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state_err, eskf_params) + + def __init__(self): + self.dim_state = self.initial_x.shape[0] + self.dim_state_err = self.initial_P_diag.shape[0] + + self.obs_noise = {ObservationKind.ODOMETRIC_SPEED: np.atleast_2d(0.2**2), + ObservationKind.PHONE_GYRO: np.diag([0.025**2, 0.025**2, 0.025**2]), + ObservationKind.PHONE_ACCEL: np.diag([.5**2, .5**2, .5**2]), + ObservationKind.CAMERA_ODO_ROTATION: np.diag([0.05**2, 0.05**2, 0.05**2]), + ObservationKind.IMU_FRAME: np.diag([0.05**2, 0.05**2, 0.05**2]), + ObservationKind.NO_ROT: np.diag([0.00025**2, 0.00025**2, 0.00025**2]), + ObservationKind.ECEF_POS: np.diag([5**2, 5**2, 5**2])} + + # init filter + self.filter = EKF_sym(self.name, self.Q, self.initial_x, np.diag(self.initial_P_diag), self.dim_state, self.dim_state_err) + + @property + def x(self): + return self.filter.state() + + @property + def t(self): + return self.filter.filter_time + + @property + def P(self): + return self.filter.covs() + + def rts_smooth(self, estimates): + return self.filter.rts_smooth(estimates, norm_quats=True) + + def init_state(self, state, covs_diag=None, covs=None, filter_time=None): + if covs_diag is not None: + P = np.diag(covs_diag) + elif covs is not None: + P = covs + else: + P = self.filter.covs() + self.filter.init_state(state, P, filter_time) + + def predict_and_observe(self, t, kind, data): + if len(data) > 0: + data = np.atleast_2d(data) + if kind == ObservationKind.CAMERA_ODO_TRANSLATION: + r = self.predict_and_update_odo_trans(data, t, kind) + elif kind == ObservationKind.CAMERA_ODO_ROTATION: + r = self.predict_and_update_odo_rot(data, t, kind) + elif kind == ObservationKind.ODOMETRIC_SPEED: + r = self.predict_and_update_odo_speed(data, t, kind) + else: + r = self.filter.predict_and_update_batch(t, kind, data, self.get_R(kind, len(data))) + + # Normalize quats + quat_norm = np.linalg.norm(self.filter.x[3:7, 0]) + + # Should not continue if the quats behave this weirdly + if not (0.1 < quat_norm < 10): + cloudlog.error("Kalman filter quaternions unstable") + raise KalmanError + + self.filter.x[States.ECEF_ORIENTATION, 0] = self.filter.x[States.ECEF_ORIENTATION, 0] / quat_norm + + return r + + def get_R(self, kind, n): + obs_noise = self.obs_noise[kind] + dim = obs_noise.shape[0] + R = np.zeros((n, dim, dim)) + for i in range(n): + R[i, :, :] = obs_noise + return R + + def predict_and_update_odo_speed(self, speed, t, kind): + z = np.array(speed) + R = np.zeros((len(speed), 1, 1)) + for i, _ in enumerate(z): + R[i, :, :] = np.diag([0.2**2]) + return self.filter.predict_and_update_batch(t, kind, z, R) + + def predict_and_update_odo_trans(self, trans, t, kind): + z = trans[:, :3] + R = np.zeros((len(trans), 3, 3)) + for i, _ in enumerate(z): + R[i, :, :] = np.diag(trans[i, 3:]**2) + return self.filter.predict_and_update_batch(t, kind, z, R) + + def predict_and_update_odo_rot(self, rot, t, kind): + z = rot[:, :3] + R = np.zeros((len(rot), 3, 3)) + for i, _ in enumerate(z): + R[i, :, :] = np.diag(rot[i, 3:]**2) + return self.filter.predict_and_update_batch(t, kind, z, R) + + +if __name__ == "__main__": + LiveKalman.generate_code() diff --git a/selfdrive/locationd/kalman/templates/compute_pos.c b/selfdrive/locationd/kalman/templates/compute_pos.c new file mode 100644 index 000000000..9499fa7b6 --- /dev/null +++ b/selfdrive/locationd/kalman/templates/compute_pos.c @@ -0,0 +1,54 @@ +#include +#include +#include + +typedef Eigen::Matrix R3M; +typedef Eigen::Matrix R1M; +typedef Eigen::Matrix O1M; +typedef Eigen::Matrix M3D; + +extern "C" { +void gauss_newton(double *in_x, double *in_poses, double *in_img_positions) { + + double res[KDIM*2] = {0}; + double jac[KDIM*6] = {0}; + + O1M x(in_x); + O1M delta; + int counter = 0; + while ((delta.squaredNorm() > 0.0001 and counter < 30) or counter == 0){ + res_fun(in_x, in_poses, in_img_positions, res); + jac_fun(in_x, in_poses, in_img_positions, jac); + R1M E(res); R3M J(jac); + delta = (J.transpose()*J).inverse() * J.transpose() * E; + x = x - delta; + memcpy(in_x, x.data(), 3 * sizeof(double)); + counter = counter + 1; + } +} + + +void compute_pos(double *to_c, double *poses, double *img_positions, double *param, double *pos) { + param[0] = img_positions[KDIM*2-2]; + param[1] = img_positions[KDIM*2-1]; + param[2] = 0.1; + gauss_newton(param, poses, img_positions); + + Eigen::Quaterniond q; + q.w() = poses[KDIM*7-4]; + q.x() = poses[KDIM*7-3]; + q.y() = poses[KDIM*7-2]; + q.z() = poses[KDIM*7-1]; + M3D RC(to_c); + Eigen::Matrix3d R = q.normalized().toRotationMatrix(); + Eigen::Matrix3d rot = R * RC.transpose(); + + pos[0] = param[0]/param[2]; + pos[1] = param[1]/param[2]; + pos[2] = 1.0/param[2]; + O1M ecef_offset(poses + KDIM*7-7); + O1M ecef_output(pos); + ecef_output = rot*ecef_output + ecef_offset; + memcpy(pos, ecef_output.data(), 3 * sizeof(double)); +} +} diff --git a/selfdrive/locationd/kalman/templates/ekf_c.c b/selfdrive/locationd/kalman/templates/ekf_c.c new file mode 100644 index 000000000..e524f81ec --- /dev/null +++ b/selfdrive/locationd/kalman/templates/ekf_c.c @@ -0,0 +1,123 @@ +#include +#include + +typedef Eigen::Matrix DDM; +typedef Eigen::Matrix EEM; +typedef Eigen::Matrix DEM; + +void predict(double *in_x, double *in_P, double *in_Q, double dt) { + typedef Eigen::Matrix RRM; + + double nx[DIM] = {0}; + double in_F[EDIM*EDIM] = {0}; + + // functions from sympy + f_fun(in_x, dt, nx); + F_fun(in_x, dt, in_F); + + + EEM F(in_F); + EEM P(in_P); + EEM Q(in_Q); + + RRM F_main = F.topLeftCorner(MEDIM, MEDIM); + P.topLeftCorner(MEDIM, MEDIM) = (F_main * P.topLeftCorner(MEDIM, MEDIM)) * F_main.transpose(); + P.topRightCorner(MEDIM, EDIM - MEDIM) = F_main * P.topRightCorner(MEDIM, EDIM - MEDIM); + P.bottomLeftCorner(EDIM - MEDIM, MEDIM) = P.bottomLeftCorner(EDIM - MEDIM, MEDIM) * F_main.transpose(); + + P = P + dt*Q; + + // copy out state + memcpy(in_x, nx, DIM * sizeof(double)); + memcpy(in_P, P.data(), EDIM * EDIM * sizeof(double)); +} + +// note: extra_args dim only correct when null space projecting +// otherwise 1 +template +void update(double *in_x, double *in_P, Hfun h_fun, Hfun H_fun, Hfun Hea_fun, double *in_z, double *in_R, double *in_ea, double MAHA_THRESHOLD) { + typedef Eigen::Matrix ZZM; + typedef Eigen::Matrix ZDM; + typedef Eigen::Matrix XEM; + //typedef Eigen::Matrix EZM; + typedef Eigen::Matrix X1M; + typedef Eigen::Matrix XXM; + + double in_hx[ZDIM] = {0}; + double in_H[ZDIM * DIM] = {0}; + double in_H_mod[EDIM * DIM] = {0}; + double delta_x[EDIM] = {0}; + double x_new[DIM] = {0}; + + + // state x, P + Eigen::Matrix z(in_z); + EEM P(in_P); + ZZM pre_R(in_R); + + // functions from sympy + h_fun(in_x, in_ea, in_hx); + H_fun(in_x, in_ea, in_H); + ZDM pre_H(in_H); + + // get y (y = z - hx) + Eigen::Matrix pre_y(in_hx); pre_y = z - pre_y; + X1M y; XXM H; XXM R; + if (Hea_fun){ + typedef Eigen::Matrix ZAM; + double in_Hea[ZDIM * EADIM] = {0}; + Hea_fun(in_x, in_ea, in_Hea); + ZAM Hea(in_Hea); + XXM A = Hea.transpose().fullPivLu().kernel(); + + + y = A.transpose() * pre_y; + H = A.transpose() * pre_H; + R = A.transpose() * pre_R * A; + } else { + y = pre_y; + H = pre_H; + R = pre_R; + } + // get modified H + H_mod_fun(in_x, in_H_mod); + DEM H_mod(in_H_mod); + XEM H_err = H * H_mod; + + // Do mahalobis distance test + if (MAHA_TEST){ + XXM a = (H_err * P * H_err.transpose() + R).inverse(); + double maha_dist = y.transpose() * a * y; + if (maha_dist > MAHA_THRESHOLD){ + R = 1.0e16 * R; + } + } + + // Outlier resilient weighting + double weight = 1;//(1.5)/(1 + y.squaredNorm()/R.sum()); + + // kalman gains and I_KH + XXM S = ((H_err * P) * H_err.transpose()) + R/weight; + XEM KT = S.fullPivLu().solve(H_err * P.transpose()); + //EZM K = KT.transpose(); TODO: WHY DOES THIS NOT COMPILE? + //EZM K = S.fullPivLu().solve(H_err * P.transpose()).transpose(); + //std::cout << "Here is the matrix rot:\n" << K << std::endl; + EEM I_KH = Eigen::Matrix::Identity() - (KT.transpose() * H_err); + + // update state by injecting dx + Eigen::Matrix dx(delta_x); + dx = (KT.transpose() * y); + memcpy(delta_x, dx.data(), EDIM * sizeof(double)); + err_fun(in_x, delta_x, x_new); + Eigen::Matrix x(x_new); + + // update cov + P = ((I_KH * P) * I_KH.transpose()) + ((KT.transpose() * R) * KT); + + // copy out state + memcpy(in_x, x.data(), DIM * sizeof(double)); + memcpy(in_P, P.data(), EDIM * EDIM * sizeof(double)); + memcpy(in_z, y.data(), y.rows() * sizeof(double)); +} + + diff --git a/selfdrive/locationd/kalman/templates/feature_handler.c b/selfdrive/locationd/kalman/templates/feature_handler.c new file mode 100644 index 000000000..330972339 --- /dev/null +++ b/selfdrive/locationd/kalman/templates/feature_handler.c @@ -0,0 +1,58 @@ +extern "C"{ +bool sane(double track [K + 1][5]) { + double diffs_x [K-1]; + double diffs_y [K-1]; + int i; + for (i = 0; i < K-1; i++) { + diffs_x[i] = fabs(track[i+2][2] - track[i+1][2]); + diffs_y[i] = fabs(track[i+2][3] - track[i+1][3]); + } + for (i = 1; i < K-1; i++) { + if (((diffs_x[i] > 0.05 or diffs_x[i-1] > 0.05) and + (diffs_x[i] > 2*diffs_x[i-1] or + diffs_x[i] < .5*diffs_x[i-1])) or + ((diffs_y[i] > 0.05 or diffs_y[i-1] > 0.05) and + (diffs_y[i] > 2*diffs_y[i-1] or + diffs_y[i] < .5*diffs_y[i-1]))){ + return false; + } + } + return true; +} + +void merge_features(double *tracks, double *features, long long *empty_idxs) { + double feature_arr [3000][5]; + memcpy(feature_arr, features, 3000 * 5 * sizeof(double)); + double track_arr [6000][K + 1][5]; + memcpy(track_arr, tracks, (K+1) * 6000 * 5 * sizeof(double)); + int match; + int empty_idx = 0; + int idx; + for (int i = 0; i < 3000; i++) { + match = feature_arr[i][4]; + if (track_arr[match][0][1] == match and track_arr[match][0][2] == 0){ + track_arr[match][0][0] = track_arr[match][0][0] + 1; + track_arr[match][0][1] = feature_arr[i][1]; + track_arr[match][0][2] = 1; + idx = track_arr[match][0][0]; + memcpy(track_arr[match][idx], feature_arr[i], 5 * sizeof(double)); + if (idx == K){ + // label complete + track_arr[match][0][3] = 1; + if (sane(track_arr[match])){ + // label valid + track_arr[match][0][4] = 1; + } + } + } else { + // gen new track with this feature + track_arr[empty_idxs[empty_idx]][0][0] = 1; + track_arr[empty_idxs[empty_idx]][0][1] = feature_arr[i][1]; + track_arr[empty_idxs[empty_idx]][0][2] = 1; + memcpy(track_arr[empty_idxs[empty_idx]][1], feature_arr[i], 5 * sizeof(double)); + empty_idx = empty_idx + 1; + } + } + memcpy(tracks, track_arr, (K+1) * 6000 * 5 * sizeof(double)); +} +} diff --git a/selfdrive/locationd/locationd.py b/selfdrive/locationd/locationd.py new file mode 100755 index 000000000..3cf9560d6 --- /dev/null +++ b/selfdrive/locationd/locationd.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +import math + +import numpy as np + +import cereal.messaging as messaging +import common.transformations.coordinates as coord +from common.transformations.orientation import (ecef_euler_from_ned, + euler2quat, + ned_euler_from_ecef, + quat2euler, + rotations_from_quats) +from selfdrive.locationd.kalman.helpers import ObservationKind, KalmanError +from selfdrive.locationd.kalman.models.live_kf import LiveKalman, States +from selfdrive.swaglog import cloudlog + +VISION_DECIMATION = 2 +SENSOR_DECIMATION = 10 + + +class Localizer(): + def __init__(self, disabled_logs=[], dog=None): + self.kf = LiveKalman() + self.reset_kalman() + self.max_age = .2 # seconds + self.disabled_logs = disabled_logs + + def liveLocationMsg(self, time): + fix = messaging.log.LiveLocationData.new_message() + + predicted_state = self.kf.x + + fix_ecef = predicted_state[States.ECEF_POS] + fix_pos_geo = coord.ecef2geodetic(fix_ecef) + fix.lat = float(fix_pos_geo[0]) + fix.lon = float(fix_pos_geo[1]) + fix.alt = float(fix_pos_geo[2]) + + fix.speed = float(np.linalg.norm(predicted_state[States.ECEF_VELOCITY])) + + orientation_ned_euler = ned_euler_from_ecef(fix_ecef, quat2euler(predicted_state[States.ECEF_ORIENTATION])) + fix.roll = math.degrees(orientation_ned_euler[0]) + fix.pitch = math.degrees(orientation_ned_euler[1]) + fix.heading = math.degrees(orientation_ned_euler[2]) + + fix.gyro = [float(predicted_state[10]), float(predicted_state[11]), float(predicted_state[12])] + fix.accel = [float(predicted_state[19]), float(predicted_state[20]), float(predicted_state[21])] + + ned_vel = self.converter.ecef2ned(predicted_state[States.ECEF_POS] + predicted_state[States.ECEF_VELOCITY]) - self.converter.ecef2ned(predicted_state[States.ECEF_POS]) + fix.vNED = [float(ned_vel[0]), float(ned_vel[1]), float(ned_vel[2])] + fix.source = 'kalman' + + #local_vel = rotations_from_quats(predicted_state[States.ECEF_ORIENTATION]).T.dot(predicted_state[States.ECEF_VELOCITY]) + #fix.pitchCalibration = math.degrees(math.atan2(local_vel[2], local_vel[0])) + #fix.yawCalibration = math.degrees(math.atan2(local_vel[1], local_vel[0])) + + imu_frame = predicted_state[States.IMU_OFFSET] + fix.imuFrame = [math.degrees(imu_frame[0]), math.degrees(imu_frame[1]), math.degrees(imu_frame[2])] + return fix + + def update_kalman(self, time, kind, meas): + if self.filter_ready: + try: + self.kf.predict_and_observe(time, kind, meas) + except KalmanError: + cloudlog.error("Error in predict and observe, kalman reset") + self.reset_kalman() + #idx = bisect_right([x[0] for x in self.observation_buffer], time) + #self.observation_buffer.insert(idx, (time, kind, meas)) + #while len(self.observation_buffer) > 0 and self.observation_buffer[-1][0] - self.observation_buffer[0][0] > self.max_age: + # else: + # self.observation_buffer.pop(0) + + def handle_gps(self, current_time, log): + self.converter = coord.LocalCoord.from_geodetic([log.latitude, log.longitude, log.altitude]) + fix_ecef = self.converter.ned2ecef([0, 0, 0]) + + # TODO initing with bad bearing not allowed, maybe not bad? + if not self.filter_ready and log.speed > 5: + self.filter_ready = True + initial_ecef = fix_ecef + gps_bearing = math.radians(log.bearing) + initial_pose_ecef = ecef_euler_from_ned(initial_ecef, [0, 0, gps_bearing]) + initial_pose_ecef_quat = euler2quat(initial_pose_ecef) + gps_speed = log.speed + quat_uncertainty = 0.2**2 + initial_pose_ecef_quat = euler2quat(initial_pose_ecef) + + initial_state = LiveKalman.initial_x + initial_covs_diag = LiveKalman.initial_P_diag + + initial_state[States.ECEF_POS] = initial_ecef + initial_state[States.ECEF_ORIENTATION] = initial_pose_ecef_quat + initial_state[States.ECEF_VELOCITY] = rotations_from_quats(initial_pose_ecef_quat).dot(np.array([gps_speed, 0, 0])) + + initial_covs_diag[States.ECEF_POS_ERR] = 10**2 + initial_covs_diag[States.ECEF_ORIENTATION_ERR] = quat_uncertainty + initial_covs_diag[States.ECEF_VELOCITY_ERR] = 1**2 + self.kf.init_state(initial_state, covs=np.diag(initial_covs_diag), filter_time=current_time) + cloudlog.info("Filter initialized") + elif self.filter_ready: + self.update_kalman(current_time, ObservationKind.ECEF_POS, fix_ecef) + gps_est_error = np.sqrt((self.kf.x[0] - fix_ecef[0])**2 + + (self.kf.x[1] - fix_ecef[1])**2 + + (self.kf.x[2] - fix_ecef[2])**2) + if gps_est_error > 50: + cloudlog.error("Locationd vs ubloxLocation difference too large, kalman reset") + self.reset_kalman() + + def handle_car_state(self, current_time, log): + self.speed_counter += 1 + + if self.speed_counter % SENSOR_DECIMATION == 0: + self.update_kalman(current_time, ObservationKind.ODOMETRIC_SPEED, [log.vEgo]) + if log.vEgo == 0: + self.update_kalman(current_time, ObservationKind.NO_ROT, [0, 0, 0]) + + def handle_cam_odo(self, current_time, log): + self.cam_counter += 1 + + if self.cam_counter % VISION_DECIMATION == 0: + self.update_kalman(current_time, + ObservationKind.CAMERA_ODO_ROTATION, + np.concatenate([log.rot, log.rotStd])) + self.update_kalman(current_time, + ObservationKind.CAMERA_ODO_TRANSLATION, + np.concatenate([log.trans, log.transStd])) + + def handle_sensors(self, current_time, log): + # TODO does not yet account for double sensor readings in the log + for sensor_reading in log: + # Gyro Uncalibrated + if sensor_reading.sensor == 5 and sensor_reading.type == 16: + self.gyro_counter += 1 + if self.gyro_counter % SENSOR_DECIMATION == 0: + if max(abs(self.kf.x[States.IMU_OFFSET])) > 0.07: + cloudlog.info('imu frame angles exceeded, correcting') + self.update_kalman(current_time, ObservationKind.IMU_FRAME, [0, 0, 0]) + + v = sensor_reading.gyroUncalibrated.v + self.update_kalman(current_time, ObservationKind.PHONE_GYRO, [-v[2], -v[1], -v[0]]) + + # Accelerometer + if sensor_reading.sensor == 1 and sensor_reading.type == 1: + self.acc_counter += 1 + if self.acc_counter % SENSOR_DECIMATION == 0: + v = sensor_reading.acceleration.v + self.update_kalman(current_time, ObservationKind.PHONE_ACCEL, [-v[2], -v[1], -v[0]]) + + def reset_kalman(self): + self.filter_time = None + self.filter_ready = False + self.observation_buffer = [] + + self.gyro_counter = 0 + self.acc_counter = 0 + self.speed_counter = 0 + self.cam_counter = 0 + + +def locationd_thread(sm, pm, disabled_logs=[]): + if sm is None: + sm = messaging.SubMaster(['gpsLocationExternal', 'sensorEvents', 'cameraOdometry']) + if pm is None: + pm = messaging.PubMaster(['liveLocation']) + + localizer = Localizer(disabled_logs=disabled_logs) + + while True: + sm.update() + + for sock, updated in sm.updated.items(): + if updated: + t = sm.logMonoTime[sock] * 1e-9 + if sock == "sensorEvents": + localizer.handle_sensors(t, sm[sock]) + elif sock == "gpsLocationExternal": + localizer.handle_gps(t, sm[sock]) + elif sock == "carState": + localizer.handle_car_state(t, sm[sock]) + elif sock == "cameraOdometry": + localizer.handle_cam_odo(t, sm[sock]) + + if localizer.filter_ready and sm.updated['gpsLocationExternal']: + t = sm.logMonoTime['gpsLocationExternal'] + msg = messaging.new_message() + msg.logMonoTime = t + + msg.init('liveLocation') + msg.liveLocation = localizer.liveLocationMsg(t * 1e-9) + + pm.send('liveLocation', msg) + + +def main(sm=None, pm=None): + locationd_thread(sm, pm) + + +if __name__ == "__main__": + import os + os.environ["OMP_NUM_THREADS"] = "1" + main() diff --git a/selfdrive/locationd/locationd_yawrate.cc b/selfdrive/locationd/locationd_yawrate.cc index 4b93b6801..8b25a2a7c 100644 --- a/selfdrive/locationd/locationd_yawrate.cc +++ b/selfdrive/locationd/locationd_yawrate.cc @@ -3,7 +3,7 @@ #include #include -#include +#include #include "cereal/gen/cpp/log.capnp.h" diff --git a/selfdrive/locationd/locationd_yawrate.h b/selfdrive/locationd/locationd_yawrate.h index c59734aa6..3b53ef561 100644 --- a/selfdrive/locationd/locationd_yawrate.h +++ b/selfdrive/locationd/locationd_yawrate.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include "cereal/gen/cpp/log.capnp.h" #define DEGREES_TO_RADIANS 0.017453292519943295 diff --git a/selfdrive/loggerd/deleter.py b/selfdrive/loggerd/deleter.py index 1c687ff4b..b9ae0e8d4 100644 --- a/selfdrive/loggerd/deleter.py +++ b/selfdrive/loggerd/deleter.py @@ -3,15 +3,19 @@ import os import shutil import threading from selfdrive.swaglog import cloudlog -from selfdrive.loggerd.config import ROOT, get_available_bytes +from selfdrive.loggerd.config import ROOT, get_available_bytes, get_available_percent from selfdrive.loggerd.uploader import listdir_by_creation +MIN_BYTES = 5 * 1024 * 1024 * 1024 +MIN_PERCENT = 10 + def deleter_thread(exit_event): while not exit_event.is_set(): - available_bytes = get_available_bytes() + out_of_bytes = get_available_bytes(default=MIN_BYTES + 1) < MIN_BYTES + out_of_percent = get_available_percent(default=MIN_PERCENT + 1) < MIN_PERCENT - if available_bytes is not None and available_bytes < (5 * 1024 * 1024 * 1024): + if out_of_percent or out_of_bytes: # remove the earliest directory we can dirs = listdir_by_creation(ROOT) for delete_dir in dirs: diff --git a/selfdrive/loggerd/loggerd.cc b/selfdrive/loggerd/loggerd.cc index 7e744fa50..0e5c5457d 100644 --- a/selfdrive/loggerd/loggerd.cc +++ b/selfdrive/loggerd/loggerd.cc @@ -588,7 +588,6 @@ int main(int argc, char** argv) { for (const auto& it : services) { std::string name = it.name; - int qlog_freq = it.decimation ? it.decimation : 0; if (it.should_log) { SubSocket * sock = SubSocket::create(s.ctx, name); @@ -601,8 +600,8 @@ int main(int argc, char** argv) { frame_sock = sock; } - qlog_counter[sock] = (qlog_freq == 0) ? -1 : 0; - qlog_freqs[sock] = qlog_freq; + qlog_counter[sock] = (it.decimation == -1) ? -1 : 0; + qlog_freqs[sock] = it.decimation; } } diff --git a/selfdrive/manager.py b/selfdrive/manager.py index 4e6c52079..0d8efc8d3 100755 --- a/selfdrive/manager.py +++ b/selfdrive/manager.py @@ -14,7 +14,7 @@ from common.android import ANDROID sys.path.append(os.path.join(BASEDIR, "pyextra")) os.environ['BASEDIR'] = BASEDIR -TOTAL_SCONS_NODES = 1170 +TOTAL_SCONS_NODES = 1195 prebuilt = os.path.exists(os.path.join(BASEDIR, 'prebuilt')) # Create folders needed for msgq @@ -142,6 +142,7 @@ managed_processes = { "ubloxd": ("selfdrive/locationd", ["./ubloxd"]), "loggerd": ("selfdrive/loggerd", ["./loggerd"]), "logmessaged": "selfdrive.logmessaged", + "locationd": "selfdrive.locationd.locationd", "tombstoned": "selfdrive.tombstoned", "logcatd": ("selfdrive/logcatd", ["./logcatd"]), "proclogd": ("selfdrive/proclogd", ["./proclogd"]), @@ -204,6 +205,7 @@ car_started_processes = [ 'modeld', 'proclogd', 'ubloxd', + 'locationd', ] if ANDROID: car_started_processes += [ @@ -289,6 +291,15 @@ def prepare_managed_process(p): subprocess.check_call(["make", "clean"], cwd=os.path.join(BASEDIR, proc[0])) subprocess.check_call(["make", "-j4"], cwd=os.path.join(BASEDIR, proc[0])) + +def join_process(process, timeout): + # Process().join(timeout) will hang due to a python 3 bug: https://bugs.python.org/issue28382 + # We have to poll the exitcode instead + t = time.time() + while time.time() - t < timeout and process.exitcode is None: + time.sleep(0.001) + + def kill_managed_process(name): if name not in running or name not in managed_processes: return @@ -302,18 +313,12 @@ def kill_managed_process(name): else: running[name].terminate() - # Process().join(timeout) will hang due to a python 3 bug: https://bugs.python.org/issue28382 - # We have to poll the exitcode instead - # running[name].join(5.0) - - t = time.time() - while time.time() - t < 5 and running[name].exitcode is None: - time.sleep(0.001) + join_process(running[name], 5) if running[name].exitcode is None: if name in unkillable_processes: cloudlog.critical("unkillable process %s failed to exit! rebooting in 15 if it doesn't die" % name) - running[name].join(15.0) + join_process(running[name], 15) if running[name].exitcode is None: cloudlog.critical("FORCE REBOOTING PHONE!") os.system("date >> /sdcard/unkillable_reboot") @@ -502,6 +507,8 @@ def main(): params.put("LastUpdateTime", t.encode('utf8')) if params.get("OpenpilotEnabledToggle") is None: params.put("OpenpilotEnabledToggle", "1") + if params.get("LaneChangeEnabled") is None: + params.put("LaneChangeEnabled", "1") # is this chffrplus? if os.getenv("PASSIVE") is not None: diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 7418267c7..1414556bf 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -bc89e6f25e88a904ad905296d516aaebb77e2207 \ No newline at end of file +917e6889be1691fb96e7566a92e0c6bbefc861a4 \ No newline at end of file diff --git a/selfdrive/test/test_car_models.py b/selfdrive/test/test_car_models.py index b3ff2a9e5..1436f404e 100755 --- a/selfdrive/test/test_car_models.py +++ b/selfdrive/test/test_car_models.py @@ -58,7 +58,6 @@ def get_route_log(route_name): sys.exit(-1) routes = { - "975b26878285314d|2018-12-25--14-42-13": { 'carFingerprint': CHRYSLER.PACIFICA_2018_HYBRID, 'enableCamera': True, @@ -290,6 +289,7 @@ routes = { "7e34a988419b5307|2019-12-18--19-13-30": { 'carFingerprint': TOYOTA.RAV4H_TSS2, 'enableCamera': True, + 'fingerprintSource': 'fixed' }, "e6a24be49a6cd46e|2019-10-29--10-52-42": { 'carFingerprint': TOYOTA.LEXUS_ES_TSS2, @@ -325,6 +325,11 @@ routes = { 'carFingerprint': TOYOTA.LEXUS_RX_TSS2, 'enableCamera': True, }, + "ec429c0f37564e3c|2020-02-01--17-28-12": { + 'carFingerprint': TOYOTA.LEXUS_NXH, + 'enableCamera': True, + 'enableDsu': False, + }, #FIXME: This works sometimes locally, but never in CI. Timing issue? #"b0f5a01cf604185c|2018-01-31--20-11-39": { # 'carFingerprint': TOYOTA.LEXUS_RXH, @@ -368,6 +373,11 @@ routes = { # 'enableDsu': False, # }, # TODO: missingsome combos for highlander + "0a302ffddbb3e3d3|2020-02-08--16-19-08": { + 'carFingerprint': TOYOTA.HIGHLANDER_TSS2, + 'enableCamera': True, + 'enableDsu': False, + }, "aa659debdd1a7b54|2018-08-31--11-12-01": { 'carFingerprint': TOYOTA.HIGHLANDER, 'enableCamera': False, @@ -507,6 +517,11 @@ if __name__ == "__main__": params.put("CommunityFeaturesToggle", "1") params.put("Passive", "1" if route in passive_routes else "0") + if checks.get('fingerprintSource', None) == 'fixed': + os.environ['FINGERPRINT'] = checks['carFingerprint'] + else: + os.environ['FINGERPRINT'] = "" + print("testing ", route, " ", checks['carFingerprint']) print("Starting processes") for p in tested_procs: diff --git a/selfdrive/test/test_fingerprints.py b/selfdrive/test/test_fingerprints.py index af544e1a7..aeb75896d 100755 --- a/selfdrive/test/test_fingerprints.py +++ b/selfdrive/test/test_fingerprints.py @@ -77,6 +77,10 @@ valid = True for idx1, f1 in enumerate(fingerprints_flat): for idx2, f2 in enumerate(fingerprints_flat): if idx1 < idx2 and not check_fingerprint_consistency(f1, f2): + if car_names[idx1] == car_names[idx2]: + print(f"Warning, overlap in {car_names[idx1]}") + continue + valid = False print("Those two fingerprints are inconsistent {0} {1}".format(car_names[idx1], car_names[idx2])) print("") diff --git a/selfdrive/test/test_openpilot.py b/selfdrive/test/test_openpilot.py index cbc43dd12..e5ea32736 100644 --- a/selfdrive/test/test_openpilot.py +++ b/selfdrive/test/test_openpilot.py @@ -105,118 +105,118 @@ def test_uploader(): print("UPLOADER") time.sleep(10.0) -# @phone_only -# def test_athena(): -# print("ATHENA") -# start_daemon_process("manage_athenad") -# params = Params() -# manage_athenad_pid = params.get("AthenadPid") -# assert manage_athenad_pid is not None -# try: -# os.kill(int(manage_athenad_pid), 0) -# # process is running -# except OSError: -# assert False, "manage_athenad is dead" +@phone_only +def test_athena(): + print("ATHENA") + start_daemon_process("manage_athenad") + params = Params() + manage_athenad_pid = params.get("AthenadPid") + assert manage_athenad_pid is not None + try: + os.kill(int(manage_athenad_pid), 0) + # process is running + except OSError: + assert False, "manage_athenad is dead" -# def expect_athena_starts(timeout=30): -# now = time.time() -# athenad_pid = None -# while athenad_pid is None: -# try: -# athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() -# return athenad_pid -# except subprocess.CalledProcessError: -# if time.time() - now > timeout: -# assert False, f"Athena did not start within {timeout} seconds" -# time.sleep(0.5) + def expect_athena_starts(timeout=30): + now = time.time() + athenad_pid = None + while athenad_pid is None: + try: + athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() + return athenad_pid + except subprocess.CalledProcessError: + if time.time() - now > timeout: + assert False, f"Athena did not start within {timeout} seconds" + time.sleep(0.5) -# def athena_post(payload, max_retries=5, wait=5): -# tries = 0 -# while 1: -# try: -# resp = requests.post( -# "https://athena.comma.ai/" + params.get("DongleId", encoding="utf-8"), -# headers={ -# "Authorization": "JWT " + os.getenv("COMMA_JWT"), -# "Content-Type": "application/json" -# }, -# data=json.dumps(payload), -# timeout=30 -# ) -# resp_json = resp.json() -# if resp_json.get('error'): -# raise Exception(resp_json['error']) -# return resp_json -# except Exception as e: -# time.sleep(wait) -# tries += 1 -# if tries == max_retries: -# raise -# else: -# print(f'athena_post failed {e}. retrying...') + def athena_post(payload, max_retries=5, wait=5): + tries = 0 + while 1: + try: + resp = requests.post( + "https://athena.comma.ai/" + params.get("DongleId", encoding="utf-8"), + headers={ + "Authorization": "JWT " + os.getenv("COMMA_JWT"), + "Content-Type": "application/json" + }, + data=json.dumps(payload), + timeout=30 + ) + resp_json = resp.json() + if resp_json.get('error'): + raise Exception(resp_json['error']) + return resp_json + except Exception as e: + time.sleep(wait) + tries += 1 + if tries == max_retries: + raise + else: + print(f'athena_post failed {e}. retrying...') -# def expect_athena_registers(): -# resp = athena_post({ -# "method": "echo", -# "params": ["hello"], -# "id": 0, -# "jsonrpc": "2.0" -# }, max_retries=12, wait=5) -# assert resp.get('result') == "hello", f'Athena failed to register ({resp})' + def expect_athena_registers(): + resp = athena_post({ + "method": "echo", + "params": ["hello"], + "id": 0, + "jsonrpc": "2.0" + }, max_retries=12, wait=5) + assert resp.get('result') == "hello", f'Athena failed to register ({resp})' -# try: -# athenad_pid = expect_athena_starts() -# # kill athenad and ensure it is restarted (check_output will throw if it is not) -# os.kill(int(athenad_pid), signal.SIGINT) -# expect_athena_starts() + try: + athenad_pid = expect_athena_starts() + # kill athenad and ensure it is restarted (check_output will throw if it is not) + os.kill(int(athenad_pid), signal.SIGINT) + expect_athena_starts() -# if not os.getenv('COMMA_JWT'): -# print('WARNING: COMMA_JWT env not set, will not test requests to athena.comma.ai') -# return + if not os.getenv('COMMA_JWT'): + print('WARNING: COMMA_JWT env not set, will not test requests to athena.comma.ai') + return -# expect_athena_registers() + expect_athena_registers() -# print("ATHENA: getSimInfo") -# resp = athena_post({ -# "method": "getSimInfo", -# "id": 0, -# "jsonrpc": "2.0" -# }) -# assert resp.get('result'), resp -# assert 'sim_id' in resp['result'], resp['result'] + print("ATHENA: getSimInfo") + resp = athena_post({ + "method": "getSimInfo", + "id": 0, + "jsonrpc": "2.0" + }) + assert resp.get('result'), resp + assert 'sim_id' in resp['result'], resp['result'] -# print("ATHENA: takeSnapshot") -# resp = athena_post({ -# "method": "takeSnapshot", -# "id": 0, -# "jsonrpc": "2.0" -# }) -# assert resp.get('result'), resp -# assert resp['result']['jpegBack'], resp['result'] + print("ATHENA: takeSnapshot") + resp = athena_post({ + "method": "takeSnapshot", + "id": 0, + "jsonrpc": "2.0" + }) + assert resp.get('result'), resp + assert resp['result']['jpegBack'], resp['result'] -# @with_processes(["thermald"]) -# def test_athena_thermal(): -# print("ATHENA: getMessage(thermal)") -# resp = athena_post({ -# "method": "getMessage", -# "params": {"service": "thermal", "timeout": 5000}, -# "id": 0, -# "jsonrpc": "2.0" -# }) -# assert resp.get('result'), resp -# assert resp['result']['thermal'], resp['result'] -# test_athena_thermal() -# finally: -# try: -# athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() -# except subprocess.CalledProcessError: -# athenad_pid = None + @with_processes(["thermald"]) + def test_athena_thermal(): + print("ATHENA: getMessage(thermal)") + resp = athena_post({ + "method": "getMessage", + "params": {"service": "thermal", "timeout": 5000}, + "id": 0, + "jsonrpc": "2.0" + }) + assert resp.get('result'), resp + assert resp['result']['thermal'], resp['result'] + test_athena_thermal() + finally: + try: + athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() + except subprocess.CalledProcessError: + athenad_pid = None -# try: -# os.kill(int(manage_athenad_pid), signal.SIGINT) -# os.kill(int(athenad_pid), signal.SIGINT) -# except (OSError, TypeError): -# pass + try: + os.kill(int(manage_athenad_pid), signal.SIGINT) + os.kill(int(athenad_pid), signal.SIGINT) + except (OSError, TypeError): + pass # TODO: re-enable when jenkins test has /data/pythonpath -> /data/openpilot # @phone_only diff --git a/selfdrive/thermald.py b/selfdrive/thermald.py index 2fcd9685c..c516acc77 100755 --- a/selfdrive/thermald.py +++ b/selfdrive/thermald.py @@ -130,7 +130,7 @@ def handle_fan_eon(max_cpu_temp, bat_temp, fan_speed, ignition): def handle_fan_uno(max_cpu_temp, bat_temp, fan_speed, ignition): - new_speed = int(interp(max_cpu_temp, [40.0, 80.0], [0, 100])) + new_speed = int(interp(max_cpu_temp, [40.0, 80.0], [0, 80])) if not ignition: new_speed = min(30, new_speed) @@ -192,10 +192,12 @@ def thermald_thread(): if health is not None: usb_power = health.health.usbPowerMode != log.HealthData.UsbPowerMode.client - try: - network_type = get_network_type() - except subprocess.CalledProcessError: - pass + # get_network_type is an expensive call. update every 10s + if (count % int(10. / DT_TRML)) == 0: + try: + network_type = get_network_type() + except subprocess.CalledProcessError: + pass msg.thermal.freeSpace = get_available_percent(default=100.0) / 100.0 msg.thermal.memUsedPercent = int(round(psutil.virtual_memory().percent)) @@ -272,13 +274,16 @@ def thermald_thread(): last_update = now dt = now - last_update - if dt.days > DAYS_NO_CONNECTIVITY_MAX: + update_failed_count = params.get("UpdateFailedCount") + update_failed_count = 0 if update_failed_count is None else int(update_failed_count) + + if dt.days > DAYS_NO_CONNECTIVITY_MAX and update_failed_count > 1: if current_connectivity_alert != "expired": current_connectivity_alert = "expired" params.delete("Offroad_ConnectivityNeededPrompt") params.put("Offroad_ConnectivityNeeded", json.dumps(OFFROAD_ALERTS["Offroad_ConnectivityNeeded"])) elif dt.days > DAYS_NO_CONNECTIVITY_PROMPT: - remaining_time = str(DAYS_NO_CONNECTIVITY_MAX - dt.days) + remaining_time = str(max(DAYS_NO_CONNECTIVITY_MAX - dt.days, 0)) if current_connectivity_alert != "prompt" + remaining_time: current_connectivity_alert = "prompt" + remaining_time alert_connectivity_prompt = copy.copy(OFFROAD_ALERTS["Offroad_ConnectivityNeededPrompt"]) diff --git a/selfdrive/updated.py b/selfdrive/updated.py index efde9adcc..29847fdb8 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -293,6 +293,7 @@ def attempt_update(): def main(gctx=None): + update_failed_count = 0 overlay_init_done = False wait_helper = WaitTimeHelper() params = Params() @@ -312,6 +313,7 @@ def main(gctx=None): raise RuntimeError("couldn't get overlay lock; is another updated running?") while True: + update_failed_count += 1 time_wrong = datetime.datetime.now().year < 2019 ping_failed = subprocess.call(["ping", "-W", "4", "-c", "1", "8.8.8.8"]) @@ -335,6 +337,7 @@ def main(gctx=None): if params.get("IsOffroad") == b"1": attempt_update() + update_failed_count = 0 else: cloudlog.info("not running updater, openpilot running") @@ -348,8 +351,8 @@ def main(gctx=None): overlay_init_done = False except Exception: cloudlog.exception("uncaught updated exception, shouldn't happen") - overlay_init_done = False + params.put("UpdateFailedCount", str(update_failed_count)) wait_between_updates(wait_helper.ready_event) if wait_helper.shutdown: break From 1d7d11f5dd55ac55a79c0f17c3f08cc07847e633 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:12:44 -0800 Subject: [PATCH 06/16] Squashed 'cereal/' changes from ab32956aa..eba4349b9 eba4349b9 put liveLocation in qlogs 6c4735f21 add fixed fingerprintSource 4c50d26c6 GitHub actions (#25) 988783f6a Merge pull request #28 from commaai/power_monitoring b43fd0673 Added offroad power usage git-subtree-dir: cereal git-subtree-split: eba4349b9ac2c15a17f7169e6c24464501308347 --- .github/workflows/tests.yml | 23 +++++++++++++++++++++++ azure-pipelines.yml | 14 -------------- car.capnp | 1 + log.capnp | 1 + service_list.yaml | 2 +- 5 files changed, 26 insertions(+), 15 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 azure-pipelines.yml diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..3b8f6ca8e --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,23 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v2 + - name: Build docker image + run: docker build -t cereal . + - name: Unit Tests + run: | + docker run cereal bash -c "scons --test --asan -j$(nproc) && messaging/test_runner" + - name: Test ZMQ + run: | + docker run cereal bash -c "ZMQ=1 python -m unittest discover ." + - name: Test MSGQ + run: | + docker run cereal bash -c "MSGQ=1 python -m unittest discover ." + diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index c14526a5c..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,14 +0,0 @@ -pr: none - -pool: - vmImage: 'ubuntu-16.04' - -steps: -- script: | - set -e - docker build -t cereal . - docker run cereal bash -c "scons --test --asan -j$(nproc) && messaging/test_runner" - docker run cereal bash -c "ZMQ=1 python -m unittest discover ." - docker run cereal bash -c "MSGQ=1 python -m unittest discover ." - - displayName: 'Run Tests' diff --git a/car.capnp b/car.capnp index 745389232..58ff3fb42 100644 --- a/car.capnp +++ b/car.capnp @@ -477,5 +477,6 @@ struct CarParams { enum FingerprintSource { can @0; fw @1; + fixed @2; } } diff --git a/log.capnp b/log.capnp index e01c8977e..46971d81a 100644 --- a/log.capnp +++ b/log.capnp @@ -280,6 +280,7 @@ struct ThermalData { batteryVoltage @16 :Int32; usbOnline @12 :Bool; networkType @22 :NetworkType; + offroadPowerUsage @23 :UInt32; # Power usage since going offroad in uWh fanSpeed @10 :UInt16; started @11 :Bool; diff --git a/service_list.yaml b/service_list.yaml index 7dd1a62e9..5747293ab 100644 --- a/service_list.yaml +++ b/service_list.yaml @@ -31,7 +31,7 @@ carState: [8021, true, 100., 10] # 8022 is reserved for sshd carControl: [8023, true, 100., 10] plan: [8024, true, 20.] -liveLocation: [8025, true, 0.] +liveLocation: [8025, true, 0., 1] gpsLocation: [8026, true, 1., 1] ethernetData: [8027, true, 0.] navUpdate: [8028, true, 0.] From 6b8726ef752f740021ccf90aa53a7513d53d68e3 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:12:45 -0800 Subject: [PATCH 07/16] Squashed 'panda/' changes from 73a60d5c9..769ade051 769ade051 Harness orientation designation fixes (#435) eb5020b7a Add the ability to communicate with BSM (#433) git-subtree-dir: panda git-subtree-split: 769ade0511483c4ba6e4e9a3b5380ffe50d01f13 --- board/boards/black.h | 12 ++++++------ board/boards/uno.h | 12 ++++++------ board/drivers/harness.h | 31 ++++++++++++------------------- board/safety/safety_toyota.h | 2 +- tests/safety/test_toyota.py | 2 +- 5 files changed, 26 insertions(+), 33 deletions(-) diff --git a/board/boards/black.h b/board/boards/black.h index 7165aa6cb..97f322f69 100644 --- a/board/boards/black.h +++ b/board/boards/black.h @@ -99,7 +99,7 @@ void black_set_can_mode(uint8_t mode){ switch (mode) { case CAN_MODE_NORMAL: case CAN_MODE_OBD_CAN2: - if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_NORMAL)) { + if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_FLIPPED)) { // B12,B13: disable OBD mode set_gpio_mode(GPIOB, 12, MODE_INPUT); set_gpio_mode(GPIOB, 13, MODE_INPUT); @@ -198,7 +198,7 @@ void black_init(void) { black_set_can_mode(CAN_MODE_NORMAL); // flip CAN0 and CAN2 if we are flipped - if (car_harness_status == HARNESS_STATUS_NORMAL) { + if (car_harness_status == HARNESS_STATUS_FLIPPED) { can_flip_buses(0, 2); } @@ -210,12 +210,12 @@ const harness_configuration black_harness_config = { .has_harness = true, .GPIO_SBU1 = GPIOC, .GPIO_SBU2 = GPIOC, - .GPIO_relay_normal = GPIOC, - .GPIO_relay_flipped = GPIOC, + .GPIO_relay_SBU1 = GPIOC, + .GPIO_relay_SBU2 = GPIOC, .pin_SBU1 = 0, .pin_SBU2 = 3, - .pin_relay_normal = 10, - .pin_relay_flipped = 11, + .pin_relay_SBU1 = 10, + .pin_relay_SBU2 = 11, .adc_channel_SBU1 = 10, .adc_channel_SBU2 = 13 }; diff --git a/board/boards/uno.h b/board/boards/uno.h index 1545a3f2d..a6a06eff3 100644 --- a/board/boards/uno.h +++ b/board/boards/uno.h @@ -113,7 +113,7 @@ void uno_set_can_mode(uint8_t mode){ switch (mode) { case CAN_MODE_NORMAL: case CAN_MODE_OBD_CAN2: - if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_NORMAL)) { + if ((bool)(mode == CAN_MODE_NORMAL) != (bool)(car_harness_status == HARNESS_STATUS_FLIPPED)) { // B12,B13: disable OBD mode set_gpio_mode(GPIOB, 12, MODE_INPUT); set_gpio_mode(GPIOB, 13, MODE_INPUT); @@ -230,7 +230,7 @@ void uno_init(void) { uno_set_can_mode(CAN_MODE_NORMAL); // flip CAN0 and CAN2 if we are flipped - if (car_harness_status == HARNESS_STATUS_NORMAL) { + if (car_harness_status == HARNESS_STATUS_FLIPPED) { can_flip_buses(0, 2); } @@ -252,12 +252,12 @@ const harness_configuration uno_harness_config = { .has_harness = true, .GPIO_SBU1 = GPIOC, .GPIO_SBU2 = GPIOC, - .GPIO_relay_normal = GPIOC, - .GPIO_relay_flipped = GPIOC, + .GPIO_relay_SBU1 = GPIOC, + .GPIO_relay_SBU2 = GPIOC, .pin_SBU1 = 0, .pin_SBU2 = 3, - .pin_relay_normal = 10, - .pin_relay_flipped = 11, + .pin_relay_SBU1 = 10, + .pin_relay_SBU2 = 11, .adc_channel_SBU1 = 10, .adc_channel_SBU2 = 13 }; diff --git a/board/drivers/harness.h b/board/drivers/harness.h index c996bff0b..4aeb41fe6 100644 --- a/board/drivers/harness.h +++ b/board/drivers/harness.h @@ -10,12 +10,12 @@ struct harness_configuration { const bool has_harness; GPIO_TypeDef *GPIO_SBU1; GPIO_TypeDef *GPIO_SBU2; - GPIO_TypeDef *GPIO_relay_normal; - GPIO_TypeDef *GPIO_relay_flipped; + GPIO_TypeDef *GPIO_relay_SBU1; + GPIO_TypeDef *GPIO_relay_SBU2; uint8_t pin_SBU1; uint8_t pin_SBU2; - uint8_t pin_relay_normal; - uint8_t pin_relay_flipped; + uint8_t pin_relay_SBU1; + uint8_t pin_relay_SBU2; uint8_t adc_channel_SBU1; uint8_t adc_channel_SBU2; }; @@ -30,9 +30,9 @@ void set_intercept_relay(bool intercept) { } if(car_harness_status == HARNESS_STATUS_NORMAL){ - set_gpio_output(current_board->harness_config->GPIO_relay_normal, current_board->harness_config->pin_relay_normal, !intercept); + set_gpio_output(current_board->harness_config->GPIO_relay_SBU2, current_board->harness_config->pin_relay_SBU2, !intercept); } else { - set_gpio_output(current_board->harness_config->GPIO_relay_flipped, current_board->harness_config->pin_relay_flipped, !intercept); + set_gpio_output(current_board->harness_config->GPIO_relay_SBU1, current_board->harness_config->pin_relay_SBU1, !intercept); } } } @@ -41,10 +41,10 @@ bool harness_check_ignition(void) { bool ret = false; switch(car_harness_status){ case HARNESS_STATUS_NORMAL: - ret = !get_gpio_input(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2); + ret = !get_gpio_input(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1); break; case HARNESS_STATUS_FLIPPED: - ret = !get_gpio_input(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1); + ret = !get_gpio_input(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2); break; default: break; @@ -62,11 +62,11 @@ uint8_t harness_detect_orientation(void) { // Detect connection and orientation if((sbu1_voltage < HARNESS_CONNECTED_THRESHOLD) || (sbu2_voltage < HARNESS_CONNECTED_THRESHOLD)){ if (sbu1_voltage < sbu2_voltage) { - // orientation normal - ret = HARNESS_STATUS_NORMAL; - } else { - // orientation flipped + // orientation flipped (PANDA_SBU1->HARNESS_SBU1(relay), PANDA_SBU2->HARNESS_SBU2(ign)) ret = HARNESS_STATUS_FLIPPED; + } else { + // orientation normal (PANDA_SBU2->HARNESS_SBU1(relay), PANDA_SBU1->HARNESS_SBU2(ign)) + ret = HARNESS_STATUS_NORMAL; } } #endif @@ -90,13 +90,6 @@ void harness_init(void) { set_gpio_mode(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1, MODE_INPUT); set_gpio_mode(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2, MODE_INPUT); - // now we have orientation, set pin ignition detection - if(car_harness_status == HARNESS_STATUS_NORMAL){ - set_gpio_mode(current_board->harness_config->GPIO_SBU2, current_board->harness_config->pin_SBU2, MODE_INPUT); - } else { - set_gpio_mode(current_board->harness_config->GPIO_SBU1, current_board->harness_config->pin_SBU1, MODE_INPUT); - } - // keep busses connected by default set_intercept_relay(false); } else { diff --git a/board/safety/safety_toyota.h b/board/safety/safety_toyota.h index 844e387fe..4bf0a5de4 100644 --- a/board/safety/safety_toyota.h +++ b/board/safety/safety_toyota.h @@ -21,7 +21,7 @@ const int TOYOTA_GAS_INTERCEPTOR_THRESHOLD = 475; // ratio between offset and g const AddrBus TOYOTA_TX_MSGS[] = {{0x283, 0}, {0x2E6, 0}, {0x2E7, 0}, {0x33E, 0}, {0x344, 0}, {0x365, 0}, {0x366, 0}, {0x4CB, 0}, // DSU bus 0 {0x128, 1}, {0x141, 1}, {0x160, 1}, {0x161, 1}, {0x470, 1}, // DSU bus 1 {0x2E4, 0}, {0x411, 0}, {0x412, 0}, {0x343, 0}, {0x1D2, 0}, // LKAS + ACC - {0x200, 0}}; // interceptor + {0x200, 0}, {0x750, 0}}; // interceptor + Blindspot monitor AddrCheckStruct toyota_rx_checks[] = { {.addr = {0x260}, .bus = 0, .check_checksum = true, .max_counter = 0U, .expected_timestep = 20000U}, diff --git a/tests/safety/test_toyota.py b/tests/safety/test_toyota.py index 101816245..493fb1f62 100644 --- a/tests/safety/test_toyota.py +++ b/tests/safety/test_toyota.py @@ -21,7 +21,7 @@ INTERCEPTOR_THRESHOLD = 475 TX_MSGS = [[0x283, 0], [0x2E6, 0], [0x2E7, 0], [0x33E, 0], [0x344, 0], [0x365, 0], [0x366, 0], [0x4CB, 0], # DSU bus 0 [0x128, 1], [0x141, 1], [0x160, 1], [0x161, 1], [0x470, 1], # DSU bus 1 [0x2E4, 0], [0x411, 0], [0x412, 0], [0x343, 0], [0x1D2, 0], # LKAS + ACC - [0x200, 0]]; # interceptor + [0x200, 0], [0x750, 0]]; # interceptor + blindspot monitor def twos_comp(val, bits): From ab3557e1075aaabf2b9815b81ca3b8069a4a1b01 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:12:46 -0800 Subject: [PATCH 08/16] Squashed 'opendbc/' changes from d68f8ff0c..2ae0327ea 2ae0327ea GitHub actions (#217) fbbba94aa GM: use common gear VALs dacd8b460 Chrysler commonize gear VALs d338b9fba Add support for 2019 Civic Sedan Diesel. Split GAS_PEDAL_2 out to each car. (#215) git-subtree-dir: opendbc git-subtree-split: 2ae0327ea25db36227d52acfaae8927a59d4e5a3 --- .github/workflows/tests.yml | 24 ++ azure-pipelines.yml | 19 - chrysler_pacifica_2017_hybrid.dbc | 2 +- generator/honda/_bosch_2018.dbc | 7 - .../honda/honda_accord_lx15t_2018_can.dbc | 7 + generator/honda/honda_accord_s2t_2018_can.dbc | 7 + .../honda_civic_hatchback_ex_2017_can.dbc | 7 + .../honda_civic_sedan_16_diesel_2019_can.dbc | 57 +++ generator/honda/honda_crv_ex_2017_can.dbc | 7 + generator/honda/honda_crv_hybrid_2019_can.dbc | 7 + generator/honda/honda_insight_ex_2019_can.dbc | 7 + gm_global_a_powertrain.dbc | 2 +- honda_accord_lx15t_2018_can_generated.dbc | 14 +- honda_accord_s2t_2018_can_generated.dbc | 14 +- ..._civic_hatchback_ex_2017_can_generated.dbc | 14 +- ...vic_sedan_16_diesel_2019_can_generated.dbc | 351 ++++++++++++++++++ honda_crv_ex_2017_can_generated.dbc | 14 +- honda_crv_hybrid_2019_can_generated.dbc | 14 +- honda_insight_ex_2019_can_generated.dbc | 14 +- 19 files changed, 518 insertions(+), 70 deletions(-) create mode 100644 .github/workflows/tests.yml delete mode 100644 azure-pipelines.yml create mode 100644 generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc create mode 100644 honda_civic_sedan_16_diesel_2019_can_generated.dbc diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 000000000..2141cb026 --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,24 @@ +name: Tests + +on: [push, pull_request] + +jobs: + test: + runs-on: ubuntu-16.04 + + steps: + - uses: actions/checkout@v2 + - run: | + set -e + docker build -t opendbc . + name: "Build" + - run: | + docker run opendbc bash -c "python -m unittest discover opendbc" + name: "Unit tests" + - run: | + docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./flake8_opendbc.sh" + docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./pylint_opendbc.sh" + name: "Python linter" + - run: | + docker run opendbc bash -c "cd opendbc/can/tests/; PYTHONPATH=/ ./test_generator.sh" + name: "Generator test" diff --git a/azure-pipelines.yml b/azure-pipelines.yml deleted file mode 100644 index 6ac0f9ddd..000000000 --- a/azure-pipelines.yml +++ /dev/null @@ -1,19 +0,0 @@ -pr: none - -pool: - vmImage: 'ubuntu-16.04' -steps: -- script: | - set -e - docker build -t opendbc . - displayName: 'Build' -- script: | - docker run opendbc bash -c "python -m unittest discover opendbc" - displayName: 'Unit tests' -- script: | - docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./flake8_opendbc.sh" - docker run opendbc bash -c "cd opendbc/can/tests/linter_python; PYTHONPATH=/ ./pylint_opendbc.sh" - displayName: 'Python linter' -- script: | - docker run opendbc bash -c "cd opendbc/can/tests/; PYTHONPATH=/ ./test_generator.sh" - displayName: 'Generator test' diff --git a/chrysler_pacifica_2017_hybrid.dbc b/chrysler_pacifica_2017_hybrid.dbc index a19219cbb..80ec2a904 100644 --- a/chrysler_pacifica_2017_hybrid.dbc +++ b/chrysler_pacifica_2017_hybrid.dbc @@ -429,5 +429,5 @@ CM_ SG_ 625 SPEED "zero on non-acc drives"; CM_ SG_ 625 ACCEL_PERHAPS "set to 7767 on non-ACC drives. ACC drive 40k is constant speed, 42k is accelerating"; CM_ SG_ 268 BRAKE_PERHAPS "triggers only on ACC braking"; CM_ SG_ 384 NEW_SIGNAL_1 "set in ACC gas driving. not set in electric human. not sure about gas human driving."; -VAL_ 746 PRNDL 5 "Low" 4 "Drive" 3 "Neutral" 2 "Reverse" 1 "Park" ; +VAL_ 746 PRNDL 5 "L" 4 "D" 3 "N" 2 "R" 1 "P" ; VAL_ 792 TURN_SIGNALS 2 "Right" 1 "Left" ; diff --git a/generator/honda/_bosch_2018.dbc b/generator/honda/_bosch_2018.dbc index 488eed424..bfbfe9b8b 100644 --- a/generator/honda/_bosch_2018.dbc +++ b/generator/honda/_bosch_2018.dbc @@ -54,13 +54,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON diff --git a/generator/honda/honda_accord_lx15t_2018_can.dbc b/generator/honda/honda_accord_lx15t_2018_can.dbc index 9e95f56db..099b1a9e5 100644 --- a/generator/honda/honda_accord_lx15t_2018_can.dbc +++ b/generator/honda/honda_accord_lx15t_2018_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/generator/honda/honda_accord_s2t_2018_can.dbc b/generator/honda/honda_accord_s2t_2018_can.dbc index e0a7ff328..4967d6062 100644 --- a/generator/honda/honda_accord_s2t_2018_can.dbc +++ b/generator/honda/honda_accord_s2t_2018_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/generator/honda/honda_civic_hatchback_ex_2017_can.dbc b/generator/honda/honda_civic_hatchback_ex_2017_can.dbc index 2de149ad5..c085466f5 100644 --- a/generator/honda/honda_civic_hatchback_ex_2017_can.dbc +++ b/generator/honda/honda_civic_hatchback_ex_2017_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc b/generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc new file mode 100644 index 000000000..23a579aa4 --- /dev/null +++ b/generator/honda/honda_civic_sedan_16_diesel_2019_can.dbc @@ -0,0 +1,57 @@ +CM_ "IMPORT _bosch_2018.dbc" + +BO_ 316 GAS_PEDAL_2: 8 XXX + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 419 GEARBOX: 8 XXX + SG_ GEAR_SHIFTER : 24|8@1+ (1,0) [0|255] "" XXX + SG_ GEAR : 32|8@1+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 432 STANDSTILL: 7 VSA + SG_ WHEELS_MOVING : 12|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_1 : 11|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_2 : 9|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" EON + +BO_ 892 CRUISE_PARAMS: 8 PCM + SG_ CRUISE_SPEED_OFFSET : 31|8@0- (0.1,0) [-128|127] "kph" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|15] "" EON + +BO_ 927 RADAR_HUD: 8 RADAR + SG_ ZEROS_BOH : 7|10@0+ (1,0) [0|127] "" BDY + SG_ CMBS_OFF : 12|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH3 : 31|32@0+ (1,0) [0|4294967295] "" XXX + SG_ RESUME_INSTRUCTION : 21|1@0+ (1,0) [0|1] "" XXX + SG_ SET_TO_1 : 13|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH2 : 11|4@0+ (1,0) [0|1] "" XXX + SG_ APPLY_BRAKES_FOR_CANC : 23|1@0+ (1,0) [0|1] "" XXX + SG_ ACC_ALERTS : 20|5@0+ (1,0) [0|1] "" BDY + SG_ SET_TO_0 : 22|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 1029 DOORS_STATUS: 8 BDY + SG_ DOOR_OPEN_FL : 37|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_FR : 38|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RL : 39|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RR : 40|1@0+ (1,0) [0|1] "" EON + SG_ TRUNK_OPEN : 41|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +VAL_ 419 GEAR_SHIFTER 2 "S" 32 "D" 16 "N" 8 "R" 4 "P" ; +VAL_ 419 GEAR 26 "S" 20 "D" 19 "N" 18 "R" 17 "P" ; +VAL_ 545 ECON_ON_2 0 "off" 3 "on" ; +VAL_ 662 CRUISE_BUTTONS 7 "tbd" 6 "tbd" 5 "tbd" 4 "accel_res" 3 "decel_set" 2 "cancel" 1 "main" 0 "none" ; +VAL_ 662 CRUISE_SETTING 3 "distance_adj" 2 "tbd" 1 "lkas_button" 0 "none" ; +VAL_ 806 CMBS_BUTTON 3 "pressed" 0 "released" ; +VAL_ 891 WIPERS 4 "High" 2 "Low" 0 "Off" ; +VAL_ 829 BEEP 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep" ; + +CM_ "CHFFR_METRIC 330 STEER_ANGLE STEER_ANGLE 0.36 180; CHFFR_METRIC 380 ENGINE_RPM ENGINE_RPM 1 0; CHFFR_METRIC 804 ENGINE_TEMPERATURE ENGINE_TEMPERATURE 1 0"; diff --git a/generator/honda/honda_crv_ex_2017_can.dbc b/generator/honda/honda_crv_ex_2017_can.dbc index e8cc456bf..8382a9fdd 100644 --- a/generator/honda/honda_crv_ex_2017_can.dbc +++ b/generator/honda/honda_crv_ex_2017_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/generator/honda/honda_crv_hybrid_2019_can.dbc b/generator/honda/honda_crv_hybrid_2019_can.dbc index 2398526e0..692ab8d01 100644 --- a/generator/honda/honda_crv_hybrid_2019_can.dbc +++ b/generator/honda/honda_crv_hybrid_2019_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/generator/honda/honda_insight_ex_2019_can.dbc b/generator/honda/honda_insight_ex_2019_can.dbc index ff988cf9d..770b3a150 100644 --- a/generator/honda/honda_insight_ex_2019_can.dbc +++ b/generator/honda/honda_insight_ex_2019_can.dbc @@ -1,5 +1,12 @@ CM_ "IMPORT _bosch_2018.dbc" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ GEAR : 7|8@0+ (1,0) [0|255] "" EON SG_ GEAR_SHIFTER : 29|6@0+ (1,0) [0|63] "" EON diff --git a/gm_global_a_powertrain.dbc b/gm_global_a_powertrain.dbc index 2e3bd7c85..48611cfc7 100644 --- a/gm_global_a_powertrain.dbc +++ b/gm_global_a_powertrain.dbc @@ -241,7 +241,7 @@ VAL_ 481 LKAButton 1 "Active" 0 "Inactive" ; VAL_ 481 ACCButtons 6 "Cancel" 5 "Main" 3 "Set" 2 "Resume" 1 "None" ; VAL_ 481 DriveModeButton 1 "Active" 0 "Inactive" ; VAL_ 452 CruiseState 4 "Standstill" 3 "Faulted" 1 "Active" 0 "Off" ; -VAL_ 309 PRNDL 3 "Reverse" 2 "Drive" 1 "Neutral" 0 "Park" ; +VAL_ 309 PRNDL 3 "R" 2 "D" 1 "N" 0 "P" ; VAL_ 309 ESPButton 1 "Active" 0 "Inactive" ; VAL_ 384 LKASteeringCmdActive 1 "Active" 0 "Inactive" ; VAL_ 880 ACCLeadCar 1 "Present" 0 "Not Present" ; diff --git a/honda_accord_lx15t_2018_can_generated.dbc b/honda_accord_lx15t_2018_can_generated.dbc index 48c571df1..00a01f6ed 100644 --- a/honda_accord_lx15t_2018_can_generated.dbc +++ b/honda_accord_lx15t_2018_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_accord_lx15t_2018_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/honda_accord_s2t_2018_can_generated.dbc b/honda_accord_s2t_2018_can_generated.dbc index b160dff85..a89d4504f 100644 --- a/honda_accord_s2t_2018_can_generated.dbc +++ b/honda_accord_s2t_2018_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_accord_s2t_2018_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/honda_civic_hatchback_ex_2017_can_generated.dbc b/honda_civic_hatchback_ex_2017_can_generated.dbc index d6c9505d9..9b5091c2f 100644 --- a/honda_civic_hatchback_ex_2017_can_generated.dbc +++ b/honda_civic_hatchback_ex_2017_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_civic_hatchback_ex_2017_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/honda_civic_sedan_16_diesel_2019_can_generated.dbc b/honda_civic_sedan_16_diesel_2019_can_generated.dbc new file mode 100644 index 000000000..c9042135d --- /dev/null +++ b/honda_civic_sedan_16_diesel_2019_can_generated.dbc @@ -0,0 +1,351 @@ +CM_ "AUTOGENERATED FILE, DO NOT EDIT" + + +CM_ "Imported file _bosch_2018.dbc starts here" +VERSION "" + + +NS_ : + NS_DESC_ + CM_ + BA_DEF_ + BA_ + VAL_ + CAT_DEF_ + CAT_ + FILTER + BA_DEF_DEF_ + EV_DATA_ + ENVVAR_DATA_ + SGTYPE_ + SGTYPE_VAL_ + BA_DEF_SGTYPE_ + BA_SGTYPE_ + SIG_TYPE_REF_ + VAL_TABLE_ + SIG_GROUP_ + SIG_VALTYPE_ + SIGTYPE_VALTYPE_ + BO_TX_BU_ + BA_DEF_REL_ + BA_REL_ + BA_DEF_DEF_REL_ + BU_SG_REL_ + BU_EV_REL_ + BU_BO_REL_ + SG_MUL_VAL_ + +BU_: EBCM EON CAM RADAR PCM EPS VSA SCM BDY XXX EPB + +BO_ 148 KINEMATICS: 8 XXX + SG_ LAT_ACCEL : 7|10@0+ (-0.035,17.92) [-20|20] "m/s2" EON + SG_ LONG_ACCEL : 25|10@0+ (-0.035,17.92) [-20|20] "m/s2" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + +BO_ 228 STEERING_CONTROL: 5 EON + SG_ STEER_TORQUE_REQUEST : 23|1@0+ (1,0) [0|1] "" EPS + SG_ SET_ME_X00 : 22|7@0+ (1,0) [0|127] "" EPS + SG_ SET_ME_X00_2 : 31|8@0+ (1,0) [0|0] "" EPS + SG_ STEER_TORQUE : 7|16@0- (1,0) [-4096|4096] "" EPS + SG_ COUNTER : 37|2@0+ (1,0) [0|3] "" EPS + SG_ CHECKSUM : 35|4@0+ (1,0) [0|15] "" EPS + +BO_ 232 BRAKE_HOLD: 7 XXX + SG_ XMISSION_SPEED : 7|14@0- (1,0) [1|0] "" XXX + SG_ COMPUTER_BRAKE : 39|16@0+ (1,0) [0|0] "" XXX + SG_ COMPUTER_BRAKE_REQUEST : 29|1@0+ (1,0) [0|0] "" XXX + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX + +BO_ 330 STEERING_SENSORS: 8 EPS + SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON + SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON + SG_ STEER_SENSOR_STATUS_1 : 34|1@0+ (1,0) [0|1] "" EON + SG_ STEER_SENSOR_STATUS_2 : 33|1@0+ (1,0) [0|1] "" EON + SG_ STEER_SENSOR_STATUS_3 : 32|1@0+ (1,0) [0|1] "" EON + SG_ STEER_WHEEL_ANGLE : 47|16@0- (-0.1,0) [-500|500] "deg" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 344 ENGINE_DATA: 8 PCM + SG_ XMISSION_SPEED : 7|16@0+ (0.01,0) [0|250] "kph" EON + SG_ ENGINE_RPM : 23|16@0+ (1,0) [0|15000] "rpm" EON + SG_ XMISSION_SPEED2 : 39|16@0+ (0.01,0) [0|250] "kph" EON + SG_ ODOMETER : 55|8@0+ (10,0) [0|2550] "m" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 380 POWERTRAIN_DATA: 8 PCM + SG_ PEDAL_GAS : 7|8@0+ (1,0) [0|255] "" EON + SG_ ENGINE_RPM : 23|16@0+ (1,0) [0|15000] "rpm" EON + SG_ GAS_PRESSED : 39|1@0+ (1,0) [0|1] "" EON + SG_ ACC_STATUS : 38|1@0+ (1,0) [0|1] "" EON + SG_ BOH_17C : 37|5@0+ (1,0) [0|1] "" EON + SG_ BRAKE_SWITCH : 32|1@0+ (1,0) [0|1] "" EON + SG_ BOH2_17C : 47|10@0+ (1,0) [0|1] "" EON + SG_ BRAKE_PRESSED : 53|1@0+ (1,0) [0|1] "" EON + SG_ BOH3_17C : 52|5@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 399 STEER_STATUS: 7 EPS + SG_ STEER_TORQUE_SENSOR : 7|16@0- (-1,0) [-31000|31000] "tbd" EON + SG_ STEER_ANGLE_RATE : 23|16@0- (-0.1,0) [-31000|31000] "deg/s" EON + SG_ STEER_STATUS : 39|4@0+ (1,0) [0|15] "" EON + SG_ STEER_CONTROL_ACTIVE : 35|1@0+ (1,0) [0|1] "" EON + SG_ STEER_CONFIG_INDEX : 43|4@0+ (1,0) [0|15] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" EON + +BO_ 420 VSA_STATUS: 8 VSA + SG_ ESP_DISABLED : 28|1@0+ (1,0) [0|1] "" EON + SG_ USER_BRAKE : 7|16@0+ (0.015625,-1.609375) [0|1000] "" EON + SG_ BRAKE_HOLD_ACTIVE : 46|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_HOLD_ENABLED : 45|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 427 STEER_MOTOR_TORQUE: 3 EPS + SG_ CONFIG_VALID : 7|1@0+ (1,0) [0|1] "" EON + SG_ MOTOR_TORQUE : 1|10@0+ (1,0) [0|256] "" EON + SG_ OUTPUT_DISABLED : 22|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 21|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 19|4@0+ (1,0) [0|15] "" EON + +BO_ 450 EPB_STATUS: 8 EPB + SG_ EPB_ACTIVE : 3|1@0+ (1,0) [0|1] "" EON + SG_ EPB_STATE : 29|2@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 464 WHEEL_SPEEDS: 8 VSA + SG_ WHEEL_SPEED_FL : 7|15@0+ (0.01,0) [0|250] "kph" EON + SG_ WHEEL_SPEED_FR : 8|15@0+ (0.01,0) [0|250] "kph" EON + SG_ WHEEL_SPEED_RL : 25|15@0+ (0.01,0) [0|250] "kph" EON + SG_ WHEEL_SPEED_RR : 42|15@0+ (0.01,0) [0|250] "kph" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + +BO_ 479 ACC_CONTROL: 8 EON + SG_ SET_TO_0 : 20|5@0+ (1,0) [0|1] "" XXX + SG_ CONTROL_ON : 23|3@0+ (1,0) [0|5] "" XXX + SG_ GAS_COMMAND : 7|16@0- (1,0) [0|0] "" XXX + SG_ ACCEL_COMMAND : 31|11@0- (0.01,0) [0|0] "m/s2" XXX + SG_ BRAKE_LIGHTS : 62|1@0+ (1,0) [0|1] "" XXX + SG_ BRAKE_REQUEST : 34|1@0+ (1,0) [0|1] "" XXX + SG_ STANDSTILL : 35|1@0+ (1,0) [0|1] "" XXX + SG_ STANDSTILL_RELEASE : 36|1@0+ (1,0) [0|1] "" XXX + SG_ AEB_STATUS : 33|1@0+ (1,0) [0|1] "" XXX + SG_ AEB_BRAKING : 47|1@0+ (1,0) [0|1] "" XXX + SG_ AEB_PREPARE : 43|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 490 VEHICLE_DYNAMICS: 8 VSA + SG_ LAT_ACCEL : 7|16@0- (0.0015,0) [-20|20] "m/s2" EON + SG_ LONG_ACCEL : 23|16@0- (0.0015,0) [-20|20] "m/s2" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + +BO_ 495 ACC_CONTROL_ON: 8 XXX + SG_ SET_TO_75 : 31|8@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_30 : 39|8@0+ (1,0) [0|255] "" XXX + SG_ ZEROS_BOH : 23|8@0+ (1,0) [0|255] "" XXX + SG_ ZEROS_BOH2 : 47|16@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_FF : 15|8@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_3 : 6|7@0+ (1,0) [0|4095] "" XXX + SG_ CONTROL_ON : 7|1@0+ (1,0) [0|1] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + +BO_ 545 XXX_16: 6 SCM + SG_ ECON_ON : 23|1@0+ (1,0) [0|1] "" XXX + SG_ DRIVE_MODE : 37|2@0+ (1,0) [0|3] "" XXX + SG_ COUNTER : 45|2@0+ (1,0) [0|3] "" BDY + SG_ CHECKSUM : 43|4@0+ (1,0) [0|15] "" BDY + +BO_ 597 ROUGH_WHEEL_SPEED: 8 VSA + SG_ WHEEL_SPEED_FL : 7|8@0+ (1,0) [0|255] "mph" EON + SG_ WHEEL_SPEED_FR : 15|8@0+ (1,0) [0|255] "mph" EON + SG_ WHEEL_SPEED_RL : 23|8@0+ (1,0) [0|255] "mph" EON + SG_ WHEEL_SPEED_RR : 31|8@0+ (1,0) [0|255] "mph" EON + SG_ SET_TO_X55 : 39|8@0+ (1,0) [0|255] "" XXX + SG_ SET_TO_X55_2 : 47|8@0+ (1,0) [0|255] "" EON + SG_ LONG_COUNTER : 55|8@0+ (1,0) [0|255] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + + BO_ 662 SCM_BUTTONS: 4 SCM + SG_ CRUISE_BUTTONS : 7|3@0+ (1,0) [0|7] "" EON + SG_ CRUISE_SETTING : 3|2@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 29|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 27|4@0+ (1,0) [0|15] "" EON + +BO_ 773 SEATBELT_STATUS: 7 BDY + SG_ SEATBELT_DRIVER_LAMP : 7|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_PASS_UNLATCHED : 10|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_PASS_LATCHED : 11|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_DRIVER_UNLATCHED : 12|1@0+ (1,0) [0|1] "" EON + SG_ SEATBELT_DRIVER_LATCHED : 13|1@0+ (1,0) [0|1] "" EON + SG_ PASS_AIRBAG_OFF : 14|1@0+ (1,0) [0|1] "" EON + SG_ PASS_AIRBAG_ON : 15|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|3] "" EON + +BO_ 777 CAR_SPEED: 8 PCM + SG_ ROUGH_CAR_SPEED : 23|8@0+ (1,0) [0|255] "mph" XXX + SG_ CAR_SPEED : 7|16@0+ (0.01,0) [0|65535] "kph" XXX + SG_ ROUGH_CAR_SPEED_3 : 39|16@0+ (0.01,0) [0|65535] "kph" XXX + SG_ ROUGH_CAR_SPEED_2 : 31|8@0+ (1,0) [0|255] "mph" XXX + SG_ LOCK_STATUS : 55|2@0+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 780 ACC_HUD: 8 ADAS + SG_ CRUISE_SPEED : 31|8@0+ (1,0) [0|255] "kph" BDY + SG_ DTC_MODE : 39|1@0+ (1,0) [0|1] "" BDY + SG_ BOH : 38|1@0+ (1,0) [0|1] "" BDY + SG_ FCM_PROBLEM : 34|1@0+ (1,0) [0|1] "" BDY + SG_ RADAR_OBSTRUCTED : 33|1@0+ (1,0) [0|1] "" BDY + SG_ ENABLE_MINI_CAR : 32|1@0+ (1,0) [0|1] "" BDY + SG_ BOH_3 : 43|1@0+ (1,0) [0|3] "" BDY + SG_ BOH_4 : 42|1@0+ (1,0) [0|3] "" BDY + SG_ BOH_5 : 41|1@0+ (1,0) [0|3] "" BDY + SG_ CRUISE_CONTROL_LABEL : 40|1@0+ (1,0) [0|3] "" BDY + SG_ ZEROS_BOH : 7|24@0+ (0.002759506,0) [0|100] "m/s" BDY + SG_ FCM_OFF : 35|1@0+ (1,0) [0|1] "" BDY + SG_ SET_TO_1 : 36|1@0+ (1,0) [0|1] "" XXX + SG_ HUD_DISTANCE : 47|2@0+ (1,0) [0|3] "" BDY + SG_ HUD_LEAD : 45|2@0+ (1,0) [0|3] "" BDY + SG_ ACC_PROBLEM : 37|1@0+ (1,0) [0|1] "" BDY + SG_ ACC_ON : 52|1@0+ (1,0) [0|1] "" XXX + SG_ BOH_6 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ SET_TO_X1 : 55|1@0+ (1,0) [0|1] "" XXX + SG_ IMPERIAL_UNIT : 54|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 804 CRUISE: 8 PCM + SG_ TRIP_FUEL_CONSUMED : 23|16@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +BO_ 806 SCM_FEEDBACK: 8 SCM + SG_ DRIVERS_DOOR_OPEN : 17|1@0+ (1,0) [0|1] "" XXX + SG_ MAIN_ON : 28|1@0+ (1,0) [0|1] "" EON + SG_ RIGHT_BLINKER : 27|1@0+ (1,0) [0|1] "" EON + SG_ LEFT_BLINKER : 26|1@0+ (1,0) [0|1] "" EON + SG_ CMBS_STATES : 22|2@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 829 LKAS_HUD: 5 ADAS + SG_ CAM_TEMP_HIGH : 7|1@0+ (1,0) [0|255] "" BDY + SG_ SET_ME_X41 : 6|7@0+ (1,0) [0|127] "" BDY + SG_ BOH : 6|7@0+ (1,0) [0|127] "" BDY + SG_ DASHED_LANES : 14|1@0+ (1,0) [0|1] "" BDY + SG_ DTC : 13|1@0+ (1,0) [0|1] "" BDY + SG_ LKAS_PROBLEM : 12|1@0+ (1,0) [0|1] "" BDY + SG_ LKAS_OFF : 11|1@0+ (1,0) [0|1] "" BDY + SG_ SOLID_LANES : 10|1@0+ (1,0) [0|1] "" BDY + SG_ LDW_RIGHT : 9|1@0+ (1,0) [0|1] "" BDY + SG_ STEERING_REQUIRED : 8|1@0+ (1,0) [0|1] "" BDY + SG_ BOH : 23|2@0+ (1,0) [0|4] "" BDY + SG_ LDW_PROBLEM : 21|1@0+ (1,0) [0|1] "" BDY + SG_ BEEP : 17|2@0+ (1,0) [0|1] "" BDY + SG_ LDW_ON : 28|1@0+ (1,0) [0|1] "" BDY + SG_ LDW_OFF : 27|1@0+ (1,0) [0|1] "" BDY + SG_ CLEAN_WINDSHIELD : 26|1@0+ (1,0) [0|1] "" BDY + SG_ SET_ME_X48 : 31|8@0+ (1,0) [0|255] "" BDY + SG_ COUNTER : 37|2@0+ (1,0) [0|3] "" BDY + SG_ CHECKSUM : 35|4@0+ (1,0) [0|15] "" BDY + +BO_ 862 CAMERA_MESSAGES: 8 CAM + SG_ ZEROS_BOH : 7|50@0+ (1,0) [0|127] "" BDY + SG_ AUTO_HIGHBEAMS_ACTIVE : 53|1@0+ (1,0) [0|1] "" XXX + SG_ HIGHBEAMS_ON : 52|1@0+ (1,0) [0|1] "" XXX + SG_ ZEROS_BOH_2 : 51|4@0+ (1,0) [0|15] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 884 STALK_STATUS: 8 XXX + SG_ AUTO_HEADLIGHTS : 46|1@0+ (1,0) [0|1] "" EON + SG_ HIGH_BEAM_HOLD : 47|1@0+ (1,0) [0|1] "" EON + SG_ HIGH_BEAM_FLASH : 45|1@0+ (1,0) [0|1] "" EON + SG_ HEADLIGHTS_ON : 54|1@0+ (1,0) [0|1] "" EON + SG_ WIPER_SWITCH : 53|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + +BO_ 891 STALK_STATUS_2: 8 XXX + SG_ WIPERS : 17|2@0+ (1,0) [0|3] "" EON + SG_ LOW_BEAMS : 35|1@0+ (1,0) [0|1] "" XXX + SG_ HIGH_BEAMS : 34|1@0+ (1,0) [0|1] "" XXX + SG_ PARK_LIGHTS : 36|1@0+ (1,0) [0|1] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + +CM_ SG_ 479 AEB_STATUS "set for the duration of AEB event"; +CM_ SG_ 479 AEB_BRAKING "set when braking is commanded during AEB event"; +CM_ SG_ 479 AEB_PREPARE "set 1s before AEB"; + +VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_speed_lockout" 2 "no_torque_alert_1" 0 "normal" ; + +CM_ "honda_civic_sedan_16_diesel_2019_can.dbc starts here" + + +BO_ 316 GAS_PEDAL_2: 8 XXX + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 419 GEARBOX: 8 XXX + SG_ GEAR_SHIFTER : 24|8@1+ (1,0) [0|255] "" XXX + SG_ GEAR : 32|8@1+ (1,0) [0|255] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 432 STANDSTILL: 7 VSA + SG_ WHEELS_MOVING : 12|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_1 : 11|1@0+ (1,0) [0|1] "" EON + SG_ BRAKE_ERROR_2 : 9|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" EON + +BO_ 892 CRUISE_PARAMS: 8 PCM + SG_ CRUISE_SPEED_OFFSET : 31|8@0- (0.1,0) [-128|127] "kph" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|15] "" EON + +BO_ 927 RADAR_HUD: 8 RADAR + SG_ ZEROS_BOH : 7|10@0+ (1,0) [0|127] "" BDY + SG_ CMBS_OFF : 12|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH3 : 31|32@0+ (1,0) [0|4294967295] "" XXX + SG_ RESUME_INSTRUCTION : 21|1@0+ (1,0) [0|1] "" XXX + SG_ SET_TO_1 : 13|1@0+ (1,0) [0|1] "" BDY + SG_ ZEROS_BOH2 : 11|4@0+ (1,0) [0|1] "" XXX + SG_ APPLY_BRAKES_FOR_CANC : 23|1@0+ (1,0) [0|1] "" XXX + SG_ ACC_ALERTS : 20|5@0+ (1,0) [0|1] "" BDY + SG_ SET_TO_0 : 22|1@0+ (1,0) [0|1] "" XXX + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" XXX + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" XXX + +BO_ 1029 DOORS_STATUS: 8 BDY + SG_ DOOR_OPEN_FL : 37|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_FR : 38|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RL : 39|1@0+ (1,0) [0|1] "" EON + SG_ DOOR_OPEN_RR : 40|1@0+ (1,0) [0|1] "" EON + SG_ TRUNK_OPEN : 41|1@0+ (1,0) [0|1] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + +VAL_ 419 GEAR_SHIFTER 2 "S" 32 "D" 16 "N" 8 "R" 4 "P" ; +VAL_ 419 GEAR 26 "S" 20 "D" 19 "N" 18 "R" 17 "P" ; +VAL_ 545 ECON_ON_2 0 "off" 3 "on" ; +VAL_ 662 CRUISE_BUTTONS 7 "tbd" 6 "tbd" 5 "tbd" 4 "accel_res" 3 "decel_set" 2 "cancel" 1 "main" 0 "none" ; +VAL_ 662 CRUISE_SETTING 3 "distance_adj" 2 "tbd" 1 "lkas_button" 0 "none" ; +VAL_ 806 CMBS_BUTTON 3 "pressed" 0 "released" ; +VAL_ 891 WIPERS 4 "High" 2 "Low" 0 "Off" ; +VAL_ 829 BEEP 3 "single_beep" 2 "triple_beep" 1 "repeated_beep" 0 "no_beep" ; + +CM_ "CHFFR_METRIC 330 STEER_ANGLE STEER_ANGLE 0.36 180; CHFFR_METRIC 380 ENGINE_RPM ENGINE_RPM 1 0; CHFFR_METRIC 804 ENGINE_TEMPERATURE ENGINE_TEMPERATURE 1 0"; diff --git a/honda_crv_ex_2017_can_generated.dbc b/honda_crv_ex_2017_can_generated.dbc index db11aa3a7..6cdb0c02d 100644 --- a/honda_crv_ex_2017_can_generated.dbc +++ b/honda_crv_ex_2017_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_crv_ex_2017_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 401 GEARBOX: 8 PCM SG_ GEAR_SHIFTER : 5|6@0+ (1,0) [0|63] "" EON SG_ BOH : 45|6@0+ (1,0) [0|63] "" XXX diff --git a/honda_crv_hybrid_2019_can_generated.dbc b/honda_crv_hybrid_2019_can_generated.dbc index ed90b9408..f744c490b 100644 --- a/honda_crv_hybrid_2019_can_generated.dbc +++ b/honda_crv_hybrid_2019_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_crv_hybrid_2019_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON SG_ CHECKSUM : 59|4@0+ (1,0) [0|3] "" EON diff --git a/honda_insight_ex_2019_can_generated.dbc b/honda_insight_ex_2019_can_generated.dbc index 775e22ada..f5cc66dcc 100644 --- a/honda_insight_ex_2019_can_generated.dbc +++ b/honda_insight_ex_2019_can_generated.dbc @@ -58,13 +58,6 @@ BO_ 232 BRAKE_HOLD: 7 XXX SG_ COUNTER : 53|2@0+ (1,0) [0|3] "" XXX SG_ CHECKSUM : 51|4@0+ (1,0) [0|15] "" XXX -BO_ 304 GAS_PEDAL_2: 8 PCM - SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON - SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON - SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON - SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON - BO_ 330 STEERING_SENSORS: 8 EPS SG_ STEER_ANGLE : 7|16@0- (-0.1,0) [-500|500] "deg" EON SG_ STEER_ANGLE_RATE : 23|16@0- (-1,0) [-3000|3000] "deg/s" EON @@ -301,6 +294,13 @@ VAL_ 399 STEER_STATUS 6 "tmp_fault" 5 "fault_1" 4 "no_torque_alert_2" 3 "low_spe CM_ "honda_insight_ex_2019_can.dbc starts here" +BO_ 304 GAS_PEDAL_2: 8 PCM + SG_ ENGINE_TORQUE_ESTIMATE : 7|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ ENGINE_TORQUE_REQUEST : 23|16@0- (1,0) [-1000|1000] "Nm" EON + SG_ CAR_GAS : 39|8@0+ (1,0) [0|255] "" EON + SG_ COUNTER : 61|2@0+ (1,0) [0|3] "" EON + SG_ CHECKSUM : 59|4@0+ (1,0) [0|15] "" EON + BO_ 419 GEARBOX: 8 PCM SG_ GEAR : 7|8@0+ (1,0) [0|255] "" EON SG_ GEAR_SHIFTER : 29|6@0+ (1,0) [0|63] "" EON From 38c9775a659ceeb81ec9d1009e967d56c5ec7064 Mon Sep 17 00:00:00 2001 From: Vehicle Researcher Date: Mon, 17 Feb 2020 18:12:52 -0800 Subject: [PATCH 09/16] openpilot v0.7.3 release --- .pylintrc | 585 ------------------ .travis.yml | 7 - README.md | 2 + RELEASES.md | 8 + SConstruct | 1 + common/params.py | 2 + selfdrive/camerad/cameras/camera_qcom.c | 57 +- selfdrive/car/car_helpers.py | 5 + selfdrive/car/chrysler/carcontroller.py | 10 +- selfdrive/car/chrysler/carstate.py | 109 ++-- selfdrive/car/chrysler/interface.py | 68 +- selfdrive/car/chrysler/values.py | 5 +- selfdrive/car/fingerprints.py | 32 +- selfdrive/car/ford/carcontroller.py | 14 +- selfdrive/car/ford/carstate.py | 76 +-- selfdrive/car/ford/interface.py | 34 +- selfdrive/car/fw_versions.py | 7 +- selfdrive/car/gm/carstate.py | 43 +- selfdrive/car/gm/gmcan.py | 4 +- selfdrive/car/gm/interface.py | 18 +- selfdrive/car/gm/values.py | 13 +- selfdrive/car/honda/carstate.py | 52 +- selfdrive/car/honda/values.py | 33 +- selfdrive/car/hyundai/carstate.py | 42 +- selfdrive/car/interfaces.py | 32 + selfdrive/car/subaru/carcontroller.py | 2 +- selfdrive/car/subaru/carstate.py | 33 +- selfdrive/car/toyota/carcontroller.py | 6 - selfdrive/car/toyota/carstate.py | 50 +- selfdrive/car/toyota/interface.py | 32 +- selfdrive/car/toyota/values.py | 163 +++-- selfdrive/car/volkswagen/carstate.py | 35 +- selfdrive/common/version.h | 2 +- selfdrive/controls/lib/pathplanner.py | 4 +- selfdrive/debug/live_cpu_and_temp.py | 96 +-- selfdrive/debug/test_fw_query_on_routes.py | 10 +- selfdrive/locationd/kalman/.gitignore | 1 + selfdrive/locationd/kalman/README.md | 52 ++ selfdrive/locationd/kalman/SConscript | 31 + selfdrive/locationd/kalman/__init__.py | 0 .../locationd/kalman/helpers/__init__.py | 191 ++++++ .../locationd/kalman/helpers/chi2_lookup.py | 22 + .../kalman/helpers/chi2_lookup_table.npy | Bin 0 -> 156928 bytes selfdrive/locationd/kalman/helpers/ekf_sym.py | 563 +++++++++++++++++ .../kalman/helpers/feature_handler.py | 158 +++++ .../kalman/helpers/lst_sq_computer.py | 176 ++++++ .../kalman/helpers}/sympy_helpers.py | 51 +- selfdrive/locationd/kalman/models/live_kf.py | 284 +++++++++ .../locationd/kalman/templates/compute_pos.c | 54 ++ selfdrive/locationd/kalman/templates/ekf_c.c | 123 ++++ .../kalman/templates/feature_handler.c | 58 ++ selfdrive/locationd/locationd.py | 202 ++++++ selfdrive/locationd/locationd_yawrate.cc | 2 +- selfdrive/locationd/locationd_yawrate.h | 2 +- selfdrive/loggerd/deleter.py | 10 +- selfdrive/loggerd/loggerd.cc | 5 +- selfdrive/manager.py | 25 +- selfdrive/test/process_replay/ref_commit | 2 +- selfdrive/test/test_car_models.py | 17 +- selfdrive/test/test_fingerprints.py | 4 + selfdrive/test/test_openpilot.py | 204 +++--- selfdrive/thermald.py | 19 +- selfdrive/updated.py | 5 +- 63 files changed, 2602 insertions(+), 1351 deletions(-) delete mode 100644 .pylintrc delete mode 100644 .travis.yml create mode 100644 selfdrive/locationd/kalman/.gitignore create mode 100644 selfdrive/locationd/kalman/README.md create mode 100644 selfdrive/locationd/kalman/SConscript create mode 100644 selfdrive/locationd/kalman/__init__.py create mode 100644 selfdrive/locationd/kalman/helpers/__init__.py create mode 100644 selfdrive/locationd/kalman/helpers/chi2_lookup.py create mode 100644 selfdrive/locationd/kalman/helpers/chi2_lookup_table.npy create mode 100644 selfdrive/locationd/kalman/helpers/ekf_sym.py create mode 100755 selfdrive/locationd/kalman/helpers/feature_handler.py create mode 100755 selfdrive/locationd/kalman/helpers/lst_sq_computer.py rename {common => selfdrive/locationd/kalman/helpers}/sympy_helpers.py (58%) create mode 100755 selfdrive/locationd/kalman/models/live_kf.py create mode 100644 selfdrive/locationd/kalman/templates/compute_pos.c create mode 100644 selfdrive/locationd/kalman/templates/ekf_c.c create mode 100644 selfdrive/locationd/kalman/templates/feature_handler.c create mode 100755 selfdrive/locationd/locationd.py diff --git a/.pylintrc b/.pylintrc deleted file mode 100644 index 64a55daf8..000000000 --- a/.pylintrc +++ /dev/null @@ -1,585 +0,0 @@ -[MASTER] - -# A comma-separated list of package or module names from where C extensions may -# be loaded. Extensions are loading into the active Python interpreter and may -# run arbitrary code -extension-pkg-whitelist=scipy - -# Add files or directories to the blacklist. They should be base names, not -# paths. -ignore=CVS - -# Add files or directories matching the regex patterns to the blacklist. The -# regex matches against base names, not paths. -ignore-patterns= - -# Python code to execute, usually for sys.path manipulation such as -# pygtk.require(). -#init-hook= - -# Use multiple processes to speed up Pylint. -jobs=4 - -# List of plugins (as comma separated values of python modules names) to load, -# usually to register additional checkers. -load-plugins= - -# Pickle collected data for later comparisons. -persistent=yes - -# Specify a configuration file. -#rcfile= - -# When enabled, pylint would attempt to guess common misconfiguration and emit -# user-friendly hints instead of false-positive error messages -suggestion-mode=yes - -# Allow loading of arbitrary C extensions. Extensions are imported into the -# active Python interpreter and may run arbitrary code. -unsafe-load-any-extension=no - - -[MESSAGES CONTROL] - -# Only show warnings with the listed confidence levels. Leave empty to show -# all. Valid levels: HIGH, INFERENCE, INFERENCE_FAILURE, UNDEFINED -confidence= - -# Disable the message, report, category or checker with the given id(s). You -# can either give multiple identifiers separated by comma (,) or put this -# option multiple times (only on the command line, not in the configuration -# file where it should appear only once).You can also use "--disable=all" to -# disable everything first and then reenable specific checks. For example, if -# you want to run only the similarities checker, you can use "--disable=all -# --enable=similarities". If you want to run only the classes checker, but have -# no Warning level messages displayed, use"--disable=all --enable=classes -# --disable=W" -disable=print-statement, - parameter-unpacking, - unpacking-in-except, - old-raise-syntax, - backtick, - long-suffix, - old-ne-operator, - old-octal-literal, - import-star-module-level, - non-ascii-bytes-literal, - raw-checker-failed, - bad-inline-option, - locally-disabled, - locally-enabled, - file-ignored, - suppressed-message, - useless-suppression, - deprecated-pragma, - apply-builtin, - basestring-builtin, - buffer-builtin, - cmp-builtin, - coerce-builtin, - execfile-builtin, - file-builtin, - long-builtin, - raw_input-builtin, - reduce-builtin, - standarderror-builtin, - unicode-builtin, - xrange-builtin, - coerce-method, - delslice-method, - getslice-method, - setslice-method, - no-absolute-import, - old-division, - dict-iter-method, - dict-view-method, - next-method-called, - metaclass-assignment, - indexing-exception, - raising-string, - reload-builtin, - oct-method, - hex-method, - nonzero-method, - cmp-method, - input-builtin, - round-builtin, - intern-builtin, - unichr-builtin, - map-builtin-not-iterating, - zip-builtin-not-iterating, - range-builtin-not-iterating, - filter-builtin-not-iterating, - using-cmp-argument, - eq-without-hash, - div-method, - idiv-method, - rdiv-method, - exception-message-attribute, - invalid-str-codec, - sys-max-int, - bad-python3-import, - deprecated-string-function, - deprecated-str-translate-call, - deprecated-itertools-function, - deprecated-types-field, - next-method-defined, - dict-items-not-iterating, - dict-keys-not-iterating, - dict-values-not-iterating, - bad-indentation, - line-too-long, - missing-docstring, - multiple-statements, - bad-continuation, - invalid-name, - too-many-arguments, - too-many-locals, - superfluous-parens, - bad-whitespace, - too-many-instance-attributes, - wrong-import-position, - ungrouped-imports, - wrong-import-order, - protected-access, - trailing-whitespace, - too-many-branches, - too-few-public-methods, - too-many-statements, - trailing-newlines, - attribute-defined-outside-init, - too-many-return-statements, - too-many-public-methods, - unused-argument, - old-style-class, - no-init, - len-as-condition, - unneeded-not, - no-self-use, - multiple-imports, - no-else-return, - logging-not-lazy, - fixme, - redefined-outer-name, - unused-variable, - unsubscriptable-object, - expression-not-assigned, - too-many-boolean-expressions, - consider-using-ternary, - invalid-unary-operand-type, - relative-import, - deprecated-lambda - - -# Enable the message, report, category or checker with the given id(s). You can -# either give multiple identifier separated by comma (,) or put this option -# multiple time (only on the command line, not in the configuration file where -# it should appear only once). See also the "--disable" option for examples. -enable=c-extension-no-member - - -[REPORTS] - -# Python expression which should return a note less than 10 (10 is the highest -# note). You have access to the variables errors warning, statement which -# respectively contain the number of errors / warnings messages and the total -# number of statements analyzed. This is used by the global evaluation report -# (RP0004). -evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) - -# Template used to display messages. This is a python new-style format string -# used to format the message information. See doc for all details -#msg-template= - -# Set the output format. Available formats are text, parseable, colorized, json -# and msvs (visual studio).You can also give a reporter class, eg -# mypackage.mymodule.MyReporterClass. -output-format=text - -# Tells whether to display a full report or only the messages -reports=no - -# Activate the evaluation score. -score=yes - - -[REFACTORING] - -# Maximum number of nested blocks for function / method body -max-nested-blocks=5 - -# Complete name of functions that never returns. When checking for -# inconsistent-return-statements if a never returning function is called then -# it will be considered as an explicit return statement and no message will be -# printed. -never-returning-functions=optparse.Values,sys.exit - - -[LOGGING] - -# Logging modules to check that the string format arguments are in logging -# function parameter format -logging-modules=logging - - -[SPELLING] - -# Limits count of emitted suggestions for spelling mistakes -max-spelling-suggestions=4 - -# Spelling dictionary name. Available dictionaries: none. To make it working -# install python-enchant package. -spelling-dict= - -# List of comma separated words that should not be checked. -spelling-ignore-words= - -# A path to a file that contains private dictionary; one word per line. -spelling-private-dict-file= - -# Tells whether to store unknown words to indicated private dictionary in -# --spelling-private-dict-file option instead of raising a message. -spelling-store-unknown-words=no - - -[MISCELLANEOUS] - -# List of note tags to take in consideration, separated by a comma. -notes=FIXME, - XXX, - TODO - - -[SIMILARITIES] - -# Ignore comments when computing similarities. -ignore-comments=yes - -# Ignore docstrings when computing similarities. -ignore-docstrings=yes - -# Ignore imports when computing similarities. -ignore-imports=no - -# Minimum lines number of a similarity. -min-similarity-lines=4 - - -[TYPECHECK] - -# List of decorators that produce context managers, such as -# contextlib.contextmanager. Add to this list to register other decorators that -# produce valid context managers. -contextmanager-decorators=contextlib.contextmanager - -# List of members which are set dynamically and missed by pylint inference -# system, and so shouldn't trigger E1101 when accessed. Python regular -# expressions are accepted. -generated-members=capnp.* cereal.* pygame.* zmq.* setproctitle.* smbus2.* usb1.* serial.* cv2.* - -# Tells whether missing members accessed in mixin class should be ignored. A -# mixin class is detected if its name ends with "mixin" (case insensitive). -ignore-mixin-members=yes - -# This flag controls whether pylint should warn about no-member and similar -# checks whenever an opaque object is returned when inferring. The inference -# can return multiple potential results while evaluating a Python object, but -# some branches might not be evaluated, which results in partial inference. In -# that case, it might be useful to still emit no-member and other checks for -# the rest of the inferred objects. -ignore-on-opaque-inference=yes - -# List of class names for which member attributes should not be checked (useful -# for classes with dynamically set attributes). This supports the use of -# qualified names. -ignored-classes=optparse.Values,thread._local,_thread._local - -# List of module names for which member attributes should not be checked -# (useful for modules/projects where namespaces are manipulated during runtime -# and thus existing member attributes cannot be deduced by static analysis. It -# supports qualified module names, as well as Unix pattern matching. -ignored-modules=flask setproctitle usb1 flask.ext.socketio smbus2 usb1.* - -# Show a hint with possible names when a member name was not found. The aspect -# of finding the hint is based on edit distance. -missing-member-hint=yes - -# The minimum edit distance a name should have in order to be considered a -# similar match for a missing member name. -missing-member-hint-distance=1 - -# The total number of similar names that should be taken in consideration when -# showing a hint for a missing member. -missing-member-max-choices=1 - - -[VARIABLES] - -# List of additional names supposed to be defined in builtins. Remember that -# you should avoid to define new builtins when possible. -additional-builtins= - -# Tells whether unused global variables should be treated as a violation. -allow-global-unused-variables=yes - -# List of strings which can identify a callback function by name. A callback -# name must start or end with one of those strings. -callbacks=cb_, - _cb - -# A regular expression matching the name of dummy variables (i.e. expectedly -# not used). -dummy-variables-rgx=_+$|(_[a-zA-Z0-9_]*[a-zA-Z0-9]+?$)|dummy|^ignored_|^unused_ - -# Argument names that match this expression will be ignored. Default to name -# with leading underscore -ignored-argument-names=_.*|^ignored_|^unused_ - -# Tells whether we should check for unused import in __init__ files. -init-import=no - -# List of qualified module names which can have objects that can redefine -# builtins. -redefining-builtins-modules=six.moves,past.builtins,future.builtins - - -[FORMAT] - -# Expected format of line ending, e.g. empty (any line ending), LF or CRLF. -expected-line-ending-format= - -# Regexp for a line that is allowed to be longer than the limit. -ignore-long-lines=^\s*(# )??$ - -# Number of spaces of indent required inside a hanging or continued line. -indent-after-paren=4 - -# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 -# tab). -indent-string=' ' - -# Maximum number of characters on a single line. -max-line-length=100 - -# Maximum number of lines in a module -max-module-lines=1000 - -# List of optional constructs for which whitespace checking is disabled. `dict- -# separator` is used to allow tabulation in dicts, etc.: {1 : 1,\n222: 2}. -# `trailing-comma` allows a space between comma and closing bracket: (a, ). -# `empty-line` allows space-only lines. -no-space-check=trailing-comma, - dict-separator - -# Allow the body of a class to be on the same line as the declaration if body -# contains single statement. -single-line-class-stmt=no - -# Allow the body of an if to be on the same line as the test if there is no -# else. -single-line-if-stmt=no - - -[BASIC] - -# Naming style matching correct argument names -argument-naming-style=snake_case - -# Regular expression matching correct argument names. Overrides argument- -# naming-style -#argument-rgx= - -# Naming style matching correct attribute names -attr-naming-style=snake_case - -# Regular expression matching correct attribute names. Overrides attr-naming- -# style -#attr-rgx= - -# Bad variable names which should always be refused, separated by a comma -bad-names=foo, - bar, - baz, - toto, - tutu, - tata - -# Naming style matching correct class attribute names -class-attribute-naming-style=any - -# Regular expression matching correct class attribute names. Overrides class- -# attribute-naming-style -#class-attribute-rgx= - -# Naming style matching correct class names -class-naming-style=PascalCase - -# Regular expression matching correct class names. Overrides class-naming-style -#class-rgx= - -# Naming style matching correct constant names -const-naming-style=UPPER_CASE - -# Regular expression matching correct constant names. Overrides const-naming- -# style -#const-rgx= - -# Minimum line length for functions/classes that require docstrings, shorter -# ones are exempt. -docstring-min-length=-1 - -# Naming style matching correct function names -function-naming-style=snake_case - -# Regular expression matching correct function names. Overrides function- -# naming-style -#function-rgx= - -# Good variable names which should always be accepted, separated by a comma -good-names=i, - j, - k, - ex, - Run, - _ - -# Include a hint for the correct naming format with invalid-name -include-naming-hint=no - -# Naming style matching correct inline iteration names -inlinevar-naming-style=any - -# Regular expression matching correct inline iteration names. Overrides -# inlinevar-naming-style -#inlinevar-rgx= - -# Naming style matching correct method names -method-naming-style=snake_case - -# Regular expression matching correct method names. Overrides method-naming- -# style -#method-rgx= - -# Naming style matching correct module names -module-naming-style=snake_case - -# Regular expression matching correct module names. Overrides module-naming- -# style -#module-rgx= - -# Colon-delimited sets of names that determine each other's naming style when -# the name regexes allow several styles. -name-group= - -# Regular expression which should only match function or class names that do -# not require a docstring. -no-docstring-rgx=^_ - -# List of decorators that produce properties, such as abc.abstractproperty. Add -# to this list to register other decorators that produce valid properties. -property-classes=abc.abstractproperty - -# Naming style matching correct variable names -variable-naming-style=snake_case - -# Regular expression matching correct variable names. Overrides variable- -# naming-style -#variable-rgx= - - -[DESIGN] - -# Maximum number of arguments for function / method -max-args=5 - -# Maximum number of attributes for a class (see R0902). -max-attributes=7 - -# Maximum number of boolean expressions in a if statement -max-bool-expr=5 - -# Maximum number of branch for function / method body -max-branches=12 - -# Maximum number of locals for function / method body -max-locals=15 - -# Maximum number of parents for a class (see R0901). -max-parents=7 - -# Maximum number of public methods for a class (see R0904). -max-public-methods=20 - -# Maximum number of return / yield for function / method body -max-returns=6 - -# Maximum number of statements in function / method body -max-statements=50 - -# Minimum number of public methods for a class (see R0903). -min-public-methods=2 - - -[CLASSES] - -# List of method names used to declare (i.e. assign) instance attributes. -defining-attr-methods=__init__, - __new__, - setUp - -# List of member names, which should be excluded from the protected access -# warning. -exclude-protected=_asdict, - _fields, - _replace, - _source, - _make - -# List of valid names for the first argument in a class method. -valid-classmethod-first-arg=cls - -# List of valid names for the first argument in a metaclass class method. -valid-metaclass-classmethod-first-arg=mcs - - -[IMPORTS] - -# Allow wildcard imports from modules that define __all__. -allow-wildcard-with-all=no - -# Analyse import fallback blocks. This can be used to support both Python 2 and -# 3 compatible code, which means that the block might have code that exists -# only in one or another interpreter, leading to false positives when analysed. -analyse-fallback-blocks=no - -# Deprecated modules which should not be used, separated by a comma -deprecated-modules=regsub, - TERMIOS, - Bastion, - rexec - -# Create a graph of external dependencies in the given file (report RP0402 must -# not be disabled) -ext-import-graph= - -# Create a graph of every (i.e. internal and external) dependencies in the -# given file (report RP0402 must not be disabled) -import-graph= - -# Create a graph of internal dependencies in the given file (report RP0402 must -# not be disabled) -int-import-graph= - -# Force import order to recognize a module as part of the standard -# compatibility libraries. -known-standard-library= - -# Force import order to recognize a module as part of a third party library. -known-third-party=enchant - - -[EXCEPTIONS] - -# Exceptions that will emit a warning when being caught. Defaults to -# "Exception" -overgeneral-exceptions=Exception diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 8683d8bee..000000000 --- a/.travis.yml +++ /dev/null @@ -1,7 +0,0 @@ -sudo: required - -services: - - docker - -script: - - ./run_docker_tests.sh diff --git a/README.md b/README.md index 73bccf297..42770fe80 100644 --- a/README.md +++ b/README.md @@ -96,6 +96,7 @@ Supported Cars | Lexus | ES Hybrid 2019 | All | openpilot | 0mph | 0mph | | Lexus | IS 2017-2019 | All | Stock | 22mph | 0mph | | Lexus | IS Hybrid 2017 | All | Stock | 0mph | 0mph | +| Lexus | NX Hybrid 2018 | All | Stock4| 0mph | 0mph | | Lexus | RX 2016-17 | All | Stock4| 0mph | 0mph | | Lexus | RX 2020 | All | openpilot | 0mph | 0mph | | Lexus | RX Hybrid 2016-19 | All | Stock4| 0mph | 0mph | @@ -113,6 +114,7 @@ Supported Cars | Toyota | Corolla Hybrid 2020 | All | openpilot | 0mph | 0mph | | Toyota | Highlander 2017-19 | All | Stock4| 0mph | 0mph | | Toyota | Highlander Hybrid 2017-19 | All | Stock4| 0mph | 0mph | +| Toyota | Highlander 2020 | All | openpilot | 0mph | 0mph | | Toyota | Prius 2016 | TSS-P | Stock4| 0mph | 0mph | | Toyota | Prius 2017-19 | All | Stock4| 0mph | 0mph | | Toyota | Prius Prime 2017-20 | All | Stock4| 0mph | 0mph | diff --git a/RELEASES.md b/RELEASES.md index b472cf9cd..c0a859003 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,3 +1,11 @@ +Version 0.7.3 (2020-02-21) +======================== + * Improved autofocus for road facing camera + * Support for 2020 Highlander thanks to che220! + * Support for 2018 Lexus NX 300h thanks to kengggg! + * Speed up ECU firmware query + * Fix bug where manager would sometimes hang after shutting down the car + Version 0.7.2 (2020-02-07) ======================== * ECU firmware version based fingerprinting for Honda & Toyota diff --git a/SConstruct b/SConstruct index 73c603ad5..5944541b5 100644 --- a/SConstruct +++ b/SConstruct @@ -228,5 +228,6 @@ if arch == "aarch64": SConscript(['selfdrive/clocksd/SConscript']) SConscript(['selfdrive/locationd/SConscript']) +SConscript(['selfdrive/locationd/kalman/SConscript']) # TODO: finish cereal, dbcbuilder, MPC diff --git a/common/params.py b/common/params.py index 009e5729d..c623e3dc3 100755 --- a/common/params.py +++ b/common/params.py @@ -81,6 +81,7 @@ keys = { "LiveParameters": [TxType.PERSISTENT], "LongitudinalControl": [TxType.PERSISTENT], "OpenpilotEnabledToggle": [TxType.PERSISTENT], + "LaneChangeEnabled": [TxType.PERSISTENT], "PandaFirmware": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], "PandaFirmwareHex": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], "PandaDongleId": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], @@ -93,6 +94,7 @@ keys = { "TermsVersion": [TxType.PERSISTENT], "TrainingVersion": [TxType.PERSISTENT], "UpdateAvailable": [TxType.CLEAR_ON_MANAGER_START], + "UpdateFailedCount": [TxType.CLEAR_ON_MANAGER_START], "Version": [TxType.PERSISTENT], "Offroad_ChargeDisabled": [TxType.CLEAR_ON_MANAGER_START, TxType.CLEAR_ON_PANDA_DISCONNECT], "Offroad_ConnectivityNeeded": [TxType.CLEAR_ON_MANAGER_START], diff --git a/selfdrive/camerad/cameras/camera_qcom.c b/selfdrive/camerad/cameras/camera_qcom.c index c0f10f13b..ddaa51e34 100644 --- a/selfdrive/camerad/cameras/camera_qcom.c +++ b/selfdrive/camerad/cameras/camera_qcom.c @@ -1710,6 +1710,9 @@ static void parse_autofocus(CameraState *s, uint8_t *d) { int good_count = 0; int16_t max_focus = -32767; int avg_focus = 0; + // force to max if not able to determine focus for long + const int patience_cnt = 100; + static int nan_cnt = 0; /*printf("FOCUS: "); for (int i = 0; i < 0x10; i++) { @@ -1717,42 +1720,56 @@ static void parse_autofocus(CameraState *s, uint8_t *d) { }*/ for (int i = 0; i < NUM_FOCUS; i++) { - int doff = i*5+5; - s->confidence[i] = d[doff]; - int16_t focus_t = (d[doff+1] << 3) | (d[doff+2] >> 5); - if (focus_t >= 1024) focus_t = -(2048-focus_t); - s->focus[i] = focus_t; - //printf("%x->%d ", d[doff], focus_t); - if (s->confidence[i] > 0x20) { + int pd_idx = (i+1)*5; + s->confidence[i] = d[pd_idx]; + int16_t focus_delta = d[pd_idx+1]; + if (focus_delta >= 128) focus_delta = - (256 - focus_delta); + s->focus[i] = focus_delta; + + if (s->confidence[i] > 64) { good_count++; max_focus = max(max_focus, s->focus[i]); avg_focus += s->focus[i]; + // printf("%d\n", s->focus[i]); } } - //printf("\n"); - if (good_count < 4) { + if (good_count < 7) { s->focus_err = nan(""); + nan_cnt += 1; + if (nan_cnt > patience_cnt) { + s->focus_err = 16; + nan_cnt = 0; + } return; } avg_focus /= good_count; - // outlier rejection - if (abs(avg_focus - max_focus) > 200) { - s->focus_err = nan(""); - return; + if (abs(avg_focus - max_focus) > 32) { + if (nan_cnt < patience_cnt) { + s->focus_err = nan(""); + nan_cnt += 1; + return; + } else { + s->focus_err = 16; + // s->focus_err = max_focus*8.0; + nan_cnt = 0; + } + } else { + s->focus_err = avg_focus; + nan_cnt = 0; } - - s->focus_err = max_focus*1.0; + // printf("fe=%f\n", s->focus_err); } static void do_autofocus(CameraState *s) { - // params for focus PI controller - const float focus_kp = 0.005; + // params for focus P controller + const float focus_kp = 0.1; float err = s->focus_err; - float offset = 0; + // don't allow big change + err = clamp(err, -16, 16); float sag = (s->last_sag_acc_z/9.8) * 128; const int dac_up = s->device == DEVICE_LP3? 634:456; @@ -1776,6 +1793,7 @@ static void do_autofocus(CameraState *s) { LOGD(debug);*/ actuator_move(s, target); + // printf("ltp=%f, clp=%d\n",s->lens_true_pos,s->cur_lens_pos); } @@ -2165,6 +2183,9 @@ void cameras_run(DualCameraState *s) { } else { uint8_t *d = c->ss[buffer].bufs[buf_idx].addr; if (buffer == 1) { + // FILE *df = fopen("/sdcard/focus_buf","wb"); + // fwrite(d, c->ss[buffer].bufs[buf_idx].len, sizeof(uint8_t), df); + // fclose(df); parse_autofocus(c, d); } c->ss[buffer].qbuf_info[buf_idx].dirty_buf = 1; diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 2d0445004..f1766075e 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -133,6 +133,11 @@ def fingerprint(logcan, sendcan, has_relay): car_fingerprint = list(fw_candidates)[0] source = car.CarParams.FingerprintSource.fw + fixed_fingerprint = os.environ.get('FINGERPRINT', "") + if len(fixed_fingerprint): + car_fingerprint = fixed_fingerprint + source = car.CarParams.FingerprintSource.fixed + cloudlog.warning("fingerprinted %s", car_fingerprint) return car_fingerprint, finger, vin, car_fw, source diff --git a/selfdrive/car/chrysler/carcontroller.py b/selfdrive/car/chrysler/carcontroller.py index 2bc4e9680..f6b55b9e1 100644 --- a/selfdrive/car/chrysler/carcontroller.py +++ b/selfdrive/car/chrysler/carcontroller.py @@ -35,14 +35,14 @@ class CarController(): # steer torque new_steer = actuators.steer * SteerLimitParams.STEER_MAX apply_steer = apply_toyota_steer_torque_limits(new_steer, self.apply_steer_last, - CS.steer_torque_motor, SteerLimitParams) + CS.out.steeringTorqueEps, SteerLimitParams) self.steer_rate_limited = new_steer != apply_steer - moving_fast = CS.v_ego > CS.CP.minSteerSpeed # for status message - if CS.v_ego > (CS.CP.minSteerSpeed - 0.5): # for command high bit + moving_fast = CS.out.vEgo > CS.CP.minSteerSpeed # for status message + if CS.out.vEgo > (CS.CP.minSteerSpeed - 0.5): # for command high bit self.gone_fast_yet = True elif self.car_fingerprint in (CAR.PACIFICA_2019_HYBRID, CAR.PACIFICA_2020_HYBRID, CAR.JEEP_CHEROKEE_2019): - if CS.v_ego < (CS.CP.minSteerSpeed - 3.0): + if CS.out.vEgo < (CS.CP.minSteerSpeed - 3.0): self.gone_fast_yet = False # < 14.5m/s stock turns off this bit, but fine down to 13.5 lkas_active = moving_fast and enabled @@ -65,7 +65,7 @@ class CarController(): if (self.ccframe % 25 == 0): # 0.25s period if (CS.lkas_car_model != -1): new_msg = create_lkas_hud( - self.packer, CS.gear_shifter, lkas_active, hud_alert, + self.packer, CS.out.gearShifter, lkas_active, hud_alert, self.hud_count, CS.lkas_car_model) can_sends.append(new_msg) self.hud_count += 1 diff --git a/selfdrive/car/chrysler/carstate.py b/selfdrive/car/chrysler/carstate.py index 93b84cf7d..bab58b963 100644 --- a/selfdrive/car/chrysler/carstate.py +++ b/selfdrive/car/chrysler/carstate.py @@ -1,14 +1,9 @@ from cereal import car from opendbc.can.parser import CANParser +from opendbc.can.can_define import CANDefine +from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from selfdrive.car.chrysler.values import DBC, STEER_THRESHOLD -from common.kalman.simple_kalman import KF1D - -GearShifter = car.CarState.GearShifter - -def parse_gear_shifter(can_gear): - return {0x1: GearShifter.park, 0x2: GearShifter.reverse, 0x3: GearShifter.neutral, - 0x4: GearShifter.drive, 0x5: GearShifter.low}.get(can_gear, GearShifter.unknown) - def get_can_parser(CP): @@ -68,83 +63,61 @@ def get_camera_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): - - self.CP = CP - self.left_blinker_on = 0 - self.right_blinker_on = 0 - - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["GEAR"]['PRNDL'] def update(self, cp, cp_cam): - # update prevs, update must run once per loop - self.prev_left_blinker_on = self.left_blinker_on - self.prev_right_blinker_on = self.right_blinker_on + ret = car.CarState.new_message() self.frame_23b = int(cp.vl["WHEEL_BUTTONS"]['COUNTER']) self.frame = int(cp.vl["EPS_STATUS"]['COUNTER']) - self.door_all_closed = not any([cp.vl["DOORS"]['DOOR_OPEN_FL'], - cp.vl["DOORS"]['DOOR_OPEN_FR'], - cp.vl["DOORS"]['DOOR_OPEN_RL'], - cp.vl["DOORS"]['DOOR_OPEN_RR']]) - self.seatbelt = (cp.vl["SEATBELT_STATUS"]['SEATBELT_DRIVER_UNLATCHED'] == 0) + ret.doorOpen = any([cp.vl["DOORS"]['DOOR_OPEN_FL'], + cp.vl["DOORS"]['DOOR_OPEN_FR'], + cp.vl["DOORS"]['DOOR_OPEN_RL'], + cp.vl["DOORS"]['DOOR_OPEN_RR']]) + ret.seatbeltUnlatched = cp.vl["SEATBELT_STATUS"]['SEATBELT_DRIVER_UNLATCHED'] == 1 + + ret.brakePressed = cp.vl["BRAKE_2"]['BRAKE_PRESSED_2'] == 5 # human-only + ret.brake = 0 + ret.brakeLights = ret.brakePressed + ret.gas = cp.vl["ACCEL_GAS_134"]['ACCEL_134'] + ret.gasPressed = ret.gas > 1e-5 - self.brake_pressed = cp.vl["BRAKE_2"]['BRAKE_PRESSED_2'] == 5 # human-only - self.pedal_gas = cp.vl["ACCEL_GAS_134"]['ACCEL_134'] - self.car_gas = self.pedal_gas self.esp_disabled = (cp.vl["TRACTION_BUTTON"]['TRACTION_OFF'] == 1) - self.v_wheel_fl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FL'] - self.v_wheel_rr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RR'] - self.v_wheel_rl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RL'] - self.v_wheel_fr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FR'] - v_wheel = (cp.vl['SPEED_1']['SPEED_LEFT'] + cp.vl['SPEED_1']['SPEED_RIGHT']) / 2. + ret.wheelSpeeds.fl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FL'] + ret.wheelSpeeds.rr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RR'] + ret.wheelSpeeds.rl = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_RL'] + ret.wheelSpeeds.fr = cp.vl['WHEEL_SPEEDS']['WHEEL_SPEED_FR'] + ret.vEgoRaw = (cp.vl['SPEED_1']['SPEED_LEFT'] + cp.vl['SPEED_1']['SPEED_RIGHT']) / 2. + ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) + ret.standstill = not ret.vEgoRaw > 0.001 - # Kalman filter - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] + ret.leftBlinker = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 1 + ret.rightBlinker = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 2 + ret.steeringAngle = cp.vl["STEERING"]['STEER_ANGLE'] + ret.steeringRate = cp.vl["STEERING"]['STEERING_RATE'] + ret.gearShifter = self.parse_gear_shifter(self.shifter_values.get(cp.vl['GEAR']['PRNDL'], None)) - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - self.standstill = not v_wheel > 0.001 + ret.cruiseState.enabled = cp.vl["ACC_2"]['ACC_STATUS_2'] == 7 # ACC is green. + ret.cruiseState.available = ret.cruiseState.enabled # FIXME: for now same as enabled + ret.cruiseState.speed = cp.vl["DASHBOARD"]['ACC_SPEED_CONFIG_KPH'] * CV.KPH_TO_MS - self.angle_steers = cp.vl["STEERING"]['STEER_ANGLE'] - self.angle_steers_rate = cp.vl["STEERING"]['STEERING_RATE'] - self.gear_shifter = parse_gear_shifter(cp.vl['GEAR']['PRNDL']) - self.main_on = cp.vl["ACC_2"]['ACC_STATUS_2'] == 7 # ACC is green. - self.left_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 1 - self.right_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 2 - - self.steer_torque_driver = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] - self.steer_torque_motor = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] - self.steer_override = abs(self.steer_torque_driver) > STEER_THRESHOLD + ret.steeringTorque = cp.vl["EPS_STATUS"]["TORQUE_DRIVER"] + ret.steeringTorqueEps = cp.vl["EPS_STATUS"]["TORQUE_MOTOR"] + ret.steeringPressed = abs(ret.steeringTorque) > STEER_THRESHOLD steer_state = cp.vl["EPS_STATUS"]["LKAS_STATE"] - self.steer_error = steer_state == 4 or (steer_state == 0 and self.v_ego > self.CP.minSteerSpeed) + self.steer_error = steer_state == 4 or (steer_state == 0 and ret.vEgo > self.CP.minSteerSpeed) - self.user_brake = 0 - self.brake_lights = self.brake_pressed - self.v_cruise_pcm = cp.vl["DASHBOARD"]['ACC_SPEED_CONFIG_KPH'] - self.pcm_acc_status = self.main_on - - self.generic_toggle = bool(cp.vl["STEERING_LEVERS"]['HIGH_BEAM_FLASH']) + ret.genericToggle = bool(cp.vl["STEERING_LEVERS"]['HIGH_BEAM_FLASH']) self.lkas_counter = cp_cam.vl["LKAS_COMMAND"]['COUNTER'] self.lkas_car_model = cp_cam.vl["LKAS_HUD"]['CAR_MODEL'] self.lkas_status_ok = cp_cam.vl["LKAS_HEARTBIT"]['LKAS_STATUS_OK'] + + return ret diff --git a/selfdrive/car/chrysler/interface.py b/selfdrive/car/chrysler/interface.py index f01ad5a09..93220fd12 100755 --- a/selfdrive/car/chrysler/interface.py +++ b/selfdrive/car/chrysler/interface.py @@ -20,6 +20,8 @@ class CarInterface(CarInterfaceBase): self.brake_pressed_prev = False self.cruise_enabled_prev = False self.low_speed_alert = False + self.left_blinker_prev = False + self.right_blinker_prev = False # *** init the major players *** self.CS = CarState(CP) @@ -113,78 +115,33 @@ class CarInterface(CarInterfaceBase): self.cp.update_strings(can_strings) self.cp_cam.update_strings(can_strings) - self.CS.update(self.cp, self.cp_cam) - - # create message - ret = car.CarState.new_message() + ret = self.CS.update(self.cp, self.cp_cam) ret.canValid = self.cp.can_valid and self.cp_cam.can_valid # speeds - ret.vEgo = self.CS.v_ego - ret.vEgoRaw = self.CS.v_ego_raw - ret.aEgo = self.CS.a_ego - ret.yawRate = self.VM.yaw_rate(self.CS.angle_steers * CV.DEG_TO_RAD, self.CS.v_ego) - ret.standstill = self.CS.standstill - ret.wheelSpeeds.fl = self.CS.v_wheel_fl - ret.wheelSpeeds.fr = self.CS.v_wheel_fr - ret.wheelSpeeds.rl = self.CS.v_wheel_rl - ret.wheelSpeeds.rr = self.CS.v_wheel_rr - - # gear shifter - ret.gearShifter = self.CS.gear_shifter - - # gas pedal - ret.gas = self.CS.car_gas - ret.gasPressed = self.CS.pedal_gas > 0 - - # brake pedal - ret.brake = self.CS.user_brake - ret.brakePressed = self.CS.brake_pressed - ret.brakeLights = self.CS.brake_lights - - # steering wheel - ret.steeringAngle = self.CS.angle_steers - ret.steeringRate = self.CS.angle_steers_rate - - ret.steeringTorque = self.CS.steer_torque_driver - ret.steeringPressed = self.CS.steer_override + ret.yawRate = self.VM.yaw_rate(ret.steeringAngle * CV.DEG_TO_RAD, ret.vEgo) ret.steeringRateLimited = self.CC.steer_rate_limited if self.CC is not None else False - # cruise state - ret.cruiseState.enabled = self.CS.pcm_acc_status # same as main_on - ret.cruiseState.speed = self.CS.v_cruise_pcm * CV.KPH_TO_MS - ret.cruiseState.available = self.CS.main_on - ret.cruiseState.speedOffset = 0. - ret.cruiseState.standstill = False - # TODO: button presses buttonEvents = [] - if self.CS.left_blinker_on != self.CS.prev_left_blinker_on: + if ret.leftBlinker != self.left_blinker_prev: be = car.CarState.ButtonEvent.new_message() be.type = ButtonType.leftBlinker - be.pressed = self.CS.left_blinker_on != 0 + be.pressed = ret.leftBlinker != 0 buttonEvents.append(be) - if self.CS.right_blinker_on != self.CS.prev_right_blinker_on: + if ret.rightBlinker != self.right_blinker_prev: be = car.CarState.ButtonEvent.new_message() be.type = ButtonType.rightBlinker - be.pressed = self.CS.right_blinker_on != 0 + be.pressed = ret.rightBlinker != 0 buttonEvents.append(be) ret.buttonEvents = buttonEvents - ret.leftBlinker = bool(self.CS.left_blinker_on) - ret.rightBlinker = bool(self.CS.right_blinker_on) - ret.doorOpen = not self.CS.door_all_closed - ret.seatbeltUnlatched = not self.CS.seatbelt self.low_speed_alert = (ret.vEgo < self.CP.minSteerSpeed) - ret.genericToggle = self.CS.generic_toggle - #ret.lkasCounter = self.CS.lkas_counter - #ret.lkasCarModel = self.CS.lkas_car_model - # events events = [] if not (ret.gearShifter in (GearShifter.drive, GearShifter.low)): @@ -195,7 +152,7 @@ class CarInterface(CarInterfaceBase): events.append(create_event('seatbeltNotLatched', [ET.NO_ENTRY, ET.SOFT_DISABLE])) if self.CS.esp_disabled: events.append(create_event('espDisabled', [ET.NO_ENTRY, ET.SOFT_DISABLE])) - if not self.CS.main_on: + if not ret.cruiseState.available: events.append(create_event('wrongCarMode', [ET.NO_ENTRY, ET.USER_DISABLE])) if ret.gearShifter == GearShifter.reverse: events.append(create_event('reverseGear', [ET.NO_ENTRY, ET.IMMEDIATE_DISABLE])) @@ -220,8 +177,13 @@ class CarInterface(CarInterfaceBase): self.gas_pressed_prev = ret.gasPressed self.brake_pressed_prev = ret.brakePressed self.cruise_enabled_prev = ret.cruiseState.enabled + self.left_blinker_prev = ret.leftBlinker + self.right_blinker_prev = ret.rightBlinker - return ret.as_reader() + # copy back carState packet to CS + self.CS.out = ret.as_reader() + + return self.CS.out # pass in a car.CarControl # to be called @ 100hz diff --git a/selfdrive/car/chrysler/values.py b/selfdrive/car/chrysler/values.py index 68af182d4..1e9cdcc5f 100644 --- a/selfdrive/car/chrysler/values.py +++ b/selfdrive/car/chrysler/values.py @@ -53,7 +53,10 @@ FINGERPRINTS = { # Based on 3c7ce223e3571b54|2019-05-11--20-16-14 { 168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 520: 8, 528: 8, 532: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 653: 8, 654: 8, 655: 8, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 799: 8, 800: 8, 804: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 1562: 8, 1570: 8 - } + }, + # Based on "d26bf42deb1910e7|2020-02-13--16-22-31" + {168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 520: 8, 528: 8, 532: 8, 542: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 653: 8, 654: 8, 655: 8, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 799: 8, 800: 8, 804: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 1262: 8, 1284: 8, 1568: 8, 1902: 8, 2015: 8, 2016: 8, 2018: 8, 2023: 8, 2024: 8, 2026: 8, 2031: 8 + }, ], CAR.PACIFICA_2020_HYBRID: [ {168: 8, 257: 5, 258: 8, 264: 8, 268: 8, 270: 8, 274: 2, 280: 8, 284: 8, 288: 7, 290: 6, 291: 8, 292: 8, 294: 8, 300: 8, 308: 8, 320: 8, 324: 8, 331: 8, 332: 8, 344: 8, 368: 8, 376: 3, 384: 8, 388: 4, 448: 6, 456: 4, 464: 8, 469: 8, 480: 8, 500: 8, 501: 8, 512: 8, 514: 8, 515: 7, 516: 7, 517: 7, 518: 7, 520: 8, 524: 8, 526: 6, 528: 8, 532: 8, 542: 8, 544: 8, 557: 8, 559: 8, 560: 8, 564: 8, 571: 3, 579: 8, 584: 8, 608: 8, 624: 8, 625: 8, 632: 8, 639: 8, 650: 8, 653: 8, 654: 8, 655: 8, 656: 4, 658: 6, 660: 8, 669: 3, 671: 8, 672: 8, 678: 8, 680: 8, 683: 8, 701: 8, 703: 8, 704: 8, 705: 8, 706: 8, 709: 8, 710: 8, 719: 8, 720: 6, 729: 5, 736: 8, 737: 8, 738: 8, 746: 5, 752: 2, 754: 8, 760: 8, 764: 8, 766: 8, 770: 8, 773: 8, 779: 8, 782: 8, 784: 8, 792: 8, 799: 8, 800: 8, 804: 8, 808: 8, 816: 8, 817: 8, 820: 8, 825: 2, 826: 8, 832: 8, 838: 2, 848: 8, 853: 8, 856: 4, 860: 6, 863: 8, 878: 8, 882: 8, 897: 8, 906: 8, 908: 8, 924: 8, 926: 3, 929: 8, 937: 8, 938: 8, 939: 8, 940: 8, 941: 8, 942: 8, 943: 8, 947: 8, 948: 8, 958: 8, 959: 8, 962: 8, 969: 4, 973: 8, 974: 5, 979: 8, 980: 8, 981: 8, 982: 8, 983: 8, 984: 8, 992: 8, 993: 7, 995: 8, 996: 8, 1000: 8, 1001: 8, 1002: 8, 1003: 8, 1008: 8, 1009: 8, 1010: 8, 1011: 8, 1012: 8, 1013: 8, 1014: 8, 1015: 8, 1024: 8, 1025: 8, 1026: 8, 1031: 8, 1033: 8, 1050: 8, 1059: 8, 1082: 8, 1083: 8, 1098: 8, 1100: 8, 2015: 8, 2016: 8, 2024: 8}, diff --git a/selfdrive/car/fingerprints.py b/selfdrive/car/fingerprints.py index 4ca9fa093..c2c39f687 100644 --- a/selfdrive/car/fingerprints.py +++ b/selfdrive/car/fingerprints.py @@ -1,11 +1,12 @@ import os from common.basedir import BASEDIR -def get_attr_from_cars(attr): + +def get_attr_from_cars(attr, result=dict): # read all the folders in selfdrive/car and return a dict where: # - keys are all the car models # - values are attr values from all car folders - result = {} + result = result() for car_folder in [x[0] for x in os.walk(BASEDIR + '/selfdrive/car')]: try: @@ -16,8 +17,11 @@ def get_attr_from_cars(attr): else: continue - for f, v in attr_values.items(): - result[f] = v + if isinstance(attr_values, dict): + for f, v in attr_values.items(): + result[f] = v + elif isinstance(attr_values, list): + result += attr_values except (ImportError, IOError): pass @@ -25,20 +29,9 @@ def get_attr_from_cars(attr): return result -def get_fw_versions_list(): - return get_attr_from_cars('FW_VERSIONS') - - -def get_fingerprint_list(): - # read all the folders in selfdrive/car and return a dict where: - # - keys are all the car models for which we have a fingerprint - # - values are lists dicts of messages that constitute the unique - # CAN fingerprint of each car model and all its variants - return get_attr_from_cars('FINGERPRINTS') - - -FW_VERSIONS = get_fw_versions_list() -_FINGERPRINTS = get_fingerprint_list() +FW_VERSIONS = get_attr_from_cars('FW_VERSIONS') +_FINGERPRINTS = get_attr_from_cars('FINGERPRINTS') +IGNORED_FINGERPRINTS = get_attr_from_cars('IGNORED_FINGERPRINTS', list) _DEBUG_ADDRESS = {1880: 8} # reserved for debug purposes @@ -61,6 +54,9 @@ def eliminate_incompatible_cars(msg, candidate_cars): compatible_cars = [] for car_name in candidate_cars: + if car_name in IGNORED_FINGERPRINTS: + continue + car_fingerprints = _FINGERPRINTS[car_name] for fingerprint in car_fingerprints: diff --git a/selfdrive/car/ford/carcontroller.py b/selfdrive/car/ford/carcontroller.py index db574f38a..d708c1358 100644 --- a/selfdrive/car/ford/carcontroller.py +++ b/selfdrive/car/ford/carcontroller.py @@ -33,27 +33,27 @@ class CarController(): if (frame % 3) == 0: - curvature = self.vehicle_model.calc_curvature(actuators.steerAngle*3.1415/180., CS.v_ego) + curvature = self.vehicle_model.calc_curvature(actuators.steerAngle*3.1415/180., CS.out.vEgo) # The use of the toggle below is handy for trying out the various LKAS modes if TOGGLE_DEBUG: - self.lkas_action += int(CS.generic_toggle and not self.generic_toggle_last) + self.lkas_action += int(CS.out.genericToggle and not self.generic_toggle_last) self.lkas_action &= 0xf else: self.lkas_action = 5 # 4 and 5 seem the best. 8 and 9 seem to aggressive and laggy can_sends.append(create_steer_command(self.packer, apply_steer, enabled, - CS.lkas_state, CS.angle_steers, curvature, self.lkas_action)) - self.generic_toggle_last = CS.generic_toggle + CS.lkas_state, CS.out.steeringAngle, curvature, self.lkas_action)) + self.generic_toggle_last = CS.out.genericToggle if (frame % 100) == 0: can_sends.append(make_can_msg(973, b'\x00\x00\x00\x00\x00\x00\x00\x00', 0)) #can_sends.append(make_can_msg(984, b'\x00\x00\x00\x00\x80\x45\x60\x30', 0)) - if (frame % 100) == 0 or (self.enabled_last != enabled) or (self.main_on_last != CS.main_on) or \ + if (frame % 100) == 0 or (self.enabled_last != enabled) or (self.main_on_last != CS.out.cruiseState.available) or \ (self.steer_alert_last != steer_alert): - can_sends.append(create_lkas_ui(self.packer, CS.main_on, enabled, steer_alert)) + can_sends.append(create_lkas_ui(self.packer, CS.out.cruiseState.available, enabled, steer_alert)) if (frame % 200) == 0: can_sends.append(make_can_msg(1875, b'\x80\xb0\x55\x55\x78\x90\x00\x00', 1)) @@ -81,7 +81,7 @@ class CarController(): can_sends.append(make_can_msg(addr, (cnt<<4).to_bytes(1, 'little') + b'\x00\x00\x00\x00\x00\x00\x00', 1)) self.enabled_last = enabled - self.main_on_last = CS.main_on + self.main_on_last = CS.out.cruiseState.available self.steer_alert_last = steer_alert return can_sends diff --git a/selfdrive/car/ford/carstate.py b/selfdrive/car/ford/carstate.py index 7484e064c..ec461ab16 100644 --- a/selfdrive/car/ford/carstate.py +++ b/selfdrive/car/ford/carstate.py @@ -1,8 +1,9 @@ +from cereal import car from opendbc.can.parser import CANParser from common.numpy_fast import mean from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from selfdrive.car.ford.values import DBC -from common.kalman.simple_kalman import KF1D WHEEL_RADIUS = 0.33 @@ -32,57 +33,28 @@ def get_can_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 0) -class CarState(): - def __init__(self, CP): - - self.CP = CP - self.left_blinker_on = 0 - self.right_blinker_on = 0 - - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - +class CarState(CarStateBase): def update(self, cp): - # update prevs, update must run once per loop - self.prev_left_blinker_on = self.left_blinker_on - self.prev_right_blinker_on = self.right_blinker_on - - # calc best v_ego estimate, by averaging two opposite corners - self.v_wheel_fl = cp.vl["WheelSpeed_CG1"]['WhlRr_W_Meas'] * WHEEL_RADIUS - self.v_wheel_fr = cp.vl["WheelSpeed_CG1"]['WhlRl_W_Meas'] * WHEEL_RADIUS - self.v_wheel_rl = cp.vl["WheelSpeed_CG1"]['WhlFr_W_Meas'] * WHEEL_RADIUS - self.v_wheel_rr = cp.vl["WheelSpeed_CG1"]['WhlFl_W_Meas'] * WHEEL_RADIUS - v_wheel = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) - - # Kalman filter - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - self.standstill = not v_wheel > 0.001 - - self.angle_steers = cp.vl["Steering_Wheel_Data_CG1"]['SteWhlRelInit_An_Sns'] - self.v_cruise_pcm = cp.vl["Cruise_Status"]['Set_Speed'] * CV.MPH_TO_MS - self.pcm_acc_status = cp.vl["Cruise_Status"]['Cruise_State'] - self.main_on = cp.vl["Cruise_Status"]['Cruise_State'] != 0 - self.lkas_state = cp.vl["Lane_Keep_Assist_Status"]['LaActAvail_D_Actl'] + ret = car.CarState.new_message() + ret.wheelSpeeds.rr = cp.vl["WheelSpeed_CG1"]['WhlRr_W_Meas'] * WHEEL_RADIUS + ret.wheelSpeeds.rl = cp.vl["WheelSpeed_CG1"]['WhlRl_W_Meas'] * WHEEL_RADIUS + ret.wheelSpeeds.fr = cp.vl["WheelSpeed_CG1"]['WhlFr_W_Meas'] * WHEEL_RADIUS + ret.wheelSpeeds.fl = cp.vl["WheelSpeed_CG1"]['WhlFl_W_Meas'] * WHEEL_RADIUS + ret.vEgoRaw = mean([ret.wheelSpeeds.rr, ret.wheelSpeeds.rl, ret.wheelSpeeds.fr, ret.wheelSpeeds.fl]) + ret.vEgo, ret.aEgo = self.update_speed_kf(ret.vEgoRaw) + ret.standstill = not ret.vEgoRaw > 0.001 + ret.steeringAngle = cp.vl["Steering_Wheel_Data_CG1"]['SteWhlRelInit_An_Sns'] + ret.steeringPressed = not cp.vl["Lane_Keep_Assist_Status"]['LaHandsOff_B_Actl'] + ret.cruiseState.speed = cp.vl["Cruise_Status"]['Set_Speed'] * CV.MPH_TO_MS + ret.cruiseState.enabled = not (cp.vl["Cruise_Status"]['Cruise_State'] in [0, 3]) + ret.cruiseState.available = cp.vl["Cruise_Status"]['Cruise_State'] != 0 + ret.gas = cp.vl["EngineData_14"]['ApedPosScal_Pc_Actl'] / 100. + ret.gasPressed = ret.gas > 1e-6 + ret.brakePressed = bool(cp.vl["Cruise_Status"]["Brake_Drv_Appl"]) + ret.brakeLights = bool(cp.vl["BCM_to_HS_Body"]["Brake_Lights"]) + ret.genericToggle = bool(cp.vl["Steering_Buttons"]["Dist_Incr"]) # TODO: we also need raw driver torque, needed for Assisted Lane Change - self.steer_override = not cp.vl["Lane_Keep_Assist_Status"]['LaHandsOff_B_Actl'] + self.lkas_state = cp.vl["Lane_Keep_Assist_Status"]['LaActAvail_D_Actl'] self.steer_error = cp.vl["Lane_Keep_Assist_Status"]['LaActDeny_B_Actl'] - self.user_gas = cp.vl["EngineData_14"]['ApedPosScal_Pc_Actl'] - self.brake_pressed = bool(cp.vl["Cruise_Status"]["Brake_Drv_Appl"]) - self.brake_lights = bool(cp.vl["BCM_to_HS_Body"]["Brake_Lights"]) - self.generic_toggle = bool(cp.vl["Steering_Buttons"]["Dist_Incr"]) + + return ret diff --git a/selfdrive/car/ford/interface.py b/selfdrive/car/ford/interface.py index d9fad4bb5..6b1f9340f 100755 --- a/selfdrive/car/ford/interface.py +++ b/selfdrive/car/ford/interface.py @@ -106,38 +106,10 @@ class CarInterface(CarInterfaceBase): # ******************* do can recv ******************* self.cp.update_strings(can_strings) - self.CS.update(self.cp) - - # create message - ret = car.CarState.new_message() + ret = self.CS.update(self.cp) ret.canValid = self.cp.can_valid - # speeds - ret.vEgo = self.CS.v_ego - ret.vEgoRaw = self.CS.v_ego_raw - ret.standstill = self.CS.standstill - ret.wheelSpeeds.fl = self.CS.v_wheel_fl - ret.wheelSpeeds.fr = self.CS.v_wheel_fr - ret.wheelSpeeds.rl = self.CS.v_wheel_rl - ret.wheelSpeeds.rr = self.CS.v_wheel_rr - - # steering wheel - ret.steeringAngle = self.CS.angle_steers - ret.steeringPressed = self.CS.steer_override - - # gas pedal - ret.gas = self.CS.user_gas / 100. - ret.gasPressed = self.CS.user_gas > 0.0001 - ret.brakePressed = self.CS.brake_pressed - ret.brakeLights = self.CS.brake_lights - - ret.cruiseState.enabled = not (self.CS.pcm_acc_status in [0, 3]) - ret.cruiseState.speed = self.CS.v_cruise_pcm - ret.cruiseState.available = self.CS.pcm_acc_status != 0 - - ret.genericToggle = self.CS.generic_toggle - # events events = [] @@ -167,7 +139,9 @@ class CarInterface(CarInterfaceBase): self.brake_pressed_prev = ret.brakePressed self.cruise_enabled_prev = ret.cruiseState.enabled - return ret.as_reader() + self.CS.out = ret.as_reader() + + return self.CS.out # pass in a car.CarControl # to be called @ 100hz diff --git a/selfdrive/car/fw_versions.py b/selfdrive/car/fw_versions.py index 5a3dacf32..96b7756c2 100755 --- a/selfdrive/car/fw_versions.py +++ b/selfdrive/car/fw_versions.py @@ -123,9 +123,12 @@ def get_fw_versions(logcan, sendcan, bus, extra=None, timeout=0.1, debug=False, ecu_types[a] = ecu_type if sub_addr is None: - parallel_addrs.append(a) + if a not in parallel_addrs: + parallel_addrs.append(a) else: - addrs.append([a]) + if [a] not in addrs: + addrs.append([a]) + addrs.insert(0, parallel_addrs) fw_versions = {} diff --git a/selfdrive/car/gm/carstate.py b/selfdrive/car/gm/carstate.py index 8a083cb53..f924c66f1 100644 --- a/selfdrive/car/gm/carstate.py +++ b/selfdrive/car/gm/carstate.py @@ -1,9 +1,10 @@ from cereal import car from common.numpy_fast import mean -from common.kalman.simple_kalman import KF1D from selfdrive.config import Conversions as CV +from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser -from selfdrive.car.gm.values import DBC, CAR, parse_gear_shifter, \ +from selfdrive.car.interfaces import CarStateBase +from selfdrive.car.gm.values import DBC, CAR, \ CruiseButtons, is_eps_status_ok, \ STEER_THRESHOLD, SUPERCRUISE_CARS @@ -50,25 +51,11 @@ def get_powertrain_can_parser(CP, canbus): return CANParser(DBC[CP.carFingerprint]['pt'], signals, [], canbus.powertrain) -class CarState(): - def __init__(self, CP, canbus): - self.CP = CP - # initialize can parser - - self.car_fingerprint = CP.carFingerprint - self.cruise_buttons = CruiseButtons.UNPRESS - self.left_blinker_on = False - self.prev_left_blinker_on = False - self.right_blinker_on = False - self.prev_right_blinker_on = False - - # vEgo kalman filter - dt = 0.01 - self.v_ego_kf = KF1D(x0=[[0.], [0.]], - A=[[1., dt], [0., 1.]], - C=[1., 0.], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0. +class CarState(CarStateBase): + def __init__(self, CP): + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["ECMPRDNL"]["PRNDL"] def update(self, pt_cp): self.prev_cruise_buttons = self.cruise_buttons @@ -78,20 +65,12 @@ class CarState(): self.v_wheel_fr = pt_cp.vl["EBCMWheelSpdFront"]['FRWheelSpd'] * CV.KPH_TO_MS self.v_wheel_rl = pt_cp.vl["EBCMWheelSpdRear"]['RLWheelSpd'] * CV.KPH_TO_MS self.v_wheel_rr = pt_cp.vl["EBCMWheelSpdRear"]['RRWheelSpd'] * CV.KPH_TO_MS - v_wheel = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) - - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - + self.v_ego_raw = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) self.standstill = self.v_ego_raw < 0.01 self.angle_steers = pt_cp.vl["PSCMSteeringAngle"]['SteeringWheelAngle'] - self.gear_shifter = parse_gear_shifter(pt_cp.vl["ECMPRDNL"]['PRNDL']) + self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(pt_cp.vl["ECMPRDNL"]['PRNDL'], None)) self.user_brake = pt_cp.vl["EBCMBrakePedalPosition"]['BrakePedalPosition'] self.pedal_gas = pt_cp.vl["AcceleratorPedal"]['AcceleratorPedal'] diff --git a/selfdrive/car/gm/gmcan.py b/selfdrive/car/gm/gmcan.py index 68b129e74..3db0f5b02 100644 --- a/selfdrive/car/gm/gmcan.py +++ b/selfdrive/car/gm/gmcan.py @@ -67,8 +67,8 @@ def create_friction_brake_command(packer, bus, apply_brake, idx, near_stop, at_f else: mode = 0xa - if at_full_stop: - mode = 0xd + if at_full_stop: + mode = 0xd # TODO: this is to have GM bringing the car to complete stop, # but currently it conflicts with OP controls, so turned off. #elif near_stop: diff --git a/selfdrive/car/gm/interface.py b/selfdrive/car/gm/interface.py index 93ee18ff2..450fce853 100755 --- a/selfdrive/car/gm/interface.py +++ b/selfdrive/car/gm/interface.py @@ -29,7 +29,7 @@ class CarInterface(CarInterfaceBase): # *** init the major players *** canbus = CanBus() - self.CS = CarState(CP, canbus) + self.CS = CarState(CP) self.VM = VehicleModel(CP) self.pt_cp = get_powertrain_can_parser(CP, canbus) self.ch_cp_dbc_name = DBC[CP.carFingerprint]['chassis'] @@ -64,6 +64,13 @@ class CarInterface(CarInterfaceBase): ret.openpilotLongitudinalControl = ret.enableCamera tire_stiffness_factor = 0.444 # not optimized yet + # Start with a baseline lateral tuning for all GM vehicles. Override tuning as needed in each model section below. + ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]] + ret.lateralTuning.pid.kf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594 + ret.steerRateCost = 1.0 + ret.steerActuatorDelay = 0.1 # Default delay, not measured yet + if candidate == CAR.VOLT: # supports stop and go, but initial engage must be above 18mph (which include conservatism) ret.minEnableSpeed = 18 * CV.MPH_TO_MS @@ -141,11 +148,6 @@ class CarInterface(CarInterfaceBase): ret.tireStiffnessFront, ret.tireStiffnessRear = scale_tire_stiffness(ret.mass, ret.wheelbase, ret.centerToFront, tire_stiffness_factor=tire_stiffness_factor) - # same tuning for Volt and CT6 for now - ret.lateralTuning.pid.kiBP, ret.lateralTuning.pid.kpBP = [[0.], [0.]] - ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.2], [0.00]] - ret.lateralTuning.pid.kf = 0.00004 # full torque for 20 deg at 80mph means 0.00007818594 - ret.steerMaxBP = [0.] # m/s ret.steerMaxV = [1.] ret.gasMaxBP = [0.] @@ -163,8 +165,6 @@ class CarInterface(CarInterfaceBase): ret.stoppingControl = True ret.startAccel = 0.8 - ret.steerActuatorDelay = 0.1 # Default delay, not measured yet - ret.steerRateCost = 1.0 ret.steerLimitTimer = 0.4 ret.radarTimeStep = 0.0667 # GM radar runs at 15Hz instead of standard 20Hz ret.steerControlType = car.CarParams.SteerControlType.torque @@ -237,7 +237,7 @@ class CarInterface(CarInterfaceBase): be.pressed = self.CS.right_blinker_on buttonEvents.append(be) - if self.CS.cruise_buttons != self.CS.prev_cruise_buttons: + if self.CS.cruise_buttons != self.CS.prev_cruise_buttons and self.CS.prev_cruise_buttons != CruiseButtons.INIT: be = car.CarState.ButtonEvent.new_message() be.type = ButtonType.unknown if self.CS.cruise_buttons != CruiseButtons.UNPRESS: diff --git a/selfdrive/car/gm/values.py b/selfdrive/car/gm/values.py index 468deb01d..ccde62ea3 100644 --- a/selfdrive/car/gm/values.py +++ b/selfdrive/car/gm/values.py @@ -14,6 +14,7 @@ class CAR: SUPERCRUISE_CARS = [CAR.CADILLAC_CT6] class CruiseButtons: + INIT = 0 UNPRESS = 1 RES_ACCEL = 2 DECEL_SET = 3 @@ -34,18 +35,6 @@ def is_eps_status_ok(eps_status, car_fingerprint): valid_eps_status += [0, 1] return eps_status in valid_eps_status -def parse_gear_shifter(can_gear): - if can_gear == 0: - return car.CarState.GearShifter.park - elif can_gear == 1: - return car.CarState.GearShifter.neutral - elif can_gear == 2: - return car.CarState.GearShifter.drive - elif can_gear == 3: - return car.CarState.GearShifter.reverse - else: - return car.CarState.GearShifter.unknown - FINGERPRINTS = { # Astra BK MY17, ASCM unplugged CAR.HOLDEN_ASTRA: [{ diff --git a/selfdrive/car/honda/carstate.py b/selfdrive/car/honda/carstate.py index 52bea3d7d..f7b8d5779 100644 --- a/selfdrive/car/honda/carstate.py +++ b/selfdrive/car/honda/carstate.py @@ -1,19 +1,11 @@ -from cereal import car from collections import defaultdict from common.numpy_fast import interp -from common.kalman.simple_kalman import KF1D from opendbc.can.can_define import CANDefine from opendbc.can.parser import CANParser from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from selfdrive.car.honda.values import CAR, DBC, STEER_THRESHOLD, SPEED_FACTOR, HONDA_BOSCH -GearShifter = car.CarState.GearShifter - -def parse_gear_shifter(gear): - return {'P': GearShifter.park, 'R': GearShifter.reverse, 'N': GearShifter.neutral, - 'D': GearShifter.drive, 'S': GearShifter.sport, 'L': GearShifter.low}.get(gear, GearShifter.unknown) - - def calc_cruise_offset(offset, speed): # euristic formula so that speed is controlled to ~ 0.3m/s below pid_speed # constraints to solve for _K0, _K1, _K2 are: @@ -188,38 +180,21 @@ def get_cam_can_parser(CP): bus_cam = 1 if CP.carFingerprint in HONDA_BOSCH and not CP.isPandaBlack else 2 return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, bus_cam) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): - self.CP = CP - self.can_define = CANDefine(DBC[CP.carFingerprint]['pt']) - self.shifter_values = self.can_define.dv["GEARBOX"]["GEAR_SHIFTER"] - self.steer_status_values = defaultdict(lambda: "UNKNOWN", self.can_define.dv["STEER_STATUS"]["STEER_STATUS"]) + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["GEARBOX"]["GEAR_SHIFTER"] + self.steer_status_values = defaultdict(lambda: "UNKNOWN", can_define.dv["STEER_STATUS"]["STEER_STATUS"]) self.user_gas, self.user_gas_pressed = 0., 0 self.brake_switch_prev = 0 self.brake_switch_ts = 0 - - self.cruise_buttons = 0 self.cruise_setting = 0 self.v_cruise_pcm_prev = 0 - self.blinker_on = 0 - - self.left_blinker_on = 0 - self.right_blinker_on = 0 - self.cruise_mode = 0 self.stopped = 0 - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - def update(self, cp, cp_cam): # car params @@ -229,8 +204,6 @@ class CarState(): # update prevs, update must run once per loop self.prev_cruise_buttons = self.cruise_buttons self.prev_cruise_setting = self.cruise_setting - self.prev_blinker_on = self.blinker_on - self.prev_left_blinker_on = self.left_blinker_on self.prev_right_blinker_on = self.right_blinker_on @@ -260,7 +233,6 @@ class CarState(): self.brake_error = cp.vl["STANDSTILL"]['BRAKE_ERROR_1'] or cp.vl["STANDSTILL"]['BRAKE_ERROR_2'] self.esp_disabled = cp.vl["VSA_STATUS"]['ESP_DISABLED'] - # calc best v_ego estimate, by averaging two opposite corners speed_factor = SPEED_FACTOR[self.CP.carFingerprint] self.v_wheel_fl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FL'] * CV.KPH_TO_MS * speed_factor self.v_wheel_fr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FR'] * CV.KPH_TO_MS * speed_factor @@ -270,16 +242,10 @@ class CarState(): # blend in transmission speed at low speed, since it has more low speed accuracy self.v_weight = interp(v_wheel, v_weight_bp, v_weight_v) - speed = (1. - self.v_weight) * cp.vl["ENGINE_DATA"]['XMISSION_SPEED'] * CV.KPH_TO_MS * speed_factor + \ + self.v_ego_raw = (1. - self.v_weight) * cp.vl["ENGINE_DATA"]['XMISSION_SPEED'] * CV.KPH_TO_MS * speed_factor + \ self.v_weight * v_wheel - if abs(speed - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[speed], [0.0]] - - self.v_ego_raw = speed - v_ego_x = self.v_ego_kf.update(speed) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) # this is a hack for the interceptor. This is now only used in the simulation # TODO: Replace tests by toyota so this can go away @@ -310,7 +276,7 @@ class CarState(): self.main_on = cp.vl["SCM_BUTTONS"]['MAIN_ON'] can_gear_shifter = int(cp.vl["GEARBOX"]['GEAR_SHIFTER']) - self.gear_shifter = parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) + self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) self.pedal_gas = cp.vl["POWERTRAIN_DATA"]['PEDAL_GAS'] # crv doesn't include cruise control diff --git a/selfdrive/car/honda/values.py b/selfdrive/car/honda/values.py index 124e44ccd..7e273e847 100644 --- a/selfdrive/car/honda/values.py +++ b/selfdrive/car/honda/values.py @@ -132,11 +132,12 @@ FW_VERSIONS = { b'37805-6A0-A640\x00\x00', b'37805-6B2-A550\x00\x00', b'37805-6B2-A650\x00\x00', + b'37805-6B2-A660\x00\x00', b'37805-6B2-M520\x00\x00', ], (Ecu.unknown, 0x18da0bf1, None): [b'54008-TVC-A910\x00\x00'], (Ecu.unknown, 0x18da1ef1, None): [b'28102-6B8-A560\x00\x00', b'28102-6B8-M520\x00\x00'], - (Ecu.unknown, 0x18da2bf1, None): [b'46114-TVA-A060\x00\x00'], + (Ecu.unknown, 0x18da2bf1, None): [b'46114-TVA-A060\x00\x00', b'46114-TVA-A080\x00\x00'], (Ecu.unknown, 0x18da28f1, None): [b'57114-TVA-C050\x00\x00'], (Ecu.eps, 0x18da30f1, None): [ b'39990-TVA-A150\x00\x00', @@ -153,7 +154,11 @@ FW_VERSIONS = { b'78109-TVC-M510\x00\x00', ], (Ecu.unknown, 0x18da61f1, None): [b'78209-TVA-A010\x00\x00'], - (Ecu.unknown, 0x18dab0f1, None): [b'36802-TVA-A160\x00\x00', b'36802-TWA-A070\x00\x00'], + (Ecu.unknown, 0x18dab0f1, None): [ + b'36802-TVA-A160\x00\x00', + b'36802-TVA-A170\x00\x00', + b'36802-TWA-A070\x00\x00', + ], (Ecu.unknown, 0x18dab5f1, None): [b'36161-TVA-A060\x00\x00', b'36161-TWA-A070\x00\x00'], (Ecu.unknown, 0x18daeff1, None): [b'38897-TVA-A010\x00\x00'], }, @@ -224,6 +229,7 @@ FW_VERSIONS = { b'37805-5AA-A670\x00\x00', b'37805-5AA-A680\x00\x00', b'37805-5AA-A810\x00\x00', + b'37805-5AA-C820\x00\x00', b'37805-5AA-L660\x00\x00', b'37805-5AJ-A610\x00\x00', b'37805-5BA-A510\x00\x00', @@ -236,6 +242,7 @@ FW_VERSIONS = { b'28101-5CG-A070\x00\x00', b'28101-5CG-A080\x00\x00', b'28101-5CG-A810\x00\x00', + b'28101-5CG-A820\x00\x00', b'28101-5DJ-A040\x00\x00', b'28101-5DJ-A060\x00\x00', b'28101-5DJ-A510\x00\x00', @@ -262,6 +269,7 @@ FW_VERSIONS = { b'78109-TBC-A510\x00\x00', b'78109-TBC-A520\x00\x00', b'78109-TBC-A530\x00\x00', + b'78109-TBC-C530\x00\x00', b'78109-TBH-A530\x00\x00', b'78109-TEG-A310\x00\x00', ], @@ -272,7 +280,6 @@ FW_VERSIONS = { b'36161-TEG-A010\x00\x00', ], (Ecu.unknown, 0x18daeff1, None): [ - b'36161-TBA-A030\x00\x00', b'38897-TBA-A010\x00\x00', b'38897-TBA-A020\x00\x00', ], @@ -285,11 +292,15 @@ FW_VERSIONS = { b'37805-5AN-A830\x00\x00', b'37805-5AN-A930\x00\x00', b'37805-5AN-L940\x00\x00', + b'37805-5AN-LH20\x00\x00', + b'37805-5AN-LJ20\x00\x00', b'37805-5AZ-E850\x00\x00', b'37805-5BB-L640\x00\x00', ], (Ecu.unknown, 0x18da1ef1, None): [ b'28101-5CG-A920\x00\x00', + b'28101-5CG-C220\x00\x00', + b'28101-5CG-C320\x00\x00', b'28101-5CK-A130\x00\x00', b'28101-5CK-A140\x00\x00', b'28101-5CK-A150\x00\x00', @@ -301,6 +312,7 @@ FW_VERSIONS = { b'57114-TBG-A340\x00\x00', b'57114-TGG-A340\x00\x00', b'57114-TGL-G330\x00\x00', + b'57114-TGG-C320\x00\x00', ], (Ecu.eps, 0x18da30f1, None): [ b'39990-TBA-C020\x00\x00', @@ -308,31 +320,41 @@ FW_VERSIONS = { b'39990-TGG-A020\x00\x00', b'39990-TGG-A120\x00\x00', b'39990-TGL-E130\x00\x00', + b'39990-TGG-A020\x00\x00', ], (Ecu.unknown, 0x18da53f1, None): [ b'77959-TBA-A060\x00\x00', b'77959-TGG-A020\x00\x00', b'77959-TGG-G010\x00\x00', + b'77959-TGG-A020\x00\x00', ], (Ecu.unknown, 0x18da60f1, None): [ b'78109-TBA-A910\x00\x00', b'78109-TBC-A740\x00\x00', b'78109-TGG-A210\x00\x00', b'78109-TGG-A310\x00\x00', + b'78109-TGG-A320\x00\x00', b'78109-TGG-A810\x00\x00', + b'78109-TGG-A820\x00\x00', b'78109-TGL-G120\x00\x00', ], (Ecu.unknown, 0x18dab0f1, None): [ b'36802-TBA-A150\x00\x00', b'36802-TGG-A050\x00\x00', b'36802-TGL-G040\x00\x00', + b'36802-TGG-A060\x00\x00', ], (Ecu.unknown, 0x18dab5f1, None): [ b'36161-TBA-A130\x00\x00', b'36161-TGG-A060\x00\x00', b'36161-TGL-G050\x00\x00', + b'36161-TGG-A080\x00\x00', + ], + (Ecu.unknown, 0x18daeff1, None): [ + b'38897-TBA-A110\x00\x00', + b'38897-TBA-A020\x00\x00', + b'38897-TBA-A020\x00\x00', ], - (Ecu.unknown, 0x18daeff1, None): [b'38897-TBA-A110\x00\x00', b'38897-TBA-A020\x00\x00'], }, CAR.CRV_5G: { (Ecu.unknown, 0x18da10f1, None): [ @@ -344,6 +366,7 @@ FW_VERSIONS = { b'37805-5PA-A680\x00\x00', b'37805-5PA-A850\x00\x00', b'37805-5PA-A870\x00\x00', + b'37805-5PA-A880\x00\x00', b'37805-5PA-A890\x00\x00', ], (Ecu.unknown, 0x18da1ef1, None): [ @@ -422,6 +445,7 @@ FW_VERSIONS = { b'28102-5MX-A610\x00\x00', b'28102-5MX-A710\x00\x00', b'28102-5MX-A910\x00\x00', + b'28102-5MX-C001\x00\x00', b'28103-5NZ-A300\x00\x00', ], (Ecu.unknown, 0x18da28f1, None): [b'57114-THR-A040\x00\x00', b'57114-THR-A110\x00\x00'], @@ -436,6 +460,7 @@ FW_VERSIONS = { b'78109-THR-AE40\x00\x00', b'78109-THR-AL10\x00\x00', b'78109-THR-C330\x00\x00', + b'78109-THR-CE20\x00\x00', ], (Ecu.unknown, 0x18da0bf1, None): [b'54008-THR-A020\x00\x00'], }, diff --git a/selfdrive/car/hyundai/carstate.py b/selfdrive/car/hyundai/carstate.py index 7ebb8d70d..5305396f3 100644 --- a/selfdrive/car/hyundai/carstate.py +++ b/selfdrive/car/hyundai/carstate.py @@ -1,8 +1,8 @@ from cereal import car from selfdrive.car.hyundai.values import DBC, STEER_THRESHOLD +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from selfdrive.config import Conversions as CV -from common.kalman.simple_kalman import KF1D GearShifter = car.CarState.GearShifter @@ -124,27 +124,7 @@ def get_camera_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): - def __init__(self, CP): - - self.CP = CP - - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - self.left_blinker_on = 0 - self.left_blinker_flash = 0 - self.right_blinker_on = 0 - self.right_blinker_flash = 0 +class CarState(CarStateBase): def update(self, cp, cp_cam): # update prevs, update must run once per Loop @@ -162,36 +142,26 @@ class CarState(): self.acc_active = cp.vl["SCC12"]['ACCMode'] != 0 self.pcm_acc_status = int(self.acc_active) - # calc best v_ego estimate, by averaging two opposite corners self.v_wheel_fl = cp.vl["WHL_SPD11"]['WHL_SPD_FL'] * CV.KPH_TO_MS self.v_wheel_fr = cp.vl["WHL_SPD11"]['WHL_SPD_FR'] * CV.KPH_TO_MS self.v_wheel_rl = cp.vl["WHL_SPD11"]['WHL_SPD_RL'] * CV.KPH_TO_MS self.v_wheel_rr = cp.vl["WHL_SPD11"]['WHL_SPD_RR'] * CV.KPH_TO_MS - v_wheel = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. + self.v_ego_raw = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) - self.low_speed_lockout = v_wheel < 1.0 + self.low_speed_lockout = self.v_ego_raw < 1.0 - # Kalman filter, even though Hyundai raw wheel speed is heaviliy filtered by default - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) is_set_speed_in_mph = int(cp.vl["CLU11"]["CF_Clu_SPEED_UNIT"]) speed_conv = CV.MPH_TO_MS if is_set_speed_in_mph else CV.KPH_TO_MS self.cruise_set_speed = cp.vl["SCC11"]['VSetDis'] * speed_conv - self.standstill = not v_wheel > 0.1 + self.standstill = not self.v_ego_raw > 0.1 self.angle_steers = cp.vl["SAS11"]['SAS_Angle'] self.angle_steers_rate = cp.vl["SAS11"]['SAS_Speed'] self.yaw_rate = cp.vl["ESP12"]['YAW_RATE'] self.main_on = True self.left_blinker_on = cp.vl["CGW1"]['CF_Gway_TSigLHSw'] - self.left_blinker_flash = cp.vl["CGW1"]['CF_Gway_TurnSigLh'] self.right_blinker_on = cp.vl["CGW1"]['CF_Gway_TSigRHSw'] - self.right_blinker_flash = cp.vl["CGW1"]['CF_Gway_TurnSigRh'] self.steer_override = abs(cp.vl["MDPS11"]['CR_Mdps_DrvTq']) > STEER_THRESHOLD self.steer_state = cp.vl["MDPS12"]['CF_Mdps_ToiActive'] #0 NOT ACTIVE, 1 ACTIVE self.steer_error = cp.vl["MDPS12"]['CF_Mdps_ToiUnavail'] diff --git a/selfdrive/car/interfaces.py b/selfdrive/car/interfaces.py index ca1972a16..e98b2681b 100644 --- a/selfdrive/car/interfaces.py +++ b/selfdrive/car/interfaces.py @@ -1,8 +1,12 @@ import os import time from cereal import car +from common.kalman.simple_kalman import KF1D +from common.realtime import DT_CTRL from selfdrive.car import gen_empty_fingerprint +GearShifter = car.CarState.GearShifter + # generic car and radar interfaces class CarInterfaceBase(): @@ -42,3 +46,31 @@ class RadarInterfaceBase(): time.sleep(self.radar_ts) # radard runs on RI updates return ret + +class CarStateBase: + def __init__(self, CP): + self.CP = CP + self.car_fingerprint = CP.carFingerprint + self.left_blinker_on = 0 + self.right_blinker_on = 0 + self.cruise_buttons = 0 + + # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) + # R = 1e3 + self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], + A=[[1.0, DT_CTRL], [0.0, 1.0]], + C=[1.0, 0.0], + K=[[0.12287673], [0.29666309]]) + + def update_speed_kf(self, v_ego_raw): + if abs(v_ego_raw - self.v_ego_kf.x[0][0]) > 2.0: # Prevent large accelerations when car starts at non zero speed + self.v_ego_kf.x = [[v_ego_raw], [0.0]] + + v_ego_x = self.v_ego_kf.update(v_ego_raw) + return float(v_ego_x[0]), float(v_ego_x[1]) + + @staticmethod + def parse_gear_shifter(gear): + return {'P': GearShifter.park, 'R': GearShifter.reverse, 'N': GearShifter.neutral, + 'E': GearShifter.eco, 'T': GearShifter.manumatic, 'D': GearShifter.drive, + 'S': GearShifter.sport, 'L': GearShifter.low, 'B': GearShifter.brake}.get(gear, GearShifter.unknown) diff --git a/selfdrive/car/subaru/carcontroller.py b/selfdrive/car/subaru/carcontroller.py index 69f573fef..dce4df810 100644 --- a/selfdrive/car/subaru/carcontroller.py +++ b/selfdrive/car/subaru/carcontroller.py @@ -53,7 +53,7 @@ class CarController(): apply_steer = apply_std_steer_torque_limits(new_steer, self.apply_steer_last, CS.steer_torque_driver, P) self.steer_rate_limited = new_steer != apply_steer - lkas_enabled = enabled and not CS.steer_not_allowed + lkas_enabled = enabled if not lkas_enabled: apply_steer = 0 diff --git a/selfdrive/car/subaru/carstate.py b/selfdrive/car/subaru/carstate.py index 3f0447432..10500ce6f 100644 --- a/selfdrive/car/subaru/carstate.py +++ b/selfdrive/car/subaru/carstate.py @@ -1,6 +1,6 @@ import copy -from common.kalman.simple_kalman import KF1D from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from selfdrive.car.subaru.values import DBC, STEER_THRESHOLD @@ -82,29 +82,12 @@ def get_camera_can_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): + super().__init__(CP) # initialize can parser - self.CP = CP - - self.car_fingerprint = CP.carFingerprint - self.left_blinker_on = False self.left_blinker_cnt = 0 - self.prev_left_blinker_on = False - self.right_blinker_on = False self.right_blinker_cnt = 0 - self.prev_right_blinker_on = False - self.steer_torque_driver = 0 - self.steer_not_allowed = False - self.main_on = False - - # vEgo kalman filter - dt = 0.01 - self.v_ego_kf = KF1D(x0=[[0.], [0.]], - A=[[1., dt], [0., 1.]], - C=[1., 0.], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0. def update(self, cp, cp_cam): @@ -124,16 +107,10 @@ class CarState(): if cp.vl["Dash_State"]['Units'] == 1: self.v_cruise_pcm *= CV.MPH_TO_KPH - v_wheel = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. + self.v_ego_raw = (self.v_wheel_fl + self.v_wheel_fr + self.v_wheel_rl + self.v_wheel_rr) / 4. # Kalman filter, even though Subaru raw wheel speed is heaviliy filtered by default - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) self.standstill = self.v_ego_raw < 0.01 self.prev_left_blinker_on = self.left_blinker_on diff --git a/selfdrive/car/toyota/carcontroller.py b/selfdrive/car/toyota/carcontroller.py index c2349784b..24c3bfed7 100644 --- a/selfdrive/car/toyota/carcontroller.py +++ b/selfdrive/car/toyota/carcontroller.py @@ -23,12 +23,6 @@ ANGLE_DELTA_BP = [0., 5., 15.] ANGLE_DELTA_V = [5., .8, .15] # windup limit ANGLE_DELTA_VU = [5., 3.5, 0.4] # unwind limit -TARGET_IDS = [0x340, 0x341, 0x342, 0x343, 0x344, 0x345, - 0x363, 0x364, 0x365, 0x370, 0x371, 0x372, - 0x373, 0x374, 0x375, 0x380, 0x381, 0x382, - 0x383] - - def accel_hysteresis(accel, accel_steady, enabled): # for small accel oscillations within ACCEL_HYST_GAP, don't change the accel command diff --git a/selfdrive/car/toyota/carstate.py b/selfdrive/car/toyota/carstate.py index 9ed0fec3f..cb172b591 100644 --- a/selfdrive/car/toyota/carstate.py +++ b/selfdrive/car/toyota/carstate.py @@ -1,17 +1,10 @@ -from cereal import car from common.numpy_fast import mean -from common.kalman.simple_kalman import KF1D from opendbc.can.can_define import CANDefine +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from selfdrive.config import Conversions as CV from selfdrive.car.toyota.values import CAR, DBC, STEER_THRESHOLD, TSS2_CAR, NO_DSU_CAR -GearShifter = car.CarState.GearShifter - -def parse_gear_shifter(gear): - return {'P': GearShifter.park, 'R': GearShifter.reverse, 'N': GearShifter.neutral, - 'D': GearShifter.drive, 'B': GearShifter.brake}.get(gear, GearShifter.unknown) - def get_can_parser(CP): @@ -89,30 +82,14 @@ def get_cam_can_parser(CP): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP): - - self.CP = CP - self.can_define = CANDefine(DBC[CP.carFingerprint]['pt']) - self.shifter_values = self.can_define.dv["GEAR_PACKET"]['GEAR'] - self.left_blinker_on = 0 - self.right_blinker_on = 0 + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["GEAR_PACKET"]['GEAR'] self.angle_offset = 0. self.init_angle_offset = False - # initialize can parser - self.car_fingerprint = CP.carFingerprint - - # vEgo kalman filter - dt = 0.01 - # Q = np.matrix([[10.0, 0.0], [0.0, 100.0]]) - # R = 1e3 - self.v_ego_kf = KF1D(x0=[[0.0], [0.0]], - A=[[1.0, dt], [0.0, 1.0]], - C=[1.0, 0.0], - K=[[0.12287673], [0.29666309]]) - self.v_ego = 0.0 - def update(self, cp, cp_cam): # update prevs, update must run once per loop self.prev_left_blinker_on = self.left_blinker_on @@ -127,25 +104,16 @@ class CarState(): self.pedal_gas = (cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS'] + cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS2']) / 2. else: self.pedal_gas = cp.vl["GAS_PEDAL"]['GAS_PEDAL'] - self.car_gas = self.pedal_gas self.esp_disabled = cp.vl["ESP_CONTROL"]['TC_DISABLED'] - # calc best v_ego estimate, by averaging two opposite corners self.v_wheel_fl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FL'] * CV.KPH_TO_MS self.v_wheel_fr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FR'] * CV.KPH_TO_MS self.v_wheel_rl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RL'] * CV.KPH_TO_MS self.v_wheel_rr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RR'] * CV.KPH_TO_MS - v_wheel = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) + self.v_ego_raw = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) - # Kalman filter - if abs(v_wheel - self.v_ego) > 2.0: # Prevent large accelerations when car starts at non zero speed - self.v_ego_kf.x = [[v_wheel], [0.0]] - - self.v_ego_raw = v_wheel - v_ego_x = self.v_ego_kf.update(v_wheel) - self.v_ego = float(v_ego_x[0]) - self.a_ego = float(v_ego_x[1]) - self.standstill = not v_wheel > 0.001 + self.standstill = not self.v_ego_raw > 0.001 if self.CP.carFingerprint in TSS2_CAR: self.angle_steers = cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] @@ -161,7 +129,7 @@ class CarState(): self.angle_steers = cp.vl["STEER_ANGLE_SENSOR"]['STEER_ANGLE'] + cp.vl["STEER_ANGLE_SENSOR"]['STEER_FRACTION'] self.angle_steers_rate = cp.vl["STEER_ANGLE_SENSOR"]['STEER_RATE'] can_gear = int(cp.vl["GEAR_PACKET"]['GEAR']) - self.gear_shifter = parse_gear_shifter(self.shifter_values.get(can_gear, None)) + self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) if self.CP.carFingerprint == CAR.LEXUS_IS: self.main_on = cp.vl["DSU_CRUISE"]['MAIN_ON'] else: diff --git a/selfdrive/car/toyota/interface.py b/selfdrive/car/toyota/interface.py index c17782c3f..ff397e442 100755 --- a/selfdrive/car/toyota/interface.py +++ b/selfdrive/car/toyota/interface.py @@ -163,6 +163,16 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.1]] ret.lateralTuning.pid.kf = 0.00006 + elif candidate == CAR.HIGHLANDER_TSS2: + stop_and_go = True + ret.safetyParam = 73 + ret.wheelbase = 2.84988 # 112.2 in = 2.84988 m + ret.steerRatio = 16.0 + tire_stiffness_factor = 0.8 + ret.mass = 4700. * CV.LB_TO_KG + STD_CARGO_KG # 4260 + 4-5 people + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.18], [0.015]] # community tuning + ret.lateralTuning.pid.kf = 0.00012 # community tuning + elif candidate in [CAR.HIGHLANDER, CAR.HIGHLANDERH]: stop_and_go = True ret.safetyParam = 73 @@ -192,7 +202,7 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.1]] ret.mass = 3370. * CV.LB_TO_KG + STD_CARGO_KG ret.lateralTuning.pid.kf = 0.00007818594 - + elif candidate == CAR.RAV4H_TSS2: stop_and_go = True ret.safetyParam = 73 @@ -253,6 +263,16 @@ class CarInterface(CarInterfaceBase): ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.3], [0.05]] ret.lateralTuning.pid.kf = 0.00007 + elif candidate == CAR.LEXUS_NXH: + stop_and_go = True + ret.safetyParam = 73 + ret.wheelbase = 2.66 + ret.steerRatio = 14.7 + tire_stiffness_factor = 0.444 # not optimized yet + ret.mass = 4070 * CV.LB_TO_KG + STD_CARGO_KG + ret.lateralTuning.pid.kpV, ret.lateralTuning.pid.kiV = [[0.6], [0.1]] + ret.lateralTuning.pid.kf = 0.00006 + ret.steerRateCost = 1. ret.centerToFront = ret.wheelbase * 0.44 @@ -276,11 +296,14 @@ class CarInterface(CarInterfaceBase): ret.brakeMaxV = [1.] ret.enableCamera = is_ecu_disconnected(fingerprint[0], FINGERPRINTS, ECU_FINGERPRINT, candidate, Ecu.fwdCamera) or has_relay + # Detect smartDSU, which intercepts ACC_CMD from the DSU allowing openpilot to send it + smartDsu = 0x2FF in fingerprint[0] # In TSS2 cars the camera does long control ret.enableDsu = is_ecu_disconnected(fingerprint[0], FINGERPRINTS, ECU_FINGERPRINT, candidate, Ecu.dsu) and candidate not in TSS2_CAR ret.enableApgs = False # is_ecu_disconnected(fingerprint[0], FINGERPRINTS, ECU_FINGERPRINT, candidate, Ecu.apgs) ret.enableGasInterceptor = 0x201 in fingerprint[0] - ret.openpilotLongitudinalControl = ret.enableCamera and (ret.enableDsu or candidate in TSS2_CAR) + # if the smartDSU is detected, openpilot can send ACC_CMD (and the smartDSU will block it from the DSU) or not (the DSU is "connected") + ret.openpilotLongitudinalControl = ret.enableCamera and (smartDsu or ret.enableDsu or candidate in TSS2_CAR) cloudlog.warning("ECU Camera Simulated: %r", ret.enableCamera) cloudlog.warning("ECU DSU Simulated: %r", ret.enableDsu) cloudlog.warning("ECU APGS Simulated: %r", ret.enableApgs) @@ -291,7 +314,8 @@ class CarInterface(CarInterfaceBase): ret.minEnableSpeed = -1. if (stop_and_go or ret.enableGasInterceptor) else 19. * CV.MPH_TO_MS # removing the DSU disables AEB and it's considered a community maintained feature - ret.communityFeature = ret.enableGasInterceptor or ret.enableDsu + # intercepting the DSU is a community feature since it requires unofficial hardware + ret.communityFeature = ret.enableGasInterceptor or ret.enableDsu or smartDsu ret.longitudinalTuning.deadzoneBP = [0., 9.] ret.longitudinalTuning.deadzoneV = [0., .15] @@ -341,7 +365,7 @@ class CarInterface(CarInterfaceBase): ret.gearShifter = self.CS.gear_shifter # gas pedal - ret.gas = self.CS.car_gas + ret.gas = self.CS.pedal_gas if self.CP.enableGasInterceptor: # use interceptor values to disengage on pedal press ret.gasPressed = self.CS.pedal_gas > 15 diff --git a/selfdrive/car/toyota/values.py b/selfdrive/car/toyota/values.py index 7fbe60512..6b09b41ab 100644 --- a/selfdrive/car/toyota/values.py +++ b/selfdrive/car/toyota/values.py @@ -22,6 +22,7 @@ class CAR: CAMRY = "TOYOTA CAMRY 2018" CAMRYH = "TOYOTA CAMRY HYBRID 2018" HIGHLANDER = "TOYOTA HIGHLANDER 2017" + HIGHLANDER_TSS2 = "TOYOTA HIGHLANDER 2020" HIGHLANDERH = "TOYOTA HIGHLANDER HYBRID 2018" AVALON = "TOYOTA AVALON 2016" RAV4_TSS2 = "TOYOTA RAV4 2019" @@ -33,28 +34,29 @@ class CAR: LEXUS_IS = "LEXUS IS300 2018" LEXUS_CTH = "LEXUS CT 200H 2018" RAV4H_TSS2 = "TOYOTA RAV4 HYBRID 2019" + LEXUS_NXH = "LEXUS NX300H 2018" # addr: (ecu, cars, bus, 1/freq*100, vl) STATIC_MSGS = [ - (0x128, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON), 1, 3, b'\xf4\x01\x90\x83\x00\x37'), + (0x128, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON), 1, 3, b'\xf4\x01\x90\x83\x00\x37'), (0x128, Ecu.dsu, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.SIENNA, CAR.LEXUS_CTH), 1, 3, b'\x03\x00\x20\x00\x00\x52'), - (0x141, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 2, b'\x00\x00\x00\x46'), - (0x160, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 7, b'\x00\x00\x08\x12\x01\x31\x9c\x51'), - (0x161, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON, CAR.LEXUS_RX), 1, 7, b'\x00\x1e\x00\x00\x00\x80\x07'), + (0x141, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 2, b'\x00\x00\x00\x46'), + (0x160, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 1, 7, b'\x00\x00\x08\x12\x01\x31\x9c\x51'), + (0x161, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.AVALON, CAR.LEXUS_RX), 1, 7, b'\x00\x1e\x00\x00\x00\x80\x07'), (0X161, Ecu.dsu, (CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.SIENNA, CAR.LEXUS_CTH), 1, 7, b'\x00\x1e\x00\xd4\x00\x00\x5b'), - (0x283, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 3, b'\x00\x00\x00\x00\x00\x00\x8c'), + (0x283, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 3, b'\x00\x00\x00\x00\x00\x00\x8c'), (0x2E6, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 3, b'\xff\xf8\x00\x08\x7f\xe0\x00\x4e'), (0x2E7, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 3, b'\xa8\x9c\x31\x9c\x00\x00\x00\x02'), (0x33E, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH), 0, 20, b'\x0f\xff\x26\x40\x00\x1f\x00'), - (0x344, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 5, b'\x00\x00\x01\x00\x00\x00\x00\x50'), - (0x365, Ecu.dsu, (CAR.PRIUS, CAR.LEXUS_RXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x00\x80\x03\x00\x08'), + (0x344, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 5, b'\x00\x00\x01\x00\x00\x00\x00\x50'), + (0x365, Ecu.dsu, (CAR.PRIUS, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x00\x80\x03\x00\x08'), (0x365, Ecu.dsu, (CAR.RAV4, CAR.RAV4H, CAR.COROLLA, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 20, b'\x00\x00\x00\x80\xfc\x00\x08'), - (0x366, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x4d\x82\x40\x02\x00'), + (0x366, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.HIGHLANDERH), 0, 20, b'\x00\x00\x4d\x82\x40\x02\x00'), (0x366, Ecu.dsu, (CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 20, b'\x00\x72\x07\xff\x09\xfe\x00'), (0x470, Ecu.dsu, (CAR.PRIUS, CAR.LEXUS_RXH), 1, 100, b'\x00\x00\x02\x7a'), (0x470, Ecu.dsu, (CAR.HIGHLANDER, CAR.HIGHLANDERH, CAR.RAV4H, CAR.SIENNA, CAR.LEXUS_CTH), 1, 100, b'\x00\x00\x01\x79'), - (0x4CB, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 100, b'\x0c\x00\x00\x00\x00\x00\x00\x00'), + (0x4CB, Ecu.dsu, (CAR.PRIUS, CAR.RAV4H, CAR.LEXUS_RXH, CAR.LEXUS_NXH, CAR.RAV4, CAR.COROLLA, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.AVALON, CAR.SIENNA, CAR.LEXUS_CTH, CAR.LEXUS_RX), 0, 100, b'\x0c\x00\x00\x00\x00\x00\x00\x00'), (0x292, Ecu.apgs, (CAR.PRIUS), 0, 3, b'\x00\x00\x00\x00\x00\x00\x00\x9e'), (0x32E, Ecu.apgs, (CAR.PRIUS), 0, 20, b'\x00\x00\x00\x00\x00\x00\x00\x00'), @@ -67,195 +69,206 @@ STATIC_MSGS = [ ECU_FINGERPRINT = { Ecu.fwdCamera: [0x2e4], # steer torque cmd - Ecu.dsu: [0x343], # accel cmd + Ecu.dsu: [0x283], # accel cmd Ecu.apgs: [0x835], # angle cmd } FINGERPRINTS = { CAR.RAV4: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8 }], CAR.RAV4H: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 296: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 548: 8, 550: 8, 552: 4, 560: 7, 562: 4, 581: 5, 608: 8, 610: 5, 643: 7, 705: 8, 713: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1212: 8, 1227: 8, 1228: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 296: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 548: 8, 550: 8, 552: 4, 560: 7, 562: 4, 581: 5, 608: 8, 610: 5, 643: 7, 705: 8, 713: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1212: 8, 1227: 8, 1228: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1656: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # Chinese RAV4 { - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 742: 8, 743: 8, 800: 8, 830: 7, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1207: 8, 1227: 8, 1235: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 830: 7, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1207: 8, 1227: 8, 1235: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1745: 8, 1779: 8 }], CAR.PRIUS: [{ # with ipas - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2,898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2,898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #2019 LE { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # 2020 Prius Prime LE { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 800: 8, 810: 2, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 767:4, 800: 8, 810: 2, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #2020 Prius Prime Limited { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8, 2026: 8, 2027: 8, 2029: 8, 2030: 8, 2031: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 824: 2, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2024: 8, 2026: 8, 2027: 8, 2029: 8, 2030: 8, 2031: 8 }, #2020 Central Europe Prime { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 800: 8, 810: 2, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 8, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 767:4, 800: 8, 810: 2, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 8, 974: 8, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8 + }, + #2017 German Prius + { + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296:8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8,740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1077: 8, 1082: 8, 1083: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1777: 8, 1779: 8, 1792: 8, 1767:4, 800: 8, 1863:8, 1904: 8, 1912: 8, 1984: 8, 1988: 8, 1990: 8, 1992: 8, 1996:8, 1998: 8, 2002: 8, 2010: 8, 2015: 8, 2016: 8, 2018: 8, 2024: 8, 2026: 8, 2030: 8 }], #Corolla w/ added Pedal Support (512L and 513L) CAR.COROLLA: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 2, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1196: 8, 1227: 8, 1235: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2021: 8, 2022: 8, 2023: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 512: 6, 513: 6, 547: 8, 548: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 2, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 4, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1196: 8, 1227: 8, 1235: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1596: 8, 1597: 8, 1600: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2016: 8, 2017: 8, 2018: 8, 2019: 8, 2020: 8, 2021: 8, 2022: 8, 2023: 8, 2024: 8 }], CAR.LEXUS_RX: [{ # 2016 Lexus RX 350 - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }, # 2017 Lexus RX 350 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.LEXUS_RXH: [{ - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513:6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1956: 8, 1960: 8, 1964: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8, 2012: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513:6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1956: 8, 1960: 8, 1964: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8, 2012: 8 }, # RX450HL # TODO: get proper fingerprint in stock mode { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # RX540H 2019 with color hud { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1952: 8, 1960: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 512: 6, 513: 6, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1777: 8, 1779: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1952: 8, 1960: 8, 1990: 8, 1998: 8 }, # 2017 RX 450h { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 744: 8, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1745: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 658: 8, 713: 8, 740: 5, 742: 8, 743: 8, 744: 8, 767:4, 800: 8, 810: 2, 812: 3, 814: 8, 818: 8, 819: 8, 820: 8, 821: 8, 822: 8, 830: 7, 835: 8, 836: 8, 845: 5, 863: 8, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 6, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1071: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1349: 8, 1350: 8, 1351: 8, 1413: 8, 1414: 8, 1415: 8, 1416: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1595: 8, 1745: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.LEXUS_RX_TSS2: [ # 2020 Lexus RX 350 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594:8, 1595: 8, 1600: 8, 1649: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 740: 5, 742: 8, 743: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594:8, 1595: 8, 1600: 8, 1649: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.CHR: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 705: 8, 740: 5, 800: 8, 810: 2, 812: 8, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 913: 8, 918: 8, 921: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 705: 8, 740: 5, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 913: 8, 918: 8, 921: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8 }], CAR.CHRH: [{ - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 800: 8, 810: 2, 812: 8, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 614: 8, 643: 7, 658: 8, 713: 8, 740: 5, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 829: 2, 830: 7, 835: 8, 836: 8, 845: 5, 869: 7, 870: 7, 871: 2, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1021: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1083: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1175: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.CAMRY: [ #XLE and LE { - 36: 8, 37: 8, 119: 6, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 119: 6, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #XSE and SE # TODO: get proper fingerprint in stock mode { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 888: 8, 889: 8, 891: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, { # 2019 XSE - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 942: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1792: 8, 1800: 8, 1808: 8, 1816: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1937: 8, 1945: 8, 1953: 8, 1961: 8, 1968: 8, 1976: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 942: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 983: 8, 984: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1412: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1792: 8, 1767:4, 800: 8, 1808: 8, 1816: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1937: 8, 1945: 8, 1953: 8, 1961: 8, 1968: 8, 1976: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.CAMRYH: [ #SE, LE and LE with Blindspot Monitor { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #SL { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, #XLE { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 761: 8, 764: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 869: 7, 870: 7, 871: 2, 888: 8, 889: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 983: 8, 984: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1011: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1595: 8, 1745: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.HIGHLANDER: [{ - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1984: 8, 1988: 8, 1992: 8, 1996: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1984: 8, 1988: 8, 1992: 8, 1996: 8, 1990: 8, 1998: 8 }, # 2019 Highlander XLE { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # 2017 Highlander Limited { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # 2018 Highlander Limited Platinum { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1585: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1988: 8, 1990: 8, 1996: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 355: 5, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 922: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1008: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1207: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1585: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1988: 8, 1990: 8, 1996: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + }], + CAR.HIGHLANDER_TSS2: [{ + # 2020 highlander limited + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 355: 5, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1816: 8, 1904: 8, 1912: 8, 1952: 8, 1960: 8, 1990: 8, 1998: 8 }], CAR.HIGHLANDERH: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, { # 2019 Highlander Hybrid Limited Platinum - 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.AVALON: [{ - 36: 8, 37: 8, 170: 8, 180: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 905: 8, 911: 1, 916: 2, 921: 8, 933: 6, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 1005: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1206: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1558: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 547: 8, 550: 8, 552: 4, 562: 6, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 767:4, 800: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 905: 8, 911: 1, 916: 2, 921: 8, 933: 6, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 1005: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1206: 8, 1227: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1558: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1596: 8, 1597: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.RAV4_TSS2: [ # LE { - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553:8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 355: 5, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8,1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553:8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # XLE, Limited, and AWD { - 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 565: 8, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1063: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 }], CAR.COROLLA_TSS2: [ # hatch 2019+ and sedan 2020+ { - 36: 8, 37: 8, 114: 5, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1809: 8, 1816: 8, 1817: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1960: 8, 1981: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8 + 36: 8, 37: 8, 114: 5, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 705: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1595: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1809: 8, 1816: 8, 1817: 8, 1840: 8, 1848: 8, 1904: 8, 1912: 8, 1940: 8, 1941: 8, 1948: 8, 1949: 8, 1952: 8, 1960: 8, 1981: 8, 1986: 8, 1990: 8, 1994: 8, 1998: 8, 2004: 8 }], CAR.COROLLAH_TSS2: [ # 2019 Taiwan Altis Hybrid { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 800: 8, 810: 2, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 918: 7, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1082: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1745: 8, 1775: 8, 1779: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 767:4, 800: 8, 810: 2, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 918: 7, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1082: 8, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1237: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1745: 8, 1775: 8, 1779: 8 }, # 2019 Chinese Levin Hybrid { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 800: 8, 810: 2, 812: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1600: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 829: 2, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 885: 8, 896: 8, 898: 8, 921: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 993: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1172: 8, 1235: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1600: 8, 1649: 8, 1745: 8, 1775: 8, 1779: 8 } ], CAR.LEXUS_ES_TSS2: [{ - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 401: 8, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 550: 8, 552: 4, 562: 6, 608: 8, 610: 8, 643: 7, 658: 8, 705: 8, 728: 8, 740: 5, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 830: 7, 835: 8, 836: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 976: 1, 987: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8, }], CAR.LEXUS_ESH_TSS2: [ { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 744: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 744: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1056: 8, 1057: 8, 1059: 1, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1775: 8, 1777: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.SIENNA: [ { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 888: 8, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 918: 7, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 767:4, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 888: 8, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 918: 7, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1228: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # XLE AWD 2018 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 426: 6, 452: 8, 464: 8, 466: 8, 467: 8, 544: 4, 545: 5, 548: 8, 550: 8, 552: 4, 562: 4, 608: 8, 610: 5, 643: 7, 705: 8, 725: 2, 740: 5, 764: 8, 767:4, 800: 8, 824: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 921: 8, 933: 8, 944: 6, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1008: 2, 1014: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1200: 8, 1201: 8, 1202: 8, 1203: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1552: 8, 1553: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1656: 8, 1664: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.LEXUS_IS: [ # IS300 2018 { - 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 400: 6, 426: 6, 452: 8, 464: 8, 466: 8, 467: 5, 544: 4, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1009: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1590: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1648: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 114: 5, 119: 6, 120: 4, 170: 8, 180: 8, 186: 4, 238: 4, 400: 6, 426: 6, 452: 8, 464: 8, 466: 8, 467: 5, 544: 4, 550: 8, 552: 4, 608: 8, 610: 5, 643: 7, 705: 8, 740: 5, 767:4, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 8, 933: 8, 944: 8, 945: 8, 951: 8, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1005: 2, 1008: 2, 1009: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1182: 8, 1183: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1235: 8, 1237: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1590: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1648: 8, 1666: 8, 1667: 8, 1728: 8, 1745: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, # IS300H 2017 { - 36: 8, 37: 8, 170: 8, 180: 8, 295: 8, 296: 8, 400: 6, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 7, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1009: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 170: 8, 180: 8, 295: 8, 296: 8, 400: 6, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 767:4, 800: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 7, 921: 7, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1009: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1112: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1187: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1232: 8, 1235: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }], CAR.RAV4H_TSS2: [ #Hybrid Limited { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913:8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 767:4, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913:8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1084: 8, 1085: 8, 1086: 8, 1114: 8, 1132: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1808: 8, 1810: 8, 1816: 8, 1818: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }, - # German Lounge - { - 36: 8, 37: 8, 166: 8, 170: 8, 180: 8, 295: 8, 296: 8, 401: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 8, 643: 7, 658: 8, 713: 8, 728: 8, 740: 5, 742: 8, 743: 8, 761: 8, 764: 8, 765: 8, 800: 8, 810: 2, 812: 8, 814: 8, 818: 8, 822: 8, 824: 8, 829: 2, 830: 7, 835: 8, 836: 8, 863: 8, 865: 8, 869: 7, 870: 7, 871: 2, 877: 8, 881: 8, 882: 8, 885: 8, 889: 8, 891: 8, 896: 8, 898: 8, 900: 6, 902: 6, 905: 8, 913: 8, 918: 8, 921: 8, 933: 8, 934: 8, 935: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 8, 955: 8, 956: 8, 971: 7, 975: 5, 987: 8, 993: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1044: 8, 1056: 8, 1057: 8, 1059: 1, 1063: 8, 1071: 8, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1172: 8, 1228: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1541: 8, 1552: 8, 1553: 8, 1556: 8, 1557: 8, 1568: 8, 1570: 8, 1571: 8, 1572: 8, 1592: 8, 1594: 8, 1595: 8, 1649: 8, 1696: 8, 1745: 8, 1775: 8, 1779: 8, 1786: 8, 1787: 8, 1788: 8, 1789: 8, 1792: 8, 1800: 8, 1872: 8, 1880: 8, 1904: 8, 1912: 8, 1937: 8, 1945: 8, 1953: 8, 1961: 8, 1968: 8, 1976: 8, 1990: 8, 1998: 8, 2015: 8, 2016: 8, 2024: 8 - }], + ], CAR.LEXUS_CTH: [{ 36: 8, 37: 8, 170: 8, 180: 8, 288: 8, 426: 6, 452: 8, 466: 8, 467: 8, 548: 8, 552: 4, 560: 7, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 800: 8, 810: 2, 832: 8, 835: 8, 836: 8, 849: 4, 869: 7, 870: 7, 871: 2, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 916: 1, 921: 8, 933: 8, 944: 6, 945: 8, 950: 8, 951: 8, 953: 3, 955: 4, 956: 8, 979: 2, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1017: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1114: 8, 1116: 8, 1160: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1190: 8, 1191: 8, 1192: 8, 1227: 8, 1235: 8, 1279: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1558: 8, 1561: 8, 1562: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1664: 8, 1728: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 + }], + CAR.LEXUS_NXH: [{ + 36: 8, 37: 8, 170: 8, 180: 8, 295: 8, 296: 8, 426: 6, 452: 8, 466: 8, 467: 8, 550: 8, 552: 4, 560: 7, 562: 6, 581: 5, 608: 8, 610: 5, 643: 7, 713: 8, 740: 5, 742: 8, 743: 8, 764: 8, 800: 8, 810: 2, 812: 3, 818: 8, 822: 8, 824: 8, 835: 8, 836: 8, 845: 5, 849: 4, 869: 7, 870: 7, 871: 2, 889: 8, 891: 8, 896: 8, 897: 8, 900: 6, 902: 6, 905: 8, 911: 8, 913: 8, 916: 3, 918: 8, 921: 8, 933: 8, 944: 8, 945: 8, 950: 8, 951: 8, 953: 3, 955: 8, 956: 8, 979: 2, 987: 8, 992: 8, 998: 5, 999: 7, 1000: 8, 1001: 8, 1002: 8, 1006: 8, 1014: 8, 1017: 8, 1020: 8, 1041: 8, 1042: 8, 1043: 8, 1056: 8, 1057: 8, 1059: 1, 1076: 8, 1077: 8, 1082: 8, 1114: 8, 1161: 8, 1162: 8, 1163: 8, 1164: 8, 1165: 8, 1166: 8, 1167: 8, 1168: 1, 1176: 8, 1177: 8, 1178: 8, 1179: 8, 1180: 8, 1181: 8, 1184: 8, 1185: 8, 1186: 8, 1189: 8, 1190: 8, 1191: 8, 1192: 8, 1195: 8, 1196: 8, 1197: 8, 1198: 8, 1199: 8, 1206: 8, 1208: 8, 1212: 8, 1227: 8, 1228: 8, 1232: 8, 1235: 8, 1237: 8, 1263: 8, 1264: 8, 1279: 8, 1408: 8, 1409: 8, 1410: 8, 1552: 8, 1553: 8, 1554: 8, 1555: 8, 1556: 8, 1557: 8, 1561: 8, 1568: 8, 1569: 8, 1570: 8, 1571: 8, 1572: 8, 1575: 8, 1584: 8, 1589: 8, 1592: 8, 1593: 8, 1595: 8, 1599: 8, 1656: 8, 1728: 8, 1745: 8, 1777: 8, 1779: 8, 1904: 8, 1912: 8, 1990: 8, 1998: 8 }] } +# Don't use theses fingerprints for fingerprinting, they are still needed for ECU detection +IGNORED_FINGERPRINTS = [CAR.RAV4H_TSS2] + FW_VERSIONS = { CAR.AVALON: { (Ecu.esp, 0x7b0, None): [b'F152607060\x00\x00\x00\x00\x00\x00'], @@ -267,6 +280,7 @@ FW_VERSIONS = { }, CAR.CAMRY: { (Ecu.engine, 0x700, None): [ + b'\x018966306L5200\x00\x00\x00\x00', b'\x018966333P4200\x00\x00\x00\x00', b'\x018966333P4300\x00\x00\x00\x00', b'\x018966333P4400\x00\x00\x00\x00', @@ -325,9 +339,18 @@ FW_VERSIONS = { b'8646F0605000 ', ], }, + CAR.CHR: { + (Ecu.dsu, 0x791, None): [b'8821FF404100 '], + (Ecu.esp, 0x7b0, None): [b'F1526F4122\x00\x00\x00\x00\x00\x00'], + (Ecu.eps, 0x7a1, None): [b'8965B10040\x00\x00\x00\x00\x00\x00'], + (Ecu.engine, 0x7e0, None): [b'\x033F424000\x00\x00\x00\x00\x00\x00\x00\x00A0202000\x00\x00\x00\x00\x00\x00\x00\x00895231203202\x00\x00\x00\x00'], + (Ecu.fwdRadar, 0x750, 0xf): [b'8821FF404100 '], + (Ecu.fwdCamera, 0x750, 0x6d): [b'8646FF404000 '], + }, CAR.COROLLA: { (Ecu.engine, 0x7e0, None): [ b'\x01896630E88000\x00\x00\x00\x00', + b'\x0230ZC2000\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x0230ZC2100\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x0230ZC2200\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x0230ZC2300\x00\x00\x00\x00\x00\x00\x00\x0050212000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -494,9 +517,11 @@ FW_VERSIONS = { b'\x02896634774100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634774200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x02896634782000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', + b'\x02896634784000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00', b'\x03896634759200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634759300\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00', b'\x03896634760000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701002\x00\x00\x00\x00', + b'\x03896634760100\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634760200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701003\x00\x00\x00\x00', b'\x03896634760200\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4701004\x00\x00\x00\x00', b'\x03896634768000\x00\x00\x00\x008966A4703000\x00\x00\x00\x00897CF4703001\x00\x00\x00\x00', @@ -551,6 +576,7 @@ FW_VERSIONS = { }, CAR.RAV4: { (Ecu.engine, 0x7e0, None): [ + b'\x02342Q1000\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02342Q1100\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02342Q1300\x00\x00\x00\x00\x00\x00\x00\x0054212000\x00\x00\x00\x00\x00\x00\x00\x00', b'\x02342Q2000\x00\x00\x00\x00\x00\x00\x00\x0054213000\x00\x00\x00\x00\x00\x00\x00\x00', @@ -561,6 +587,7 @@ FW_VERSIONS = { b'8965B42083\x00\x00\x00\x00\x00\x00', ], (Ecu.esp, 0x7b0, None): [ + b'F15260R102\x00\x00\x00\x00\x00\x00', b'F15260R103\x00\x00\x00\x00\x00\x00', b'F152642493\x00\x00\x00\x00\x00\x00', ], @@ -619,9 +646,11 @@ FW_VERSIONS = { b'\x018966342V3100\x00\x00\x00\x00', b'\x018966342X5000\x00\x00\x00\x00', b'\x01896634A05000\x00\x00\x00\x00', + b'\x01896634A19000\x00\x00\x00\x00', b'\x01896634A22000\x00\x00\x00\x00', b'\x01F152642551\x00\x00\x00\x00\x00\x00', b'\x028966342Y8000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00', + b'\x02896634A18000\x00\x00\x00\x00897CF1201001\x00\x00\x00\x00', ], (Ecu.esp, 0x7b0, None): [ b'F152606230\x00\x00\x00\x00\x00\x00', @@ -695,9 +724,17 @@ FW_VERSIONS = { (Ecu.fwdCamera, 0x750, 0x6d): [b'8646F5301400\x00\x00\x00\x00'], }, CAR.SIENNA: { - (Ecu.engine, 0x700, None): [b'\x01896630832100\x00\x00\x00\x00'], + (Ecu.engine, 0x700, None): [ + b'\x01896630832100\x00\x00\x00\x00', + b'\x01896630842000\x00\x00\x00\x00', + b'\x01896630851100\x00\x00\x00\x00', + b'\x01896630860000\x00\x00\x00\x00', + ], (Ecu.eps, 0x7a1, None): [b'8965B45070\x00\x00\x00\x00\x00\x00'], - (Ecu.fwdRadar, 0x750, 0xf): [b'8821F4702100\x00\x00\x00\x00'], + (Ecu.fwdRadar, 0x750, 0xf): [ + b'8821F4702100\x00\x00\x00\x00', + b'8821F4702300\x00\x00\x00\x00', + ], (Ecu.fwdCamera, 0x750, 0x6d): [b'8646F0801100\x00\x00\x00\x00'], }, CAR.LEXUS_RXH: { @@ -747,6 +784,7 @@ DBC = { CAR.CAMRY: dbc_dict('toyota_nodsu_pt_generated', 'toyota_adas'), CAR.CAMRYH: dbc_dict('toyota_camry_hybrid_2018_pt_generated', 'toyota_adas'), CAR.HIGHLANDER: dbc_dict('toyota_highlander_2017_pt_generated', 'toyota_adas'), + CAR.HIGHLANDER_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), CAR.HIGHLANDERH: dbc_dict('toyota_highlander_hybrid_2018_pt_generated', 'toyota_adas'), CAR.AVALON: dbc_dict('toyota_avalon_2017_pt_generated', 'toyota_adas'), CAR.RAV4_TSS2: dbc_dict('toyota_nodsu_pt_generated', 'toyota_tss2_adas'), @@ -758,8 +796,9 @@ DBC = { CAR.LEXUS_IS: dbc_dict('lexus_is_2018_pt_generated', 'toyota_adas'), CAR.LEXUS_CTH: dbc_dict('lexus_ct200h_2018_pt_generated', 'toyota_adas'), CAR.RAV4H_TSS2: dbc_dict('toyota_nodsu_hybrid_pt_generated', 'toyota_tss2_adas'), + CAR.LEXUS_NXH: dbc_dict('lexus_nx300h_2018_pt_generated', 'toyota_adas'), } -NO_DSU_CAR = [CAR.CHR, CAR.CHRH, CAR.CAMRY, CAR.CAMRYH, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2] -TSS2_CAR = [CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2] -NO_STOP_TIMER_CAR = [CAR.RAV4H, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.SIENNA, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2] # no resume button press required +NO_DSU_CAR = [CAR.CHR, CAR.CHRH, CAR.CAMRY, CAR.CAMRYH, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2] +TSS2_CAR = [CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2] +NO_STOP_TIMER_CAR = [CAR.RAV4H, CAR.HIGHLANDERH, CAR.HIGHLANDER, CAR.RAV4_TSS2, CAR.COROLLA_TSS2, CAR.COROLLAH_TSS2, CAR.LEXUS_ES_TSS2, CAR.LEXUS_ESH_TSS2, CAR.SIENNA, CAR.RAV4H_TSS2, CAR.LEXUS_RX_TSS2, CAR.HIGHLANDER_TSS2] # no resume button press required diff --git a/selfdrive/car/volkswagen/carstate.py b/selfdrive/car/volkswagen/carstate.py index 59357edbe..79cd06310 100644 --- a/selfdrive/car/volkswagen/carstate.py +++ b/selfdrive/car/volkswagen/carstate.py @@ -1,13 +1,10 @@ import numpy as np -from cereal import car -from common.kalman.simple_kalman import KF1D from selfdrive.config import Conversions as CV +from selfdrive.car.interfaces import CarStateBase from opendbc.can.parser import CANParser from opendbc.can.can_define import CANDefine from selfdrive.car.volkswagen.values import DBC, BUTTON_STATES, CarControllerParams -GEAR = car.CarState.GearShifter - def get_mqb_pt_can_parser(CP, canbus): # this function generates lists for signal, messages and initial values signals = [ @@ -99,29 +96,14 @@ def get_mqb_cam_can_parser(CP, canbus): return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, canbus.cam) -def parse_gear_shifter(gear): - # Return mapping of gearshift position to selected gear. - return {'P': GEAR.park, 'R': GEAR.reverse, 'N': GEAR.neutral, - 'D': GEAR.drive, 'E': GEAR.eco, 'S': GEAR.sport, 'T': GEAR.manumatic}.get(gear, GEAR.unknown) -class CarState(): +class CarState(CarStateBase): def __init__(self, CP, canbus): - # initialize can parser - self.CP = CP - self.car_fingerprint = CP.carFingerprint - self.can_define = CANDefine(DBC[CP.carFingerprint]['pt']) - - self.shifter_values = self.can_define.dv["Getriebe_11"]['GE_Fahrstufe'] - + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["Getriebe_11"]['GE_Fahrstufe'] self.buttonStates = BUTTON_STATES.copy() - # vEgo Kalman filter - dt = 0.01 - self.v_ego_kf = KF1D(x0=[[0.], [0.]], - A=[[1., dt], [0., 1.]], - C=[1., 0.], - K=[[0.12287673], [0.29666309]]) - def update(self, pt_cp): # Update vehicle speed and acceleration from ABS wheel speeds. self.wheelSpeedFL = pt_cp.vl["ESP_19"]['ESP_VL_Radgeschw_02'] * CV.KPH_TO_MS @@ -130,9 +112,8 @@ class CarState(): self.wheelSpeedRR = pt_cp.vl["ESP_19"]['ESP_HR_Radgeschw_02'] * CV.KPH_TO_MS self.vEgoRaw = float(np.mean([self.wheelSpeedFL, self.wheelSpeedFR, self.wheelSpeedRL, self.wheelSpeedRR])) - v_ego_x = self.v_ego_kf.update(self.vEgoRaw) - self.vEgo = float(v_ego_x[0]) - self.aEgo = float(v_ego_x[1]) + self.vEgo, self.aEgo = self.update_speed_kf(self.vEgoRaw) + self.standstill = self.vEgoRaw < 0.1 # Update steering angle, rate, yaw rate, and driver input torque. VW send @@ -152,7 +133,7 @@ class CarState(): # Update gear and/or clutch position data. can_gear_shifter = int(pt_cp.vl["Getriebe_11"]['GE_Fahrstufe']) - self.gearShifter = parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) + self.gearShifter = self.parse_gear_shifter(self.shifter_values.get(can_gear_shifter, None)) # Update door and trunk/hatch lid open status. self.doorOpen = any([pt_cp.vl["Gateway_72"]['ZV_FT_offen'], diff --git a/selfdrive/common/version.h b/selfdrive/common/version.h index 86fe8c7c0..74d3ae1ba 100644 --- a/selfdrive/common/version.h +++ b/selfdrive/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.7.2-release" +#define COMMA_VERSION "0.7.3-release" diff --git a/selfdrive/controls/lib/pathplanner.py b/selfdrive/controls/lib/pathplanner.py index de43c0418..83b1190ff 100644 --- a/selfdrive/controls/lib/pathplanner.py +++ b/selfdrive/controls/lib/pathplanner.py @@ -6,6 +6,7 @@ from selfdrive.controls.lib.lateral_mpc import libmpc_py from selfdrive.controls.lib.drive_helpers import MPC_COST_LAT from selfdrive.controls.lib.lane_planner import LanePlanner from selfdrive.config import Conversions as CV +from common.params import Params import cereal.messaging as messaging from cereal import log @@ -53,6 +54,7 @@ class PathPlanner(): self.setup_mpc() self.solution_invalid_cnt = 0 + self.lane_change_enabled = Params().get('LaneChangeEnabled') == b'1' self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none self.lane_change_timer = 0.0 @@ -97,7 +99,7 @@ class PathPlanner(): elif sm['carState'].rightBlinker: self.lane_change_direction = LaneChangeDirection.right - if (not active) or (self.lane_change_timer > LANE_CHANGE_TIME_MAX) or (not one_blinker): + if (not active) or (self.lane_change_timer > LANE_CHANGE_TIME_MAX) or (not one_blinker) or (not self.lane_change_enabled): self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none else: diff --git a/selfdrive/debug/live_cpu_and_temp.py b/selfdrive/debug/live_cpu_and_temp.py index 1f4f2426e..4f324711d 100755 --- a/selfdrive/debug/live_cpu_and_temp.py +++ b/selfdrive/debug/live_cpu_and_temp.py @@ -1,64 +1,72 @@ #!/usr/bin/env python3 +import argparse + import numpy as np from cereal.messaging import SubMaster + def cputime_total(ct): - return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq + return ct.user + ct.nice + ct.system + ct.idle + ct.iowait + ct.irq + ct.softirq def cputime_busy(ct): - return ct.user + ct.nice + ct.system + ct.irq + ct.softirq + return ct.user + ct.nice + ct.system + ct.irq + ct.softirq +if __name__ == "__main__": + parser = argparse.ArgumentParser() + parser.add_argument('--mem', action='store_true') + args = parser.parse_args() -sm = SubMaster(['thermal', 'procLog']) + sm = SubMaster(['thermal', 'procLog']) -last_temp = 0.0 -last_mem = 0.0 -total_times = [0., 0., 0., 0.] -busy_times = [0., 0., 0.0, 0.] + last_temp = 0.0 + last_mem = 0.0 + total_times = [0., 0., 0., 0.] + busy_times = [0., 0., 0.0, 0.] + + while True: + sm.update() + + if sm.updated['thermal']: + t = sm['thermal'] + last_temp = np.mean([t.cpu0, t.cpu1, t.cpu2, t.cpu3]) / 10. + last_mem = t.memUsedPercent + + if sm.updated['procLog']: + m = sm['procLog'] -while True: - sm.update() + cores = [0., 0., 0., 0.] + total_times_new = [0., 0., 0., 0.] + busy_times_new = [0., 0., 0.0, 0.] - if sm.updated['thermal']: - t = sm['thermal'] - last_temp = np.mean([t.cpu0, t.cpu1, t.cpu2, t.cpu3]) / 10. - last_mem = t.memUsedPercent + for c in m.cpuTimes: + n = c.cpuNum + total_times_new[n] = cputime_total(c) + busy_times_new[n] = cputime_busy(c) - if sm.updated['procLog']: - m = sm['procLog'] + for n in range(4): + t_busy = busy_times_new[n] - busy_times[n] + t_total = total_times_new[n] - total_times[n] + cores[n] = t_busy / t_total - mems = {} - for proc in m.procs: - name = proc.name - if len(proc.cmdline): - name = proc.cmdline[0] - if len(proc.exe): - name = proc.exe + " - " + name - mems[name] = float(proc.memRss) / 1e6 + total_times = total_times_new[:] + busy_times = busy_times_new[:] - cores = [0., 0., 0., 0.] - total_times_new = [0., 0., 0., 0.] - busy_times_new = [0., 0., 0.0, 0.] + print("CPU %.2f%% - RAM: %.2f - Temp %.2f" % (100. * np.mean(cores), last_mem, last_temp)) - for c in m.cpuTimes: - n = c.cpuNum - total_times_new[n] = cputime_total(c) - busy_times_new[n] = cputime_busy(c) - - for n in range(4): - t_busy = busy_times_new[n] - busy_times[n] - t_total = total_times_new[n] - total_times[n] - cores[n] = t_busy / t_total - - total_times = total_times_new[:] - busy_times = busy_times_new[:] - - print() - print("CPU %.2f%% - RAM: %.2f - Temp %.2f" % (100. * np.mean(cores), last_mem, last_temp)) - print("Top memory usage:") - for k, v in sorted(mems.items(), key=lambda item: item[1], reverse=True)[:10]: - print(f"{k.rjust(70)} {v:.2f} MB") + if args.mem: + mems = {} + for proc in m.procs: + name = proc.name + if len(proc.cmdline): + name = proc.cmdline[0] + if len(proc.exe): + name = proc.exe + " - " + name + mems[name] = float(proc.memRss) / 1e6 + print("Top memory usage:") + for k, v in sorted(mems.items(), key=lambda item: item[1], reverse=True)[:10]: + print(f"{k.rjust(70)} {v:.2f} MB") + print() diff --git a/selfdrive/debug/test_fw_query_on_routes.py b/selfdrive/debug/test_fw_query_on_routes.py index 82bd8299c..5bf8c4a76 100755 --- a/selfdrive/debug/test_fw_query_on_routes.py +++ b/selfdrive/debug/test_fw_query_on_routes.py @@ -1,4 +1,5 @@ #!/usr/bin/env python3 +import os import traceback import sys from tqdm import tqdm @@ -13,14 +14,19 @@ from selfdrive.car.honda.values import FINGERPRINTS as HONDA_FINGERPRINTS if __name__ == "__main__": if len(sys.argv) < 2: - print("Usage: ./test_fw_query_on_routes.py ") + print("Usage: ./test_fw_query_on_routes.py /") sys.exit(1) + if os.path.exists(sys.argv[1]): + routes = list(open(sys.argv[1])) + else: + routes = [sys.argv[1]] + wrong = 0 good = 0 dongles = [] - for route in tqdm(list(open(sys.argv[1]))): + for route in tqdm(routes): route = route.rstrip() dongle_id, time = route.split('|') qlog_path = f"cd:/{dongle_id}/{time}/0/qlog.bz2" diff --git a/selfdrive/locationd/kalman/.gitignore b/selfdrive/locationd/kalman/.gitignore new file mode 100644 index 000000000..9ab870da8 --- /dev/null +++ b/selfdrive/locationd/kalman/.gitignore @@ -0,0 +1 @@ +generated/ diff --git a/selfdrive/locationd/kalman/README.md b/selfdrive/locationd/kalman/README.md new file mode 100644 index 000000000..4c4ebd59c --- /dev/null +++ b/selfdrive/locationd/kalman/README.md @@ -0,0 +1,52 @@ +# Kalman filter library + +## Introduction +The kalman filter framework described here is an incredibly powerful tool for any optimization problem, +but particularly for visual odometry, sensor fusion localization or SLAM. It is designed to provide very +accurate results, work online or offline, be fairly computationally efficient, be easy to design filters with in +python. + +## Feature walkthrough + +### Extended Kalman Filter with symbolic Jacobian computation +Most dynamic systems can be described as a Hidden Markov Process. To estimate the state of such a system with noisy +measurements one can use a Recursive Bayesian estimator. For a linear Markov Process a regular linear Kalman filter is optimal. +Unfortunately, a lot of systems are non-linear. Extended Kalman Filters can model systems by linearizing the non-linear +system at every step, this provides a close to optimal estimator when the linearization is good enough. If the linearization +introduces too much noise, one can use an Iterated Extended Kalman Filter, Unscented Kalman Filter or a Particle Filter. For +most applications those estimators are overkill and introduce too much complexity and require a lot of additional compute. + +Conventionally Extended Kalman Filters are implemented by writing the system's dynamic equations and then manually symbolically +calculating the Jacobians for the linearization. For complex systems this is time consuming and very prone to calculation errors. +This library symbolically computes the Jacobians using sympy to simplify the system's definition and remove the possiblity of introducing calculation errors. + +### Error State Kalman Filter +3D localization algorithms ussually also require estimating orientation of an object in 3D. Orientation is generally represented +with euler angles or quaternions. + +Euler angles have several problems, there are mulitple ways to represent the same orientation, +gimbal lock can cause the loss of a degree of freedom and lastly their behaviour is very non-linear when errors are large. +Quaternions with one strictly positive dimension don't suffer from these issues, but have another set of problems. +Quaternions need to be normalized otherwise they will grow unbounded, this is cannot be cleanly enforced in a kalman filter. +Most importantly though a quaternion has 4 dimensions, but only represents 3 degrees of freedom, so there is one redundant dimension. + +Kalman filters are designed to minimize the error of the system's state. It is possible to have a kalman filter where state and the error of the state are represented in a different space. As long as there is an error function that can compute the error based on the true state and estimated state. It is problematic to have redundant dimensions in the error of the kalman filter, but not in the state. A good compromise then, is to use the quaternion to represent the system's attitude state and use euler angles to describe the error in attitude. This library supports and defining an arbitrary error that is in a different space than the state. [Joan Solà](https://arxiv.org/abs/1711.02508) has written a comprehensive description of using ESKFs for robust 3D orientation estimation. + +### Multi-State Constraint Kalman Filter +How do you integrate feature-based visual odometry with a Kalman filter? The problem is that one cannot write an observation equation for 2D feature observations in image space for a localization kalman filter. One needs to give the feature observation a depth so it has a 3D position, then one can write an obvervation equation in the kalman filter. This is possible by tracking the feature across frames and then estimating the depth. However, the solution is not that simple, the depth estimated by tracking the feature across frames depends on the location of the camera at those frames, and thus the state of the kalman filter. This creates a positive feedback loop where the kalman filter wrongly gains confidence in it's position because the feature position updates reinforce it. + +The solution is to use an [MSCKF](http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.437.1085&rep=rep1&type=pdf), which this library fully supports. + +### Rauch–Tung–Striebel smoothing +When doing offline estimation with a kalman filter there can be an initialization period where states are badly estimated. +Global estimators don't suffer from this, to make our kalman filter competitive with global optimizers we can run the filter +backwards using an RTS smoother. Those combined with potentially multiple forward and backwards passes of the data should make +performance very close to global optimization. + +### Mahalanobis distance outlier rejector +A lot of measurements do not come from a Gaussian distribution and as such have outliers that do not fit the statistical model +of the Kalman filter. This can cause a lot of performance issues if not dealt with. This library allows the use of a mahalanobis +distance statistical test on the incoming measurements to deal with this. Note that good initialization is critical to prevent +good measurements from being rejected. + + diff --git a/selfdrive/locationd/kalman/SConscript b/selfdrive/locationd/kalman/SConscript new file mode 100644 index 000000000..2a67ad64f --- /dev/null +++ b/selfdrive/locationd/kalman/SConscript @@ -0,0 +1,31 @@ +Import('env') + +templates = Glob('templates/*') +sympy_helpers = "helpers/sympy_helpers.py" +ekf_sym = "helpers/ekf_sym.py" + +to_build = { + 'pos_computer_4': 'helpers/lst_sq_computer.py', + 'pos_computer_5': 'helpers/lst_sq_computer.py', + 'feature_handler_5': 'helpers/feature_handler.py', + 'gnss': 'models/gnss_kf.py', + 'loc_4': 'models/loc_kf.py', + 'live': 'models/live_kf.py', + 'lane': '#xx/pipeline/lib/ekf/lane_kf.py', +} + +found = {} + +for target, command in to_build.items(): + if File(command).exists(): + found[target] = command + +for target, command in found.items(): + target_files = File([f'generated/{target}.cpp', f'generated/{target}.h']) + command_file = File(command) + env.Command(target_files, + [templates, command_file, sympy_helpers, ekf_sym], + command_file.get_abspath()+" "+target + ) + + env.SharedLibrary('generated/' + target, target_files[0]) diff --git a/selfdrive/locationd/kalman/__init__.py b/selfdrive/locationd/kalman/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/selfdrive/locationd/kalman/helpers/__init__.py b/selfdrive/locationd/kalman/helpers/__init__.py new file mode 100644 index 000000000..5ac7fb3f2 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/__init__.py @@ -0,0 +1,191 @@ +import numpy as np +import os +from bisect import bisect +from tqdm import tqdm +from cffi import FFI + +TEMPLATE_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'templates')) +GENERATED_DIR = os.path.abspath(os.path.join(os.path.dirname(__file__), '..', 'generated')) + + +def write_code(name, code, header): + if not os.path.exists(GENERATED_DIR): + os.mkdir(GENERATED_DIR) + + open(os.path.join(GENERATED_DIR, f"{name}.cpp"), 'w').write(code) + open(os.path.join(GENERATED_DIR, f"{name}.h"), 'w').write(header) + + +def load_code(name): + shared_fn = os.path.join(GENERATED_DIR, f"lib{name}.so") + header_fn = os.path.join(GENERATED_DIR, f"{name}.h") + header = open(header_fn).read() + + ffi = FFI() + ffi.cdef(header) + return (ffi, ffi.dlopen(shared_fn)) + + +class KalmanError(Exception): + pass + + +class ObservationKind(): + UNKNOWN = 0 + NO_OBSERVATION = 1 + GPS_NED = 2 + ODOMETRIC_SPEED = 3 + PHONE_GYRO = 4 + GPS_VEL = 5 + PSEUDORANGE_GPS = 6 + PSEUDORANGE_RATE_GPS = 7 + SPEED = 8 + NO_ROT = 9 + PHONE_ACCEL = 10 + ORB_POINT = 11 + ECEF_POS = 12 + CAMERA_ODO_TRANSLATION = 13 + CAMERA_ODO_ROTATION = 14 + ORB_FEATURES = 15 + MSCKF_TEST = 16 + FEATURE_TRACK_TEST = 17 + LANE_PT = 18 + IMU_FRAME = 19 + PSEUDORANGE_GLONASS = 20 + PSEUDORANGE_RATE_GLONASS = 21 + PSEUDORANGE = 22 + PSEUDORANGE_RATE = 23 + + names = ['Unknown', + 'No observation', + 'GPS NED', + 'Odometric speed', + 'Phone gyro', + 'GPS velocity', + 'GPS pseudorange', + 'GPS pseudorange rate', + 'Speed', + 'No rotation', + 'Phone acceleration', + 'ORB point', + 'ECEF pos', + 'camera odometric translation', + 'camera odometric rotation', + 'ORB features', + 'MSCKF test', + 'Feature track test', + 'Lane ecef point', + 'imu frame eulers', + 'GLONASS pseudorange', + 'GLONASS pseudorange rate'] + + @classmethod + def to_string(cls, kind): + return cls.names[kind] + + + +SAT_OBS = [ObservationKind.PSEUDORANGE_GPS, + ObservationKind.PSEUDORANGE_RATE_GPS, + ObservationKind.PSEUDORANGE_GLONASS, + ObservationKind.PSEUDORANGE_RATE_GLONASS] + + +def run_car_ekf_offline(kf, observations_by_kind): + from laika.raw_gnss import GNSSMeasurement + observations = [] + # create list of observations with element format: [kind, time, data] + for kind in observations_by_kind: + for t, data in zip(observations_by_kind[kind][0], observations_by_kind[kind][1]): + observations.append([t, kind, data]) + observations.sort(key=lambda obs: obs[0]) + + times, estimates = run_observations_through_filter(kf, observations) + + forward_states = np.stack(e[1] for e in estimates) + forward_covs = np.stack(e[3] for e in estimates) + smoothed_states, smoothed_covs = kf.rts_smooth(estimates) + + observations_dict = {} + # TODO assuming observations and estimates + # are same length may not work with VO + for e in estimates: + t = e[4] + kind = str(int(e[5])) + res = e[6] + z = e[7] + ea = e[8] + if len(z) == 0: + continue + if kind not in observations_dict: + observations_dict[kind] = {} + observations_dict[kind]['t'] = np.array(len(z)*[t]) + observations_dict[kind]['z'] = np.array(z) + observations_dict[kind]['ea'] = np.array(ea) + observations_dict[kind]['residual'] = np.array(res) + else: + observations_dict[kind]['t'] = np.append(observations_dict[kind]['t'], np.array(len(z)*[t])) + observations_dict[kind]['z'] = np.vstack((observations_dict[kind]['z'], np.array(z))) + observations_dict[kind]['ea'] = np.vstack((observations_dict[kind]['ea'], np.array(ea))) + observations_dict[kind]['residual'] = np.vstack((observations_dict[kind]['residual'], np.array(res))) + + # add svIds to gnss data + for kind in map(str, SAT_OBS): + if int(kind) in observations_by_kind and kind in observations_dict: + observations_dict[kind]['svIds'] = np.array([]) + observations_dict[kind]['CNO'] = np.array([]) + observations_dict[kind]['std'] = np.array([]) + for obs in observations_by_kind[int(kind)][1]: + observations_dict[kind]['svIds'] = np.append(observations_dict[kind]['svIds'], + np.array([obs[:,GNSSMeasurement.PRN]])) + observations_dict[kind]['std'] = np.append(observations_dict[kind]['std'], + np.array([obs[:,GNSSMeasurement.PR_STD]])) + return smoothed_states, smoothed_covs, forward_states, forward_covs, times, observations_dict + + +def run_observations_through_filter(kf, observations, filter_time=None): + estimates = [] + + for obs in tqdm(observations): + t = obs[0] + kind = obs[1] + data = obs[2] + estimates.append(kf.predict_and_observe(t, kind, data)) + times = [x[4] for x in estimates] + return times, estimates + + +def save_residuals_plot(obs, save_path, data_name): + import matplotlib.pyplot as plt + import mpld3 # pylint: disable=import-error + fig = plt.figure(figsize=(10,20)) + fig.suptitle('Residuals of ' + data_name, fontsize=24) + n = len(list(obs.keys())) + start_times = [obs[kind]['t'][0] for kind in obs] + start_time = min(start_times) + xlims = [start_time + 3, start_time + 60] + + for i, kind in enumerate(obs): + ax = fig.add_subplot(n, 1, i+1) + ax.set_xlim(xlims) + t = obs[kind]['t'] + res = obs[kind]['residual'] + start_idx = bisect(t, xlims[0]) + if len(res) == start_idx: + continue + ylim = max(np.linalg.norm(res[start_idx:], axis=1)) + ax.set_ylim([-ylim, ylim]) + if int(kind) in SAT_OBS: + svIds = obs[kind]['svIds'] + for svId in set(svIds): + svId_idx = (svIds == svId) + t = obs[kind]['t'][svId_idx] + res = obs[kind]['residual'][svId_idx] + ax.plot(t, res, label='SV ' + str(int(svId))) + ax.legend(loc='right') + else: + ax.plot(t, res) + plt.title('Residual of kind ' + ObservationKind.to_string(int(kind)), fontsize=20) + plt.tight_layout() + os.makedirs(save_path) + mpld3.save_html(fig, save_path + 'residuals_plot.html') diff --git a/selfdrive/locationd/kalman/helpers/chi2_lookup.py b/selfdrive/locationd/kalman/helpers/chi2_lookup.py new file mode 100644 index 000000000..e22cc9729 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/chi2_lookup.py @@ -0,0 +1,22 @@ +import os + +import numpy as np + + +def gen_chi2_ppf_lookup(max_dim=200): + from scipy.stats import chi2 + table = np.zeros((max_dim, 98)) + for dim in range(1, max_dim): + table[dim] = chi2.ppf(np.arange(.01, .99, .01), dim) + + np.save('chi2_lookup_table', table) + + +def chi2_ppf(p, dim): + table = np.load(os.path.dirname(os.path.realpath(__file__)) + '/chi2_lookup_table.npy') + result = np.interp(p, np.arange(.01, .99, .01), table[dim]) + return result + + +if __name__ == "__main__": + gen_chi2_ppf_lookup() diff --git a/selfdrive/locationd/kalman/helpers/chi2_lookup_table.npy b/selfdrive/locationd/kalman/helpers/chi2_lookup_table.npy new file mode 100644 index 0000000000000000000000000000000000000000..6f1bd959c194cbc2f955d8658ac913c088ffcab6 GIT binary patch literal 156928 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�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

    2|Eb$b`${30 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 IZR-&756-ay5C8xG literal 0 HcmV?d00001 diff --git a/selfdrive/locationd/kalman/helpers/ekf_sym.py b/selfdrive/locationd/kalman/helpers/ekf_sym.py new file mode 100644 index 000000000..98d4a87b1 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/ekf_sym.py @@ -0,0 +1,563 @@ +import os +from bisect import bisect_right + +import numpy as np +import sympy as sp +from numpy import dot + +from selfdrive.locationd.kalman.helpers.sympy_helpers import sympy_into_c +from selfdrive.locationd.kalman.helpers import (TEMPLATE_DIR, load_code, + write_code) + +from selfdrive.locationd.kalman.helpers.chi2_lookup import chi2_ppf + + +def solve(a, b): + if a.shape[0] == 1 and a.shape[1] == 1: + return b / a[0][0] + else: + return np.linalg.solve(a, b) + + +def null(H, eps=1e-12): + u, s, vh = np.linalg.svd(H) + padding = max(0, np.shape(H)[1] - np.shape(s)[0]) + null_mask = np.concatenate(((s <= eps), np.ones((padding,), dtype=bool)), axis=0) + null_space = np.compress(null_mask, vh, axis=0) + return np.transpose(null_space) + + +def gen_code(name, f_sym, dt_sym, x_sym, obs_eqs, dim_x, dim_err, eskf_params=None, msckf_params=None, maha_test_kinds=[]): + # optional state transition matrix, H modifier + # and err_function if an error-state kalman filter (ESKF) + # is desired. Best described in "Quaternion kinematics + # for the error-state Kalman filter" by Joan Sola + + if eskf_params: + err_eqs = eskf_params[0] + inv_err_eqs = eskf_params[1] + H_mod_sym = eskf_params[2] + f_err_sym = eskf_params[3] + x_err_sym = eskf_params[4] + else: + nom_x = sp.MatrixSymbol('nom_x', dim_x, 1) + true_x = sp.MatrixSymbol('true_x', dim_x, 1) + delta_x = sp.MatrixSymbol('delta_x', dim_x, 1) + err_function_sym = sp.Matrix(nom_x + delta_x) + inv_err_function_sym = sp.Matrix(true_x - nom_x) + err_eqs = [err_function_sym, nom_x, delta_x] + inv_err_eqs = [inv_err_function_sym, nom_x, true_x] + + H_mod_sym = sp.Matrix(np.eye(dim_x)) + f_err_sym = f_sym + x_err_sym = x_sym + + # This configures the multi-state augmentation + # needed for EKF-SLAM with MSCKF (Mourikis et al 2007) + if msckf_params: + msckf = True + dim_main = msckf_params[0] # size of the main state + dim_augment = msckf_params[1] # size of one augment state chunk + dim_main_err = msckf_params[2] + dim_augment_err = msckf_params[3] + N = msckf_params[4] + feature_track_kinds = msckf_params[5] + assert dim_main + dim_augment * N == dim_x + assert dim_main_err + dim_augment_err * N == dim_err + else: + msckf = False + dim_main = dim_x + dim_augment = 0 + dim_main_err = dim_err + dim_augment_err = 0 + N = 0 + + # linearize with jacobians + F_sym = f_err_sym.jacobian(x_err_sym) + for sym in x_err_sym: + F_sym = F_sym.subs(sym, 0) + for i in range(len(obs_eqs)): + obs_eqs[i].append(obs_eqs[i][0].jacobian(x_sym)) + if msckf and obs_eqs[i][1] in feature_track_kinds: + obs_eqs[i].append(obs_eqs[i][0].jacobian(obs_eqs[i][2])) + else: + obs_eqs[i].append(None) + + # collect sympy functions + sympy_functions = [] + + # error functions + sympy_functions.append(('err_fun', err_eqs[0], [err_eqs[1], err_eqs[2]])) + sympy_functions.append(('inv_err_fun', inv_err_eqs[0], [inv_err_eqs[1], inv_err_eqs[2]])) + + # H modifier for ESKF updates + sympy_functions.append(('H_mod_fun', H_mod_sym, [x_sym])) + + # state propagation function + sympy_functions.append(('f_fun', f_sym, [x_sym, dt_sym])) + sympy_functions.append(('F_fun', F_sym, [x_sym, dt_sym])) + + # observation functions + for h_sym, kind, ea_sym, H_sym, He_sym in obs_eqs: + sympy_functions.append(('h_%d' % kind, h_sym, [x_sym, ea_sym])) + sympy_functions.append(('H_%d' % kind, H_sym, [x_sym, ea_sym])) + if msckf and kind in feature_track_kinds: + sympy_functions.append(('He_%d' % kind, He_sym, [x_sym, ea_sym])) + + # Generate and wrap all th c code + header, code = sympy_into_c(sympy_functions) + extra_header = "#define DIM %d\n" % dim_x + extra_header += "#define EDIM %d\n" % dim_err + extra_header += "#define MEDIM %d\n" % dim_main_err + extra_header += "typedef void (*Hfun)(double *, double *, double *);\n" + + extra_header += "\nvoid predict(double *x, double *P, double *Q, double dt);" + + extra_post = "" + + for h_sym, kind, ea_sym, H_sym, He_sym in obs_eqs: + if msckf and kind in feature_track_kinds: + He_str = 'He_%d' % kind + # ea_dim = ea_sym.shape[0] + else: + He_str = 'NULL' + # ea_dim = 1 # not really dim of ea but makes c function work + maha_thresh = chi2_ppf(0.95, int(h_sym.shape[0])) # mahalanobis distance for outlier detection + maha_test = kind in maha_test_kinds + extra_post += """ + void update_%d(double *in_x, double *in_P, double *in_z, double *in_R, double *in_ea) { + update<%d,%d,%d>(in_x, in_P, h_%d, H_%d, %s, in_z, in_R, in_ea, MAHA_THRESH_%d); + } + """ % (kind, h_sym.shape[0], 3, maha_test, kind, kind, He_str, kind) + extra_header += "\nconst static double MAHA_THRESH_%d = %f;" % (kind, maha_thresh) + extra_header += "\nvoid update_%d(double *, double *, double *, double *, double *);" % kind + + code += '\nextern "C"{\n' + extra_header + "\n}\n" + code += "\n" + open(os.path.join(TEMPLATE_DIR, "ekf_c.c")).read() + code += '\nextern "C"{\n' + extra_post + "\n}\n" + header += "\n" + extra_header + + write_code(name, code, header) + + +class EKF_sym(): + def __init__(self, name, Q, x_initial, P_initial, dim_main, dim_main_err, + N=0, dim_augment=0, dim_augment_err=0, maha_test_kinds=[]): + """Generates process function and all observation functions for the kalman filter.""" + self.msckf = N > 0 + self.N = N + self.dim_augment = dim_augment + self.dim_augment_err = dim_augment_err + self.dim_main = dim_main + self.dim_main_err = dim_main_err + + # state + x_initial = x_initial.reshape((-1, 1)) + self.dim_x = x_initial.shape[0] + self.dim_err = P_initial.shape[0] + assert dim_main + dim_augment * N == self.dim_x + assert dim_main_err + dim_augment_err * N == self.dim_err + assert Q.shape == P_initial.shape + + # kinds that should get mahalanobis distance + # tested for outlier rejection + self.maha_test_kinds = maha_test_kinds + + # process noise + self.Q = Q + + # rewind stuff + self.rewind_t = [] + self.rewind_states = [] + self.rewind_obscache = [] + self.init_state(x_initial, P_initial, None) + + ffi, lib = load_code(name) + kinds, self.feature_track_kinds = [], [] + for func in dir(lib): + if func[:2] == 'h_': + kinds.append(int(func[2:])) + if func[:3] == 'He_': + self.feature_track_kinds.append(int(func[3:])) + + # wrap all the sympy functions + def wrap_1lists(name): + func = eval("lib.%s" % name, {"lib": lib}) + + def ret(lst1, out): + func(ffi.cast("double *", lst1.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return ret + + def wrap_2lists(name): + func = eval("lib.%s" % name, {"lib": lib}) + + def ret(lst1, lst2, out): + func(ffi.cast("double *", lst1.ctypes.data), + ffi.cast("double *", lst2.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return ret + + def wrap_1list_1float(name): + func = eval("lib.%s" % name, {"lib": lib}) + + def ret(lst1, fl, out): + func(ffi.cast("double *", lst1.ctypes.data), + ffi.cast("double", fl), + ffi.cast("double *", out.ctypes.data)) + return ret + + self.f = wrap_1list_1float("f_fun") + self.F = wrap_1list_1float("F_fun") + + self.err_function = wrap_2lists("err_fun") + self.inv_err_function = wrap_2lists("inv_err_fun") + self.H_mod = wrap_1lists("H_mod_fun") + + self.hs, self.Hs, self.Hes = {}, {}, {} + for kind in kinds: + self.hs[kind] = wrap_2lists("h_%d" % kind) + self.Hs[kind] = wrap_2lists("H_%d" % kind) + if self.msckf and kind in self.feature_track_kinds: + self.Hes[kind] = wrap_2lists("He_%d" % kind) + + # wrap the C++ predict function + def _predict_blas(x, P, dt): + lib.predict(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", P.ctypes.data), + ffi.cast("double *", self.Q.ctypes.data), + ffi.cast("double", dt)) + return x, P + + # wrap the C++ update function + def fun_wrapper(f, kind): + f = eval("lib.%s" % f, {"lib": lib}) + + def _update_inner_blas(x, P, z, R, extra_args): + f(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", P.ctypes.data), + ffi.cast("double *", z.ctypes.data), + ffi.cast("double *", R.ctypes.data), + ffi.cast("double *", extra_args.ctypes.data)) + if self.msckf and kind in self.feature_track_kinds: + y = z[:-len(extra_args)] + else: + y = z + return x, P, y + return _update_inner_blas + + self._updates = {} + for kind in kinds: + self._updates[kind] = fun_wrapper("update_%d" % kind, kind) + + def _update_blas(x, P, kind, z, R, extra_args=[]): + return self._updates[kind](x, P, z, R, extra_args) + + # assign the functions + self._predict = _predict_blas + # self._predict = self._predict_python + self._update = _update_blas + # self._update = self._update_python + + def init_state(self, state, covs, filter_time): + self.x = np.array(state.reshape((-1, 1))).astype(np.float64) + self.P = np.array(covs).astype(np.float64) + self.filter_time = filter_time + self.augment_times = [0] * self.N + self.rewind_obscache = [] + self.rewind_t = [] + self.rewind_states = [] + + def augment(self): + # TODO this is not a generalized way of doing this and implies that the augmented states + # are simply the first (dim_augment_state) elements of the main state. + assert self.msckf + d1 = self.dim_main + d2 = self.dim_main_err + d3 = self.dim_augment + d4 = self.dim_augment_err + + # push through augmented states + self.x[d1:-d3] = self.x[d1 + d3:] + self.x[-d3:] = self.x[:d3] + assert self.x.shape == (self.dim_x, 1) + + # push through augmented covs + assert self.P.shape == (self.dim_err, self.dim_err) + P_reduced = self.P + P_reduced = np.delete(P_reduced, np.s_[d2:d2 + d4], axis=1) + P_reduced = np.delete(P_reduced, np.s_[d2:d2 + d4], axis=0) + assert P_reduced.shape == (self.dim_err - d4, self.dim_err - d4) + to_mult = np.zeros((self.dim_err, self.dim_err - d4)) + to_mult[:-d4, :] = np.eye(self.dim_err - d4) + to_mult[-d4:, :d4] = np.eye(d4) + self.P = to_mult.dot(P_reduced.dot(to_mult.T)) + self.augment_times = self.augment_times[1:] + self.augment_times.append(self.filter_time) + assert self.P.shape == (self.dim_err, self.dim_err) + + def state(self): + return np.array(self.x).flatten() + + def covs(self): + return self.P + + def rewind(self, t): + # find where we are rewinding to + idx = bisect_right(self.rewind_t, t) + assert self.rewind_t[idx - 1] <= t + assert self.rewind_t[idx] > t # must be true, or rewind wouldn't be called + + # set the state to the time right before that + self.filter_time = self.rewind_t[idx - 1] + self.x[:] = self.rewind_states[idx - 1][0] + self.P[:] = self.rewind_states[idx - 1][1] + + # return the observations we rewound over for fast forwarding + ret = self.rewind_obscache[idx:] + + # throw away the old future + # TODO: is this making a copy? + self.rewind_t = self.rewind_t[:idx] + self.rewind_states = self.rewind_states[:idx] + self.rewind_obscache = self.rewind_obscache[:idx] + + return ret + + def checkpoint(self, obs): + # push to rewinder + self.rewind_t.append(self.filter_time) + self.rewind_states.append((np.copy(self.x), np.copy(self.P))) + self.rewind_obscache.append(obs) + + # only keep a certain number around + REWIND_TO_KEEP = 512 + self.rewind_t = self.rewind_t[-REWIND_TO_KEEP:] + self.rewind_states = self.rewind_states[-REWIND_TO_KEEP:] + self.rewind_obscache = self.rewind_obscache[-REWIND_TO_KEEP:] + + def predict_and_update_batch(self, t, kind, z, R, extra_args=[[]], augment=False): + # TODO handle rewinding at this level" + + # rewind + if self.filter_time is not None and t < self.filter_time: + if len(self.rewind_t) == 0 or t < self.rewind_t[0] or t < self.rewind_t[-1] - 1.0: + print("observation too old at %.3f with filter at %.3f, ignoring" % (t, self.filter_time)) + return None + rewound = self.rewind(t) + else: + rewound = [] + + ret = self._predict_and_update_batch(t, kind, z, R, extra_args, augment) + + # optional fast forward + for r in rewound: + self._predict_and_update_batch(*r) + + return ret + + def _predict_and_update_batch(self, t, kind, z, R, extra_args, augment=False): + """The main kalman filter function + Predicts the state and then updates a batch of observations + + dim_x: dimensionality of the state space + dim_z: dimensionality of the observation and depends on kind + n: number of observations + + Args: + t (float): Time of observation + kind (int): Type of observation + z (vec [n,dim_z]): Measurements + R (mat [n,dim_z, dim_z]): Measurement Noise + extra_args (list, [n]): Values used in H computations + """ + # initialize time + if self.filter_time is None: + self.filter_time = t + + # predict + dt = t - self.filter_time + assert dt >= 0 + self.x, self.P = self._predict(self.x, self.P, dt) + self.filter_time = t + xk_km1, Pk_km1 = np.copy(self.x).flatten(), np.copy(self.P) + + # update batch + y = [] + for i in range(len(z)): + # these are from the user, so we canonicalize them + z_i = np.array(z[i], dtype=np.float64, order='F') + R_i = np.array(R[i], dtype=np.float64, order='F') + extra_args_i = np.array(extra_args[i], dtype=np.float64, order='F') + # update + self.x, self.P, y_i = self._update(self.x, self.P, kind, z_i, R_i, extra_args=extra_args_i) + y.append(y_i) + xk_k, Pk_k = np.copy(self.x).flatten(), np.copy(self.P) + + if augment: + self.augment() + + # checkpoint + self.checkpoint((t, kind, z, R, extra_args)) + + return xk_km1, xk_k, Pk_km1, Pk_k, t, kind, y, z, extra_args + + def _predict_python(self, x, P, dt): + x_new = np.zeros(x.shape, dtype=np.float64) + self.f(x, dt, x_new) + + F = np.zeros(P.shape, dtype=np.float64) + self.F(x, dt, F) + + if not self.msckf: + P = dot(dot(F, P), F.T) + else: + # Update the predicted state covariance: + # Pk+1|k = |F*Pii*FT + Q*dt F*Pij | + # |PijT*FT Pjj | + # Where F is the jacobian of the main state + # predict function, Pii is the main state's + # covariance and Q its process noise. Pij + # is the covariance between the augmented + # states and the main state. + # + d2 = self.dim_main_err # known at compile time + F_curr = F[:d2, :d2] + P[:d2, :d2] = (F_curr.dot(P[:d2, :d2])).dot(F_curr.T) + P[:d2, d2:] = F_curr.dot(P[:d2, d2:]) + P[d2:, :d2] = P[d2:, :d2].dot(F_curr.T) + + P += dt * self.Q + return x_new, P + + def _update_python(self, x, P, kind, z, R, extra_args=[]): + # init vars + z = z.reshape((-1, 1)) + h = np.zeros(z.shape, dtype=np.float64) + H = np.zeros((z.shape[0], self.dim_x), dtype=np.float64) + + # C functions + self.hs[kind](x, extra_args, h) + self.Hs[kind](x, extra_args, H) + + # y is the "loss" + y = z - h + + # *** same above this line *** + + if self.msckf and kind in self.Hes: + # Do some algebraic magic to decorrelate + He = np.zeros((z.shape[0], len(extra_args)), dtype=np.float64) + self.Hes[kind](x, extra_args, He) + + # TODO: Don't call a function here, do projection locally + A = null(He.T) + + y = A.T.dot(y) + H = A.T.dot(H) + R = A.T.dot(R.dot(A)) + + # TODO If nullspace isn't the dimension we want + if A.shape[1] + He.shape[1] != A.shape[0]: + print('Warning: null space projection failed, measurement ignored') + return x, P, np.zeros(A.shape[0] - He.shape[1]) + + # if using eskf + H_mod = np.zeros((x.shape[0], P.shape[0]), dtype=np.float64) + self.H_mod(x, H_mod) + H = H.dot(H_mod) + + # Do mahalobis distance test + # currently just runs on msckf observations + # could run on anything if needed + if self.msckf and kind in self.maha_test_kinds: + a = np.linalg.inv(H.dot(P).dot(H.T) + R) + maha_dist = y.T.dot(a.dot(y)) + if maha_dist > chi2_ppf(0.95, y.shape[0]): + R = 10e16 * R + + # *** same below this line *** + + # Outlier resilient weighting as described in: + # "A Kalman Filter for Robust Outlier Detection - Jo-Anne Ting, ..." + weight = 1 # (1.5)/(1 + np.sum(y**2)/np.sum(R)) + + S = dot(dot(H, P), H.T) + R / weight + K = solve(S, dot(H, P.T)).T + I_KH = np.eye(P.shape[0]) - dot(K, H) + + # update actual state + delta_x = dot(K, y) + P = dot(dot(I_KH, P), I_KH.T) + dot(dot(K, R), K.T) + + # inject observed error into state + x_new = np.zeros(x.shape, dtype=np.float64) + self.err_function(x, delta_x, x_new) + return x_new, P, y.flatten() + + def maha_test(self, x, P, kind, z, R, extra_args=[], maha_thresh=0.95): + # init vars + z = z.reshape((-1, 1)) + h = np.zeros(z.shape, dtype=np.float64) + H = np.zeros((z.shape[0], self.dim_x), dtype=np.float64) + + # C functions + self.hs[kind](x, extra_args, h) + self.Hs[kind](x, extra_args, H) + + # y is the "loss" + y = z - h + + # if using eskf + H_mod = np.zeros((x.shape[0], P.shape[0]), dtype=np.float64) + self.H_mod(x, H_mod) + H = H.dot(H_mod) + + a = np.linalg.inv(H.dot(P).dot(H.T) + R) + maha_dist = y.T.dot(a.dot(y)) + if maha_dist > chi2_ppf(maha_thresh, y.shape[0]): + return False + else: + return True + + def rts_smooth(self, estimates, norm_quats=False): + ''' + Returns rts smoothed results of + kalman filter estimates + + If the kalman state is augmented with + old states only the main state is smoothed + ''' + xk_n = estimates[-1][0] + Pk_n = estimates[-1][2] + Fk_1 = np.zeros(Pk_n.shape, dtype=np.float64) + + states_smoothed = [xk_n] + covs_smoothed = [Pk_n] + for k in range(len(estimates) - 2, -1, -1): + xk1_n = xk_n + if norm_quats: + xk1_n[3:7] /= np.linalg.norm(xk1_n[3:7]) + Pk1_n = Pk_n + + xk1_k, _, Pk1_k, _, t2, _, _, _, _ = estimates[k + 1] + _, xk_k, _, Pk_k, t1, _, _, _, _ = estimates[k] + dt = t2 - t1 + self.F(xk_k, dt, Fk_1) + + d1 = self.dim_main + d2 = self.dim_main_err + Ck = np.linalg.solve(Pk1_k[:d2, :d2], Fk_1[:d2, :d2].dot(Pk_k[:d2, :d2].T)).T + xk_n = xk_k + delta_x = np.zeros((Pk_n.shape[0], 1), dtype=np.float64) + self.inv_err_function(xk1_k, xk1_n, delta_x) + delta_x[:d2] = Ck.dot(delta_x[:d2]) + x_new = np.zeros((xk_n.shape[0], 1), dtype=np.float64) + self.err_function(xk_k, delta_x, x_new) + xk_n[:d1] = x_new[:d1, 0] + Pk_n = Pk_k + Pk_n[:d2, :d2] = Pk_k[:d2, :d2] + Ck.dot(Pk1_n[:d2, :d2] - Pk1_k[:d2, :d2]).dot(Ck.T) + states_smoothed.append(xk_n) + covs_smoothed.append(Pk_n) + + return np.flipud(np.vstack(states_smoothed)), np.stack(covs_smoothed, 0)[::-1] diff --git a/selfdrive/locationd/kalman/helpers/feature_handler.py b/selfdrive/locationd/kalman/helpers/feature_handler.py new file mode 100755 index 000000000..74eb531f5 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/feature_handler.py @@ -0,0 +1,158 @@ +#!/usr/bin/env python3 + +import os + +import numpy as np + +import common.transformations.orientation as orient +from selfdrive.locationd.kalman.helpers import (TEMPLATE_DIR, load_code, + write_code) +from selfdrive.locationd.kalman.helpers.sympy_helpers import quat_matrix_l + + +def sane(track): + img_pos = track[1:, 2:4] + diffs_x = abs(img_pos[1:, 0] - img_pos[:-1, 0]) + diffs_y = abs(img_pos[1:, 1] - img_pos[:-1, 1]) + for i in range(1, len(diffs_x)): + if ((diffs_x[i] > 0.05 or diffs_x[i - 1] > 0.05) and + (diffs_x[i] > 2 * diffs_x[i - 1] or + diffs_x[i] < .5 * diffs_x[i - 1])) or \ + ((diffs_y[i] > 0.05 or diffs_y[i - 1] > 0.05) and + (diffs_y[i] > 2 * diffs_y[i - 1] or + diffs_y[i] < .5 * diffs_y[i - 1])): + return False + return True + + +class FeatureHandler(): + name = 'feature_handler' + + @staticmethod + def generate_code(K=5): + # Wrap c code for slow matching + c_header = "\nvoid merge_features(double *tracks, double *features, long long *empty_idxs);" + + c_code = "#include \n" + c_code += "#include \n" + c_code += "#define K %d\n" % K + c_code += "\n" + open(os.path.join(TEMPLATE_DIR, "feature_handler.c")).read() + + filename = f"{FeatureHandler.name}_{K}" + write_code(filename, c_code, c_header) + + def __init__(self, K=5): + self.MAX_TRACKS = 6000 + self.K = K + + # Array of tracks, each track has K 5D features preceded + # by 5 params that inidicate [f_idx, last_idx, updated, complete, valid] + # f_idx: idx of current last feature in track + # idx of of last feature in frame + # bool for whether this track has been update + # bool for whether this track is complete + # bool for whether this track is valid + self.tracks = np.zeros((self.MAX_TRACKS, K + 1, 5)) + self.tracks[:] = np.nan + + name = f"{FeatureHandler.name}_{K}" + ffi, lib = load_code(name) + + def merge_features_c(tracks, features, empty_idxs): + lib.merge_features(ffi.cast("double *", tracks.ctypes.data), + ffi.cast("double *", features.ctypes.data), + ffi.cast("long long *", empty_idxs.ctypes.data)) + + # self.merge_features = self.merge_features_python + self.merge_features = merge_features_c + + def reset(self): + self.tracks[:] = np.nan + + def merge_features_python(self, tracks, features, empty_idxs): + empty_idx = 0 + for f in features: + match_idx = int(f[4]) + if tracks[match_idx, 0, 1] == match_idx and tracks[match_idx, 0, 2] == 0: + tracks[match_idx, 0, 0] += 1 + tracks[match_idx, 0, 1] = f[1] + tracks[match_idx, 0, 2] = 1 + tracks[match_idx, int(tracks[match_idx, 0, 0])] = f + if tracks[match_idx, 0, 0] == self.K: + tracks[match_idx, 0, 3] = 1 + if sane(tracks[match_idx]): + tracks[match_idx, 0, 4] = 1 + else: + if empty_idx == len(empty_idxs): + print('need more empty space') + continue + tracks[empty_idxs[empty_idx], 0, 0] = 1 + tracks[empty_idxs[empty_idx], 0, 1] = f[1] + tracks[empty_idxs[empty_idx], 0, 2] = 1 + tracks[empty_idxs[empty_idx], 1] = f + empty_idx += 1 + + def update_tracks(self, features): + last_idxs = np.copy(self.tracks[:, 0, 1]) + real = np.isfinite(last_idxs) + self.tracks[last_idxs[real].astype(int)] = self.tracks[real] + + mask = np.ones(self.MAX_TRACKS, np.bool) + mask[last_idxs[real].astype(int)] = 0 + empty_idxs = np.arange(self.MAX_TRACKS)[mask] + + self.tracks[empty_idxs] = np.nan + self.tracks[:, 0, 2] = 0 + self.merge_features(self.tracks, features, empty_idxs) + + def handle_features(self, features): + self.update_tracks(features) + valid_idxs = self.tracks[:, 0, 4] == 1 + complete_idxs = self.tracks[:, 0, 3] == 1 + stale_idxs = self.tracks[:, 0, 2] == 0 + valid_tracks = self.tracks[valid_idxs] + self.tracks[complete_idxs] = np.nan + self.tracks[stale_idxs] = np.nan + return valid_tracks[:, 1:, :4].reshape((len(valid_tracks), self.K * 4)) + + +def generate_orient_error_jac(K): + import sympy as sp + from selfdrive.locationd.kalman.helpers.sympy_helpers import quat_rotate + + x_sym = sp.MatrixSymbol('abr', 3, 1) + dtheta = sp.MatrixSymbol('dtheta', 3, 1) + delta_quat = sp.Matrix(np.ones(4)) + delta_quat[1:, :] = sp.Matrix(0.5 * dtheta[0:3, :]) + poses_sym = sp.MatrixSymbol('poses', 7 * K, 1) + img_pos_sym = sp.MatrixSymbol('img_positions', 2 * K, 1) + alpha, beta, rho = x_sym + to_c = sp.Matrix(orient.rot_matrix(-np.pi / 2, -np.pi / 2, 0)) + pos_0 = sp.Matrix(np.array(poses_sym[K * 7 - 7:K * 7 - 4])[:, 0]) + q = quat_matrix_l(poses_sym[K * 7 - 4:K * 7]) * delta_quat + quat_rot = quat_rotate(*q) + rot_g_to_0 = to_c * quat_rot.T + rows = [] + for i in range(K): + pos_i = sp.Matrix(np.array(poses_sym[i * 7:i * 7 + 3])[:, 0]) + q = quat_matrix_l(poses_sym[7 * i + 3:7 * i + 7]) * delta_quat + quat_rot = quat_rotate(*q) + rot_g_to_i = to_c * quat_rot.T + rot_0_to_i = rot_g_to_i * (rot_g_to_0.T) + trans_0_to_i = rot_g_to_i * (pos_0 - pos_i) + funct_vec = rot_0_to_i * sp.Matrix([alpha, beta, 1]) + rho * trans_0_to_i + h1, h2, h3 = funct_vec + rows.append(h1 / h3 - img_pos_sym[i * 2 + 0]) + rows.append(h2 / h3 - img_pos_sym[i * 2 + 1]) + img_pos_residual_sym = sp.Matrix(rows) + + # sympy into c + sympy_functions = [] + sympy_functions.append(('orient_error_jac', img_pos_residual_sym.jacobian(dtheta), [x_sym, poses_sym, img_pos_sym, dtheta])) + + return sympy_functions + + +if __name__ == "__main__": + # TODO: get K from argparse + FeatureHandler.generate_code() diff --git a/selfdrive/locationd/kalman/helpers/lst_sq_computer.py b/selfdrive/locationd/kalman/helpers/lst_sq_computer.py new file mode 100755 index 000000000..669c76f49 --- /dev/null +++ b/selfdrive/locationd/kalman/helpers/lst_sq_computer.py @@ -0,0 +1,176 @@ +#!/usr/bin/env python3 +import os +import sys + +import numpy as np +import sympy as sp + +import common.transformations.orientation as orient +from selfdrive.locationd.kalman.helpers import (TEMPLATE_DIR, load_code, + write_code) +from selfdrive.locationd.kalman.helpers.sympy_helpers import (quat_rotate, + sympy_into_c) + + +def generate_residual(K): + x_sym = sp.MatrixSymbol('abr', 3, 1) + poses_sym = sp.MatrixSymbol('poses', 7 * K, 1) + img_pos_sym = sp.MatrixSymbol('img_positions', 2 * K, 1) + alpha, beta, rho = x_sym + to_c = sp.Matrix(orient.rot_matrix(-np.pi / 2, -np.pi / 2, 0)) + pos_0 = sp.Matrix(np.array(poses_sym[K * 7 - 7:K * 7 - 4])[:, 0]) + q = poses_sym[K * 7 - 4:K * 7] + quat_rot = quat_rotate(*q) + rot_g_to_0 = to_c * quat_rot.T + rows = [] + + for i in range(K): + pos_i = sp.Matrix(np.array(poses_sym[i * 7:i * 7 + 3])[:, 0]) + q = poses_sym[7 * i + 3:7 * i + 7] + quat_rot = quat_rotate(*q) + rot_g_to_i = to_c * quat_rot.T + rot_0_to_i = rot_g_to_i * rot_g_to_0.T + trans_0_to_i = rot_g_to_i * (pos_0 - pos_i) + funct_vec = rot_0_to_i * sp.Matrix([alpha, beta, 1]) + rho * trans_0_to_i + h1, h2, h3 = funct_vec + rows.append(h1 / h3 - img_pos_sym[i * 2 + 0]) + rows.append(h2 / h3 - img_pos_sym[i * 2 + 1]) + img_pos_residual_sym = sp.Matrix(rows) + + # sympy into c + sympy_functions = [] + sympy_functions.append(('res_fun', img_pos_residual_sym, [x_sym, poses_sym, img_pos_sym])) + sympy_functions.append(('jac_fun', img_pos_residual_sym.jacobian(x_sym), [x_sym, poses_sym, img_pos_sym])) + + return sympy_functions + + +class LstSqComputer(): + name = 'pos_computer' + + @staticmethod + def generate_code(K=4): + sympy_functions = generate_residual(K) + header, code = sympy_into_c(sympy_functions) + + code += "\n#define KDIM %d\n" % K + code += "\n" + open(os.path.join(TEMPLATE_DIR, "compute_pos.c")).read() + + header += """ + void compute_pos(double *to_c, double *in_poses, double *in_img_positions, double *param, double *pos); + """ + + filename = f"{LstSqComputer.name}_{K}" + write_code(filename, code, header) + + def __init__(self, K=4, MIN_DEPTH=2, MAX_DEPTH=500): + self.to_c = orient.rot_matrix(-np.pi / 2, -np.pi / 2, 0) + self.MAX_DEPTH = MAX_DEPTH + self.MIN_DEPTH = MIN_DEPTH + + name = f"{LstSqComputer.name}_{K}" + ffi, lib = load_code(name) + + # wrap c functions + def residual_jac(x, poses, img_positions): + out = np.zeros(((K * 2, 3)), dtype=np.float64) + lib.jac_fun(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", poses.ctypes.data), + ffi.cast("double *", img_positions.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return out + self.residual_jac = residual_jac + + def residual(x, poses, img_positions): + out = np.zeros((K * 2), dtype=np.float64) + lib.res_fun(ffi.cast("double *", x.ctypes.data), + ffi.cast("double *", poses.ctypes.data), + ffi.cast("double *", img_positions.ctypes.data), + ffi.cast("double *", out.ctypes.data)) + return out + self.residual = residual + + def compute_pos_c(poses, img_positions): + pos = np.zeros(3, dtype=np.float64) + param = np.zeros(3, dtype=np.float64) + # Can't be a view for the ctype + img_positions = np.copy(img_positions) + lib.compute_pos(ffi.cast("double *", self.to_c.ctypes.data), + ffi.cast("double *", poses.ctypes.data), + ffi.cast("double *", img_positions.ctypes.data), + ffi.cast("double *", param.ctypes.data), + ffi.cast("double *", pos.ctypes.data)) + return pos, param + self.compute_pos_c = compute_pos_c + + def compute_pos(self, poses, img_positions, debug=False): + pos, param = self.compute_pos_c(poses, img_positions) + # pos, param = self.compute_pos_python(poses, img_positions) + + depth = 1 / param[2] + if debug: + # orient_err_jac = self.orient_error_jac(param, poses, img_positions, np.zeros(3)).reshape((-1,2,3)) + jac = self.residual_jac(param, poses, img_positions).reshape((-1, 2, 3)) + res = self.residual(param, poses, img_positions).reshape((-1, 2)) + return pos, param, res, jac # , orient_err_jac + elif (self.MIN_DEPTH < depth < self.MAX_DEPTH): + return pos + else: + return None + + def gauss_newton(self, fun, jac, x, args): + poses, img_positions = args + delta = 1 + counter = 0 + while abs(np.linalg.norm(delta)) > 1e-4 and counter < 30: + delta = np.linalg.pinv(jac(x, poses, img_positions)).dot(fun(x, poses, img_positions)) + x = x - delta + counter += 1 + return [x] + + def compute_pos_python(self, poses, img_positions, check_quality=False): + import scipy.optimize as opt + + # This procedure is also described + # in the MSCKF paper (Mourikis et al. 2007) + x = np.array([img_positions[-1][0], + img_positions[-1][1], 0.1]) + res = opt.leastsq(self.residual, x, Dfun=self.residual_jac, args=(poses, img_positions)) # scipy opt + # res = self.gauss_newton(self.residual, self.residual_jac, x, (poses, img_positions)) # diy gauss_newton + + alpha, beta, rho = res[0] + rot_0_to_g = (orient.rotations_from_quats(poses[-1, 3:])).dot(self.to_c.T) + return (rot_0_to_g.dot(np.array([alpha, beta, 1]))) / rho + poses[-1, :3] + + +# EXPERIMENTAL CODE +def unroll_shutter(img_positions, poses, v, rot_rates, ecef_pos): + # only speed correction for now + t_roll = 0.016 # 16ms rolling shutter? + vroll, vpitch, vyaw = rot_rates + A = 0.5 * np.array([[-1, -vroll, -vpitch, -vyaw], + [vroll, 0, vyaw, -vpitch], + [vpitch, -vyaw, 0, vroll], + [vyaw, vpitch, -vroll, 0]]) + q_dot = A.dot(poses[-1][3:7]) + v = np.append(v, q_dot) + v = np.array([v[0], v[1], v[2], 0, 0, 0, 0]) + current_pose = poses[-1] + v * 0.05 + poses = np.vstack((current_pose, poses)) + dt = -img_positions[:, 1] * t_roll / 0.48 + errs = project(poses, ecef_pos) - project(poses + np.atleast_2d(dt).T.dot(np.atleast_2d(v)), ecef_pos) + return img_positions - errs + + +def project(poses, ecef_pos): + img_positions = np.zeros((len(poses), 2)) + for i, p in enumerate(poses): + cam_frame = orient.rotations_from_quats(p[3:]).T.dot(ecef_pos - p[:3]) + img_positions[i] = np.array([cam_frame[1] / cam_frame[0], cam_frame[2] / cam_frame[0]]) + return img_positions + + +if __name__ == "__main__": + K = int(sys.argv[1].split("_")[-1]) + LstSqComputer.generate_code(K=K) + diff --git a/common/sympy_helpers.py b/selfdrive/locationd/kalman/helpers/sympy_helpers.py similarity index 58% rename from common/sympy_helpers.py rename to selfdrive/locationd/kalman/helpers/sympy_helpers.py index 78688a84c..f8c4f1c5f 100644 --- a/common/sympy_helpers.py +++ b/selfdrive/locationd/kalman/helpers/sympy_helpers.py @@ -2,31 +2,35 @@ import sympy as sp import numpy as np + def cross(x): - ret = sp.Matrix(np.zeros((3,3))) - ret[0,1], ret[0,2] = -x[2], x[1] - ret[1,0], ret[1,2] = x[2], -x[0] - ret[2,0], ret[2,1] = -x[1], x[0] + ret = sp.Matrix(np.zeros((3, 3))) + ret[0, 1], ret[0, 2] = -x[2], x[1] + ret[1, 0], ret[1, 2] = x[2], -x[0] + ret[2, 0], ret[2, 1] = -x[1], x[0] return ret + def euler_rotate(roll, pitch, yaw): # make symbolic rotation matrix from eulers - matrix_roll = sp.Matrix([[1, 0, 0], - [0, sp.cos(roll), -sp.sin(roll)], - [0, sp.sin(roll), sp.cos(roll)]]) - matrix_pitch = sp.Matrix([[sp.cos(pitch), 0, sp.sin(pitch)], - [0, 1, 0], - [-sp.sin(pitch), 0, sp.cos(pitch)]]) - matrix_yaw = sp.Matrix([[sp.cos(yaw), -sp.sin(yaw), 0], - [sp.sin(yaw), sp.cos(yaw), 0], - [0, 0, 1]]) - return matrix_yaw*matrix_pitch*matrix_roll + matrix_roll = sp.Matrix([[1, 0, 0], + [0, sp.cos(roll), -sp.sin(roll)], + [0, sp.sin(roll), sp.cos(roll)]]) + matrix_pitch = sp.Matrix([[sp.cos(pitch), 0, sp.sin(pitch)], + [0, 1, 0], + [-sp.sin(pitch), 0, sp.cos(pitch)]]) + matrix_yaw = sp.Matrix([[sp.cos(yaw), -sp.sin(yaw), 0], + [sp.sin(yaw), sp.cos(yaw), 0], + [0, 0, 1]]) + return matrix_yaw * matrix_pitch * matrix_roll + def quat_rotate(q0, q1, q2, q3): # make symbolic rotation matrix from quat - return sp.Matrix([[q0**2 + q1**2 - q2**2 - q3**2, 2*(q1*q2 + q0*q3), 2*(q1*q3 - q0*q2)], - [2*(q1*q2 - q0*q3), q0**2 - q1**2 + q2**2 - q3**2, 2*(q2*q3 + q0*q1)], - [2*(q1*q3 + q0*q2), 2*(q2*q3 - q0*q1), q0**2 - q1**2 - q2**2 + q3**2]]).T + return sp.Matrix([[q0**2 + q1**2 - q2**2 - q3**2, 2 * (q1 * q2 + q0 * q3), 2 * (q1 * q3 - q0 * q2)], + [2 * (q1 * q2 - q0 * q3), q0**2 - q1**2 + q2**2 - q3**2, 2 * (q2 * q3 + q0 * q1)], + [2 * (q1 * q3 + q0 * q2), 2 * (q2 * q3 - q0 * q1), q0**2 - q1**2 - q2**2 + q3**2]]).T + def quat_matrix_l(p): return sp.Matrix([[p[0], -p[1], -p[2], -p[3]], @@ -34,6 +38,7 @@ def quat_matrix_l(p): [p[2], p[3], p[0], -p[1]], [p[3], -p[2], p[1], p[0]]]) + def quat_matrix_r(p): return sp.Matrix([[p[0], -p[1], -p[2], -p[3]], [p[1], p[0], p[3], -p[2]], @@ -49,10 +54,11 @@ def sympy_into_c(sympy_functions): # argument ordering input to sympy is broken with function with output arguments nargs = [] + # reorder the input arguments for aa in args: if aa is None: - nargs.append(codegen.InputArgument(sp.Symbol('unused'), dimensions=[1,1])) + nargs.append(codegen.InputArgument(sp.Symbol('unused'), dimensions=[1, 1])) continue found = False for a in r.arguments: @@ -62,20 +68,23 @@ def sympy_into_c(sympy_functions): break if not found: # [1,1] is a hack for Matrices - nargs.append(codegen.InputArgument(aa, dimensions=[1,1])) + nargs.append(codegen.InputArgument(aa, dimensions=[1, 1])) + # add the output arguments for a in r.arguments: if type(a) == codegen.OutputArgument: nargs.append(a) - #assert len(r.arguments) == len(args)+1 + # assert len(r.arguments) == len(args)+1 r.arguments = nargs # add routine to list routines.append(r) [(c_name, c_code), (h_name, c_header)] = codegen.get_code_generator('C', 'ekf', 'C99').write(routines, "ekf") + c_header = '\n'.join(x for x in c_header.split("\n") if len(x) > 0 and x[0] != '#') + c_code = '\n'.join(x for x in c_code.split("\n") if len(x) > 0 and x[0] != '#') - c_header = '\n'.join(x for x in c_header.split("\n") if len(x) > 0 and x[0] != '#') + c_code = 'extern "C" {\n#include \n' + c_code + "\n}\n" return c_header, c_code diff --git a/selfdrive/locationd/kalman/models/live_kf.py b/selfdrive/locationd/kalman/models/live_kf.py new file mode 100755 index 000000000..1ee8d74ac --- /dev/null +++ b/selfdrive/locationd/kalman/models/live_kf.py @@ -0,0 +1,284 @@ +#!/usr/bin/env python3 +import numpy as np +import sympy as sp + +from selfdrive.locationd.kalman.helpers import KalmanError, ObservationKind +from selfdrive.locationd.kalman.helpers.ekf_sym import EKF_sym, gen_code +from selfdrive.locationd.kalman.helpers.sympy_helpers import (euler_rotate, + quat_matrix_r, + quat_rotate) +from selfdrive.swaglog import cloudlog + +EARTH_GM = 3.986005e14 # m^3/s^2 (gravitational constant * mass of earth) + + +class States(): + ECEF_POS = slice(0, 3) # x, y and z in ECEF in meters + ECEF_ORIENTATION = slice(3, 7) # quat for pose of phone in ecef + ECEF_VELOCITY = slice(7, 10) # ecef velocity in m/s + ANGULAR_VELOCITY = slice(10, 13) # roll, pitch and yaw rates in device frame in radians/s + GYRO_BIAS = slice(13, 16) # roll, pitch and yaw biases + ODO_SCALE = slice(16, 17) # odometer scale + ACCELERATION = slice(17, 20) # Acceleration in device frame in m/s**2 + IMU_OFFSET = slice(20, 23) # imu offset angles in radians + + # Error-state has different slices because it is an ESKF + ECEF_POS_ERR = slice(0, 3) + ECEF_ORIENTATION_ERR = slice(3, 6) # euler angles for orientation error + ECEF_VELOCITY_ERR = slice(6, 9) + ANGULAR_VELOCITY_ERR = slice(9, 12) + GYRO_BIAS_ERR = slice(12, 15) + ODO_SCALE_ERR = slice(15, 16) + ACCELERATION_ERR = slice(16, 19) + IMU_OFFSET_ERR = slice(19, 22) + + +class LiveKalman(): + name = 'live' + + initial_x = np.array([-2.7e6, 4.2e6, 3.8e6, + 1, 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 0, 0, 0, + 1, + 0, 0, 0, + 0, 0, 0]) + + # state covariance + initial_P_diag = np.array([10000**2, 10000**2, 10000**2, + 10**2, 10**2, 10**2, + 10**2, 10**2, 10**2, + 1**2, 1**2, 1**2, + 0.05**2, 0.05**2, 0.05**2, + 0.02**2, + 1**2, 1**2, 1**2, + (0.01)**2, (0.01)**2, (0.01)**2]) + + # process noise + Q = np.diag([0.03**2, 0.03**2, 0.03**2, + 0.0**2, 0.0**2, 0.0**2, + 0.0**2, 0.0**2, 0.0**2, + 0.1**2, 0.1**2, 0.1**2, + (0.005 / 100)**2, (0.005 / 100)**2, (0.005 / 100)**2, + (0.02 / 100)**2, + 3**2, 3**2, 3**2, + (0.05 / 60)**2, (0.05 / 60)**2, (0.05 / 60)**2]) + + @staticmethod + def generate_code(): + name = LiveKalman.name + dim_state = LiveKalman.initial_x.shape[0] + dim_state_err = LiveKalman.initial_P_diag.shape[0] + + state_sym = sp.MatrixSymbol('state', dim_state, 1) + state = sp.Matrix(state_sym) + x, y, z = state[States.ECEF_POS, :] + q = state[States.ECEF_ORIENTATION, :] + v = state[States.ECEF_VELOCITY, :] + vx, vy, vz = v + omega = state[States.ANGULAR_VELOCITY, :] + vroll, vpitch, vyaw = omega + roll_bias, pitch_bias, yaw_bias = state[States.GYRO_BIAS, :] + odo_scale = state[States.ODO_SCALE, :][0,:] + acceleration = state[States.ACCELERATION, :] + imu_angles = state[States.IMU_OFFSET, :] + + dt = sp.Symbol('dt') + + # calibration and attitude rotation matrices + quat_rot = quat_rotate(*q) + + # Got the quat predict equations from here + # A New Quaternion-Based Kalman Filter for + # Real-Time Attitude Estimation Using the Two-Step + # Geometrically-Intuitive Correction Algorithm + A = 0.5 * sp.Matrix([[0, -vroll, -vpitch, -vyaw], + [vroll, 0, vyaw, -vpitch], + [vpitch, -vyaw, 0, vroll], + [vyaw, vpitch, -vroll, 0]]) + q_dot = A * q + + # Time derivative of the state as a function of state + state_dot = sp.Matrix(np.zeros((dim_state, 1))) + state_dot[States.ECEF_POS, :] = v + state_dot[States.ECEF_ORIENTATION, :] = q_dot + state_dot[States.ECEF_VELOCITY, 0] = quat_rot * acceleration + + # Basic descretization, 1st order intergrator + # Can be pretty bad if dt is big + f_sym = state + dt * state_dot + + state_err_sym = sp.MatrixSymbol('state_err', dim_state_err, 1) + state_err = sp.Matrix(state_err_sym) + quat_err = state_err[States.ECEF_ORIENTATION_ERR, :] + v_err = state_err[States.ECEF_VELOCITY_ERR, :] + omega_err = state_err[States.ANGULAR_VELOCITY_ERR, :] + acceleration_err = state_err[States.ACCELERATION_ERR, :] + + # Time derivative of the state error as a function of state error and state + quat_err_matrix = euler_rotate(quat_err[0], quat_err[1], quat_err[2]) + q_err_dot = quat_err_matrix * quat_rot * (omega + omega_err) + state_err_dot = sp.Matrix(np.zeros((dim_state_err, 1))) + state_err_dot[States.ECEF_POS_ERR, :] = v_err + state_err_dot[States.ECEF_ORIENTATION_ERR, :] = q_err_dot + state_err_dot[States.ECEF_VELOCITY_ERR, :] = quat_err_matrix * quat_rot * (acceleration + acceleration_err) + f_err_sym = state_err + dt * state_err_dot + + # Observation matrix modifier + H_mod_sym = sp.Matrix(np.zeros((dim_state, dim_state_err))) + H_mod_sym[States.ECEF_POS, States.ECEF_POS_ERR] = np.eye(States.ECEF_POS.stop - States.ECEF_POS.start) + H_mod_sym[States.ECEF_ORIENTATION, States.ECEF_ORIENTATION_ERR] = 0.5 * quat_matrix_r(state[3:7])[:, 1:] + H_mod_sym[States.ECEF_ORIENTATION.stop:, States.ECEF_ORIENTATION_ERR.stop:] = np.eye(dim_state - States.ECEF_ORIENTATION.stop) + + # these error functions are defined so that say there + # is a nominal x and true x: + # true x = err_function(nominal x, delta x) + # delta x = inv_err_function(nominal x, true x) + nom_x = sp.MatrixSymbol('nom_x', dim_state, 1) + true_x = sp.MatrixSymbol('true_x', dim_state, 1) + delta_x = sp.MatrixSymbol('delta_x', dim_state_err, 1) + + err_function_sym = sp.Matrix(np.zeros((dim_state, 1))) + delta_quat = sp.Matrix(np.ones((4))) + delta_quat[1:, :] = sp.Matrix(0.5 * delta_x[States.ECEF_ORIENTATION_ERR, :]) + err_function_sym[States.ECEF_POS, :] = sp.Matrix(nom_x[States.ECEF_POS, :] + delta_x[States.ECEF_POS_ERR, :]) + err_function_sym[States.ECEF_ORIENTATION, 0] = quat_matrix_r(nom_x[States.ECEF_ORIENTATION, 0]) * delta_quat + err_function_sym[States.ECEF_ORIENTATION.stop:, :] = sp.Matrix(nom_x[States.ECEF_ORIENTATION.stop:, :] + delta_x[States.ECEF_ORIENTATION_ERR.stop:, :]) + + inv_err_function_sym = sp.Matrix(np.zeros((dim_state_err, 1))) + inv_err_function_sym[States.ECEF_POS_ERR, 0] = sp.Matrix(-nom_x[States.ECEF_POS, 0] + true_x[States.ECEF_POS, 0]) + delta_quat = quat_matrix_r(nom_x[States.ECEF_ORIENTATION, 0]).T * true_x[States.ECEF_ORIENTATION, 0] + inv_err_function_sym[States.ECEF_ORIENTATION_ERR, 0] = sp.Matrix(2 * delta_quat[1:]) + inv_err_function_sym[States.ECEF_ORIENTATION_ERR.stop:, 0] = sp.Matrix(-nom_x[States.ECEF_ORIENTATION.stop:, 0] + true_x[States.ECEF_ORIENTATION.stop:, 0]) + + eskf_params = [[err_function_sym, nom_x, delta_x], + [inv_err_function_sym, nom_x, true_x], + H_mod_sym, f_err_sym, state_err_sym] + # + # Observation functions + # + imu_rot = euler_rotate(*imu_angles) + h_gyro_sym = imu_rot * sp.Matrix([vroll + roll_bias, + vpitch + pitch_bias, + vyaw + yaw_bias]) + + pos = sp.Matrix([x, y, z]) + gravity = quat_rot.T * ((EARTH_GM / ((x**2 + y**2 + z**2)**(3.0 / 2.0))) * pos) + h_acc_sym = imu_rot * (gravity + acceleration) + h_phone_rot_sym = sp.Matrix([vroll, vpitch, vyaw]) + + speed = sp.sqrt(vx**2 + vy**2 + vz**2) + h_speed_sym = sp.Matrix([speed * odo_scale]) + + h_pos_sym = sp.Matrix([x, y, z]) + h_imu_frame_sym = sp.Matrix(imu_angles) + + h_relative_motion = sp.Matrix(quat_rot.T * v) + + obs_eqs = [[h_speed_sym, ObservationKind.ODOMETRIC_SPEED, None], + [h_gyro_sym, ObservationKind.PHONE_GYRO, None], + [h_phone_rot_sym, ObservationKind.NO_ROT, None], + [h_acc_sym, ObservationKind.PHONE_ACCEL, None], + [h_pos_sym, ObservationKind.ECEF_POS, None], + [h_relative_motion, ObservationKind.CAMERA_ODO_TRANSLATION, None], + [h_phone_rot_sym, ObservationKind.CAMERA_ODO_ROTATION, None], + [h_imu_frame_sym, ObservationKind.IMU_FRAME, None]] + + gen_code(name, f_sym, dt, state_sym, obs_eqs, dim_state, dim_state_err, eskf_params) + + def __init__(self): + self.dim_state = self.initial_x.shape[0] + self.dim_state_err = self.initial_P_diag.shape[0] + + self.obs_noise = {ObservationKind.ODOMETRIC_SPEED: np.atleast_2d(0.2**2), + ObservationKind.PHONE_GYRO: np.diag([0.025**2, 0.025**2, 0.025**2]), + ObservationKind.PHONE_ACCEL: np.diag([.5**2, .5**2, .5**2]), + ObservationKind.CAMERA_ODO_ROTATION: np.diag([0.05**2, 0.05**2, 0.05**2]), + ObservationKind.IMU_FRAME: np.diag([0.05**2, 0.05**2, 0.05**2]), + ObservationKind.NO_ROT: np.diag([0.00025**2, 0.00025**2, 0.00025**2]), + ObservationKind.ECEF_POS: np.diag([5**2, 5**2, 5**2])} + + # init filter + self.filter = EKF_sym(self.name, self.Q, self.initial_x, np.diag(self.initial_P_diag), self.dim_state, self.dim_state_err) + + @property + def x(self): + return self.filter.state() + + @property + def t(self): + return self.filter.filter_time + + @property + def P(self): + return self.filter.covs() + + def rts_smooth(self, estimates): + return self.filter.rts_smooth(estimates, norm_quats=True) + + def init_state(self, state, covs_diag=None, covs=None, filter_time=None): + if covs_diag is not None: + P = np.diag(covs_diag) + elif covs is not None: + P = covs + else: + P = self.filter.covs() + self.filter.init_state(state, P, filter_time) + + def predict_and_observe(self, t, kind, data): + if len(data) > 0: + data = np.atleast_2d(data) + if kind == ObservationKind.CAMERA_ODO_TRANSLATION: + r = self.predict_and_update_odo_trans(data, t, kind) + elif kind == ObservationKind.CAMERA_ODO_ROTATION: + r = self.predict_and_update_odo_rot(data, t, kind) + elif kind == ObservationKind.ODOMETRIC_SPEED: + r = self.predict_and_update_odo_speed(data, t, kind) + else: + r = self.filter.predict_and_update_batch(t, kind, data, self.get_R(kind, len(data))) + + # Normalize quats + quat_norm = np.linalg.norm(self.filter.x[3:7, 0]) + + # Should not continue if the quats behave this weirdly + if not (0.1 < quat_norm < 10): + cloudlog.error("Kalman filter quaternions unstable") + raise KalmanError + + self.filter.x[States.ECEF_ORIENTATION, 0] = self.filter.x[States.ECEF_ORIENTATION, 0] / quat_norm + + return r + + def get_R(self, kind, n): + obs_noise = self.obs_noise[kind] + dim = obs_noise.shape[0] + R = np.zeros((n, dim, dim)) + for i in range(n): + R[i, :, :] = obs_noise + return R + + def predict_and_update_odo_speed(self, speed, t, kind): + z = np.array(speed) + R = np.zeros((len(speed), 1, 1)) + for i, _ in enumerate(z): + R[i, :, :] = np.diag([0.2**2]) + return self.filter.predict_and_update_batch(t, kind, z, R) + + def predict_and_update_odo_trans(self, trans, t, kind): + z = trans[:, :3] + R = np.zeros((len(trans), 3, 3)) + for i, _ in enumerate(z): + R[i, :, :] = np.diag(trans[i, 3:]**2) + return self.filter.predict_and_update_batch(t, kind, z, R) + + def predict_and_update_odo_rot(self, rot, t, kind): + z = rot[:, :3] + R = np.zeros((len(rot), 3, 3)) + for i, _ in enumerate(z): + R[i, :, :] = np.diag(rot[i, 3:]**2) + return self.filter.predict_and_update_batch(t, kind, z, R) + + +if __name__ == "__main__": + LiveKalman.generate_code() diff --git a/selfdrive/locationd/kalman/templates/compute_pos.c b/selfdrive/locationd/kalman/templates/compute_pos.c new file mode 100644 index 000000000..9499fa7b6 --- /dev/null +++ b/selfdrive/locationd/kalman/templates/compute_pos.c @@ -0,0 +1,54 @@ +#include +#include +#include + +typedef Eigen::Matrix R3M; +typedef Eigen::Matrix R1M; +typedef Eigen::Matrix O1M; +typedef Eigen::Matrix M3D; + +extern "C" { +void gauss_newton(double *in_x, double *in_poses, double *in_img_positions) { + + double res[KDIM*2] = {0}; + double jac[KDIM*6] = {0}; + + O1M x(in_x); + O1M delta; + int counter = 0; + while ((delta.squaredNorm() > 0.0001 and counter < 30) or counter == 0){ + res_fun(in_x, in_poses, in_img_positions, res); + jac_fun(in_x, in_poses, in_img_positions, jac); + R1M E(res); R3M J(jac); + delta = (J.transpose()*J).inverse() * J.transpose() * E; + x = x - delta; + memcpy(in_x, x.data(), 3 * sizeof(double)); + counter = counter + 1; + } +} + + +void compute_pos(double *to_c, double *poses, double *img_positions, double *param, double *pos) { + param[0] = img_positions[KDIM*2-2]; + param[1] = img_positions[KDIM*2-1]; + param[2] = 0.1; + gauss_newton(param, poses, img_positions); + + Eigen::Quaterniond q; + q.w() = poses[KDIM*7-4]; + q.x() = poses[KDIM*7-3]; + q.y() = poses[KDIM*7-2]; + q.z() = poses[KDIM*7-1]; + M3D RC(to_c); + Eigen::Matrix3d R = q.normalized().toRotationMatrix(); + Eigen::Matrix3d rot = R * RC.transpose(); + + pos[0] = param[0]/param[2]; + pos[1] = param[1]/param[2]; + pos[2] = 1.0/param[2]; + O1M ecef_offset(poses + KDIM*7-7); + O1M ecef_output(pos); + ecef_output = rot*ecef_output + ecef_offset; + memcpy(pos, ecef_output.data(), 3 * sizeof(double)); +} +} diff --git a/selfdrive/locationd/kalman/templates/ekf_c.c b/selfdrive/locationd/kalman/templates/ekf_c.c new file mode 100644 index 000000000..e524f81ec --- /dev/null +++ b/selfdrive/locationd/kalman/templates/ekf_c.c @@ -0,0 +1,123 @@ +#include +#include + +typedef Eigen::Matrix DDM; +typedef Eigen::Matrix EEM; +typedef Eigen::Matrix DEM; + +void predict(double *in_x, double *in_P, double *in_Q, double dt) { + typedef Eigen::Matrix RRM; + + double nx[DIM] = {0}; + double in_F[EDIM*EDIM] = {0}; + + // functions from sympy + f_fun(in_x, dt, nx); + F_fun(in_x, dt, in_F); + + + EEM F(in_F); + EEM P(in_P); + EEM Q(in_Q); + + RRM F_main = F.topLeftCorner(MEDIM, MEDIM); + P.topLeftCorner(MEDIM, MEDIM) = (F_main * P.topLeftCorner(MEDIM, MEDIM)) * F_main.transpose(); + P.topRightCorner(MEDIM, EDIM - MEDIM) = F_main * P.topRightCorner(MEDIM, EDIM - MEDIM); + P.bottomLeftCorner(EDIM - MEDIM, MEDIM) = P.bottomLeftCorner(EDIM - MEDIM, MEDIM) * F_main.transpose(); + + P = P + dt*Q; + + // copy out state + memcpy(in_x, nx, DIM * sizeof(double)); + memcpy(in_P, P.data(), EDIM * EDIM * sizeof(double)); +} + +// note: extra_args dim only correct when null space projecting +// otherwise 1 +template +void update(double *in_x, double *in_P, Hfun h_fun, Hfun H_fun, Hfun Hea_fun, double *in_z, double *in_R, double *in_ea, double MAHA_THRESHOLD) { + typedef Eigen::Matrix ZZM; + typedef Eigen::Matrix ZDM; + typedef Eigen::Matrix XEM; + //typedef Eigen::Matrix EZM; + typedef Eigen::Matrix X1M; + typedef Eigen::Matrix XXM; + + double in_hx[ZDIM] = {0}; + double in_H[ZDIM * DIM] = {0}; + double in_H_mod[EDIM * DIM] = {0}; + double delta_x[EDIM] = {0}; + double x_new[DIM] = {0}; + + + // state x, P + Eigen::Matrix z(in_z); + EEM P(in_P); + ZZM pre_R(in_R); + + // functions from sympy + h_fun(in_x, in_ea, in_hx); + H_fun(in_x, in_ea, in_H); + ZDM pre_H(in_H); + + // get y (y = z - hx) + Eigen::Matrix pre_y(in_hx); pre_y = z - pre_y; + X1M y; XXM H; XXM R; + if (Hea_fun){ + typedef Eigen::Matrix ZAM; + double in_Hea[ZDIM * EADIM] = {0}; + Hea_fun(in_x, in_ea, in_Hea); + ZAM Hea(in_Hea); + XXM A = Hea.transpose().fullPivLu().kernel(); + + + y = A.transpose() * pre_y; + H = A.transpose() * pre_H; + R = A.transpose() * pre_R * A; + } else { + y = pre_y; + H = pre_H; + R = pre_R; + } + // get modified H + H_mod_fun(in_x, in_H_mod); + DEM H_mod(in_H_mod); + XEM H_err = H * H_mod; + + // Do mahalobis distance test + if (MAHA_TEST){ + XXM a = (H_err * P * H_err.transpose() + R).inverse(); + double maha_dist = y.transpose() * a * y; + if (maha_dist > MAHA_THRESHOLD){ + R = 1.0e16 * R; + } + } + + // Outlier resilient weighting + double weight = 1;//(1.5)/(1 + y.squaredNorm()/R.sum()); + + // kalman gains and I_KH + XXM S = ((H_err * P) * H_err.transpose()) + R/weight; + XEM KT = S.fullPivLu().solve(H_err * P.transpose()); + //EZM K = KT.transpose(); TODO: WHY DOES THIS NOT COMPILE? + //EZM K = S.fullPivLu().solve(H_err * P.transpose()).transpose(); + //std::cout << "Here is the matrix rot:\n" << K << std::endl; + EEM I_KH = Eigen::Matrix::Identity() - (KT.transpose() * H_err); + + // update state by injecting dx + Eigen::Matrix dx(delta_x); + dx = (KT.transpose() * y); + memcpy(delta_x, dx.data(), EDIM * sizeof(double)); + err_fun(in_x, delta_x, x_new); + Eigen::Matrix x(x_new); + + // update cov + P = ((I_KH * P) * I_KH.transpose()) + ((KT.transpose() * R) * KT); + + // copy out state + memcpy(in_x, x.data(), DIM * sizeof(double)); + memcpy(in_P, P.data(), EDIM * EDIM * sizeof(double)); + memcpy(in_z, y.data(), y.rows() * sizeof(double)); +} + + diff --git a/selfdrive/locationd/kalman/templates/feature_handler.c b/selfdrive/locationd/kalman/templates/feature_handler.c new file mode 100644 index 000000000..330972339 --- /dev/null +++ b/selfdrive/locationd/kalman/templates/feature_handler.c @@ -0,0 +1,58 @@ +extern "C"{ +bool sane(double track [K + 1][5]) { + double diffs_x [K-1]; + double diffs_y [K-1]; + int i; + for (i = 0; i < K-1; i++) { + diffs_x[i] = fabs(track[i+2][2] - track[i+1][2]); + diffs_y[i] = fabs(track[i+2][3] - track[i+1][3]); + } + for (i = 1; i < K-1; i++) { + if (((diffs_x[i] > 0.05 or diffs_x[i-1] > 0.05) and + (diffs_x[i] > 2*diffs_x[i-1] or + diffs_x[i] < .5*diffs_x[i-1])) or + ((diffs_y[i] > 0.05 or diffs_y[i-1] > 0.05) and + (diffs_y[i] > 2*diffs_y[i-1] or + diffs_y[i] < .5*diffs_y[i-1]))){ + return false; + } + } + return true; +} + +void merge_features(double *tracks, double *features, long long *empty_idxs) { + double feature_arr [3000][5]; + memcpy(feature_arr, features, 3000 * 5 * sizeof(double)); + double track_arr [6000][K + 1][5]; + memcpy(track_arr, tracks, (K+1) * 6000 * 5 * sizeof(double)); + int match; + int empty_idx = 0; + int idx; + for (int i = 0; i < 3000; i++) { + match = feature_arr[i][4]; + if (track_arr[match][0][1] == match and track_arr[match][0][2] == 0){ + track_arr[match][0][0] = track_arr[match][0][0] + 1; + track_arr[match][0][1] = feature_arr[i][1]; + track_arr[match][0][2] = 1; + idx = track_arr[match][0][0]; + memcpy(track_arr[match][idx], feature_arr[i], 5 * sizeof(double)); + if (idx == K){ + // label complete + track_arr[match][0][3] = 1; + if (sane(track_arr[match])){ + // label valid + track_arr[match][0][4] = 1; + } + } + } else { + // gen new track with this feature + track_arr[empty_idxs[empty_idx]][0][0] = 1; + track_arr[empty_idxs[empty_idx]][0][1] = feature_arr[i][1]; + track_arr[empty_idxs[empty_idx]][0][2] = 1; + memcpy(track_arr[empty_idxs[empty_idx]][1], feature_arr[i], 5 * sizeof(double)); + empty_idx = empty_idx + 1; + } + } + memcpy(tracks, track_arr, (K+1) * 6000 * 5 * sizeof(double)); +} +} diff --git a/selfdrive/locationd/locationd.py b/selfdrive/locationd/locationd.py new file mode 100755 index 000000000..3cf9560d6 --- /dev/null +++ b/selfdrive/locationd/locationd.py @@ -0,0 +1,202 @@ +#!/usr/bin/env python3 +import math + +import numpy as np + +import cereal.messaging as messaging +import common.transformations.coordinates as coord +from common.transformations.orientation import (ecef_euler_from_ned, + euler2quat, + ned_euler_from_ecef, + quat2euler, + rotations_from_quats) +from selfdrive.locationd.kalman.helpers import ObservationKind, KalmanError +from selfdrive.locationd.kalman.models.live_kf import LiveKalman, States +from selfdrive.swaglog import cloudlog + +VISION_DECIMATION = 2 +SENSOR_DECIMATION = 10 + + +class Localizer(): + def __init__(self, disabled_logs=[], dog=None): + self.kf = LiveKalman() + self.reset_kalman() + self.max_age = .2 # seconds + self.disabled_logs = disabled_logs + + def liveLocationMsg(self, time): + fix = messaging.log.LiveLocationData.new_message() + + predicted_state = self.kf.x + + fix_ecef = predicted_state[States.ECEF_POS] + fix_pos_geo = coord.ecef2geodetic(fix_ecef) + fix.lat = float(fix_pos_geo[0]) + fix.lon = float(fix_pos_geo[1]) + fix.alt = float(fix_pos_geo[2]) + + fix.speed = float(np.linalg.norm(predicted_state[States.ECEF_VELOCITY])) + + orientation_ned_euler = ned_euler_from_ecef(fix_ecef, quat2euler(predicted_state[States.ECEF_ORIENTATION])) + fix.roll = math.degrees(orientation_ned_euler[0]) + fix.pitch = math.degrees(orientation_ned_euler[1]) + fix.heading = math.degrees(orientation_ned_euler[2]) + + fix.gyro = [float(predicted_state[10]), float(predicted_state[11]), float(predicted_state[12])] + fix.accel = [float(predicted_state[19]), float(predicted_state[20]), float(predicted_state[21])] + + ned_vel = self.converter.ecef2ned(predicted_state[States.ECEF_POS] + predicted_state[States.ECEF_VELOCITY]) - self.converter.ecef2ned(predicted_state[States.ECEF_POS]) + fix.vNED = [float(ned_vel[0]), float(ned_vel[1]), float(ned_vel[2])] + fix.source = 'kalman' + + #local_vel = rotations_from_quats(predicted_state[States.ECEF_ORIENTATION]).T.dot(predicted_state[States.ECEF_VELOCITY]) + #fix.pitchCalibration = math.degrees(math.atan2(local_vel[2], local_vel[0])) + #fix.yawCalibration = math.degrees(math.atan2(local_vel[1], local_vel[0])) + + imu_frame = predicted_state[States.IMU_OFFSET] + fix.imuFrame = [math.degrees(imu_frame[0]), math.degrees(imu_frame[1]), math.degrees(imu_frame[2])] + return fix + + def update_kalman(self, time, kind, meas): + if self.filter_ready: + try: + self.kf.predict_and_observe(time, kind, meas) + except KalmanError: + cloudlog.error("Error in predict and observe, kalman reset") + self.reset_kalman() + #idx = bisect_right([x[0] for x in self.observation_buffer], time) + #self.observation_buffer.insert(idx, (time, kind, meas)) + #while len(self.observation_buffer) > 0 and self.observation_buffer[-1][0] - self.observation_buffer[0][0] > self.max_age: + # else: + # self.observation_buffer.pop(0) + + def handle_gps(self, current_time, log): + self.converter = coord.LocalCoord.from_geodetic([log.latitude, log.longitude, log.altitude]) + fix_ecef = self.converter.ned2ecef([0, 0, 0]) + + # TODO initing with bad bearing not allowed, maybe not bad? + if not self.filter_ready and log.speed > 5: + self.filter_ready = True + initial_ecef = fix_ecef + gps_bearing = math.radians(log.bearing) + initial_pose_ecef = ecef_euler_from_ned(initial_ecef, [0, 0, gps_bearing]) + initial_pose_ecef_quat = euler2quat(initial_pose_ecef) + gps_speed = log.speed + quat_uncertainty = 0.2**2 + initial_pose_ecef_quat = euler2quat(initial_pose_ecef) + + initial_state = LiveKalman.initial_x + initial_covs_diag = LiveKalman.initial_P_diag + + initial_state[States.ECEF_POS] = initial_ecef + initial_state[States.ECEF_ORIENTATION] = initial_pose_ecef_quat + initial_state[States.ECEF_VELOCITY] = rotations_from_quats(initial_pose_ecef_quat).dot(np.array([gps_speed, 0, 0])) + + initial_covs_diag[States.ECEF_POS_ERR] = 10**2 + initial_covs_diag[States.ECEF_ORIENTATION_ERR] = quat_uncertainty + initial_covs_diag[States.ECEF_VELOCITY_ERR] = 1**2 + self.kf.init_state(initial_state, covs=np.diag(initial_covs_diag), filter_time=current_time) + cloudlog.info("Filter initialized") + elif self.filter_ready: + self.update_kalman(current_time, ObservationKind.ECEF_POS, fix_ecef) + gps_est_error = np.sqrt((self.kf.x[0] - fix_ecef[0])**2 + + (self.kf.x[1] - fix_ecef[1])**2 + + (self.kf.x[2] - fix_ecef[2])**2) + if gps_est_error > 50: + cloudlog.error("Locationd vs ubloxLocation difference too large, kalman reset") + self.reset_kalman() + + def handle_car_state(self, current_time, log): + self.speed_counter += 1 + + if self.speed_counter % SENSOR_DECIMATION == 0: + self.update_kalman(current_time, ObservationKind.ODOMETRIC_SPEED, [log.vEgo]) + if log.vEgo == 0: + self.update_kalman(current_time, ObservationKind.NO_ROT, [0, 0, 0]) + + def handle_cam_odo(self, current_time, log): + self.cam_counter += 1 + + if self.cam_counter % VISION_DECIMATION == 0: + self.update_kalman(current_time, + ObservationKind.CAMERA_ODO_ROTATION, + np.concatenate([log.rot, log.rotStd])) + self.update_kalman(current_time, + ObservationKind.CAMERA_ODO_TRANSLATION, + np.concatenate([log.trans, log.transStd])) + + def handle_sensors(self, current_time, log): + # TODO does not yet account for double sensor readings in the log + for sensor_reading in log: + # Gyro Uncalibrated + if sensor_reading.sensor == 5 and sensor_reading.type == 16: + self.gyro_counter += 1 + if self.gyro_counter % SENSOR_DECIMATION == 0: + if max(abs(self.kf.x[States.IMU_OFFSET])) > 0.07: + cloudlog.info('imu frame angles exceeded, correcting') + self.update_kalman(current_time, ObservationKind.IMU_FRAME, [0, 0, 0]) + + v = sensor_reading.gyroUncalibrated.v + self.update_kalman(current_time, ObservationKind.PHONE_GYRO, [-v[2], -v[1], -v[0]]) + + # Accelerometer + if sensor_reading.sensor == 1 and sensor_reading.type == 1: + self.acc_counter += 1 + if self.acc_counter % SENSOR_DECIMATION == 0: + v = sensor_reading.acceleration.v + self.update_kalman(current_time, ObservationKind.PHONE_ACCEL, [-v[2], -v[1], -v[0]]) + + def reset_kalman(self): + self.filter_time = None + self.filter_ready = False + self.observation_buffer = [] + + self.gyro_counter = 0 + self.acc_counter = 0 + self.speed_counter = 0 + self.cam_counter = 0 + + +def locationd_thread(sm, pm, disabled_logs=[]): + if sm is None: + sm = messaging.SubMaster(['gpsLocationExternal', 'sensorEvents', 'cameraOdometry']) + if pm is None: + pm = messaging.PubMaster(['liveLocation']) + + localizer = Localizer(disabled_logs=disabled_logs) + + while True: + sm.update() + + for sock, updated in sm.updated.items(): + if updated: + t = sm.logMonoTime[sock] * 1e-9 + if sock == "sensorEvents": + localizer.handle_sensors(t, sm[sock]) + elif sock == "gpsLocationExternal": + localizer.handle_gps(t, sm[sock]) + elif sock == "carState": + localizer.handle_car_state(t, sm[sock]) + elif sock == "cameraOdometry": + localizer.handle_cam_odo(t, sm[sock]) + + if localizer.filter_ready and sm.updated['gpsLocationExternal']: + t = sm.logMonoTime['gpsLocationExternal'] + msg = messaging.new_message() + msg.logMonoTime = t + + msg.init('liveLocation') + msg.liveLocation = localizer.liveLocationMsg(t * 1e-9) + + pm.send('liveLocation', msg) + + +def main(sm=None, pm=None): + locationd_thread(sm, pm) + + +if __name__ == "__main__": + import os + os.environ["OMP_NUM_THREADS"] = "1" + main() diff --git a/selfdrive/locationd/locationd_yawrate.cc b/selfdrive/locationd/locationd_yawrate.cc index 4b93b6801..8b25a2a7c 100644 --- a/selfdrive/locationd/locationd_yawrate.cc +++ b/selfdrive/locationd/locationd_yawrate.cc @@ -3,7 +3,7 @@ #include #include -#include +#include #include "cereal/gen/cpp/log.capnp.h" diff --git a/selfdrive/locationd/locationd_yawrate.h b/selfdrive/locationd/locationd_yawrate.h index c59734aa6..3b53ef561 100644 --- a/selfdrive/locationd/locationd_yawrate.h +++ b/selfdrive/locationd/locationd_yawrate.h @@ -1,6 +1,6 @@ #pragma once -#include +#include #include "cereal/gen/cpp/log.capnp.h" #define DEGREES_TO_RADIANS 0.017453292519943295 diff --git a/selfdrive/loggerd/deleter.py b/selfdrive/loggerd/deleter.py index 1c687ff4b..b9ae0e8d4 100644 --- a/selfdrive/loggerd/deleter.py +++ b/selfdrive/loggerd/deleter.py @@ -3,15 +3,19 @@ import os import shutil import threading from selfdrive.swaglog import cloudlog -from selfdrive.loggerd.config import ROOT, get_available_bytes +from selfdrive.loggerd.config import ROOT, get_available_bytes, get_available_percent from selfdrive.loggerd.uploader import listdir_by_creation +MIN_BYTES = 5 * 1024 * 1024 * 1024 +MIN_PERCENT = 10 + def deleter_thread(exit_event): while not exit_event.is_set(): - available_bytes = get_available_bytes() + out_of_bytes = get_available_bytes(default=MIN_BYTES + 1) < MIN_BYTES + out_of_percent = get_available_percent(default=MIN_PERCENT + 1) < MIN_PERCENT - if available_bytes is not None and available_bytes < (5 * 1024 * 1024 * 1024): + if out_of_percent or out_of_bytes: # remove the earliest directory we can dirs = listdir_by_creation(ROOT) for delete_dir in dirs: diff --git a/selfdrive/loggerd/loggerd.cc b/selfdrive/loggerd/loggerd.cc index 7e744fa50..0e5c5457d 100644 --- a/selfdrive/loggerd/loggerd.cc +++ b/selfdrive/loggerd/loggerd.cc @@ -588,7 +588,6 @@ int main(int argc, char** argv) { for (const auto& it : services) { std::string name = it.name; - int qlog_freq = it.decimation ? it.decimation : 0; if (it.should_log) { SubSocket * sock = SubSocket::create(s.ctx, name); @@ -601,8 +600,8 @@ int main(int argc, char** argv) { frame_sock = sock; } - qlog_counter[sock] = (qlog_freq == 0) ? -1 : 0; - qlog_freqs[sock] = qlog_freq; + qlog_counter[sock] = (it.decimation == -1) ? -1 : 0; + qlog_freqs[sock] = it.decimation; } } diff --git a/selfdrive/manager.py b/selfdrive/manager.py index 4e6c52079..0d8efc8d3 100755 --- a/selfdrive/manager.py +++ b/selfdrive/manager.py @@ -14,7 +14,7 @@ from common.android import ANDROID sys.path.append(os.path.join(BASEDIR, "pyextra")) os.environ['BASEDIR'] = BASEDIR -TOTAL_SCONS_NODES = 1170 +TOTAL_SCONS_NODES = 1195 prebuilt = os.path.exists(os.path.join(BASEDIR, 'prebuilt')) # Create folders needed for msgq @@ -142,6 +142,7 @@ managed_processes = { "ubloxd": ("selfdrive/locationd", ["./ubloxd"]), "loggerd": ("selfdrive/loggerd", ["./loggerd"]), "logmessaged": "selfdrive.logmessaged", + "locationd": "selfdrive.locationd.locationd", "tombstoned": "selfdrive.tombstoned", "logcatd": ("selfdrive/logcatd", ["./logcatd"]), "proclogd": ("selfdrive/proclogd", ["./proclogd"]), @@ -204,6 +205,7 @@ car_started_processes = [ 'modeld', 'proclogd', 'ubloxd', + 'locationd', ] if ANDROID: car_started_processes += [ @@ -289,6 +291,15 @@ def prepare_managed_process(p): subprocess.check_call(["make", "clean"], cwd=os.path.join(BASEDIR, proc[0])) subprocess.check_call(["make", "-j4"], cwd=os.path.join(BASEDIR, proc[0])) + +def join_process(process, timeout): + # Process().join(timeout) will hang due to a python 3 bug: https://bugs.python.org/issue28382 + # We have to poll the exitcode instead + t = time.time() + while time.time() - t < timeout and process.exitcode is None: + time.sleep(0.001) + + def kill_managed_process(name): if name not in running or name not in managed_processes: return @@ -302,18 +313,12 @@ def kill_managed_process(name): else: running[name].terminate() - # Process().join(timeout) will hang due to a python 3 bug: https://bugs.python.org/issue28382 - # We have to poll the exitcode instead - # running[name].join(5.0) - - t = time.time() - while time.time() - t < 5 and running[name].exitcode is None: - time.sleep(0.001) + join_process(running[name], 5) if running[name].exitcode is None: if name in unkillable_processes: cloudlog.critical("unkillable process %s failed to exit! rebooting in 15 if it doesn't die" % name) - running[name].join(15.0) + join_process(running[name], 15) if running[name].exitcode is None: cloudlog.critical("FORCE REBOOTING PHONE!") os.system("date >> /sdcard/unkillable_reboot") @@ -502,6 +507,8 @@ def main(): params.put("LastUpdateTime", t.encode('utf8')) if params.get("OpenpilotEnabledToggle") is None: params.put("OpenpilotEnabledToggle", "1") + if params.get("LaneChangeEnabled") is None: + params.put("LaneChangeEnabled", "1") # is this chffrplus? if os.getenv("PASSIVE") is not None: diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 7418267c7..1414556bf 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -bc89e6f25e88a904ad905296d516aaebb77e2207 \ No newline at end of file +917e6889be1691fb96e7566a92e0c6bbefc861a4 \ No newline at end of file diff --git a/selfdrive/test/test_car_models.py b/selfdrive/test/test_car_models.py index b3ff2a9e5..1436f404e 100755 --- a/selfdrive/test/test_car_models.py +++ b/selfdrive/test/test_car_models.py @@ -58,7 +58,6 @@ def get_route_log(route_name): sys.exit(-1) routes = { - "975b26878285314d|2018-12-25--14-42-13": { 'carFingerprint': CHRYSLER.PACIFICA_2018_HYBRID, 'enableCamera': True, @@ -290,6 +289,7 @@ routes = { "7e34a988419b5307|2019-12-18--19-13-30": { 'carFingerprint': TOYOTA.RAV4H_TSS2, 'enableCamera': True, + 'fingerprintSource': 'fixed' }, "e6a24be49a6cd46e|2019-10-29--10-52-42": { 'carFingerprint': TOYOTA.LEXUS_ES_TSS2, @@ -325,6 +325,11 @@ routes = { 'carFingerprint': TOYOTA.LEXUS_RX_TSS2, 'enableCamera': True, }, + "ec429c0f37564e3c|2020-02-01--17-28-12": { + 'carFingerprint': TOYOTA.LEXUS_NXH, + 'enableCamera': True, + 'enableDsu': False, + }, #FIXME: This works sometimes locally, but never in CI. Timing issue? #"b0f5a01cf604185c|2018-01-31--20-11-39": { # 'carFingerprint': TOYOTA.LEXUS_RXH, @@ -368,6 +373,11 @@ routes = { # 'enableDsu': False, # }, # TODO: missingsome combos for highlander + "0a302ffddbb3e3d3|2020-02-08--16-19-08": { + 'carFingerprint': TOYOTA.HIGHLANDER_TSS2, + 'enableCamera': True, + 'enableDsu': False, + }, "aa659debdd1a7b54|2018-08-31--11-12-01": { 'carFingerprint': TOYOTA.HIGHLANDER, 'enableCamera': False, @@ -507,6 +517,11 @@ if __name__ == "__main__": params.put("CommunityFeaturesToggle", "1") params.put("Passive", "1" if route in passive_routes else "0") + if checks.get('fingerprintSource', None) == 'fixed': + os.environ['FINGERPRINT'] = checks['carFingerprint'] + else: + os.environ['FINGERPRINT'] = "" + print("testing ", route, " ", checks['carFingerprint']) print("Starting processes") for p in tested_procs: diff --git a/selfdrive/test/test_fingerprints.py b/selfdrive/test/test_fingerprints.py index af544e1a7..aeb75896d 100755 --- a/selfdrive/test/test_fingerprints.py +++ b/selfdrive/test/test_fingerprints.py @@ -77,6 +77,10 @@ valid = True for idx1, f1 in enumerate(fingerprints_flat): for idx2, f2 in enumerate(fingerprints_flat): if idx1 < idx2 and not check_fingerprint_consistency(f1, f2): + if car_names[idx1] == car_names[idx2]: + print(f"Warning, overlap in {car_names[idx1]}") + continue + valid = False print("Those two fingerprints are inconsistent {0} {1}".format(car_names[idx1], car_names[idx2])) print("") diff --git a/selfdrive/test/test_openpilot.py b/selfdrive/test/test_openpilot.py index cbc43dd12..e5ea32736 100644 --- a/selfdrive/test/test_openpilot.py +++ b/selfdrive/test/test_openpilot.py @@ -105,118 +105,118 @@ def test_uploader(): print("UPLOADER") time.sleep(10.0) -# @phone_only -# def test_athena(): -# print("ATHENA") -# start_daemon_process("manage_athenad") -# params = Params() -# manage_athenad_pid = params.get("AthenadPid") -# assert manage_athenad_pid is not None -# try: -# os.kill(int(manage_athenad_pid), 0) -# # process is running -# except OSError: -# assert False, "manage_athenad is dead" +@phone_only +def test_athena(): + print("ATHENA") + start_daemon_process("manage_athenad") + params = Params() + manage_athenad_pid = params.get("AthenadPid") + assert manage_athenad_pid is not None + try: + os.kill(int(manage_athenad_pid), 0) + # process is running + except OSError: + assert False, "manage_athenad is dead" -# def expect_athena_starts(timeout=30): -# now = time.time() -# athenad_pid = None -# while athenad_pid is None: -# try: -# athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() -# return athenad_pid -# except subprocess.CalledProcessError: -# if time.time() - now > timeout: -# assert False, f"Athena did not start within {timeout} seconds" -# time.sleep(0.5) + def expect_athena_starts(timeout=30): + now = time.time() + athenad_pid = None + while athenad_pid is None: + try: + athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() + return athenad_pid + except subprocess.CalledProcessError: + if time.time() - now > timeout: + assert False, f"Athena did not start within {timeout} seconds" + time.sleep(0.5) -# def athena_post(payload, max_retries=5, wait=5): -# tries = 0 -# while 1: -# try: -# resp = requests.post( -# "https://athena.comma.ai/" + params.get("DongleId", encoding="utf-8"), -# headers={ -# "Authorization": "JWT " + os.getenv("COMMA_JWT"), -# "Content-Type": "application/json" -# }, -# data=json.dumps(payload), -# timeout=30 -# ) -# resp_json = resp.json() -# if resp_json.get('error'): -# raise Exception(resp_json['error']) -# return resp_json -# except Exception as e: -# time.sleep(wait) -# tries += 1 -# if tries == max_retries: -# raise -# else: -# print(f'athena_post failed {e}. retrying...') + def athena_post(payload, max_retries=5, wait=5): + tries = 0 + while 1: + try: + resp = requests.post( + "https://athena.comma.ai/" + params.get("DongleId", encoding="utf-8"), + headers={ + "Authorization": "JWT " + os.getenv("COMMA_JWT"), + "Content-Type": "application/json" + }, + data=json.dumps(payload), + timeout=30 + ) + resp_json = resp.json() + if resp_json.get('error'): + raise Exception(resp_json['error']) + return resp_json + except Exception as e: + time.sleep(wait) + tries += 1 + if tries == max_retries: + raise + else: + print(f'athena_post failed {e}. retrying...') -# def expect_athena_registers(): -# resp = athena_post({ -# "method": "echo", -# "params": ["hello"], -# "id": 0, -# "jsonrpc": "2.0" -# }, max_retries=12, wait=5) -# assert resp.get('result') == "hello", f'Athena failed to register ({resp})' + def expect_athena_registers(): + resp = athena_post({ + "method": "echo", + "params": ["hello"], + "id": 0, + "jsonrpc": "2.0" + }, max_retries=12, wait=5) + assert resp.get('result') == "hello", f'Athena failed to register ({resp})' -# try: -# athenad_pid = expect_athena_starts() -# # kill athenad and ensure it is restarted (check_output will throw if it is not) -# os.kill(int(athenad_pid), signal.SIGINT) -# expect_athena_starts() + try: + athenad_pid = expect_athena_starts() + # kill athenad and ensure it is restarted (check_output will throw if it is not) + os.kill(int(athenad_pid), signal.SIGINT) + expect_athena_starts() -# if not os.getenv('COMMA_JWT'): -# print('WARNING: COMMA_JWT env not set, will not test requests to athena.comma.ai') -# return + if not os.getenv('COMMA_JWT'): + print('WARNING: COMMA_JWT env not set, will not test requests to athena.comma.ai') + return -# expect_athena_registers() + expect_athena_registers() -# print("ATHENA: getSimInfo") -# resp = athena_post({ -# "method": "getSimInfo", -# "id": 0, -# "jsonrpc": "2.0" -# }) -# assert resp.get('result'), resp -# assert 'sim_id' in resp['result'], resp['result'] + print("ATHENA: getSimInfo") + resp = athena_post({ + "method": "getSimInfo", + "id": 0, + "jsonrpc": "2.0" + }) + assert resp.get('result'), resp + assert 'sim_id' in resp['result'], resp['result'] -# print("ATHENA: takeSnapshot") -# resp = athena_post({ -# "method": "takeSnapshot", -# "id": 0, -# "jsonrpc": "2.0" -# }) -# assert resp.get('result'), resp -# assert resp['result']['jpegBack'], resp['result'] + print("ATHENA: takeSnapshot") + resp = athena_post({ + "method": "takeSnapshot", + "id": 0, + "jsonrpc": "2.0" + }) + assert resp.get('result'), resp + assert resp['result']['jpegBack'], resp['result'] -# @with_processes(["thermald"]) -# def test_athena_thermal(): -# print("ATHENA: getMessage(thermal)") -# resp = athena_post({ -# "method": "getMessage", -# "params": {"service": "thermal", "timeout": 5000}, -# "id": 0, -# "jsonrpc": "2.0" -# }) -# assert resp.get('result'), resp -# assert resp['result']['thermal'], resp['result'] -# test_athena_thermal() -# finally: -# try: -# athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() -# except subprocess.CalledProcessError: -# athenad_pid = None + @with_processes(["thermald"]) + def test_athena_thermal(): + print("ATHENA: getMessage(thermal)") + resp = athena_post({ + "method": "getMessage", + "params": {"service": "thermal", "timeout": 5000}, + "id": 0, + "jsonrpc": "2.0" + }) + assert resp.get('result'), resp + assert resp['result']['thermal'], resp['result'] + test_athena_thermal() + finally: + try: + athenad_pid = subprocess.check_output(["pgrep", "-P", manage_athenad_pid], encoding="utf-8").strip() + except subprocess.CalledProcessError: + athenad_pid = None -# try: -# os.kill(int(manage_athenad_pid), signal.SIGINT) -# os.kill(int(athenad_pid), signal.SIGINT) -# except (OSError, TypeError): -# pass + try: + os.kill(int(manage_athenad_pid), signal.SIGINT) + os.kill(int(athenad_pid), signal.SIGINT) + except (OSError, TypeError): + pass # TODO: re-enable when jenkins test has /data/pythonpath -> /data/openpilot # @phone_only diff --git a/selfdrive/thermald.py b/selfdrive/thermald.py index 2fcd9685c..c516acc77 100755 --- a/selfdrive/thermald.py +++ b/selfdrive/thermald.py @@ -130,7 +130,7 @@ def handle_fan_eon(max_cpu_temp, bat_temp, fan_speed, ignition): def handle_fan_uno(max_cpu_temp, bat_temp, fan_speed, ignition): - new_speed = int(interp(max_cpu_temp, [40.0, 80.0], [0, 100])) + new_speed = int(interp(max_cpu_temp, [40.0, 80.0], [0, 80])) if not ignition: new_speed = min(30, new_speed) @@ -192,10 +192,12 @@ def thermald_thread(): if health is not None: usb_power = health.health.usbPowerMode != log.HealthData.UsbPowerMode.client - try: - network_type = get_network_type() - except subprocess.CalledProcessError: - pass + # get_network_type is an expensive call. update every 10s + if (count % int(10. / DT_TRML)) == 0: + try: + network_type = get_network_type() + except subprocess.CalledProcessError: + pass msg.thermal.freeSpace = get_available_percent(default=100.0) / 100.0 msg.thermal.memUsedPercent = int(round(psutil.virtual_memory().percent)) @@ -272,13 +274,16 @@ def thermald_thread(): last_update = now dt = now - last_update - if dt.days > DAYS_NO_CONNECTIVITY_MAX: + update_failed_count = params.get("UpdateFailedCount") + update_failed_count = 0 if update_failed_count is None else int(update_failed_count) + + if dt.days > DAYS_NO_CONNECTIVITY_MAX and update_failed_count > 1: if current_connectivity_alert != "expired": current_connectivity_alert = "expired" params.delete("Offroad_ConnectivityNeededPrompt") params.put("Offroad_ConnectivityNeeded", json.dumps(OFFROAD_ALERTS["Offroad_ConnectivityNeeded"])) elif dt.days > DAYS_NO_CONNECTIVITY_PROMPT: - remaining_time = str(DAYS_NO_CONNECTIVITY_MAX - dt.days) + remaining_time = str(max(DAYS_NO_CONNECTIVITY_MAX - dt.days, 0)) if current_connectivity_alert != "prompt" + remaining_time: current_connectivity_alert = "prompt" + remaining_time alert_connectivity_prompt = copy.copy(OFFROAD_ALERTS["Offroad_ConnectivityNeededPrompt"]) diff --git a/selfdrive/updated.py b/selfdrive/updated.py index efde9adcc..29847fdb8 100755 --- a/selfdrive/updated.py +++ b/selfdrive/updated.py @@ -293,6 +293,7 @@ def attempt_update(): def main(gctx=None): + update_failed_count = 0 overlay_init_done = False wait_helper = WaitTimeHelper() params = Params() @@ -312,6 +313,7 @@ def main(gctx=None): raise RuntimeError("couldn't get overlay lock; is another updated running?") while True: + update_failed_count += 1 time_wrong = datetime.datetime.now().year < 2019 ping_failed = subprocess.call(["ping", "-W", "4", "-c", "1", "8.8.8.8"]) @@ -335,6 +337,7 @@ def main(gctx=None): if params.get("IsOffroad") == b"1": attempt_update() + update_failed_count = 0 else: cloudlog.info("not running updater, openpilot running") @@ -348,8 +351,8 @@ def main(gctx=None): overlay_init_done = False except Exception: cloudlog.exception("uncaught updated exception, shouldn't happen") - overlay_init_done = False + params.put("UpdateFailedCount", str(update_failed_count)) wait_between_updates(wait_helper.ready_event) if wait_helper.shutdown: break From 39f912506e1e4ac0ff3714ac8fbda8d55de7f5f6 Mon Sep 17 00:00:00 2001 From: dragonpilot Date: Tue, 18 Feb 2020 15:26:03 +1000 Subject: [PATCH 10/16] 0.7.3 apks From bc0c5934017f023848e6a4a4c7d38d6b09457bec Mon Sep 17 00:00:00 2001 From: dragonpilot Date: Tue, 18 Feb 2020 17:34:02 +1000 Subject: [PATCH 11/16] dp 0.7.3 --- cereal/log.capnp | 2 +- common/params.py | 2 - selfdrive/controls/lib/pathplanner.py | 18 +- selfdrive/dragonpilot/appd/appd.py | 229 +++++++++---------- selfdrive/dragonpilot/dragonconf/__init__.py | 2 - 5 files changed, 118 insertions(+), 135 deletions(-) diff --git a/cereal/log.capnp b/cereal/log.capnp index 1c545aa09..00e063b65 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -293,7 +293,7 @@ struct ThermalData { memUsedPercent @19 :Int8; cpuPerc @20 :Int8; - ipAddr @23 :Text; # dragonpilot + ipAddr @24 :Text; # dragonpilot enum ThermalStatus { green @0; # all processes run diff --git a/common/params.py b/common/params.py index e651536c9..d1e939a46 100755 --- a/common/params.py +++ b/common/params.py @@ -153,14 +153,12 @@ keys = { "DragonUIBlinker": [TxType.PERSISTENT], "DragonEnableDriverMonitoring": [TxType.PERSISTENT], "DragonCarModel": [TxType.PERSISTENT], - "DragonCarVIN": [TxType.PERSISTENT], #deprecated "DragonEnableSlowOnCurve": [TxType.PERSISTENT], "DragonEnableLeadCarMovingAlert": [TxType.PERSISTENT], "DragonToyotaSnGMod": [TxType.PERSISTENT], "DragonEnableSRLearner": [TxType.PERSISTENT], "DragonWazeMode": [TxType.PERSISTENT], "DragonRunWaze": [TxType.PERSISTENT], - "DragonEnableAssistedLC": [TxType.PERSISTENT], "DragonEnableAutoLC": [TxType.PERSISTENT], "DragonAssistedLCMinMPH": [TxType.PERSISTENT], "DragonAutoLCMinMPH": [TxType.PERSISTENT], diff --git a/selfdrive/controls/lib/pathplanner.py b/selfdrive/controls/lib/pathplanner.py index f64b30ac1..1f2eea032 100644 --- a/selfdrive/controls/lib/pathplanner.py +++ b/selfdrive/controls/lib/pathplanner.py @@ -68,8 +68,8 @@ class PathPlanner(): self.dragon_auto_lc_enabled = False self.dragon_auto_lc_allowed = False self.dragon_auto_lc_timer = None - self.dragon_assisted_lc_min_mph = 37 - self.dragon_auto_lc_min_mph = 60 + self.dragon_assisted_lc_min_mph = 37 * CV.MPH_TO_MS + self.dragon_auto_lc_min_mph = 60 * CV.MPH_TO_MS self.dragon_auto_lc_delay = 2. self.last_ts = 0. @@ -93,16 +93,16 @@ class PathPlanner(): # dragonpilot cur_time = sec_since_boot() if cur_time - self.last_ts > 5.: - self.dragon_assisted_lc_enabled = True if self.params.get("DragonEnableAssistedLC", encoding='utf8') == "1" else False + self.dragon_assisted_lc_enabled = self.lane_change_enabled if self.dragon_assisted_lc_enabled: self.dragon_auto_lc_enabled = True if self.params.get("DragonEnableAutoLC", encoding='utf8') == "1" else False # adjustable assisted lc min speed - self.dragon_assisted_lc_min_mph = int(self.params.get("DragonAssistedLCMinMPH", encoding='utf8')) + self.dragon_assisted_lc_min_mph = int(self.params.get("DragonAssistedLCMinMPH", encoding='utf8')) * CV.MPH_TO_MS if self.dragon_assisted_lc_min_mph < 0: self.dragon_assisted_lc_min_mph = 0 if self.dragon_auto_lc_enabled: # adjustable auto lc min speed - self.dragon_auto_lc_min_mph = int(self.params.get("DragonAutoLCMinMPH", encoding='utf8')) + self.dragon_auto_lc_min_mph = int(self.params.get("DragonAutoLCMinMPH", encoding='utf8')) * CV.MPH_TO_MS if self.dragon_auto_lc_min_mph < 0: self.dragon_auto_lc_min_mph = 0 # when auto lc is smaller than assisted lc, we set assisted lc to the same speed as auto lc @@ -129,7 +129,7 @@ class PathPlanner(): # Lane change logic one_blinker = sm['carState'].leftBlinker != sm['carState'].rightBlinker - below_lane_change_speed = not self.dragon_assisted_lc_enabled or v_ego < self.dragon_assisted_lc_min_mph * CV.MPH_TO_MS + below_lane_change_speed = v_ego < self.dragon_assisted_lc_min_mph if sm['carState'].leftBlinker: self.lane_change_direction = LaneChangeDirection.left @@ -147,7 +147,7 @@ class PathPlanner(): lane_change_prob = self.LP.l_lane_change_prob + self.LP.r_lane_change_prob # dragonpilot auto lc - if not below_lane_change_speed and self.dragon_auto_lc_enabled and v_ego >= self.dragon_auto_lc_min_mph * CV.MPH_TO_MS: + if not below_lane_change_speed and self.dragon_auto_lc_enabled and v_ego >= self.dragon_auto_lc_min_mph: # we allow auto lc when speed reached dragon_auto_lc_min_mph self.dragon_auto_lc_allowed = True @@ -155,7 +155,7 @@ class PathPlanner(): # we only set timer when in preLaneChange state, dragon_auto_lc_delay delay if self.lane_change_state == LaneChangeState.preLaneChange: self.dragon_auto_lc_timer = cur_time + self.dragon_auto_lc_delay - elif cur_time > self.dragon_auto_lc_timer: + elif cur_time >= self.dragon_auto_lc_timer: # if timer is up, we set torque_applied to True to fake user input torque_applied = True else: @@ -169,7 +169,7 @@ class PathPlanner(): # State transitions # off - if self.dragon_assisted_lc_enabled and self.lane_change_state == LaneChangeState.off and one_blinker and not self.prev_one_blinker and not below_lane_change_speed: + if self.lane_change_state == LaneChangeState.off and one_blinker and not self.prev_one_blinker and not below_lane_change_speed: self.lane_change_state = LaneChangeState.preLaneChange # pre diff --git a/selfdrive/dragonpilot/appd/appd.py b/selfdrive/dragonpilot/appd/appd.py index 82bdb0b5f..a6a4e7112 100644 --- a/selfdrive/dragonpilot/appd/appd.py +++ b/selfdrive/dragonpilot/appd/appd.py @@ -75,35 +75,32 @@ class App(): self.last_ts = sec_since_boot() def read_params(self): - cur_time = sec_since_boot() - if cur_time - self.last_ts > 5: - self.last_is_enabled = self.is_enabled - if self.enable_param is None: - self.is_enabled = False - else: - self.is_enabled = True if params.get(self.enable_param, encoding='utf8') == "1" else False + self.last_is_enabled = self.is_enabled + if self.enable_param is None: + self.is_enabled = False + else: + self.is_enabled = True if params.get(self.enable_param, encoding='utf8') == "1" else False - if self.is_enabled: - # a service app should run automatically and not manual controllable. - if self.app_type in [App.TYPE_SERVICE, App.TYPE_GPS_SERVICE]: - self.is_auto_runnable = True + if self.is_enabled: + # a service app should run automatically and not manual controllable. + if self.app_type in [App.TYPE_SERVICE, App.TYPE_GPS_SERVICE]: + self.is_auto_runnable = True + self.manual_ctrl_status = self.MANUAL_IDLE + else: + if self.manual_ctrl_param is None: self.manual_ctrl_status = self.MANUAL_IDLE else: - if self.manual_ctrl_param is None: - self.manual_ctrl_status = self.MANUAL_IDLE - else: - self.manual_ctrl_status = params.get(self.manual_ctrl_param, encoding='utf8') + self.manual_ctrl_status = params.get(self.manual_ctrl_param, encoding='utf8') + if self.manual_ctrl_status == self.MANUAL_IDLE: if self.auto_run_param is None: self.is_auto_runnable = False else: self.is_auto_runnable = True if params.get(self.auto_run_param, encoding='utf8') == "1" else False - else: - self.is_auto_runnable = False - self.manual_ctrl_status = self.MANUAL_IDLE - self.manually_ctrled = False - - self.last_ts = cur_time + else: + self.is_auto_runnable = False + self.manual_ctrl_status = self.MANUAL_IDLE + self.manually_ctrled = False def run(self, force = False): if force or self.is_enabled: @@ -164,6 +161,16 @@ class App(): returncode=e.returncode) def init_apps(apps): + apps.append(App( + "cn.dragonpilot.gpsservice", + "cn.dragonpilot.gpsservice.MainService", + "DragonGreyPandaMode", + None, + None, + App.TYPE_GPS_SERVICE, + [], + [], + )) apps.append(App( # v1.16.2 "com.tomtom.speedcams.android.map", @@ -231,16 +238,6 @@ def init_apps(apps): "SYSTEM_ALERT_WINDOW", ] )) - apps.append(App( - "cn.dragonpilot.gpsservice", - "cn.dragonpilot.gpsservice.MainService", - "DragonGreyPandaMode", - None, - None, - App.TYPE_GPS_SERVICE, - [], - [], - )) apps.append(App( # v4.57.2.0 "com.waze", @@ -272,8 +269,6 @@ def main(): system(f"LD_LIBRARY_PATH= input tap 995 160") system(f"pkill com.android.settings") - init_apps(apps) - last_started = False thermal_sock = messaging.sub_sock('thermal') @@ -283,105 +278,97 @@ def main(): allow_auto_run = True last_thermal_status = None thermal_status = None - - set_location_provider_allowed = False + start_ts = sec_since_boot() + init_done = False while 1: #has_enabled_apps: - has_fullscreen_apps = False - has_gps_apps = False - has_gps_service_apps = False + if not init_done and sec_since_boot() - start_ts >= 10: + init_apps(apps) + init_done = True - for app in apps: - # read params loop - app.read_params() - if app.last_is_enabled and not app.is_enabled and app.is_running: - app.kill(True) + if init_done: + enabled_apps = [] + has_fullscreen_apps = False - if app.is_enabled: - if not has_fullscreen_apps and app.app_type == App.TYPE_FULLSCREEN: - has_fullscreen_apps = True - elif not has_gps_apps and app.app_type == App.TYPE_GPS: - has_gps_apps = True - elif not has_gps_service_apps and app.app_type == App.TYPE_GPS_SERVICE: - has_gps_service_apps = True - - # process manual ctrl apps - if app.manual_ctrl_status != App.MANUAL_IDLE: - if app.manual_ctrl_status == App.MANUAL_ON: - app.run(True) - else: - app.kill(True) - - # set location provider accuracy - if not set_location_provider_allowed and (has_gps_apps or has_gps_service_apps): - system(f"settings put secure location_providers_allowed -gps") - system(f"settings put secure location_providers_allowed -network") - system(f"settings put secure location_providers_allowed +gps,network") - set_location_provider_allowed = True - - msg = messaging.recv_sock(thermal_sock, wait=True) - started = msg.thermal.started - # when car is running - if started: - stop_delay = None - # apps start 5 secs later - if start_delay is None: - start_delay = frame + 5 - - thermal_status = msg.thermal.thermalStatus - if thermal_status <= ThermalStatus.yellow: - allow_auto_run = True - # when temp reduce from red to yellow, we add start up delay as well - # so apps will not start up immediately - if last_thermal_status == ThermalStatus.red: - start_delay = frame + 60 - elif thermal_status >= ThermalStatus.red: - allow_auto_run = False - - last_thermal_status = thermal_status - - # we run service apps and kill all util apps - # only run once - if last_started != started: - for app in apps: - if app.app_type in [App.TYPE_SERVICE, App.TYPE_GPS_SERVICE]: - app.run() - elif app.app_type == App.TYPE_UTIL: - app.kill() - - # only run apps that's not manually ctrled for app in apps: - if not app.manually_ctrled: - if has_fullscreen_apps: - if app.app_type == App.TYPE_FULLSCREEN: + # read params loop + app.read_params() + if app.last_is_enabled and not app.is_enabled and app.is_running: + app.kill(True) + + if app.is_enabled: + if not has_fullscreen_apps and app.app_type == App.TYPE_FULLSCREEN: + has_fullscreen_apps = True + + # process manual ctrl apps + if app.manual_ctrl_status != App.MANUAL_IDLE: + app.run(True) if app.manual_ctrl_status == App.MANUAL_ON else app.kill(True) + + enabled_apps.append(app) + + msg = messaging.recv_sock(thermal_sock, wait=True) + started = msg.thermal.started + # when car is running + if started: + stop_delay = None + # apps start 5 secs later + if start_delay is None: + start_delay = frame + 5 + + thermal_status = msg.thermal.thermalStatus + if thermal_status <= ThermalStatus.yellow: + allow_auto_run = True + # when temp reduce from red to yellow, we add start up delay as well + # so apps will not start up immediately + if last_thermal_status == ThermalStatus.red: + start_delay = frame + 60 + elif thermal_status >= ThermalStatus.red: + allow_auto_run = False + + last_thermal_status = thermal_status + + # we run service apps and kill all util apps + # only run once + if last_started != started: + for app in enabled_apps: + if app.app_type in [App.TYPE_SERVICE, App.TYPE_GPS_SERVICE]: app.run() - elif app.app_type in [App.TYPE_GPS, App.TYPE_UTIL]: + elif app.app_type == App.TYPE_UTIL: app.kill() - else: - if not allow_auto_run: - app.kill() - else: - if frame > start_delay and app.is_auto_runnable and app.app_type == App.TYPE_GPS: + + # only run apps that's not manually ctrled + for app in enabled_apps: + if not app.manually_ctrled: + if has_fullscreen_apps: + if app.app_type == App.TYPE_FULLSCREEN: app.run() - # when car is stopped - else: - start_delay = None - # set delay to 30 seconds - if stop_delay is None: - stop_delay = frame + 30 + elif app.app_type in [App.TYPE_GPS, App.TYPE_UTIL]: + app.kill() + else: + if not allow_auto_run: + app.kill() + else: + if frame >= start_delay and app.is_auto_runnable and app.app_type == App.TYPE_GPS: + app.run() + # when car is stopped + else: + start_delay = None + # set delay to 30 seconds + if stop_delay is None: + stop_delay = frame + 30 - for app in apps: - if app.is_running and not app.manually_ctrled: - if has_fullscreen_apps or frame > stop_delay: - app.kill() + for app in enabled_apps: + if app.is_running and not app.manually_ctrled: + if has_fullscreen_apps or frame >= stop_delay: + app.kill() - if last_started != started: - for app in apps: - app.manually_ctrled = False + if last_started != started: + for app in enabled_apps: + app.manually_ctrled = False - last_started = started - frame += 3 - time.sleep(3) + last_started = started + frame += 3 + time.sleep(3) def system(cmd): try: diff --git a/selfdrive/dragonpilot/dragonconf/__init__.py b/selfdrive/dragonpilot/dragonconf/__init__.py index c41e92aa0..87fb015c3 100644 --- a/selfdrive/dragonpilot/dragonconf/__init__.py +++ b/selfdrive/dragonpilot/dragonconf/__init__.py @@ -58,7 +58,6 @@ default_conf = { 'DragonEnableSRLearner': '1', 'DragonWazeMode': '0', 'DragonRunWaze': '0', - 'DragonEnableAssistedLC': '1', 'DragonEnableAutoLC': '0', 'DragonAssistedLCMinMPH': 45, 'DragonAutoLCMinMPH': 60, @@ -68,7 +67,6 @@ default_conf = { } deprecated_conf = { - 'DragonCarVIN': '', } deprecated_conf_invert = { From 18474d2bc9aa5259dff03f9a36c8cbdadebe6f31 Mon Sep 17 00:00:00 2001 From: dragonpilot Date: Wed, 19 Feb 2020 09:29:08 +1000 Subject: [PATCH 12/16] fix broken toyota --- selfdrive/car/toyota/carstate.py | 438 +++++++++++++++---------------- 1 file changed, 219 insertions(+), 219 deletions(-) diff --git a/selfdrive/car/toyota/carstate.py b/selfdrive/car/toyota/carstate.py index 0ca0e919d..bb98f1972 100644 --- a/selfdrive/car/toyota/carstate.py +++ b/selfdrive/car/toyota/carstate.py @@ -1,219 +1,219 @@ -from common.numpy_fast import mean -from opendbc.can.can_define import CANDefine -from selfdrive.car.interfaces import CarStateBase -from opendbc.can.parser import CANParser -from selfdrive.config import Conversions as CV -from selfdrive.car.toyota.values import CAR, DBC, STEER_THRESHOLD, TSS2_CAR, NO_DSU_CAR -from common.realtime import sec_since_boot -from common.params import Params -params = Params() - - -def get_can_parser(CP): - - signals = [ - # sig_name, sig_address, default - ("STEER_ANGLE", "STEER_ANGLE_SENSOR", 0), - ("GEAR", "GEAR_PACKET", 0), - ("BRAKE_PRESSED", "BRAKE_MODULE", 0), - ("WHEEL_SPEED_FL", "WHEEL_SPEEDS", 0), - ("WHEEL_SPEED_FR", "WHEEL_SPEEDS", 0), - ("WHEEL_SPEED_RL", "WHEEL_SPEEDS", 0), - ("WHEEL_SPEED_RR", "WHEEL_SPEEDS", 0), - ("DOOR_OPEN_FL", "SEATS_DOORS", 1), - ("DOOR_OPEN_FR", "SEATS_DOORS", 1), - ("DOOR_OPEN_RL", "SEATS_DOORS", 1), - ("DOOR_OPEN_RR", "SEATS_DOORS", 1), - ("SEATBELT_DRIVER_UNLATCHED", "SEATS_DOORS", 1), - ("TC_DISABLED", "ESP_CONTROL", 1), - ("STEER_FRACTION", "STEER_ANGLE_SENSOR", 0), - ("STEER_RATE", "STEER_ANGLE_SENSOR", 0), - ("CRUISE_ACTIVE", "PCM_CRUISE", 0), - ("CRUISE_STATE", "PCM_CRUISE", 0), - ("STEER_TORQUE_DRIVER", "STEER_TORQUE_SENSOR", 0), - ("STEER_TORQUE_EPS", "STEER_TORQUE_SENSOR", 0), - ("TURN_SIGNALS", "STEERING_LEVERS", 3), # 3 is no blinkers - ("LKA_STATE", "EPS_STATUS", 0), - ("IPAS_STATE", "EPS_STATUS", 1), - ("BRAKE_LIGHTS_ACC", "ESP_CONTROL", 0), - ] - - checks = [ - ("WHEEL_SPEEDS", 80), - ("STEER_ANGLE_SENSOR", 80), - ("PCM_CRUISE", 33), - ("STEER_TORQUE_SENSOR", 50), - ("EPS_STATUS", 25), - ] - - if CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: - signals.append(("GAS_PEDAL", "GAS_PEDAL_ALT", 0)) - signals.append(("MAIN_ON", "PCM_CRUISE_ALT", 0)) - signals.append(("SET_SPEED", "PCM_CRUISE_ALT", 0)) - signals.append(("AUTO_HIGH_BEAM", "LIGHT_STALK_ISH", 0)) - checks += [ - ("BRAKE_MODULE", 50), - ("GAS_PEDAL_ALT", 50), - ("PCM_CRUISE_ALT", 1), - ] - else: - signals += [ - ("AUTO_HIGH_BEAM", "LIGHT_STALK", 0), - ("GAS_PEDAL", "GAS_PEDAL", 0), - ] - checks += [ - ("BRAKE_MODULE", 40), - ("GAS_PEDAL", 33), - ] - - if CP.carFingerprint == CAR.LEXUS_IS: - signals.append(("MAIN_ON", "DSU_CRUISE", 0)) - signals.append(("SET_SPEED", "DSU_CRUISE", 0)) - checks.append(("DSU_CRUISE", 5)) - else: - signals.append(("MAIN_ON", "PCM_CRUISE_2", 0)) - signals.append(("SET_SPEED", "PCM_CRUISE_2", 0)) - signals.append(("LOW_SPEED_LOCKOUT", "PCM_CRUISE_2", 0)) - checks.append(("PCM_CRUISE_2", 33)) - - if CP.carFingerprint in NO_DSU_CAR or CP.carFingerprint == CAR.LEXUS_ISH: - signals += [("STEER_ANGLE", "STEER_TORQUE_SENSOR", 0)] - - if CP.carFingerprint == CAR.PRIUS: - signals += [("STATE", "AUTOPARK_STATUS", 0)] - - # add gas interceptor reading if we are using it - if CP.enableGasInterceptor: - signals.append(("INTERCEPTOR_GAS", "GAS_SENSOR", 0)) - signals.append(("INTERCEPTOR_GAS2", "GAS_SENSOR", 0)) - checks.append(("GAS_SENSOR", 50)) - - checks = [] - - return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 0) - - -def get_cam_can_parser(CP): - - signals = [("FORCE", "PRE_COLLISION", 0), ("PRECOLLISION_ACTIVE", "PRE_COLLISION", 0)] - - # use steering message to check if panda is connected to frc - checks = [("STEERING_LKA", 42)] - - return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) - - -class CarState(CarStateBase): - def __init__(self, CP): - super().__init__(CP) - can_define = CANDefine(DBC[CP.carFingerprint]['pt']) - self.shifter_values = can_define.dv["GEAR_PACKET"]['GEAR'] - self.angle_offset = 0. - self.init_angle_offset = False - - # dragonpilot - self.dragon_toyota_stock_dsu = False - self.ts_last_check = 0. - - def update(self, cp, cp_cam): - # dragonpilot, don't check for param too often as it's a kernel call - ts = sec_since_boot() - if ts - self.ts_last_check > 5.: - self.dragon_toyota_stock_dsu = True if params.get("DragonToyotaStockDSU", encoding='utf8') == "1" else False - self.ts_last_check = ts - - # update prevs, update must run once per loop - self.prev_left_blinker_on = self.left_blinker_on - self.prev_right_blinker_on = self.right_blinker_on - - self.door_all_closed = not any([cp.vl["SEATS_DOORS"]['DOOR_OPEN_FL'], cp.vl["SEATS_DOORS"]['DOOR_OPEN_FR'], - cp.vl["SEATS_DOORS"]['DOOR_OPEN_RL'], cp.vl["SEATS_DOORS"]['DOOR_OPEN_RR']]) - self.seatbelt = not cp.vl["SEATS_DOORS"]['SEATBELT_DRIVER_UNLATCHED'] - - self.brake_pressed = cp.vl["BRAKE_MODULE"]['BRAKE_PRESSED'] - if self.CP.enableGasInterceptor: - self.pedal_gas = (cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS'] + cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS2']) / 2. - elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: - self.pedal_gas = cp.vl["GAS_PEDAL_ALT"]['GAS_PEDAL'] - else: - self.pedal_gas = cp.vl["GAS_PEDAL"]['GAS_PEDAL'] - self.esp_disabled = cp.vl["ESP_CONTROL"]['TC_DISABLED'] - - self.v_wheel_fl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FL'] * CV.KPH_TO_MS - self.v_wheel_fr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FR'] * CV.KPH_TO_MS - self.v_wheel_rl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RL'] * CV.KPH_TO_MS - self.v_wheel_rr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RR'] * CV.KPH_TO_MS - self.v_ego_raw = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) - self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) - - self.standstill = not self.v_ego_raw > 0.001 - - if self.CP.carFingerprint in TSS2_CAR: - self.angle_steers = cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] - elif self.CP.carFingerprint in NO_DSU_CAR or self.CP.carFingerprint == CAR.LEXUS_ISH: - # cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] is zeroed to where the steering angle is at start. - # need to apply an offset as soon as the steering angle measurements are both received - self.angle_steers = cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] - self.angle_offset - angle_wheel = cp.vl["STEER_ANGLE_SENSOR"]['STEER_ANGLE'] + cp.vl["STEER_ANGLE_SENSOR"]['STEER_FRACTION'] - if abs(angle_wheel) > 1e-3 and abs(self.angle_steers) > 1e-3 and not self.init_angle_offset: - self.init_angle_offset = True - self.angle_offset = self.angle_steers - angle_wheel - else: - self.angle_steers = cp.vl["STEER_ANGLE_SENSOR"]['STEER_ANGLE'] + cp.vl["STEER_ANGLE_SENSOR"]['STEER_FRACTION'] - self.angle_steers_rate = cp.vl["STEER_ANGLE_SENSOR"]['STEER_RATE'] - can_gear = int(cp.vl["GEAR_PACKET"]['GEAR']) - self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) - if self.CP.carFingerprint == CAR.LEXUS_IS: - self.main_on = cp.vl["DSU_CRUISE"]['MAIN_ON'] - elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: - self.main_on = cp.vl["PCM_CRUISE_ALT"]['MAIN_ON'] - else: - self.main_on = cp.vl["PCM_CRUISE_2"]['MAIN_ON'] - self.left_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 1 - self.right_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 2 - - # 2 is standby, 10 is active. TODO: check that everything else is really a faulty state - self.steer_state = cp.vl["EPS_STATUS"]['LKA_STATE'] - self.steer_error = cp.vl["EPS_STATUS"]['LKA_STATE'] not in [1, 5] - self.ipas_active = cp.vl['EPS_STATUS']['IPAS_STATE'] == 3 - self.brake_error = 0 - self.steer_torque_driver = cp.vl["STEER_TORQUE_SENSOR"]['STEER_TORQUE_DRIVER'] - self.steer_torque_motor = cp.vl["STEER_TORQUE_SENSOR"]['STEER_TORQUE_EPS'] - # we could use the override bit from dbc, but it's triggered at too high torque values - self.steer_override = abs(self.steer_torque_driver) > STEER_THRESHOLD - - self.user_brake = 0 - if self.CP.carFingerprint == CAR.LEXUS_IS: - self.v_cruise_pcm = cp.vl["DSU_CRUISE"]['SET_SPEED'] - self.low_speed_lockout = False - elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: - self.v_cruise_pcm = cp.vl["PCM_CRUISE_ALT"]['SET_SPEED'] - self.low_speed_lockout = False - else: - self.v_cruise_pcm = cp.vl["PCM_CRUISE_2"]['SET_SPEED'] - self.low_speed_lockout = cp.vl["PCM_CRUISE_2"]['LOW_SPEED_LOCKOUT'] == 2 - if self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: - # Lexus ISH does not have curise status value (always 0), so we use curise_active value instead - self.pcm_acc_status = cp.vl["PCM_CRUISE"]['CRUISE_ACTIVE'] - else: - self.pcm_acc_status = cp.vl["PCM_CRUISE"]['CRUISE_STATE'] - self.pcm_acc_active = bool(cp.vl["PCM_CRUISE"]['CRUISE_ACTIVE']) - self.brake_lights = bool(cp.vl["ESP_CONTROL"]['BRAKE_LIGHTS_ACC'] or self.brake_pressed) - if self.CP.carFingerprint == CAR.PRIUS: - self.generic_toggle = cp.vl["AUTOPARK_STATUS"]['STATE'] != 0 - elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: - self.generic_toggle = bool(cp.vl["LIGHT_STALK_ISH"]['AUTO_HIGH_BEAM']) - else: - self.generic_toggle = bool(cp.vl["LIGHT_STALK"]['AUTO_HIGH_BEAM']) - - self.stock_aeb = bool(cp_cam.vl["PRE_COLLISION"]["PRECOLLISION_ACTIVE"] and cp_cam.vl["PRE_COLLISION"]["FORCE"] < -1e-5) - - if self.dragon_toyota_stock_dsu and self.generic_toggle and self.main_on: - enable_acc = True - if not self.gear_shifter == GearShifter.drive or not self.seatbelt or not self.door_all_closed: - enable_acc = False - self.pcm_acc_active = enable_acc - if self.standstill: - self.pcm_acc_status = 7 - else: - self.pcm_acc_status = 1 +from common.numpy_fast import mean +from opendbc.can.can_define import CANDefine +from selfdrive.car.interfaces import CarStateBase +from opendbc.can.parser import CANParser +from selfdrive.config import Conversions as CV +from selfdrive.car.toyota.values import CAR, DBC, STEER_THRESHOLD, TSS2_CAR, NO_DSU_CAR +from common.realtime import sec_since_boot +from common.params import Params +params = Params() + + +def get_can_parser(CP): + + signals = [ + # sig_name, sig_address, default + ("STEER_ANGLE", "STEER_ANGLE_SENSOR", 0), + ("GEAR", "GEAR_PACKET", 0), + ("BRAKE_PRESSED", "BRAKE_MODULE", 0), + ("WHEEL_SPEED_FL", "WHEEL_SPEEDS", 0), + ("WHEEL_SPEED_FR", "WHEEL_SPEEDS", 0), + ("WHEEL_SPEED_RL", "WHEEL_SPEEDS", 0), + ("WHEEL_SPEED_RR", "WHEEL_SPEEDS", 0), + ("DOOR_OPEN_FL", "SEATS_DOORS", 1), + ("DOOR_OPEN_FR", "SEATS_DOORS", 1), + ("DOOR_OPEN_RL", "SEATS_DOORS", 1), + ("DOOR_OPEN_RR", "SEATS_DOORS", 1), + ("SEATBELT_DRIVER_UNLATCHED", "SEATS_DOORS", 1), + ("TC_DISABLED", "ESP_CONTROL", 1), + ("STEER_FRACTION", "STEER_ANGLE_SENSOR", 0), + ("STEER_RATE", "STEER_ANGLE_SENSOR", 0), + ("CRUISE_ACTIVE", "PCM_CRUISE", 0), + ("CRUISE_STATE", "PCM_CRUISE", 0), + ("STEER_TORQUE_DRIVER", "STEER_TORQUE_SENSOR", 0), + ("STEER_TORQUE_EPS", "STEER_TORQUE_SENSOR", 0), + ("TURN_SIGNALS", "STEERING_LEVERS", 3), # 3 is no blinkers + ("LKA_STATE", "EPS_STATUS", 0), + ("IPAS_STATE", "EPS_STATUS", 1), + ("BRAKE_LIGHTS_ACC", "ESP_CONTROL", 0), + ] + + checks = [ + ("WHEEL_SPEEDS", 80), + ("STEER_ANGLE_SENSOR", 80), + ("PCM_CRUISE", 33), + ("STEER_TORQUE_SENSOR", 50), + ("EPS_STATUS", 25), + ] + + if CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: + signals.append(("GAS_PEDAL", "GAS_PEDAL_ALT", 0)) + signals.append(("MAIN_ON", "PCM_CRUISE_ALT", 0)) + signals.append(("SET_SPEED", "PCM_CRUISE_ALT", 0)) + signals.append(("AUTO_HIGH_BEAM", "LIGHT_STALK_ISH", 0)) + checks += [ + ("BRAKE_MODULE", 50), + ("GAS_PEDAL_ALT", 50), + ("PCM_CRUISE_ALT", 1), + ] + else: + signals += [ + ("AUTO_HIGH_BEAM", "LIGHT_STALK", 0), + ("GAS_PEDAL", "GAS_PEDAL", 0), + ] + checks += [ + ("BRAKE_MODULE", 40), + ("GAS_PEDAL", 33), + ] + + if CP.carFingerprint == CAR.LEXUS_IS: + signals.append(("MAIN_ON", "DSU_CRUISE", 0)) + signals.append(("SET_SPEED", "DSU_CRUISE", 0)) + checks.append(("DSU_CRUISE", 5)) + else: + signals.append(("MAIN_ON", "PCM_CRUISE_2", 0)) + signals.append(("SET_SPEED", "PCM_CRUISE_2", 0)) + signals.append(("LOW_SPEED_LOCKOUT", "PCM_CRUISE_2", 0)) + checks.append(("PCM_CRUISE_2", 33)) + + if CP.carFingerprint in NO_DSU_CAR or CP.carFingerprint == CAR.LEXUS_ISH: + signals += [("STEER_ANGLE", "STEER_TORQUE_SENSOR", 0)] + + if CP.carFingerprint == CAR.PRIUS: + signals += [("STATE", "AUTOPARK_STATUS", 0)] + + # add gas interceptor reading if we are using it + if CP.enableGasInterceptor: + signals.append(("INTERCEPTOR_GAS", "GAS_SENSOR", 0)) + signals.append(("INTERCEPTOR_GAS2", "GAS_SENSOR", 0)) + checks.append(("GAS_SENSOR", 50)) + + checks = [] + + return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 0) + + +def get_cam_can_parser(CP): + + signals = [("FORCE", "PRE_COLLISION", 0), ("PRECOLLISION_ACTIVE", "PRE_COLLISION", 0)] + + # use steering message to check if panda is connected to frc + checks = [("STEERING_LKA", 42)] + + return CANParser(DBC[CP.carFingerprint]['pt'], signals, checks, 2) + + +class CarState(CarStateBase): + def __init__(self, CP): + super().__init__(CP) + can_define = CANDefine(DBC[CP.carFingerprint]['pt']) + self.shifter_values = can_define.dv["GEAR_PACKET"]['GEAR'] + self.angle_offset = 0. + self.init_angle_offset = False + + # dragonpilot + self.dragon_toyota_stock_dsu = False + self.ts_last_check = 0. + + def update(self, cp, cp_cam): + # dragonpilot, don't check for param too often as it's a kernel call + ts = sec_since_boot() + if ts - self.ts_last_check > 5.: + self.dragon_toyota_stock_dsu = True if params.get("DragonToyotaStockDSU", encoding='utf8') == "1" else False + self.ts_last_check = ts + + # update prevs, update must run once per loop + self.prev_left_blinker_on = self.left_blinker_on + self.prev_right_blinker_on = self.right_blinker_on + + self.door_all_closed = not any([cp.vl["SEATS_DOORS"]['DOOR_OPEN_FL'], cp.vl["SEATS_DOORS"]['DOOR_OPEN_FR'], + cp.vl["SEATS_DOORS"]['DOOR_OPEN_RL'], cp.vl["SEATS_DOORS"]['DOOR_OPEN_RR']]) + self.seatbelt = not cp.vl["SEATS_DOORS"]['SEATBELT_DRIVER_UNLATCHED'] + + self.brake_pressed = cp.vl["BRAKE_MODULE"]['BRAKE_PRESSED'] + if self.CP.enableGasInterceptor: + self.pedal_gas = (cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS'] + cp.vl["GAS_SENSOR"]['INTERCEPTOR_GAS2']) / 2. + elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: + self.pedal_gas = cp.vl["GAS_PEDAL_ALT"]['GAS_PEDAL'] + else: + self.pedal_gas = cp.vl["GAS_PEDAL"]['GAS_PEDAL'] + self.esp_disabled = cp.vl["ESP_CONTROL"]['TC_DISABLED'] + + self.v_wheel_fl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FL'] * CV.KPH_TO_MS + self.v_wheel_fr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_FR'] * CV.KPH_TO_MS + self.v_wheel_rl = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RL'] * CV.KPH_TO_MS + self.v_wheel_rr = cp.vl["WHEEL_SPEEDS"]['WHEEL_SPEED_RR'] * CV.KPH_TO_MS + self.v_ego_raw = mean([self.v_wheel_fl, self.v_wheel_fr, self.v_wheel_rl, self.v_wheel_rr]) + self.v_ego, self.a_ego = self.update_speed_kf(self.v_ego_raw) + + self.standstill = not self.v_ego_raw > 0.001 + + if self.CP.carFingerprint in TSS2_CAR: + self.angle_steers = cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] + elif self.CP.carFingerprint in NO_DSU_CAR or self.CP.carFingerprint == CAR.LEXUS_ISH: + # cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] is zeroed to where the steering angle is at start. + # need to apply an offset as soon as the steering angle measurements are both received + self.angle_steers = cp.vl["STEER_TORQUE_SENSOR"]['STEER_ANGLE'] - self.angle_offset + angle_wheel = cp.vl["STEER_ANGLE_SENSOR"]['STEER_ANGLE'] + cp.vl["STEER_ANGLE_SENSOR"]['STEER_FRACTION'] + if abs(angle_wheel) > 1e-3 and abs(self.angle_steers) > 1e-3 and not self.init_angle_offset: + self.init_angle_offset = True + self.angle_offset = self.angle_steers - angle_wheel + else: + self.angle_steers = cp.vl["STEER_ANGLE_SENSOR"]['STEER_ANGLE'] + cp.vl["STEER_ANGLE_SENSOR"]['STEER_FRACTION'] + self.angle_steers_rate = cp.vl["STEER_ANGLE_SENSOR"]['STEER_RATE'] + can_gear = int(cp.vl["GEAR_PACKET"]['GEAR']) + self.gear_shifter = self.parse_gear_shifter(self.shifter_values.get(can_gear, None)) + if self.CP.carFingerprint == CAR.LEXUS_IS: + self.main_on = cp.vl["DSU_CRUISE"]['MAIN_ON'] + elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: + self.main_on = cp.vl["PCM_CRUISE_ALT"]['MAIN_ON'] + else: + self.main_on = cp.vl["PCM_CRUISE_2"]['MAIN_ON'] + self.left_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 1 + self.right_blinker_on = cp.vl["STEERING_LEVERS"]['TURN_SIGNALS'] == 2 + + # 2 is standby, 10 is active. TODO: check that everything else is really a faulty state + self.steer_state = cp.vl["EPS_STATUS"]['LKA_STATE'] + self.steer_error = cp.vl["EPS_STATUS"]['LKA_STATE'] not in [1, 5] + self.ipas_active = cp.vl['EPS_STATUS']['IPAS_STATE'] == 3 + self.brake_error = 0 + self.steer_torque_driver = cp.vl["STEER_TORQUE_SENSOR"]['STEER_TORQUE_DRIVER'] + self.steer_torque_motor = cp.vl["STEER_TORQUE_SENSOR"]['STEER_TORQUE_EPS'] + # we could use the override bit from dbc, but it's triggered at too high torque values + self.steer_override = abs(self.steer_torque_driver) > STEER_THRESHOLD + + self.user_brake = 0 + if self.CP.carFingerprint == CAR.LEXUS_IS: + self.v_cruise_pcm = cp.vl["DSU_CRUISE"]['SET_SPEED'] + self.low_speed_lockout = False + elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: + self.v_cruise_pcm = cp.vl["PCM_CRUISE_ALT"]['SET_SPEED'] + self.low_speed_lockout = False + else: + self.v_cruise_pcm = cp.vl["PCM_CRUISE_2"]['SET_SPEED'] + self.low_speed_lockout = cp.vl["PCM_CRUISE_2"]['LOW_SPEED_LOCKOUT'] == 2 + if self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: + # Lexus ISH does not have curise status value (always 0), so we use curise_active value instead + self.pcm_acc_status = cp.vl["PCM_CRUISE"]['CRUISE_ACTIVE'] + else: + self.pcm_acc_status = cp.vl["PCM_CRUISE"]['CRUISE_STATE'] + self.pcm_acc_active = bool(cp.vl["PCM_CRUISE"]['CRUISE_ACTIVE']) + self.brake_lights = bool(cp.vl["ESP_CONTROL"]['BRAKE_LIGHTS_ACC'] or self.brake_pressed) + if self.CP.carFingerprint == CAR.PRIUS: + self.generic_toggle = cp.vl["AUTOPARK_STATUS"]['STATE'] != 0 + elif self.CP.carFingerprint in [CAR.LEXUS_ISH, CAR.LEXUS_GSH]: + self.generic_toggle = bool(cp.vl["LIGHT_STALK_ISH"]['AUTO_HIGH_BEAM']) + else: + self.generic_toggle = bool(cp.vl["LIGHT_STALK"]['AUTO_HIGH_BEAM']) + + self.stock_aeb = bool(cp_cam.vl["PRE_COLLISION"]["PRECOLLISION_ACTIVE"] and cp_cam.vl["PRE_COLLISION"]["FORCE"] < -1e-5) + + if self.dragon_toyota_stock_dsu and self.generic_toggle and self.main_on: + enable_acc = True + if not self.seatbelt or not self.door_all_closed: + enable_acc = False + self.pcm_acc_active = enable_acc + if self.standstill: + self.pcm_acc_status = 7 + else: + self.pcm_acc_status = 1 From cdc715fe2098cbbe8c30008510e83b5591d44312 Mon Sep 17 00:00:00 2001 From: Willem Melching Date: Wed, 19 Feb 2020 10:39:33 -0800 Subject: [PATCH 13/16] apk: revise offroad drive rating images From 241051910a21d0fd4d2ec9436fa7dfd33f87acc7 Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Thu, 20 Feb 2020 13:41:28 -0800 Subject: [PATCH 14/16] AF: improve general responsiveness (#1137) * sync control with readout; always assume worst * faster recover --- selfdrive/camerad/cameras/camera_qcom.c | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/selfdrive/camerad/cameras/camera_qcom.c b/selfdrive/camerad/cameras/camera_qcom.c index ddaa51e34..7e9e845a6 100644 --- a/selfdrive/camerad/cameras/camera_qcom.c +++ b/selfdrive/camerad/cameras/camera_qcom.c @@ -1711,7 +1711,7 @@ static void parse_autofocus(CameraState *s, uint8_t *d) { int16_t max_focus = -32767; int avg_focus = 0; // force to max if not able to determine focus for long - const int patience_cnt = 100; + const int patience_cnt = 20; static int nan_cnt = 0; /*printf("FOCUS: "); @@ -1757,7 +1757,7 @@ static void parse_autofocus(CameraState *s, uint8_t *d) { nan_cnt = 0; } } else { - s->focus_err = avg_focus; + s->focus_err = max_focus; nan_cnt = 0; } // printf("fe=%f\n", s->focus_err); @@ -2084,7 +2084,6 @@ static void* ops_thread(void* arg) { if (cmsg.type == CAMERA_MSG_AUTOEXPOSE) { if (cmsg.camera_num == 0) { do_autoexposure(&s->rear, cmsg.grey_frac); - do_autofocus(&s->rear); } else { do_autoexposure(&s->front, cmsg.grey_frac); } @@ -2187,6 +2186,7 @@ void cameras_run(DualCameraState *s) { // fwrite(d, c->ss[buffer].bufs[buf_idx].len, sizeof(uint8_t), df); // fclose(df); parse_autofocus(c, d); + do_autofocus(&s->rear); } c->ss[buffer].qbuf_info[buf_idx].dirty_buf = 1; ioctl(c->isp_fd, VIDIOC_MSM_ISP_ENQUEUE_BUF, &c->ss[buffer].qbuf_info[buf_idx]); From f0213b6948c8a787a4c9582175ab348d81c8db13 Mon Sep 17 00:00:00 2001 From: dragonpilot Date: Fri, 21 Feb 2020 10:03:06 +1000 Subject: [PATCH 15/16] fix EOFError exception --- selfdrive/car/car_helpers.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/selfdrive/car/car_helpers.py b/selfdrive/car/car_helpers.py index 7283bf9b8..61b776955 100644 --- a/selfdrive/car/car_helpers.py +++ b/selfdrive/car/car_helpers.py @@ -73,15 +73,15 @@ def fingerprint(logcan, sendcan, has_relay): dragon_source = car.CarParams.FingerprintSource.can dragon_has_cache = False - if dragon_cache_car == "1": - cached_source = params.get("DragonCachedSource") + try: + if dragon_cache_car == "1": + cached_source = params.get("DragonCachedSource") - dragon_source = car.CarParams.FingerprintSource.can if cached_source == "" else pickle.loads(cached_source) + dragon_source = car.CarParams.FingerprintSource.can if cached_source == b'' else pickle.loads(cached_source) - cached_finger = params.get("DragonCachedFP") - cached_model = params.get("DragonCachedModel") - if cached_finger != "" and cached_model != "": - try: + cached_finger = params.get("DragonCachedFP") + cached_model = params.get("DragonCachedModel") + if cached_finger != "" and cached_model != "": dragon_car_fingerprint = pickle.loads(cached_model) dragon_finger = pickle.loads(cached_finger) @@ -100,8 +100,8 @@ def fingerprint(logcan, sendcan, has_relay): # set relay to false if cache is right has_relay = False dragon_has_cache = True - except EOFError as e: - pass # dragon_has_cache = False + except EOFError as e: + pass # dragon_has_cache = False if has_relay: # Vin query only reliably works thorugh OBDII From a68ca97c0f5884af5c05f0ef9ba8e0667d9fb528 Mon Sep 17 00:00:00 2001 From: andyh2 Date: Wed, 19 Feb 2020 23:25:19 -0500 Subject: [PATCH 16/16] apk: Fix permission to read /sdcard/ --- common/apk.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/apk.py b/common/apk.py index 32bd2f54c..d77b83f14 100644 --- a/common/apk.py +++ b/common/apk.py @@ -33,10 +33,9 @@ def start_frame(): def set_package_permissions(): pm_grant("ai.comma.plus.offroad", "android.permission.ACCESS_FINE_LOCATION") pm_grant("ai.comma.plus.offroad", "android.permission.READ_PHONE_STATE") + pm_grant("ai.comma.plus.offroad", "android.permission.READ_EXTERNAL_STORAGE") appops_set("ai.comma.plus.offroad", "SU", "allow") appops_set("ai.comma.plus.offroad", "WIFI_SCAN", "allow") - appops_set("ai.comma.plus.offroad", "READ_EXTERNAL_STORAGE", "allow") - appops_set("ai.comma.plus.offroad", "WRITE_EXTERNAL_STORAGE", "allow") def appops_set(package, op, mode): system(f"LD_LIBRARY_PATH= appops set {package} {op} {mode}")