diff --git a/satrs-book/src/images/event_man_arch.graphml b/images/events/event_man_arch.graphml similarity index 67% rename from satrs-book/src/images/event_man_arch.graphml rename to images/events/event_man_arch.graphml index 1336793..66ca56e 100644 --- a/satrs-book/src/images/event_man_arch.graphml +++ b/images/events/event_man_arch.graphml @@ -1,6 +1,6 @@ - + @@ -20,7 +20,7 @@ - Example Event Flow + Example Event Flow @@ -31,7 +31,7 @@ - Event Manager + Event Manager @@ -39,10 +39,10 @@ - + - Event + Event Creator 0 @@ -54,7 +54,7 @@ Creator 0 - Event + Event Creator 2 @@ -63,10 +63,10 @@ Creator 2 - + - Event + Event Creator 1 @@ -78,7 +78,7 @@ Creator 1 - Event + Event Creator 3 @@ -87,10 +87,10 @@ Creator 3 - + - PUS Service 5 + PUS Service 5 Event Reporting @@ -100,10 +100,10 @@ Event Reporting - + - PUS Service 19 + PUS Service 19 Event Action @@ -112,10 +112,10 @@ Event Action - + - Telemetry + Telemetry Sink @@ -124,10 +124,10 @@ Sink - + - Subscriptions + Subscriptions 1. Event Creator 0 subscribes for event 0 @@ -144,10 +144,10 @@ Sink - + - event 1 + event 1 (group 1) @@ -161,7 +161,7 @@ Sink - event 0 + event 0 (group 0) @@ -173,7 +173,7 @@ Sink - event 2 + event 2 (group 3) @@ -187,8 +187,8 @@ Sink - event 3 (group 2) -event 4 (group 2) + event 3 (group 2) +event 4 (group 2) @@ -196,10 +196,10 @@ event 4 (group 2) - + - <<all events>> + <<all events>> @@ -207,10 +207,10 @@ event 4 (group 2) - + - <<all events>> + <<all events>> @@ -219,11 +219,11 @@ event 4 (group 2) - + - event 1 + event 1 event 2 @@ -232,10 +232,10 @@ event 2 - + - group 2 + group 2 @@ -243,10 +243,10 @@ event 2 - + - enabled Events + enabled Events as PUS 5 TM diff --git a/images/events/event_man_arch.pdf b/images/events/event_man_arch.pdf new file mode 100644 index 0000000..21610b1 --- /dev/null +++ b/images/events/event_man_arch.pdf @@ -0,0 +1,1821 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj + << + /Title () + /Author () + /Subject () + /Keywords () + /Creator (yExport 1.5) + /Producer (org.freehep.graphicsio.pdf.YPDFGraphics2D 1.5) + /CreationDate (D:20240130232547+01'00') + /ModDate (D:20240130232547+01'00') + /Trapped /False + >> +endobj +2 0 obj + << + /Type /Catalog + /Pages 3 0 R + /ViewerPreferences 4 0 R + /OpenAction [5 0 R /Fit] + >> +endobj +4 0 obj + << + /FitWindow true + /CenterWindow false + >> +endobj +5 0 obj + << + /Parent 3 0 R + /Type /Page + /Contents 6 0 R + >> +endobj +6 0 obj + << + /Length 7 0 R + /Filter [/ASCII85Decode /FlateDecode] + >> +stream +GauF[bDmo2Y9c*1VoT\<[-j9S7gCa+J_L9fNqI+r)$DiS>9h& +%g@[%*Nq#qr]bDps7]n#l`XbN^bD>r]\YQp1Pl$-g\dT;H(3jJ5>-?P4nA@fj"GY)c3g(*=8ET>o\$m, +.Y#;1F/QYM:HUg6#+hHtqupTCh`o,!ZQ5ljY&_eN_.*SVraDa,TKIa>`R@ocpd3lnJU;]HlfW.NGlg,> +XK?j,qUlX'gLr/J.!M=pZbTD=Du#n:q"]aiN1mLpq5i/VI]fifBqEmlYDZcMcgc=!riC`j*GE\4W94D^ +c`Qa.\VYEQB!]mBd171RTu+8%RhMdnbWKgliM#00>06%+oZ`7.<3!^2@m5$E +'XNB(S[5<(Nl>>FpQPp7oi\o?a*sLr>e$(1M84tVdd(cmTCmo=rJs,A+UdfmG^Gq@PDkV=UK5FI87rlc +k=D4J[1@V)+ePhFA32(#oAW8ZB[*0?2!NrF#OfI'54`I(K+AkgcZ20EHgiA;g03!@gB+;R2587-_UVue +%?+;90RMsso6m4B8GcH\Sj%\j7VcG5X]#s1!e$X86u/pfQ^R_,St!5'.`\/tOr.%".Vp&0I1tKoN?>UD +oiWV/i9"Y2^o4no:XtET7_\0R7Q[S&E9Ak2p,YS7>^Ii"Q7Mpl7eq\i,aSd=WI#Z#['KDdLW>u`r<=!X +EK%:N#F8'[2tE`Ief[3ONn2Ihe)&hERCY91#PS_`.U$He=rCu_8DGuMf:LKrrPp/(YoXcEqk0k3qKA4& +*IFT6AAF,l+W>`(UK#TT2%qOVS)]j"W'Id-%1URin3)[lY.i3`KCcT[eeBKD^;o+gQT]CP%(aX(MLGjXcdq]r=@TWbJM!:f[Xq5B[s8lR6F.];or=oKW+LkK-hn9n[kQnnK-Hu=r7!a +%mG45mc>:mH]<@n"A)cNdg,:"ZA`ilhnn&_C+^nQUIo-&hP/]]PL9M$ZVVgQP9]MfFgW"QcXI$-N8PON +f=EiR2%?_R,,.2Nr%-.>id_*$pMbnZRhth@!%QZILJKD\XK'j#,[bWK(XBII^2Mf;"7$(>N@8;/Cu:;- +qe5+JMF;/-r>p-B +#Dqmbhn!X)I04E1,*OD#D_[,\M?rYo.P;;`j]&)[2eh:bmZr?eL_JM2q9,F!HleLLc%[R;"oYj&3UFH? +MJ'FTjJ=F2Iq_#S9379TldL98g$kbVW9IQ(NUFC7I@3 +eUn*>5M:7#oHmqhF:cF-"V7at./okO[`L50658U(LXX3n2\S:=b]M9>%m!O]Gj?RB@I4J;/SIeamB_'Z +BLju76u-5feF>\!>Ch\AcI`u)j(E3Z+OE1*4noDNOIZ0S(u=mI4JNC6Yk8%BqY"U^8'4Q"c/_Yd9%3bcr5.c]OTCJYhJo5th6Z8+Aa#ouJaA61Kp(Sb:($<3Fe?H&FhcZg%F2Ui#!D1\i?R"PA%rjPX7TS?1JErl;gDn +GZr4_M0j;ZLGh[64`**u1Zie$YK^]b]!kK031q#+L[3e%l+(YHjlMJhgkX;r`mkUu$P>C6cuW\YA7K2Y +BC3oNlce1UZIZSN7$g,i6OLIC0E*+b%;JG(Xc0$dTCC'F*R.!,T9)NecKMlcE2KPY9:# +T-Ifsep-%]hM'"/T'aBo<#=uL.Yc$fG$Z8]*7ncr)Wh@LprI&4s%$VV7Ds8eK?^,IH""`W\GnK6XI414Jk_4:ECsD4-:2=pB0clb- +4fkANpDm-EWCE,/"qgUsK,6PH_%1GN`1hLM56C0)g]t\0V2\s&Y'ic!<_1mKV0^8A=Z@M2As]g3LM(WF +9\DU@j\Pco3,_ZDR][PkD48c.DV93Wf1n+3QIfW.^U8P;UD><+bgsCL$FJI!D]kgP/DjpJl8LFDSE]eR +Fe-rBOA>%&3Ba^@1G"MW+$C&cE;XfGb/c>+CQ?kd+e>tFV`7.A1*;SN=bB)qd%nF9Eob'-Xe!,VBX_KR +P2"15\[_.ieCZ_Or1_``am&Ff)m2]g[sR[+DJe`hq;^kDLRa*S&aAX^7i8hn/u)+(%@m8\!dh;lf@7nZ3OJ%L2kn!V"[U< +4D8a/;:=o!$.3.;m,;q8dli%sQZl#i#Kp2Oehtc,-dQl(h!$o2PQ!W;@mom7`d"DR:beG.MJB#HdQ-Nq +r%&+'/Zc!OOIFY_rT52B<2N),]c7l8g!H=nQY2J_P[69']'Eop/mI%^[+D+?%JL^k9[,ttoWfY9Y+V+o +lE5su#-J_!mnCWo>bL@Ue*TW3_F5ic4=q>;O'X^qNd2CPk2_ZV8!O4??G>p(QNMe2(P5u-nGOWrAY=GC +1?bs&?8ap#lX`Gq\R%2*[!MAMiE]_B)dR&h?DG\[7k]KIos@KG<@rbo-1!b[.o-`KZ6=]j!Y'9EKW[(8 +a3i(DT<=OJ[k#'s`bMn'3^@S,:i.04VDqlXd//BO5Y*3dM[.aeuPa +B2?2L3mFFdNDb)N0&1*ios,b:/0u5`H`<@JM8T",:fr%sYWRc%IDNghX&I,7d`ZR?5j%,oc]NV5L>A;I +7[`:Yg5,G^kKfP8V&djR1\^AAgV+Yl3BDX.hW_CBQSKni/4uOqn(4eh!b7p(^GI"S)tMVA#bS_SWn9cD +6X%[W^@K`=P8`;Re>#K`.+_N`8A<*Zd^h>o>5j-8CV2?m*W.gPUkL7IoCCG>rKO3aBZBdlqNs.T8NQu7 +gJH_B6OufTpu@=Qo&Yjj'kkS"M7,Hl*ab[NpF!8s]nm2sjl9&k>`/Cs%Ai4D1ueW4T.(SEl6BX\3$^WX +L0?XP7`1*$fhT`8[FEu"l(2+#^Q7SS^AWnLYAolj]M&,q&PUfcJMEV^e,KYbd,fsuY8`X+TE7\H+=:m6 +;jOEOTIdEp5%rCL69_m(It!rcBW'[Y(N%=YP::,L?HTR +o9Dq]1/6C=;U3F&lC0cF@=?=A%PY9\Ypg..j>Nj._-DtfDj7^,%VDWTD\W-!Gp7P*,<3'Mfbe?Ga*n5] +$s!4^FU":W:i?9JFp?[cTA*^l2G&5fs+u/"]uGg>)pp:1ghcNqd#(Y!OZkcmmIJ@::>qD+MJU]g(J$_M +a6o.\9Ml(fS7D<8"bofjdbQLIS=tE-jFG,qLDbb@cHltq!W0%haiu`\&"eCCjcc0-D0!7Klm89o+[$&o +\MkNpffhuuC9RD\j(CHu5'Wg=@F(%JMQh:5lO5c*W]\*fSB`03)N^_OG*r=-OUAe4=/?D"WqBlZl@'i( +OBl"Be+V2UL%CefPJZ!ShI6K>VuD,GXqg\qs-;%)7jq_mP4E].c3$?2jQlLgl=>:uTg>>#80 +*^hI>(uFHN-1^Ze\bYlg4Bn3gU_0"):,-)LL\ko3>M_PkVG&:T3eX%K.[_#>RdG1)&q/eV-0fh-=2oa3 +FZ[odIaZ/=,JJjDOWS&FiYKeY3JfP75'&bE(QqD9TqpV)Ipmt6>bp6sh*'c/-cR=_P;r_J_nB^fBq/%# +[q&113+Y!hO,Rt^AC-cPo1AA`eoR$Z@eSAcXnEWpUYl\laS5HUotjY5a$b1_kf`]1-1sgse"A!H +R-@i6TJ>MQ:)0]Vdp_ROntBIralD!D^XO-Q-aUX6IR=/PAg_N7"R>li*_(>!QeCdOHPG*(cCh'f1tX4= +=j:!dV`N5d3[13Gn0Tc^5CIHn6621#Z'VX/Billhje6rn+KVWLaIEL"SUfBMVr/1Xba[JFF,r`XmX?n_ +mQBCk8[;gc4i@5'(km(57OSX'`]lS,&$m4H1Q@eQ2a?L]_jW5lQ&Ul^B"dFlao>P(jh=-aPVO=j*LFg# +0UkKLM?B/W.#L5IZdZlVS,6?YV+?%(*FncfQ.pB@2Li:67IQZ#91O;(`RhX>AtmPiSRSk'.oqAuo[m`- +?9@%qO_Lio3a_TTI38(WRc?X0mEYN0k-':7O8nps4rLW/ZO#V0h5IXatlG5B3s1nmKWmBn*sCXJ>e+glQEspH:FPT,?Ver\8kk +m@&"$pBnt4Dr>4PL0.OkhE6/B@osUpp4$`K_:R-P0!TF4r>pC6.L5Xn2 +Iln%09:Y`=;X)ka5)R`I@ZGCaNjmCp\L=5+^O^/*IYMU9(]9mc1u>j#^6J$!MlBT0bY.`7_qoULWe +6?9W]jtZ$#.?jMCNsU-+q*N/>]VFBI5d2_k&)cCdQP5HPFfCp;2q8(&[sUD6&uhie_q +bjVn#22%odqB>D0) +mi+>U@(+i@ .OjrR84[&Dct/S"4iB18c@@UST>m; +8&JnjA2!kYi5EmR?9XmrpZZuOV#UDHr9$*HG8"bP^[A$.ihE3[>,3+h61Ru7%^!@*7K3Ui\)P9Ra:C["S,D*91FR`so%!-o>M*\<^QX_Ho37]-"EjEj7LgJC +p^S/]2Uj[*p9eB6#(QH9$\h8oA:9g=74TA+(E,],CPNFIJMUI;Xb^/3de7mJ-T60>S`#m7^)KVsZ=W]G +gNSII.b'=?4ZDAn^\ILM-6IlHpB]V5D7UC4:SD\/@l3)@s%g;lQZh/G5QjLP!c'4OOE`B64=/2'GL=tu +M6ar%OQ,=XA5.8[[huD.AVQ8>:'o#_[h8CYh8,VAc7co_7^Pq3qVhmpB\g+j423$se2W3-?*l'NEI9^f +,g#-L,YK$<8Af9aX^ek3lSSU,\t_*pVhaZ^pLO93-]E;Soe@-hjlVA?24;d)X%m/uBc+oggYE+VKepa>Aqo&<1"d%(Pe,s7;#LZV8n +$QHtpAcX.)cldp9c*5;(g+%gaXltPDI,r\Ti"iM`!3Ho;42%KBJ +'O1^-@G'rZ)o^`<"/Ibag7&[irYFZg\.T4X$EEJ#e9Umnrgllr/3ZW]T+bndSt8;tTqRArOWiq/KZJ!- +R&d"iBR$)1%7Am$'sJSi:BLJ,'$#'kGT^S1[r&3meS:0Nk4"W\De7)J(g71dd^e15i\Et*d72*YYd]U`QZCc]l_RVA[R(3CD#rCEbYlW"YCPfuG^ +/1u'kk=+4XN9iWE5>@pWeEY"gH(+4b]Oi#m,<.JTf,hr>GrjJ4pZ_CgA?3(bjtBGha'LJJc%qpTFUmRe +b;uKs*mDOrYNG9HklaWMg=s0e,2gbm[,WGIQm6Z_7de!nO=6+'PdG/3=@*c\C3`CNYj*CN.ck$Y\4R2W +$!8VW,os%%0P#dJALXEj5m[si\qWf.Jp_KF6gf#@Sn@XAK^c?(r7kcA.am4!`@dA;WZpjEIV+9.[e8K< +AM3n(ZLrM6B!),YB'VUJe?BZ:SK7g!EJE1foG-ufej`6)?g@8*c]rDReI2jtNT$R@Jb-Bu%\r"!\(Vm2 +bn]Tu"a'W9$to8k5%"jeBAVj+\9a/kl!6m>m*CscAeg:aFgl,pom*aj=iW1tCtcT.\?^082O`2VQ%8ju +FKo"?]6Oh<9"*)fO;D:[f'KUHH8(s8b>IK+ql1<5lp?\OZ(2fRbtHO^?]FFN#hGr!$X%U9Yl +/C[Of#UeEhlWCf4p[i^@UoA6AfLTRkg&"h^KgYS^.+`)bpM@A,f;-*eR6,(6=2VG=d]U#P0E70hB^NkKrU; +D;9]+j-Q%`fiR#FnQi'S$==2ibeDa&c-N=7qqA.#.f"n9FNDAFP6Rg$T@>Xl=_JoVkg"C+DhTk+)n8NV +Z+$fkPkZD^mmZr?aLo^pq?C]::6$i.*`BF$RBO>)9D,kS9]W@*90)RQ*Z\C'/aPCE>O>O0=9THaEa,WeDHh0[EA*3>B-dV`RiaDD`&$"X6*N(<4Hhb1&9S+\:\J63MmO(:WfARp!0f$=q-Nk +a)#KO.s8&UQW@/D@p3kHp+Bnrb7D*^C-&o4ZW_^1("]>+2rcoI.WVUMVV\m!B6?^b=E,qR&>a8Km[:Jr +B,+*\=E0AVMA$IMY0h#./$:HnH"b1%%+hcD=T6LG32ZJ^'uq$GA&U&c)ekZ/ZR +ijbQ!A>WkU=f%[i[DGEZ.uj]p#qjuZT`ftjf#Q@7@nVI?CMMJJVdG4ALS:FPq;OBZ]0 +-U(V0+g^1l>!5n+$aI2VB3P^0(I_S4.'((_3o[0>?Gl^q"\OOMaq?t +q']pq20mLDGu)Oj`S"L?=E-fcE`mDLBYR,f:1:'.7LAH_S4UnO>-#[^b_gIekZ/[=hmaEA'L9.eS3Ped +A(^KWEQ6-]X>mMr>B0RkmoXR(B5:[j[c9^6^#^=]@\4\ZDX[mTH?1"Iobiqmi`T/HKl+3@HDcE^nOc06 +Eko%UO.B&ee%L5O8ecYRr?`^OM#e?GRe)Hr'A/3gK.ao/U=)>3(e(?8-W +K.-=6XG7W;J52d4bp9Y_tGU +V]74te$nD?Z6ZBjqP:m^Xm\Gf.?^1sU@/-k[/4nD*N_J5EkqFckI7lXR%6AV*ipun_:h5HhpL'^iLLsm +7p2HQLiXSnABOZh!C>cb`^`A3\B/[9AP<V*e`GaQ0!2C0'mfR]@7/b$i*l[0lDQ\4`SnZ4P?.i6Cmi-Bj] +[t#\>b:&L.o9Z/eQ0(hNhR7I0$uMR3''S?g[>:*2n)G\13(CTVmMm\i% +aBYf0'\.EnRMj2Piba>r(X'f_[j02kjC1,>0Gb+=)1DXl^9hI>Me*UieE!A*h76Y-V2P3\r/s53^eQO- +^N-TprEP?#^?NQH?Maq-NDYsf"h@a&4XH91^4r@d0OqTTOX_.TX!Rd36gMk5!ll*IU.dYNc"!udqa\8G +'^U0eZ*;Ou(X#W&s3l0JjOZhF(n;GR=AE;N`S15b+li]MR(6TeCLE/*lJj==? +c0@b`GdHRTd`RrJk@7*k'"qA,k73_r0ch#WksNHL\jH`Y-tgJ +d#E*7Y0ut4$ZiJBBD56Jnc[te@""MWkc(u>o?2IA+'DtH8bCTo]37JZ+DO:&n4(\#at[pA)`'T7l[*p4 +Go$-ug8X7\<`Xsmf!D8TO(5?a?J0_ZO8fAj+?XH>f.R]lJ,bT0p1kq$T%Gf/_XCj1k`[!9#G-t+s!RO> +s5Xt%s8:OKjZOSgMUmm7rB%6KEG"$,A3p-EX4Lf)>Q9Pn_B(p8q%FutG4kllOU^j*\uF^cbjg/^(dG#t +21.mB._^K0%K,^cNM(Et`nP6<79B8Z)GfRuYkpuM7d4D?FLS[;;kCf<07&h^k,)o>%Ir-8%FG__Jh"@g +K.k\`'SuP'#`D8_NM"a:7s<,!\'laY1?XC`59!L3BZs!'/C]YsGY_p_kf1/;D>_sUM6;`"4F%mSHM8uK +Bd:&IG5OJFAs8D?hDi-m>/$b>NKS#c-_RF)]@Ua;>Yk;2"M(*c841'*^6TTJ]URMt;+iEO6a$0O:@^Zs +!A:4A&i-,3K#eb@k^E%5/).q2[o&uYb-o0XPtCNAR_=!N-FHl4Ze?p'CQ-28@GWq(r!d(`b/,%0OAg/s +0SFE[p+8JNc2,suQH!\H$uPP@Wo2m@/u4r5=0$#[>5DMe?;/bFQ7+c7Wk!:M$4$GnE9sp7j/\G(XrN\h +h%st?$_ci;/5!!iF#j`Mhn\6c9"JC`:"C'1:0O[Tgu1AM]GR:EFs4/R:p5"$,$in`Xdr7>-'!ks'@.9( +#Bu)t$]T(9P<>=*@<"B[C8t%9m=b_&FcDrd]l]o)1gji3'jg4E5oTPhV#.c)5K +45eZc"'62pI::l+r:[hoH5T(Qgt+Eu,TX0Nqp)SrReu)t+mhjl4dp/*iut#,1&(JU#/oR/[VXh(2@3%Y)d(b:"_=?h`.e:7J-V:@1HNZjg,0+4bVY19#O**#:ng]4rZdZeDmB!=4Pbo967%cF5!1CUhe2iF$C$0'N:4t'i8Q_1e8Zs\h7M[oNMjj/$74Y@gE&ii[78)X#M6D//o8W-Y7HJEO +'nZu?kASW\0!N01;]B53W'sEKRrY/?]\AYQBs_bV3Z9VVL^UA948`"GK'sfZaKqDmZY$+sNoW[dj_$Z. +GVCf":uRsl[eIE*=Fg`o]YqV41EAimBoWs9+0%rX]B0N'hMl0TU]VFBdNtR)]AN*PA2@6;d>D:/U,p`S +*._ho'muM!W4MNS#/o>(n3pl'Rs..Al+?IX'1:]9j`aBqa/NrtJU01a)Lsi.L3Ro.Bj?"09F!)A)7&'t +G[_fl_B!o66$_dh9GtK1#Vd5?4r6YcnD'&9X%YRZ_U)[3.)s'5Tq6mFh-F#:&b"%\S.#/HnkTL'_cs>l +B*8"i^6ciD!SA31=OJt;,7@ITk\]n*@;";]YX;ZuYQC;&]0W8em[1sTiK-N;W"o!M>`Ho'r_0IOI+!]F +rjXkt.nl6SdW5XD`W6Oq>ORMt"A:eX%#0!tQ`@e(GD4FJC9#p"(f/!R14u!r`bAJ/ +]HBg]BO:F-)NPYRg&f.)@R@oW_Nd1m=*kfg*/!MJmWKE<$Oq%J*Sa(X*(qQkU[?YM&q\7LmtUS=^3:On68!E+E;u6K`;gJ=np:AQCC+\D3P+U:'"ajsU[Acu>.f9F^KeG)Xht?Y4<)\!$H/D@X89h1Kc>E.H*\H, +jGp6=r`2*9BnYhGH`^0iG'o*(kk##;bZf/\"hNcqM\4rMl^'9(2R$]^D#TJ +o:J8pfBJUsi_;iVfOQDRWaS)TpVgAADi/'#HZr3>1AHqUSj6DUI+(!opR(n9q +AMtM!YrJaE]cFbhf#G>4!cahR>ghG3k]8RBP=K-1@63N[:67Q.[h(1[EVeBkC:/3!PGIW.KtJpN[ZGAY +ENUqk)R`,Bngr16HUEu:G?s'MAt^aqXnZ*J.68maFb:H*7[.QC]W"QpN;%R>no5jGPWQ6>)&e85j!Z2M +bbN#8)^3LLSlXkWfm;c,`GIc5)TMuk17]&T5O*?9bI+@fkDl$'H&e_ja\\>cf@8FG),k4OSMKKNHf&3Y +nag>PQqWRMArk7J#=%3bFSQ=\UsdQ9C.VSSY2uOW\3>#DR#i`FUUBM_@L]1;9*UM6Rkb=pc>oQMbhHs# +O+BBLXsLuD1^=!Dr_?X9]f7AlEe3;'GdriN-`(@="o=r$@fr\/fU/u8S%&62a1.Hih5H16]DTOkobFT5 +XU1T%bl+fY]OI:U&(Oel@mtW4(0-99U@(71?-i7Qm?&fUkXFL&?G(!V48M#0M:S-@0,ID59mk3%>MqdX +3QKL,pREN:j]gnd#JKcf>SK1?1s9H;d:]Y(8u@)f:o`#b,MKklH*]NUIFpTnm4_HS9YXH1a_Kg[bqedr +\_$@E/)*BO1[:1POj4ooU$8Gg*I>n1X&;;u/I+/)mWWdjc<'I[2m%]RI'J+AV!qb-g$cT1$VB7&-*Ro0/o"IGG:k:3G@GUq8MA]AIuC?>Bdgf>rM9 +MYL5m#AA`makFGp41K>eT53W;:;27LSh9%.*TCZR0!3lNr'Csq=Zc<:.BZm&8uh1*J?jje)n +mnRembD>Fa!NHS1F,cYm)g?nfI#TLX$`2+B=.r1)$2HcI4I**7b?ipn!`m&T:Uir"+$=ceCSom)gi82E +;"hM('D98gB,'%nmTe?bj&#iGKd1K@`[P&Km,T<.DH_d!O^QmVMg)[sj[a0n:TNPdEZX&BlTJuCi]Vo_ +U]!Triig^]cHeO+o>\=qUF.a*S6_;W7/T'j1b?i,7^SCfN(^5[J +N;0IA8]6);]pl<7[R[T5al2s1:-KbY.<\=a@:G+B2go/!EAqT_`L@?QS@KLbnM<-LR?A#@gXGOWY"G=N +`mH8AZ[#&MOro](\%$%k%Q2am&@ks)YTY%<(MsYk6i/(*9Rh)\bY5\*;q>IP]0Bk2QJqR;]$0O5mY"!q +ZLcT"?,d];5:ZJtrROToTmK1tcsG:S\u\ftil8H&O/!H<"nu[g5%e+4`klj=N&(!`@'\$So4Tg4;^`-j +[)n]dRi`O.RP%/jlrM063l^7GnpE4VkG)tf$[j>uNB1*EOlN$j)%E4s(N39K-cAc<6QQTE$?=+]oIaVq +h0!&Oh35mg0e0$&hl8R'Bi;;Rb'O#lG['a+b"h-%ddA"cEs0d5PFm4VRhS&Z$rkDELD5&nEC&9Gj$F>j +$X@uSWatU8kIH:lKCMuQElNq)nNr0nl.ka6_%h#G;oZ:8#7C")?V[H+,9"*ZL5EWa_A;gXPq1n0m,#22 +LN)1q;1A`a3r83RVSg,%d,.1 +Xo+K"Ki3n;(>!kY/ok(SXdQVV7MJB5P24R>jAPo=1>\WbUriP:PaO]ZSrS32e%*5IO+9e^snijkeH +hE(R.ZcgCY^:^<4p+e@kR!X(KAu/k+)KSrG5f7MSFT2hhBq.845;PEaES"bsiD5t7DKLBfCsL#C,B:DM +5LF5-QLH9-,3eVW:$,u?5B/U,OOhp,HK^1EDm:MYl^sG*lV:4-4@d3Gs81`m8FU55 +#@@ZGf"*t`XT_$JhX!@*VI`3"eA>6HZ)WNHJtf[%/#Zfu7ZHE<_`a`-*UZ.s&NTD!LLY_8lb?skO?Y\` +L3b@e&0I$^mM`;+gjM*;RbVhdT]Ri+N@ +CpMA,1X3:)K+5'ja(ucAY2O2nY-=ND@WbBkIpQkR^TF;+\#D +;,CR:k<+$$SGK.+?,(2nJPY0'om3ZT`:b/KChs>30@,.ejfSV$T"V5@8VuNLrs+2W5 +7u[F3nA'IJH=-[[`d#7JN8J6:P?fL.4:uS!ho+ouE""nnftDW;D+)s`p_E%G'Aeb]++PitrCE@>c&\an +2d,%Nm'gUQG+IRAS)_q\o"^XP4V'/X'Y2t^I=tY$cH!-&gMu"g;!ADk.eml2><,bq4oplD]sDk@XcKUB +j)n]Mk*%O>7t^kBTP*-`flK'drW8\,]EnX.If4.N'%^Vin8S_cQg`c3<5 +EZFF-1s.:C*I&42NdT4'N0;P0KYC)T@d@'1.hAYUC1m2_i([NGCY!=Q5"N/EhY>V.rX[NA]C1!Pc9]Bk +?Hq`]*If.>9@N-78)chG'QRE$\dZ`>5tYeqY7Ju'd%O+>DEJ56!['\Il+OL:n_AmlljN+5pGG3M3t2E? +6fn*n?FU(e\9\jklW2ojr&k4.2NF.[B5XI1#^rVBbLYYCBcXc!>"G;)>>$=Q8Vu9FQ(4>pb#46Z47Olr +YB)dLG=gXM@;W6Og)4+AiS$4tge!m8A]Su],[uG2J1.P8R0@W,%seY:MrDnP[Hq1>Htm$=k*1/#Eqd%: +b31k13Mlm?h>#/>ma;r(MAs(,ZU77LAkF3/`3-Wtoqf#>f/gc3YA_!Kk1$F0hJ!/G2d5"E2PWkrh"\81 +e5jYkQ6\iNJCZ@OO_se,qnmIh*^'*:[SE0;[aXM^R3TFPH;=dtDq@@9890B1X)^\.OTHeR/4:uQ\JE=7 +j_VRrPHm4\^92U+Zu0/fH[qKTfF(LCVa+8RCZN4to-<[mXd"6&kf!jN\t&,_gH7St>mpDj9-$aK3:okd +?g#-MRHj_5Xnd[ha48e0<$b=^;7pVnp"\YWp"]KX*!/&>:j/so^.fi*0"$:JBAX^d^$g9p^Y\\:Rub\E +XWDi/34l^a,Q"A6DXFe:ec.#Rr5/-DOFflBT0

AKCYWHhiCFmOhE19kGc8Mn,a +Cd5S?]-`q6bn9gGgJ2S&q\CAZ]@*p#\2B4G:hJY.6_I,kS_RHh8F*YI)BeXnRp%0ONSBb9Xt=8ELq0C( +&QM>ilb$Dj\)p'VinVq]FslNPop2>dSB]ViMrm(I\%!KO7/aPC)f0i0.>ciW%-5ij3:i&]i"t/_nGRt] +H'[2(VFOq5,PV^aGN0;(c+B5_A6"WUhLksKap8s>dY@.,JIT&i'6WXMfq!$l6AES3%tnO_)0#7Z1N"I5copuI:Q[_HrslI:LY`=0;o0i#7TI/FZN +g4:Tt.R)HRqR<+#5FXAWD6==!8&KMV0gD$*70h3?9&%`0ppmIM+DKba0lQq_"]9XGn=-/Zjl,1 +#gj(@%m10U',^j)_1RB2d(k@gqqBl:'=f"=ZiuJtb!M,?T*&TDFj>th9/gd$T2n1f +*EIQkijZ#d^'FNZm0pUS@'?jCb=6Ht0LiQ3H<(U1aB]A:>1i6(`)nt@J>uU[?Wk_^B;(0.7_qYhHj:^h +Hk;?:eg/Bno&Z%C,6omt*;J9R-u8b&9BRYmkXr3&aHK8^c?tQY]];/7nd^J(VsiFjgqd^E&M>MN3Q`"# +Jdu9.$Mg0sA,#-0,$I7,c/LY5K*UpUX*?3.D'JAQV<>]":9`hldJdBs9?H&"'6gnU/^QTbD=:+Mmb@\? +Q+27\>:WL6$.eaMDQb@2@qMQKS"QBd'j'>c2QNHXddXtc/_;Xa6!V`ijVMk&qB\l_WV^pqpZ79Sq7TB[oA;-CS"!]=d@[?RmmZ/[_mhmc"A>&10T\pjFgtN^/d@TFbs2#?eAnM9[&(V5--"o9Mf; +/>#fU-U>Zn_n0$.'_:)GU)s.*8lH@b]nX3_GgB!"LN4(ZnU-Xlp&I2ncYSmloW9b'lA'H!_M>0eQPJ\?lr"O<-"g'V +kD>]c2d\)e!a.WY?%\>spj7_e,54m9X*+[]nU'/4-]+k%XnV^#"Ccmp$^*<8G)+Al>-5qgJ_%-T^EJW@ +Y!KG_hHGrXMOd[ULDEH4=C0]oQ"RHtXk$r/;SSb +eiUk&-0r$=^3./4*UieDc5#uRcUleachj6nEdHj1N4G+8-hRp3p^a*pF2!t-)]0VA4_aHAAOS_m6 +Gk:]-6aD(Q9:hDn%kh8^R)2bP]DH,a?)i6W=-B2_lBg3IjCu*iaE]9j"35+H9+Qi,VkPNeOgqGs +X_p@`)(U'00fd6cZcFAs9>s,CVrA;I-I",F[^>HiEMrW\-;-B.?>[^Fk+ZE&a>G`X,clGTl']n>Q+?>Q +Xm&]iXaL>e+QHUG\F2Z"e&cBN0iL]?WEghYV`E!7j\mHo4&FC4hN"rYq._L-0"cIm&`#<=.=)N8ZH/11 +*OHKsIj_5H8V^4:Q;Psll/AJAP,Ia6k?Gtj'!Ut,E+*JH^m$6I;LPA\a[Xg-)LZH@7>njK?[Jlq(++`n +Zs-m-Sl\O@O9q2DKAnoulR:Fh,79h2T!FE)XQ37#_toBsni\&CoFlfNUU&IWoCkYhTC6.7?DPEGj:jGE +h@jN?NC'Nfm^S?tQi*%8>lH9C&@/Cj69hJ(7f@c3s26=/q82V5$8'42QfP`u?Q;H1QRuESS[csMXR[VVn2]ruO0ibn#Ei0oZq4`TpYj,k?EH_"2\3G80 +F*5a5*YiIjIa0!l,*!m(),n0nlFpIo[?/n]cfa!Gh9O]FEC,k_kWOT;j]WZhIHQ3Cee+NXDFgd7;CR]F']G7N@n0r+(k<%Nn_`bD]h +L;=q3:.^MG/D#qb^ZfA57Dd\U8H?8Gal_!k]1ef:aR)>*GT^[eil4M +iY8J3FaEu2jYp^]FnqT_H#@-#!h8OIdYg($>moM?@cF8%OU'W^*QR6U6-V,SA+0>:$P6L +HYme`Fh=72Ej]\Bk8@-TbLm'YonQF4)>UV'*!X3NAV9H&j2A8S^pM%E\X4trUl,2PZZamcm +fOW?R8NX`r:r??K<:!\eqh+[eY4IiA(X]p@X4s2I+*9Kcoe!9fr>B`l-Z)f\@,HhbS<"PjWDVMo;";F?bn:8T$$j +M3F`6S5+AHZ>%!8:ASi&'AN-m7JhnMb-RuXL>PeeQWLE)4GI^OY;A"EHI;"A_Q!lQI6G&tpR3:RZDeO8 +eT_gcCQ`c0+#:k1'-D^5MmHIan9D&3',b;nkh'5mif_5]2V+Fhcs6]$XrJ;0C>[`2piYIh8ac/`gkrJh +m7S&mP,>GCO&03;UW=MB;u6m4[SPfLPg4\Lid0JbmY*4V_u2Q@ma+O7W0KOJj;7H^1U_\"]t8-9^0RWQ +m/PpJ92G$"?-Kc9kttPO]h,]S_XC4Q"Wf4g:&5*X2lO>hPoW8?/jE[1Ao!lOjuW8:'J +F2Uup8DUs9n0iI#hf9fPYAVZC[SjP!$hl->MR.Ynr;eM*S,=sFaef`GY?dJAaFu-0bmJAjmlGrhFlnMg +(73SiKkc2ERbN@@L_H7]WLJdT8LL3Oo3.u]cpMhYHs,9?kUs-Kq@_c:(J']60Ka5>R5U+tRm#W^.2Od9 +1E)D8TRA;9H,cmEUZfM71:3;>7e2a`kAHV^UCTdi+d',J;N-1=iO&0Xn2U*DG923Gj:hC&2uRb'nX[;L +r/bV?FN-ldnJ1M'5H"-'.Zq?DW7'R:49&%tml)uaj1j]3\J(uT,H]IrinJe`5EDr:=%1,O,3SaAVAFm_ +lu@*p@I.+fY.i7jjQGKKWdh/b]D$H,f'2`"8_M5\[4Jo'24H3I>VC!*jS(m.n%X*/=le)mrl9&"j;iRW +^%_P`s'+R]s79at=8Di2U2!B_q`FNLr\]gkpEs!js'dS)5I'eVXaaK'_925-r]dXMpFgU(NPAl_PJ,"T +ZX!qPO*i_#V0HV/>&^r#e)!om+-D>n(//=Oko3741UGZLX\k3*[J6$4)$tped2O6RJ'p_FbOZcr*on0W +7,9gEiC'B$a?R/J1S9%?_lJ'k\tQf#PC6:77\76NWXQn-f$:fj6X't47K0sYbmaVbpY"mg1Ucl4I/oiGSTS>pH9!5RMPa,4%fN,`d +g<#Sa1AVHfr=Sl)CSP'-[#4"epKsCPmeQj:#Ou9h-<2/b09Gc'Du=?-r]d@-GMhq*IJ*b>jS+QBj!4-n +%?(*1l!f!rq>R$[55G24>PGdXUI:+NJ"qYbk9[_3F]r5#.,WoYSL<=Pn>tf`W,AJ[Im\LPI"0%VT?KsG +-%"4<4_\cZpW%$uPOEF"g<9*d%CO:O\(U+io)1;TdqTlM4PrBeU3qHc>2,Y;Db-,F*IKj=S[#^AmX8R`*4%4=OmhbFB>2-S')DO""LGn+o7b^5Sp-.$ADXR_d#CC\jSOQb +/Qrr!+Q`KQ:AX+MM.:;o>j^0VSJ#VT(n?I^3T,gc)#=0(0Te=,gHt`8_8DIAMn5UEn,K6mABr2Sg6(iB +\8f994&KHUdU6osc4p*RCSQiBo)0",k3tSG>^Q':b4jdBOcI53>kY!!EPHb4^Yqr4A9ZBI>ZIYKs.'M; +X*fE7BX_X]1m8**Erdf>*D9?+!jSKlG=2)H./02#VHZGn; +pm7'V*h6V_``IhJ,hFU]e'^">CG4?W3Z=0$]7^?SDe_#gq]!iFZnCY)5Z<*3ep4346Yk_<)HXW9>C`Qa +3V*-.@l0pOJ_"eBA::B%c#i1S;oZPhSXeC]Ve?sre>_\q>Q+]o6c$THb(N5'=GF7q\pU:[-l:% +"fUdOmr,^+^;$@%4l7?'RO@!BB7tIqlPOdSOSRZ!q>Q-6p$F4uOZ_OY7bAr\hiKDU\MVoE:TLFKI",K2 +chBA'MebLD.J!F]lHJNo^;&VJq>Q+PnU,gR[2tgcE/AslT]A#mKd9rWQ1#gmU3uu"5OpA7##?D6n%\X, +@7*'G?WT`hr5\nC*LC+^hg_mbWUg!($TWmdb]&Jfs2@e&(!B`2ZPrZ=Ml*WIJejWIo)140?0R^l?Uqe- +jnn)K=2)G_2+PBqTcX%/MZjS8hqrC2I".W2ML>Fm'-QlE(Ma^6hqoiN_CK#-?MXSYpKp=(ThIbd^S0IZ +^Af+:]R\)ji]Wb$>8Ye4"J"5^jWM_?2q5.rq]"g%^;&Ve4Q#&C>+Y2tGVIm[ol8U8KV +S,g(T,!KXa8%q4#3Qe&`5Xk3ME8ne^-Ap+elB.RL.&%nLB6Z.SA2`qQoO3-m:o4@`>fgrApme%XkhQAV +mZ`_cc'*#.DlFp]R5Z&ZM$$EaHp1S_a]%!E*7C#9';O'KuA$Sg`kA$:f0W`!r7(rqW9'Fr&KOGMhrdg4#Dl==4$u]fWT@'*u\B@9C +bDV^$.c5_1fdlXl%<%9LAE_OpD2!BWF_n-sk8/@%.>7AZDZJ*pG4Yr6A1a\o8mD,jL*kLp\\Cqt2OV[H/?IT,qe]Q6@pCq_)!*ML9p#@6-shqr%X))DD.:<_;\rNTOh(5ZfuBjpuM$XX)NAbp,#n+YbXmOk]3pJ$e=+8ZKBre:V5:pcE) +AM!EN5uW3d1Oud:Uq?KcFQA$u\3&<>jE11n5o4ifS*P%Sq]"DHZ$Ce6\ks8NGPQr`lfeXIO\KMf-nhjK +.os1SORI*I/-!BBa_)t>R``hJpGD"3-aXZO#f&OP\,shIO821Z9BVYdhatO?YI@[?4ljb"52LA,pP3ZM +o)7NmB-c9BHki0?4l7>fqT/Ci1taq16-Ar:E+_R-hcF^0V-NiqL:aOa5qdXNCNH]>QWCPMFCs#MVi]13 +l-.b;mGEu="Cgm\)9<)I"0Kso)6COOm`feMel@4f06=DEVUkI +52PO/0Ko"00qdh-reC!/mr-UL`H=+B[^*+6@,.`@9:N$Lei^V!R8s?IOpb,34/R[2I#Ih"Li-e]^$Ihk +F#or*\Za/9Ga@Oi,>&gKrnqbXs%MjdW`)Ug3)QZSBTTt%n"^+mghFuh"o;R +mV6Uo$#9&h?BO[eaeT>paZ/DVq``,amgg9`HYji2W-nUQ;d1=PM)%VM7Ci]L;Te_=k"`1FetUcPq>M6" +](]<>'nf0dGP?3$;G$R?4XdBiW-j(kR$nQOfXNiiGtHF^Xcea14KA!X4]0B?mf>Z=$T^a:V&)X!h]rSu +R\!c%pasO$C[QAlYuZ:Ij,Y0&^^Da&i`0-(aS?]TRpXR9$Fa@--Uq%a: +`2%C6+Q"8_'&q\Cg%s?p:]$#S2>jo&\U$3D*Z8bIeSWLG-t;Kc1H3!/+]12c4E&Isj%WEe/`OFWZeM0r +7%BA7re?0/;T>/gcaC*_C`\df4)n'"/&KAMe8o1+Qu"iTd9pG0p\-Cg^AjZE)%!5o8Ll"d_QZ'0/S-XL +%;$P0^/X@Y=nFdW8jHq.,=WA'AB_"\f0=LqMR*>Y4K4m!J&KdpCB2:fLAqn6 +R-Qg8`8op[*];o4%$c.YCG?KnHk9R);%k8Ya^^j-qNc2[gOkeVm5@IlZN[3anm?S.]C6YuUqU.iAphOQ +O<,(q1mAu-`k:/,LhC=qm&T(!J1u@W3.YWqVI`+qMbFjKZ)cX[YK7];/\EQYDhd.X!'2*Oc +W.i5Z1el['N)&d0mjuK-<&*I-Ih#7"FKaXI&)oiiNI64Cio8s[\.RU3)fiVRM:?aMDcaW6Xf,OKDEZp[ +;*Vug.'Ju9.Xt6*UjRrAYp!k[*EsE+>T?\LaqnD&Tp-'dc^jJ-BT2H&A'c+Jo^Rs&'+K'`+_#it:[J'K +_cdZ=LknLM)DTg9@oB[m/!AuFO-Mlo%Sk)tImGXF2t1?_lOa7!3H@@^Wg6>P2@ +Q:N'7f&Fj$-*>^$b(KC*,J&/R1rjuNFT)2/o5Jb_Q&)AUSN":GN7uk"4#$R3`;_F[E<#a=rQDe,\ONu- +e12)j3nq2F%;lcZK4c:D-gpdK+]bF^UdSRMHICU.C`c0J\FKIs4&DYEa\bH+@) +q.5to7V@0QVM`fJCXej!r%^pqW&)3C;Pe,f2sOLXIS6Off9tNKW<.8*[pr8W4#^1`AYha]e[$HMmM^q: +kGr`2^YGjCld+CZja-6ANG*gef6FbS[UV5OCU[cM>fT3eSl?"2<9gR;/II@?K&/HdLngtbOL+A`A(KJ] +r:WF![kkdILq;?/?Y$,'G367EWk4h6fAt3qAQ.,`X%&2_=3kI8Y#/5>l;6TuN#?muh#oQ!,OZCrf2\(t +GH[?KhfTdIno/Xk:-&2@h@ObUloQag??c:`Y?(.B+P(`rGe@5I41BiC]BS6r[kkc6;r)<<7?C-0q6UW;]^BsoQSeUU1j?,I]7oOl_ZeA(cbfpTSqd\qjl$FA[-']6.eZ[7mB$pu[goSjOCmN-2Tc^Yt&bbF.6EWp[=ack/@( +)>ML6hgj#seT\.?BN.PW4DcpY>YJHEc"h*ieaIL^YM\mflHrOH/*Y!I+Pf%iNG'qp;AefF@'s,!N=[U[nE +5,`4Fj>VNjB:/M2I@o3!^5em'-Od3F%\5bQs1guH._AKSYqg5F>KMBQne&:9,u^B7]X>M9EbVTKeWUnU +Q'Ekk;"!dmT$VQQ7rSb"DB"JW=7t[%TM%fHrA%\mHJe:fJ%=sO2]n6iB0GsIEV=L$7eY(HB/JMDs%i!( +[h"3-,ZW#N3kYpU;g0#/g&?c>557J5_OE@>'/AA&_ +`P4$[mT3[SjkO9^r8JSEaI=KCLC6/mPD9OY4V&>C*1gjPqUUjc$bWXnm?P+1d?TIp]KHf"3WFA)DK%r8 +RbX?99j[_;``od`&@P2>R)#i(=4hi"^,OB>XqZ#f2+H-PcDhZ!]%^AQ +UqH,=^2BNt$VZ$cH"grhYWR0^M!*^u +.hf+8"LfKa2g0fDE.K5E2\9c,3BI*)QoiTo*l&OE?9:W"=6Z_^!WFKEL#R'QkMZKkkM''WYc:H3Q[9c+ +W"^sPTZB;!I3UOBCQYf$Y>>78[!G_#NM1ZHf$o=B@bI(?'?C/faIr9\d>raaRA&s'%V +R!:COGibk)f#;A#P':,Y>paeb0Zp6R[A_#t+\-eZW)m7Eq4#6>%Q)sZ%Wp=EaYdflA!c"oEcUi1h77gg +;^]W4E5+E[>ph#OW/QA<7V/+c6JMC?j#<#HZVHOqPne(T<(iCf'Fu'L[PUs3E3[ZdiiP@2A +(*TlnI;jVB[X9=ee%h'Lc"m!2I3\!cQFn]N4K1nPB?!@VpU+R(6HSm9FU9uU&^h/2W42+,?+dT@SHJ2q +I@F(_264TXoR?D3gsDOj%HV`AF7W"ImKn%uMk3_JCWCRWC/FJR2Qs!e52:q+jO&cU;,crR9&"1T3f9[8 +?&l&=>`(kN1p?f_rR&8-Uru`h!a.Y/C_WC-lB[ral*\4e@/>\M_7JC)ID_W*>W]7Z[U[nE2T3Zl3O^@%mV,ShdAYrcq6,o/$bm5*>L2P_1>p:dn7EQX-\G'Z5]=6TPO!EtYjSsYi>?68-u*dHpBsK-)(>&pJXB)0UG)6!=a/VIZgP8qmQ2$Eb/4CKSY&).HD/L8 +L74m@1S<-bY$ZK,D=KKshQ"2)Li6S>HQI==j]bgb7X0GPN2h-:%ka*6-b48bZGb!TJub&qs>\lYfZM7,@H +Gb71TbGr/.NdCItgQ=&EnQXNidsII;lQ03T_f_S&o5K-U9=FeuIMU=jH/oigg+Br)Q*nYSm[A%X3U\[U +b^%I>`dE3/eaI)f1\g-r`a8JJM4]soiHhlPo5K8klR9rBepSt7P[<'S8r%7P]D,qD/@.UEOf(0d.,)Z/ +*fAfpjl(N5CO-\3I7>9\a0VN.[N\hR]BpTp[H>R_SE;YjI[]lh`OjP6-*BaN.09=c`"TSs.@s0DBCtUSY=$>R%g>o;(UgFA[^%<=XU?cFglGd +lM4Qp07BgR3H"6R.aHnR',5YK9O"1:aRDn#3?,)Z0i!"R7biEgLl(#V_MCe=KXF!q:0r/!n@7l4iFd*n +fh^Graes46d[7W/iHh"JAEHm)>B@J^f6-hQf&E4O]@Vh@MN)c)_Zc3'g:a]V;FO,jo+]0`-m_ZBOkHo1 +6rR*0q]P^7'l3KRgE+!!]%\le<$]72P,_'gIG9N#g(d!=b!-&qk7eMeYT6K_aR`RuqS]I:O&RhY(9!aN +=cWSj`b-GQ/<2eVW^9cV.bH&2:JQ"p2HWhNVJep(_r#=WTEb8-gYj'iV&On?i!K3 +r`kejI-^dUf80.$::PA'Ag:(!9Z:X1LZpi\M[M1dIe,4(mdRQN3-r)1"*WF$I[3Pn=2(cV-A0>+8V.ad +KWcf&\?As-2Xm&^^Pb>.c[Slqobp\Ssd +jLX45BAZlVhXi,5kN-:O9cW&;H^r8:jpZ-+F=M#aY7H\l\aO0k\*fgTMo;!\=7!pU@(I^`r&($`Tu3aS +-5jpoDi`B>lFHI5c?WV)HE0/Tdjo3lWq3ra:U*8OO3ac+Xmu/OQ9U;d?(T.%[g2_Mg[2tMmP3p_D/-`$ +qrZX_a;Ef1I/JgWcm?;!-*&-c+Kl06O)#_TH"adnf%aM!&csL"FgG!k.D5IOEGa]"VDOelTbSXcaY@6[(a"p[maVio,_sEJII?lAiSLQgFME +@!ogB4FZ5U38T8>/%b5?*W2Rhd8nm:cQqi(5V92 +'T!K9YG$1UX;`:S +epNIdW:I8W#cm0kM>$fr?B]Z?[plb7LiH;FH)oead8^o!oe@nmR_:BA4E>&_0-OCtHejf8?M$c@GGn#W +=4DCg(GA5Cn0_qb`]MM\)RN0c.5HGPUM`Yj#85#"00#,e/EKJ_"[-Cle&_qcZ +@*:]MUdK9C=c$Fi,,W*P4ofiUQi#HDnLT_M\6eYDmDo`i,rM8)tqJN_9^/hm+*@[YKamrP-l.>\n +qQg%KdVQB=ef8YpO,DA1F5YK+;/p)8+;ARt^;LKD9Ddg\S/?R^gGMBpfS/Ie\pbA%V2q\Y["Q0jGc4JMROMu#-''Y`kKOXmlS:Z0O-UB4Lb4OD$O/kTo`#BI4&a8ft&q8gBY:U&O(<*jOG?>FFIY&"kF1T's]'0\^o)eH6ZIW +iDj611RZt9(DEDk37lALM("DLjFcALeBS>;e:&43hL#aW?A6m[N-dO28Kq!n/ag?-\5&^:?XFXu)mr&P +$l4&%X$"QB*ZNa$[8:iWFR(%=cGK%M$n$)U,>tEW)hkkt!B%<.:kmMjTNS@71"Bg5%%tVjUiojZq3aA" +ei.[4RH)i#c&u]ILEj_(=WI\aU$F$:Zq.'92k1fZU@HUmb]+7O1aj13`p1>j2;,GC>"t#V=A+42NK8LA +';%*/Uf-Fo6_d\[>[R8"#b9#)gIuCl8JmF\UCr]$-J,r=fujhAXg'iLCQ4>*l6&MWGd\f7BX261+^,7s +e1*)3^\M)+eX>s^4=Gb.hCAIo*G7LMg"B\7885qGgDN3ef5[qAMU-g(-;^PgYHd45bDf,PcY_YAU/=@s +`\G\tf#l[u/mI$%P3A(bDQ7NftQ*Bo6dXRe_Ss3#tVZ_F'fHl'm]]7T#k9pJQf`< +\V8@@>V<>i(h@eQW:>fY(g\d]<>(99P'e3QI'tIE9?,20)L2R9Voq"r<37jQJ@H@h3e-rD;[V-.h)T%Z +%dm?2Kee16:LNU+8/e"N6XI:15&o,;@H`Ei[=IL)R5fEQ2A$W**N4r/1kYFt`$j-5fSt`o5t1mq?Y8N] +PrU1*JQkW0CMY76*j*9U*5)XM/t8f%?1Le?t%e%$DRp9`FI*CSR@i+EAf,D:D`3mT;Y!YluCsh.J +L/jnHTf&o8<=c4jaL0%(M,2hU[)N;1Nn1GH>uG?qh'e2[(EOLFI;+\chPqTm3(_V%o,V"Ej+A0" +)TDgLdMfaoH.quQ7R4l!L@IdE["csiLXr]!F@Kr'K9i]8)5/p2V)G(!fTk1J[Dpe^L0A(gl;hYZNMMl8 +269t`$a$]()5_0Yj8F7;%GMa`[)VOh\^VP3Vp`O+7r.J\#*^.o(0Y3Db%PN!]9?`ZehDQ_[DlO#/%#R3 +0Y,c=4%%_u2>6$$N*9^EmNQBT8$4]h;ep1PBj>H+*?quDfFZT920]LblNNW/Xu2`DC9=k#WASiFc!5&N +9$%8A?$)f^>Oq(c5m#]iK)C>PeBT8Rn(=X7,ROdNdrFA@!YmY+[[Djhp2mnC"E*ZXW +=12"Bm)CbO$f!cE2>g?m=eAP9gG,a&Vm*2',%!qp/UckSi3nNO? +g=tgRgM+3ta_#Z*8RL2<&iUn*K*ir[`H3Km/N^jq^^j_ +8_\q6_onjH_j]L"d2k%)NAb?'Y)Y1`hTL$NY81Ik[kIG/;`gKg0?RbjF3>m^Mf`H7japGN8Eil1Y`5\^ +<'&_cK4]1ZQ!>b>_hP>I;@K\5bpfu7i@N.S5'ZV4B/Gn-9R?77FnV/C;<:C.;Pe;>GmNS6X]fBM_,`Qe +X'YfXQF"V7.(WAK@``h'N!C90phS]:Cc"$)CSm(eu +F2n`@qh$s*N##:DoS@eiAPM=(,9&0-&6bDae]cF14!RgKnQ>HA4Na>MA+S;]DGR(sj;ns&40+WGJr+qQ +V&aq7V-;!G>rGcd?=TsJ*b@"#H(W)W)W%8LWrK,EmBK'.HEj<548U;1eGm6mA_07C`VXIe@>%$TqAK0r +LG)9d.j86p6&?49?hAAul/gJc^PZ@c3SI5$>pJ96\@gVN):,.G>6f@am,_:4IsSYJE7N@ZXY\N8F=ju? +D7cBYe?]>qD>[M0qd#mpQ1f5;Gp9oVVO_XO\ZIjfH+[B,Hb%/gE\BJSi?ESC(Y=`!j5m_jq4O'Y#CQu, +\H`YK(=7c$`:4Y(P)p.$6l2tk,Ji\d$lhCC+9!'^EjW:9SaF=]/_PIt^"`5X2Q8$13WWruRX22,FG=+> +PBBSQ\&H(uRFe=S@S%-d`Z4K0j:CmkiJ6cp2Wq@$*NIO^XQ[a*&"dEb"$fh[![$TM[pV#hQ11Y)P\&BC>^"g309'PFY`Dsd9XZ.#6]j/go +>GjV88AQ0jDGqN"^I`O2LXjIOh?PqRV/ME]>6jQFm+&2nB5qD!9.-A)52%?`?e?k[4PIF!i0j]mSk#=H?uaiX +Trq]!M4*bM0iD1QSU2LhL905)/F_N\O@Lp3K%\OHl$Yd#^Q=((A>^!3F[[CXEp!K*"rohlj7>hbI +4\.*hgkBp3q%g0koB^R0lM)35;@h`L,G;7`odmKon!e4M3GH-kjr@PG=,sATa%?&>re*SfD=*:p"d +Hk$=`gR@/pV!(@1/F1`5]E>)_:=*FSF_47RY.GadZmtIs/is%Prs("L8:RWUH[LIlDXA(f(M1?_f$Pa5 +.'?qOjd'X&/`+7[*Q?"Sm("(J.ru3`@QCk_htGibd[>g@12e=2OWT`T1[!;bYM^1d#f+Y0,P$W@D^P5N +**U$-B%\JiXY!'$->->aku]MBF^*FE$9K(L0mU;3.JMW&^5S#ADlHdTWb[Ok1:lE_c[F +?bc+&XrJ!9;;D&=*:4sMGF6P:F(Hk=I.Uoia3G9f`dG;=)\h&hcJXEkCR[&d/PE?cmu\YOh6s(=a56iX +LWi)V%E?a3#cde/g`bEImO+d,Ipqf(F<&3n)e0NUr)joPn\B\Do7,`AHGm."'D?@'rdOn4s#4sjOk`0' +iUdZMM05>0d25Gp..qAh3j%s2mFq&rD(C& +W-c?"_VN*>Of88OB5t.[p`'!!q@5UbTJPIGglAQJ]W(Ka)d8uR+.U!qH*FaK6&-(?29QJ9`+M#WO?ZlZ +dlG/f/$"3>+:#(R4pZ(YG!oYS_MkmsB+Hr5208ch:R&;7hO@)(I?h>hmSW+ngttEt<21<:FEsZ5pjD>A +P*r21ZDC"+_;2_rpBU_[UJTJ&oLaDj_FRj!(3dW^#;^lfZcY#A7NZrnm-mJN4M03th?dte/$HUi?P?T* +Sb&A>K8t'lq#D#dq`$62WgV.nsXdOI8 +JWtX%\5]n[Y(o?n3:#7>(rkIKL&=qAQR70]Ue:p$EUS/10F[#2q/SC_BZrrD*$le7g5OUNpnl'D6>$4U +lAQs`L?#t4<$RH"NqMUe,?fiAH7Xi&UoJ@kA+T3j!]#BS2i*L'5#e$6o]"LG-iKN[dDti!n/G.&=#*[V +o@(9%T2#TCl-bhu1Dg_ap#.m:5Y?-i?l0kDk@tEd>pgPR.*Sc8bZ1@N3V_gpJP$$j8f?[TF58N[*$p"< +`Q9329&i[l4EbFGj@m].XrP\%>;NV$Z"f\(12uGHk[oCX2'h>+ar-G_IGHID_DQTo@(adHNcUU6U'2I5"0]]2#N^4]sHF&udTeN]h."ojiP"T;D59&`@dABj]('VNgPmn"m_N +2_u35an2(Y=[AJDm*8`UXaBX5g4*Ku)a?#3BG9PIAs"kZqfXkG/[g\>=;X1Q)M=b+g\ +`3NS)YU18ArYL`;;8UUGpk*T05+Bu;SacXUjGL`YZZm!l"cdF"B=(_52_iK^G*c_[6AN>Y*4+-UR:WSr +DXie-L.og"M_6dj^[65]`'JnCm;GZ^#%sS+kKp[c[iSuGCYos^rZe$7Kh'^n$tC+SaDB+c:X$53FP>$b +T@=@lOldOcJLt\NEKA7'h8mdqP_,*E\Xi;@0e\m^iJEB0IDpaOrI^F&`fhdm/?.$e(QY=M:[@Nq#>tuU +fJC:AS7C63Pr\jm?jN!J-4s>M687tFS[GDkRbp6;:[WE[9V_6AQP +eDi9rP<$-5$d,BBcTVo[FggV0+Z!P"[ERd>M*Bq(iKOXq&9FQ.2I[.?hA`R[^Spl#=n+!2pViFukD%Xe +2su[U*0@d&?T9R"[aHrQ5f_@A?KGCWIV!sb1U_XQ?8-JHFcNo3pW@lXA:?*uRsfg2K%qFCgRNP&Rc3V- +/<0#O,O(o=*2-)Kign6>lC54-G_S6r)-ktS(lEj%H)Un[:EpVMHr)r)COukJ+)JflT6`2`-J?g-;H6k&oOj0McTZSgR)4(3tbk +ri@nAmE[!RF4d*JPc3QW*]fPT?V-TpD_&.-kg(="p[r0];:b-\Jni:(XRjX%cQ53s[4K[2m*?`;Zi+%N +7_epRHBIR?SfWo/O7aX[.EN_.W+mVKd%:cYaI`XdZMiI/7#B2_em5IDMep[UJ+fO=mEZiLGSK29M5Moh +cTWl@gu\L>DRbkn"k]FNDO+<.am25nk%8^*r[_Ysg9*W<-@;iPpctQIIp8,`,b;BGF`NFM?_uS$[[>#q +g]ssW*^1'T@M^?Zg$qTFf;d+?*U1]MnhC#FB/eS;rM0fW:78^01<2@7cV8C+X<2&qF_AAJC&dG!QTQk# +Q,=sf-g1!PcUl1m*,7&jMS=)DA=gK1O*WPfpFmTu>)LgL%1jThP\Yg",Zof[TJC>O+!c<1m?U=TgA&`c +bcsbrpHN-sk=NitXg2SQ)>l?D/q49R>/@3d]J(3C:pBj))5;Y)p%11Pb_= +o>h?g6j_tC]m)6[:c<3"p0a).`]P5dk=C%fNad6<#7^J26bTMlS'ISbmW50$>)_3[;\?/jAB22ZUg2$Z +OYKJ!I/Oqk-HRe.cP,oj4>213GQ;VUE(De:$85H93-iQ+425a,4=-'DBu[B=CYZEt#I?E0ji[):1C'3X +SqEp"]osrj,!+_^Ea,4SnP@GX;&j;<95)Kk"*Qp&,k=U]-Ec4VXCq"5,d4&\N7,lAX?8`0.!0"@"G`Y- +lR2]d=")aS9?o^84sR!T,kV"T-J'Y)]5/m!ac*JS/XIZ2N#j=PbD%uk+SVsVjXoU':?jS%f9L'2).+D% +7A_7b6&$rGPA<2DD=,o8ph+Z;hV76Ol)U)s/bOo[$+I[N@H,[RLY!b^h#>`Q41qGR,1<:F7Hr(UjXm,( +B?-%,hqMR5n6*_fOCcHRFZ=@.DfCM%dr@@f,ON_.&KMiq1mL*SoJQkaiJG9Ws,$SijimLldqt5P=k`hS +lt5p(44^;.b2Uuca@RH;[VdYAmS#(.A.Bmh5PUR#;UX2;D2\qbK.:a)b$rf,CC@gYJYXaf?=kngX3m*4k0UFHX>?Y.I(4W?O_me7k]9>6b9g;9ZW;:HX[X+=Io#)0R$1h`H:P!&k=\B.)erNZb@8"=2#jRD(6oiXc*2n(GBDW +ai@9XfMZfS!@_VVqn2LL(SrYRpUXhVo..OI>G^nLBm4G\mC$;\%9+uVE9*u91&#KJXj+MER#.]2&\Y[- +>/XLC,sc,aZMI3s,oeYZ7D(:X +ilgli0C3q)@)SOcG&V#(L5"[4#FgJfZghQn1,gjbRLa"UUtBg"m\Ji_LKBpffH$f(M%RMBm&>I>8=>I6-&fCA!U7unD;ZnG(QQ78jHDdKR"^SFrP<$TAOnR)*J'<(TENbZNpQ\Cj#ZEfK; +CY9RHeRKWa[YefR`*t>5Da%9#d`q^09RVD,S]]/1a.&4[fWAEiC14G%(4M0d1U9Cn_nhb'N&Lu>]\1s- +k27W+Rm6_2H&BL?-$!s*CYn79=qSIgbNFB8Rfg!#V[j'%r;eOgp`RP=O;4RiAlkj3j(\7J\sEXN>(0N( +7Ff]k-ar$,N%+P8N["8!3#ir%;YIEmlP!W"Su>i5a$3_SikCa,?N!kq@.bq0qAX[`[I-+S]f5./5,b7u +"U-Q-R?bB(h84X.agI:Z440D'D8.TVY2mWlmlY#$CVJtf+>QkKO2m\j^7\]2F#bS0GD3ki]nPk&R'k'X +SUCCmggB3Fo29o3_S&IhJ4P.\p9Tdr(:88)Dbn*=]juO[cDfLaVOI7Dg_:p(C`(b(I=@GlM\qjPH%k;(*-Z*T53H; +;&mh?p5l:9*jgI)2i+I?CEuD:d$hfj4+&/(YB`#N(4c>FL]noZ>!&;+Bq9Oj/9SJ6+p4+[REfDQIXIdE +iDOHhk3igQ?F>@S5q%`1o_+J5?_9?#40]\l!a;p;%obf9gg+qiKQ%+nkA`nLnI^qX_7o_`! +UIGQ.JLbp)8+oen7kVg;DuWbdkZsQukc3%B0NXWd(Y,TPe`^;-.j;f'hkds- +iXbYIU9fYp*WiA7n`mnNQbP?J-@]N,NBVTKY!6Cmr7bj5.a[+a$_1rnhq!:,UsMC` +h&1J#kLeMOQWo73&,n_LnQ'K/T#u#U>oP@bTdnG,Fa\9')&:LOa19\U%lt6%WmM&)SSo[LAH)2@3'O;2 +$nLTr?s[rPFci_,EFF(8W6UpNUVVo[$qHk%e`k\M!kE(*dKB*Mc:G)XV'(2e6!,^G"1YI/a[+g)\ANKEDpMJ0V +oS5$+K620"Xa2n%OR+R0nobZO87.`K<+g;rZke\aX>6?mGQ#kDmTIVifoVNbm6rO^QWshIIDnE0f@VAu/]\o.fS[6&/5g6-6O76nE=\nak*HBIKB#"JR;K3\/B4d(13>C>SU:)P:k'gG&$ +:((PZ+G8Xt7D&"du//)A9Mj'k6B6>N*udT:5V +\C9E.W3%4pb#eiA9`L)EmL73*N)X +^'qE(@cX4?H?NRkoJ]p.*3IVPX0IarHD4rHf'(=OOk)4lf!m^(9fo^OB*NKlNF&@51Ihq)LO,XQei:"e +kZkf=AW0ktUQ=o/ZK?D5cl1[^[FXQ*FoAF"\4(_O7N^1iLJRedh)+VaDEtMYkYW5oBT:l?gB=8Nef)5) +';2beilBi--W*%ij_Bc]>^q`M70W*FbWjk^(_8Mio=o31*Ste/hgsa]m +40q(R_\?*dnXTZ>F4&1+20s7BPG:!]%c&tO,/,rS$]u3,H*J!$W?Z)!Z1EL=Oj_X8QpuZ[]L_R<75Rg9W&4bdb;*Z7+qUG+(XAu'`V +JPf5/6"f"\<79s9oqV2]hSD6^Y,;Fa9]L'L.8RI*C7>s/Na<[Q:-+qcn=hLSV#Askdp2lk_5R&l.Fgn1 +-"XGA^'Oqp9Orqt1n/1inQtY3b/YT4a.]*U_61#[rc_RH-PAD5jsK4jqHij%p5%Gi-Q_B]dSR;[O8@AP +3mSSa+1:[`&aKT(>uXT-<9$UJH/V&`6mp+s3To\o-cj:OcZW2ggg95,YGu4kD-CVY==ID]GMQXQNMo7# +HU9,4cC,uu-cVS0+3b]m1caGpLGKauo[Q:s5;0Q,r(og()W4*l.Y +j@E.cQE/^52)S69hT4cao3,Wk2&oUll]WqKMS:HR+Lu7ZefADA#;,5r,8o[H4,acBRKt>dPI_9!P%%=P +]5$uSY04jEWF"rN5:'gDX1!;5Z)U88P_CG\l&U3Qc]X;*&`-3]s-Nof.ZO1T)QB7_-f#;Qer+9&YV,QK +71E=X4ub^-bgl8K'e,"6-JokqYGS63?&4ZG$U1&i8f;Ujb8TjP]5$uSP,e?I(iRN)e`_X-B;"IEA8G8* +5%qELB(*BBc)g9b7+LMG$XQhsq&i[g_CTMDRH($*[r2.b^ +&Dq39SIE2>?AWtf/+71L($"oC16'&"([\2V!MJidl1!PL_m#I7pKB/f'o<,k&_cFe,?OIlK3Dq4'bWcomg2>U_>FRQK/=R`Dmo$@bc/B +=+WWKZZt4*lRY9!q:QhZs7dt/,2R)em&qW33-p7hd*Z3#NBVU<\+T]4ZZiDCo!H'Eg7B=A#H>Ih0cL!h +,E3%CU-nmPP'SA\^@[+YfZZHue`a]TpMu9GYdS<[ia")&EIn$jbPM4j15i&0=>frJpYNrmM?Jh*YMkB' +-h`%-8<\/[HFCs3aCq]]"mV"ZNu/[2I(4LYM%f[H2-'._="-o\H`.jL[B[KRTkT^nl$KGUmb(L/=k'_Z +*SAB^.4R(-FHGh]s*SCUf\JoE2f2I%Alk?]14*Ya,$Wli\F*8rt!(6J1L14Q*ms.SXS1c/JPo,+uY2E#cU2X-dBT;?gl +S\VMXC+5X'u!&m],aK".V;2SuV*n6Y'm>:HUWi56/h"3:]]\^4S +K_pI-pPJ):q7_&!Tka59nq,?':ZH[:hP<-2s6CUR$@:YmQo^>tnQY9W:U.3LNf'OkQ:)0W07>3_'ZuN. +I.L8]IpDe8M%VmWoQ#rUjmMCQTT<[!)o#o:GIP*4J?<*^4+ML"]\M=Klh5S6RoNtZjmKrj"^@+9fGZe$ +Ins*je/FjanRI8*gDO\d#s*CC3LsUZf?4=-0D>_M&FN1tDBO58J`5]Sk_Db@igocZf8nK)Z1oGf\[u&i +d`^IH]M1&'a.:&e?T:!6aTcIRMf13$TL=6R3hks"q"M&]B28?/oQ']u%1$Vf;)LLD]%G3^h>[ED?Zn@SNgIA'qBil1h)tC" +T$='M--mo@]9$:3F-0es>q&k`n%.IMdZ'[gE]n_p\U2BQ]cCVWrH?_t-!t>,/o16eRoKg5bLO8;Fi9&-nfdgmllXC,@3G,XPu[3HjEnccgm'`RU&u +Fq3O7(NA1+LGmKp;FJ3]s7S[9=T=%KU/8I(!h9B07\KF0\e#ZML<(r@Ds:S&jW"`Hun*9$_Nh0(i9` +C7Rn%:@d&EPG`'(C0M30eA6qD0_aIR/4B)8m4Wn$b6R>,gWGqaYiL>!*D,*2J$Z(::q]"Q.^uc_7@qp]JBqtR +!pBQkVKPgGXPgoF^TH]s+SW^>lR5CeJO9L(=B4pJ>-,MsJpUgD(<,T[*il-8a?TO +^?$1NqV/;d,pi3FUu4%0,!T5U26,7+$1,gpaL>CRJ\BZ9FIr=flhB@&QS"0)C9I<*m."Y+08%nEd[cLd]u.,HlFe$LJAZ9;RO$j4JtbjFnhFLF$? +'D9&I1..ZTW^;Ol@rL2Oj)_-]c&(V8P]^,N)FRroBopX/`$n"5l)9J%%e64,X?Ah`s,GG!>IFNRD0aN`4hVmYph:I^lBH`-0G2Wb@G^4S3Ne(\6L#s9-g(4)e">UVZ"3)RT[ +2OtJ_b`&Y!U%\flHn3*VX)9+;9SF/OaggNN$;Cc=Q.ram@'fhHo$VWG59U^-Xk\Z$;P!rkbV-VogGVE5 +;5mqZ2-?dHSMA.<*1Msk<:8Do7$#ot7=uRXSVio^/rQa?aMAOdR&;/c,0MV.ITeQ!Y6$GUH1BS\BlU`J +`%Kbe*Oh>/LSo1I_4CL(6l5Al"^U!-D9gdDjgNDJDq0!a5-_5i:S:@lXj^o):DE(!]p7=^L[#t]^"jY.^s7ABW,t+l/C,@quNTlprjUogb3Y_Z[>(lAc&F]L2?#= +1SMmG,UL`R5*Ns\EneF>EZ9$LF^g8SXdQ*VV_al`\uTdVA#5.Sosnt-X3DXu=)J,;7-`jW``$j7bk]cX +kA8C&@=(h9gg_Do>IeV"H%0!r<86%hf=u3PYUV4L(UlPmgGW".i]QgE3)c*s^kk19XXqr1EZ=RCo9B^6 +Ps73KbBs&Bb&*bmJ^!Y`AY!X--D'L%.IX9?)B`7!hfGUJkH"p_hJ:nE^mq=Mn+frDT;C;BC6=/E;8[$J +),[<9dE-Rm`:;?bF5kj0l.dQ7nOX+jWg;7m2J5;D?*R$7!4Wb%;%U(aA9Sjr6%2oor94YB`hE@)DN_`5PgBfQ!]-flcr +IR'0,2Tu-"p<2UWeu0"`Gsji"Rk(#$q5-E;;tPDnhs,7Q"'Ar"Q!lQc85t>s/:;WKc;=`;99irT?`k(( +1o\&ZG-4^$//DjIpr863>bbcHZ+:9H]2Z[c:!A;@_]X_hqH"P<3\rqpCuPcp`lsc[D4I"1j*Cb989GZ4 +n!)fi&jC[2`q`FP3f+ +&#sKMEDUaA9]E<>AU- +cg#j>o\SlfVXRStI(Dhe2`miZWo!_:"(VkqOFNYFEG`ie)ho(jOZC69l4df80P0XEbZ3(%b-%"`=O,K' +,a,#P'872XncMi_9@;7QagARhYue,t!6hga83?RfQ0F@EZBD8J,j +?,_9cfb[f1eO#:4>1HUdWDu?_cKsWB>U=JG%qGl*cZ[p#7r.kD0,RoSl6+Aq:fTB3-L0<%U93]'cF?\> +$(_6#*_",``Bdhh);eTqYmj``mRB3-ont^uZa8&oi",Xb>M1gY%),;uiQrf`ip*V1lFT1tB?jbLJ6&q8iRhZOJ.AZd-1>g#.LG!Ph$T'VF_*Pp];W^h +Xb/K5LTNV8AR:qA\6$]l0oJBKdl,L7XQc3[oOcoXQ1QEfMlcC"(1nnQl?[)l-VI5Ooma0WZ?uA,Rp;r8 +E*:N<*#ae%XSY3`ERL(mXL#*S!Rco0;bFGfE(=`>`iUceA,0#9!\'sWKr5am$lRE]e5ie>Y+s0D)'S`XG>m2EE4(*-Th +qefL$.V5#q`%%$pUA7eIMro(n>8ae?2JY+Y$YSp>&UUHJ#s<6Kd`EX[`2*GK3^ho.P-43Hff*"o7tD^9Ka,NbVkP)rJZfrFTj( +^,bh<.c)29f`*2JGI)F<1r;](isCZIeit%9np;C2nlHDsM]VDYL&:2;h(;i)XlJH6_c6d2?:l5fFYF#o +*X,Gk!tVicE^r/qbqJnGQV([,MLq2/gbVUU4`kPu#7p0nO+3TU"qY@cia,L!^*IJRrugZ2dcgdIk#+=r +s5>HFIe($qj%FNiJVp9YSur#`MW1[:XVLa(f_'f#s+q#RnRFUHo@UC\#ML@2Pk[[RrY=Y,s_kT4_ +g(.04Xf\>8_2>$(k2+<%G1K++)Dk45MHR+*4D2=YT(-N5gc4ao&-@,n9i:WJTs#lY7%(uApOrKB``Sd3`#,QHJ/i&&(,)C>rtBQR0Rm4oYrlHhlag]%TNNrJ4X]Y=-3;mL/3rT&-s'N:2n6A:] +@Aj)"5E"\AKYIV-h\<6VIq(_C^KIi0J%oq%;oLfQN]"O_K?=!'2H"AJkg0P14smDk]:,N!W%!&8aT=pE$eE +*E7E%e[b+V_q"e=2fqc[PrRu4_qPbK?5^@U6N?'5Q#e+I>B+065sok"D"b\hYFJ6@H.*jQLe/Ff:)=Aq +n9FKuBK`g7>f6u;(U)p^KI.p*5`8^]GLKt>J+GS%nYL$_6'mP2iD0X+S4N+N],WS$B](Z@$+f&HH5nb% +FMV(gDbmE]P?KJNa;^d-00ARUoL:.:3;-L%MFH:P5dZ +V60=*pWWN+E5Fns@pS?-ib6,HhVr_&eai*G]H[.' +DS!o_S5GPNLm3IL:b-"L#7]Ns +=84=JB]]Ke]p`h`0.X-e'R_YM.p$fH^/ke[^%nP81DXsS%fRGQGX'pE8.Xg6Hq'Y*@,)-E8f*l>G*+;1$TB'epr7N,"P(>25Ig`Q_98;^=M@G/N-?j_5tmTS +"e&aD#;n]G5%C:[QQ*&*X,j`1/Zh_'rdtPi?E#6aX17)<$$b-I1L3&)ZC,L[dbL5<]OZ%EV0U1Q2,!Om +fXmt?.n*X1QU3-/C!IW\QKG#aI$EcIf-?&-0462$o@!D6b%k$N^!aP97k@hh6+Ne:=^8M'jolVXQKA@k +Te4NK/Bogh+0B%d8@Rmf?><1Xc-(F/2sF00,L$VUD3j=<=7JnOZ\IZ)TRtl+QF_e%4+!i[V%fbO)15Ai +l:lp*?.JjDo7FiOn_QhD5\XPL'mN8jhTU&;H,4B`V0SJpmCJphY"+osPSj.p/]_J0]IqhW\C/$38nbVq +,:(N&Q]U@-SnSXJ??#Jmd_)7,p6Fd:GjLDP$]2L>m?fCso9*f,;4;iC9Ws/^DKtX!UboG809[oVd_&7G +$;0oRDqZ:I?A<'[rmUj8Xl%;B,eAIt]Kf,:DFms2Lm]HH9NBjO-`?m[E$ +?tirID8*l"b7&)kA7&K.J'\?SVt)N<+'=<2:?k,]BHL31Ni&i/;:[]% +lstT-n$#B"mHI9QdXVhomu!6O[-i61\nrsU%\")fXh8KVDouFu>mPko\nd`5!HpmB64ZriC:18jWc&G@ +3@'%]D30S]/UuT$<6R'GmNM_Qm#,shauFd7LAfZ;0GCE,N1+L>RC%2tDdHV3o#3O@9lg2Al_$^/R[*!H +%\g[NM7eVga44_X+L)A4jfJ%X8md(SdYX^Q]MXKH? ++^M0W4Y,E*&WoPqaap;HVX>5'PIeC'=Wl%GE6u@CbMNgMo3pF&$e]DP+16Hu3$/ljY<3n>4jGC;2K5^fei0A(9UN)g,qo( +@lDH:+mWj8[BIp^[J66@@D\B13P;4*;nhg_W()&E/'jS`*_T2\aud/Xj)t]_9jfscu[IT]+Cb)^2#,*'hE#@bAH#6KtYdGj*oV;8sS876Z9M*]\f?U&E3ED`6Xh9S48*a81 +Rd9RuV:P3K/$ESX@p=4@Z@Au=*cSQQf*5e2Xh6Q.I8RI3Bae0TD3@\.a8O@rL/fV4[+i1jVSeld;;9jJ +l]Pk-DbnmV[.8OK:JlOe;0%_THZ3 +Q=f.t?*f3La+>Gcb2`G1[dol'Uh+Eco_E1^Z*9q(4Kj_.[;a_,XLt-%ga'M4r0)h"Y^EZWa&jP$4A8m"ZD6gU9+? +E,E[@h;e(S"Z!!"g&nQdrp]?H@f1XoV3Hl#J)bfBiIcD_AaeT7o-Ub04.b"mmsOlh>*T9).0DW-l.0.8 +]nc?GdCRtVDO9f:eduE.i!aM+K443enm^6E-boAskb_N@K:=VhQJT%i>*R[/2qon/$;0-*:-qCqNn4C! +bJ)B^%9&/Yi`7%&'@[HW;ra>#m"JJ#DWW+?&94d#hpu\9Z[!(OU2,pONNkFaLPE(*Q!/\F^R;-oeQC/b +MnIXN,@A+6Nj:<8AlZu8``IKJ9J,-&2]*p=j_Bb8SBEPpB-4j;nRL@FUc:`hh+@!['*Hk":R7a*D-!sO +H!enGIg9l*8j.:LB\%"E[q33R?.52Yl.;R9Bd;%MI3?$LqNSCsT=;7O` +#+`JfbOuP:l>!V7FAl$qdQL&qCC+.^4-(rAI.NW_L\PWX5L7Sl,=d6aSlOR2G0@K:iTP'7117Ne@>rTPbk:'hUCmSS'<[ +'5Ee5a"cpt +;eD(^.:7<-f_!sCNi(6BeEp_/Oh'VZ:>'(Rk9f3EC.$\cM9Q8slQ1I;(3`9Gl+P0K36etQVN&96e(pnV +pKdE$o;d7^8k7hKi^*-kG]t9OfqAf,]&gd,bfpR"QunaMhO`se98n;YkpO&*1S^@:>'4FH_K#GMI&D&J +<^HcE=_5^iJV$1O#9"*#NlS]pN\qloZUrIiEp4\U(HL,)4Bu$W(ObeXV[)ZqjjeX?!q2fBFno8cAsocb +r4VmOEs(_FN4d,e*h`OM4e=6$7E.BnLGifjO37>.;6_1@h\B3-[iV*OM4PD7a$UA=nEdB7`hS?286XDJ +nU58qYlgG@p9&c1'//4Q2-FP]%q9T-DGPYS=iWR9_8\a??h@_hHXm[dn.G9H5hRm,ZgoHc+Zk]I;1^^? +fd+fLBfC$*n3d8;<1T!\:)"."LcSCF09ILkGL'-=O+$8*!Z5Kl5-l%Nu\[.[.h +^G$P3a3m)KOb0g#VB`dFHUMTh=HeBHEgB;24I4G$U.N;EO.sDlmPd+WUH+d< +-Y5gN':M5"a]%6;80MmphL7"*5T#_BN.3;&QU +[EA59#/KGj/.fI='Al+SOV^["FV`L$58[9J;t;Kf$toO1Z(ZIekQ$OHcoR_]4!9JRT5qQ/4-i:brE!dt +kg1\kJQG@WJugs'M%OmsBi(uHIp\^J&M'IQNV,DX8IANRl7S\f*/fLFTZL6i@BYL2P0U5;D$KJ$Y),7" +]WK^t9$U[(fSY/V')K;Ad2ZMI8;nTE#3PpMI+#j8Tqh9o49%*8lGS%;Z/Dg[IX(K5OSUK/dHI2@WSSS9 +crWX^ogfdEl,u)'PV!0#r;kd'5)_GP@9nPbG2`?RN]jmUH'^OLr#A\Gla*H#j8]'lV$5&:g]r[3ZnJi/ +Ym>BA4btTTa;kT/_Uu%oP]t0&BXBQC[Q"f?7!u33H)/\hVTd$QF!.$P$llpmiMla/S$F5s&P2ZAaFne? +/C4Oak^AX\\N@?#dOCBb.tKa^ONHA"b*;=,oIO[&d*Xd'G;i[%ShrY.)\ZZ6`J\7:lJ-SF>)l.#/H'Ff +r(?-)9$t!%UJ#p!4+jbRTRRa#/-C/nZ*T3UB!C&,;!/<5Hc-l8lgWO2hj]gH,j\76GAP,^J+;oE$pJ;$ +5AX&a4]s%qj+8V&FEKAsTRO +@au!/S7KEkrIc'c7's^?V_r(l9Ph@MLP[i=Nj)SgAtAfMWJ&Ll=>0^RS_O()URT`;[(lIBbYUa8Su\TF +^PsH?=O7#enbiEW&Y.<1nFkDY+iKE4"2ZJ:Eo(sL\H/H,gcDjq%RNV(;k=t"\`<#*N]Z^`-Qi7+m0#bX +cuTcN#g[\,afWg+8jii]lIi1;'FV[JTY.-Td!l:l64Gr,TIX#QZH)SE7S)f +;_M^A't#i]m_f3V>MI9c:XD1rdKXGP_oh_J%ljBI<,lmmojP2A^o+qYP:l%_K84':!0JLFWZi`jcVuPo +%4un)HCjCdOQ\p#+%_IKYb4@+ED>rp;ES`AVgV.#f.E["[C33DbQNDOW6<8l&BFls#kf9*#(Yf9jnf$RA):Th79 +=UT>B;d_pXn(W/5lQF]ZjLRRgR3-aYc-qH4(i/`]dOS[L@d+ln"c\Xm)n9#0*(fiGGk0'ti5HSg:&G71 +fNchY.qCSY%JXJ'_m%"%(H46miu[2L[SAsnY2@]5bo,5.?c?`k9s2=`=eP"?WkrsB)mfL"Z2,s?dd$_J +M%rS.QOQ%aHcD>0ZT[%u5a=t9d+[lBlh5R7_!_&sa+SM;d9>)$g5-._%p.".U+>NnC'HV4=24@-)$b(s +lqI?QKi-/Vj4)fY:P'?;B#WMp/7$U!F'=a\LqpSs+8l+P(*[LL@:-jcI*SF0\hpnWB9MJ]mZV)fra'Al)k4cuN43 +TQ$)AcC$EpIeOAI"G=r07*]/6r6Tik89CM;%A=j:Z\Gbe%9o<&fUP?`?Qk+YAMpp#JHk;H]nkCm(kk$1 +T3])p.BaaU?s-Us"7cgDt7@<[GT@P:h@3j!F'2qAk'>V-e9:S:9aWMU%/6,G&K7g*/0YVTV_$J@F9,t<>8Z-(E +k>ER6UpZ,[S-N84Fe3_oj\1pS!_%GhANC' +-X7#/+msi"4Fc6Z9C9I`>&O1'XBNN1kZGeAed7JN8XmoipXK7.`3l +XhD'n4u?($fCHtg+.b`U[C(f9n&_!Begg$u[Uebk_@)(Q&=.4c5@kSf]oT;Q67/H"_jYNB,)tuPEu8WD +qTUcl[Q^W([QZ)V[f/?qPT+J^VN="m/FQH#O_<@%XlB.S;0oQkBi4Wp_\R1X[DeVFT3E^l]XLFLn>f!@ +JitTUhNb=X8,ZAXV;KMnf(B5jL+KhD0iqN +\p89IoP?%+Z^%s&['.i^U_LQ':]H8)rUPGId/X!pHr-j9H)2-cBRNK)i-2!g;g5qTJ)>Js0Ah"#YJ(R2 +_,RHpjF]A18-e=Z?4TjneTib[=M_\B6>dQ!QhX-^KePREq3bFEJ+5q!t +o>\;+.+!5aa(UhIRg)&TE'(as9rtBaEn_RjcCP:NY^XT#O_2#c)^(bd-n3$_a(k"tVi)l/$ +ndEJ.NX`_`>o.b*6?`S0M:)0l>L+qD+\AYA3u_s`N)rZ2?f(U_emN8.i]kVa-Tn]0aS9MiB\]A!^U!ai +nr+ng8$q[X^3$TU74MGM\+0,_?W)8llpEj``6UK6o2>_8.A'WkH[>B]%Ch&S`PQYd;`][qGBl6P"*)GMj(Jj2mpU\!]H<`N5[IiNsYk%6U\=_It^Z08lCbFUbh +.a-83X3*tq6=I'Wr'h#pq2?#&2(2?>fb$sVne!>#khPG_,5g`f.&*f4'n5WH$AfqjoZA!4\,8JG`TF:T +j@W2cEK?9!,RnGLA\$qbB_2OVL/(SDX>k._X5"Uf@B)(rmE]"\><$E2W0M=D&ZHOVl\5AQQ/^cf?6? +hdR':MW@e_72GXHl?bB+Dk1X,9i@;?!I]d94b:&OD!BV)/sD&mI-Yh%J&R&dGF#!TASOD1B9,'BqaQBu +1tK.Dl;hRf;2`I1dFmkM3[S)nV1F`nB*U&reb%0]h,h[)adO5ihLc].a8Ti,P_oXY9l\a@U>OA0n6T]P +*5qSt[pa(.O@nSq+qAkm8,F9!X/7]Pnf;o5S,Z:%[d3JsmZ,.#:A^.m1H= +B+pi3CIB'$?[F`+Z:;&Y\luSXrE"qul^*nSUYHrS92X$ME0Y5t/u2k9IBo7^?CgAj1e$L8ZC,^0)AF#V +a[ksTqQr0.&pg]=]gq["X(DfIBB?^icQa.O7H$>"ngR"`YLMP&^N%N,O:eZ\rL=J55,^ZY%a1YW(SVV4:Xjg#R*`9lMPdA9Tt]?Hmq +I^BS.b[WT@eqd)`/!%Tjn9'GSU5f)K]J@41%dO7H[8(`qH_alNe_`D.:Z?'Cb)o_pVDK=Y['r=N +eY1VoepZnDTbOoV;AR9ob=cd0I!q)9WS[+=DKPV^;2]Qi;eXHTH2c?p4`8@WKkZ7l8K)V$BU>FI)M*iW +*;R[BEm^_-[4IkUrb'okhYH$6*#I[;MsLoHW8LDtpQnGCDRWW92hF3HqoYOr6 +m6,;62+9T9]P^%E])hK-aJ*VH8=/EkdS>LXM+ +[CrJT\?4/=kFYarP3jjDaS)4E4\B(td:%C:rT>)%>uF'gDp`$.[,W""X,U7DMEs)jp#@Gtmg9C08aR)UbmL,7`CcNloa5(_OTRVSi-]3f.ma +LLW8<399K'AK*8VNf%F"Hg%^'Q<.HsG9!mfak5WV]qRfo:XEaeX-GEbBJ&qK&/iZS$$eLR-Ye7]8QIWh +Fs+inP3BmKFI)W=l,s[mM?6lE"8RtZ<=$%VU/b($!t]PTrdJk001Ih!R);TlZRAt_V<@-IQ58#XMAX[Q +CQ-ZtBWZ.epIb1pn`Yj_`/9l?X,;."KAY=\r3(oHZPQba=6_lq\6*[hIXne6lj&1`j/cT:a0A+7[<7B% +5J4=NA,pVH%m_Vq$lb<+J0Z_5B_S5\:t[OcrQ[OhmF61+BF +4*SHA:VU?BlM6@1p`U4#0Ai>.WoT`I#?+"*9%Zcp-1"F$juk*N:;5+?#;'DuGJpGhUUBDMq!X%5!SoV>;LSTDK-N"/084%"0=iBA +Nb%A,Lpk%6cZjM"+q>%)>@IV=FPbE4;o?6+o9k8QYAj6;]RjcK"k`rF8/G$Z+XgsiMQh#$US +53PXskE*6Y9Fg273o!fBAakiSL%e;q5Ba7V:gr[Tm0A>hL1euiRdB4.ol-#4,Dn/?,0]XV>VE4;m#N#g +g6=EYliZ3Kom6Tr%Y(Q*7uaEm`a2`?2p/$QaWE%G7E^qXD-Wc)7'!5.L-J^J5D/r;ac?3;InVXBIn]GK +rn,9h;/2Aj-kZ%\9?E(ZV"6KT>U(^@`*Uh<4BU!$HoZSf#PSWo@SaI4,U0+8Z!6;B4)I5Q#^n;t+\E9>Jfr +\h?]:"!r+I+I1-3a1i[;7kj!1XRbJ&f($J`:7[[$=-&qI?ga&QQ7-XMFlq_HBdMHI9q:nM^,I^uX8$F9 +nm'au/r)"2BG_3Yl&kPM%iph?0h7G"kgTic8b'C*7g-$23%Iq67%F29%@"FT`?rtO<@TjM7>HZ*PJR)! +#:1TW47"\r[6$QYLmg1AT'`tTn_SG!:4ZB"k.uIn`BOa0b.WHQs,,],H!&FKTj=+uM7AVcr7TPKq?4f.:H$R-&poeYiE)`QTg)!7LGLgY +76N4P/R'p:>tk=;f&Lt?>/g$k2IYh+Ddr+Jp9/]SI)u*.eCaru!b]-?L\-P]s0*NN&*DFlbG$Z\@J3bj +(ToSs2qn'FiYNiA6=F,J*Ydu[\ZNshcdH\`U3GY!Nn`G9q"@Dk)WHI*Tg4g_4bC[CkSb+o6:RjIe- +i72L1EG/:?`f3fZ:@n1S%KkP6!6XS,qH47RI +%nFfC$Qe`EkodJ(^1E7r?Yd?9-Ze@Aq9"g`?RaI;$\+o1V/OZi\S=+l!,Xl^C`l8!TqJp",dmV#);ji7 +CLuh4bRIX7biaD%pmiZhfL])&h1[Baq(\6,=TUsf?ecn%%`G,-!D<>J@&tgrjCO9o,aQqL34603C8oE5>0f)/D!"0 +_5g@tgT\[Kl2r9 +EZ>-u+It2B6XtPp@?8g[M?M/2t76QCGT%QJ8X8L#k5H +/&Tm:OC=nK4!&%9>aqtQ`j<_Tcpa8TR*mMG(F-%1@9ILEVrbaVoP5QH/^'.:H>L,$>@t1r1n(Z(`5An9 +6l0!DUO;DQPS*doM658M>ZF8!gkOj79s*tf/&+Dh8X:=[Dp+,Uc8'P7)\:=uT1C&J`GN/*h-\?BfZMt. +;Qai=j[[$]B?[>^Ubelh!Dj[(#VF9hc8H4Ne4 +(d!6#:W(nE2f\$)hcZt&E-afQYQk!s8=\?sFl9>[WW>`]X5N4[&LY7Nao2M%c6T"f]I.gcSK*>d?h)Id +p`Rlp9K7BANQ&1]cM@/oM1@$^AL]c?]HulVRrA)Kp_:c*7IS\?_+cu +EJ`5QJql;4Rq#Atq.OoZ*,PK(]i1GAjX8*Gn8sLs\j@"X'b2a(mZ0M;iTlE'cTF>i5@V4I]+.b&]/_c\ +=b:@Ahn?nH&O/(m?2H^9X1^=!6GA_Xnr;mS5LpHa799$XnpK8\mQ5p*R[/"a\J%u/m8K(uRT^D/p7OV= +P;2N5L2njC`j"6OA(Bl?hfqPL6d`&)GXK@=4n_^*$\$4+Pa21$j7R,nUF9>`41A8VI+Qu5gXTO6<4iU: +pGu*&?S1-Dl5.E#ht@'/HYd=%_Cs;`6a#^TnnE9geU@<6Q:1)+Z?lXnqOSS-Yssnkm!bums9W4pP3YCr?KM"ljmH;>Pc,Sg=$[rCNnO)LBY?X"%*;#?&C_$"c]:,b&5XS +_].=\)GO1Yr12Y^PRu0LHd`-2[\q;:u;L2-'l0mBe? +mM9m*C9m:-`6=dQah%LcpR6KXLfC<%GdO!laQQb3oo&E&ajP2b*k0?uop6)oNh94AO +6hf1c;dX>&oQNDJlAT=E$hVIl]^AYFm]&c.nh2_&(L:MA*46XnZ1IjNNm\Bee,M@EQN#3Bn@SjCRa,(Y_*4\Y)-pQ9WbqY+) +PMC&Gm0>WY/Y:,XM'pdmff"OT/%P3'aAp*(]ha)&NfbOj(O&$C>tVesUJH3eSlno4(gk4-cW0K;7L[RU +P[QC>QS*[Pesa3[Bc6qn<\ks)2*:]n6jsR"PW9cucC1edB/ +7(>@"7LVs:7golf1Q"E,M\WCPX1uC`EqH6FhXa.Pk`.Jm?BHR/sLdB9[((Ji#G*]+'Ydc%oQf]a8[T)O%gn]SKj'bum[$qC+f'@uURrqc^WS&HbT.71:_%P1K1K?[-T/ojN@LJ$,YL +/Rm]OB5k,E5&`C!$/5p=f'PcH'G(%`B!_k\n3NBu/QO$=2+\M2Rn\].8;+ +ZLAo,e^kgI[4d6.^VX+g>ku^IqS.DrE[E0KHDlM>TBK-Kn=fe;`\f2bnlJKT%[MGeF?,"VUX;:` +;SMnRJR8j9,p?1UTs)`aF8_cO`ZJ[7(JanFF(nk/;i<('f$4D!CF.DclYM5eZbF?.,T3gcc*NLE`8cZe +oV>[4?:d*]+]R6PC`5t#ACBTV^="9r3hLYtlMn\rB"^?C96F-rW&Hr\j=6s.EjMdgCVQ)<*ZqMQ#>n7r +N7tS,=ES:;!`ff0rh`Gg5P(d/fSJ)J^?%Qqf;K@>;>c+e-"s,CkrKcR,pcgZZ<2jOoiog>b84bm>j51V +,ZTj#R0OIt?;0`E[aA +=%cu(jg%T.X;]6&SPUo(m]+E.X:&.PG$A92^/o)@FNKtu;PIjm_*cFb\K$A#V73i?q)gXo/#b'<-;fU> +iY12[s$V"`NF-4q<6J>'P4M=0n_>3Baa3La+B#jhl4onu\,=h`6?:Us-+^+jih,OW@at8uAU\Y2?V7rQFfc$25*ffF)JBXH.L]MPkc2@PmYP]e^I]uHIMfe:97?*M0P;/ +=qk.qrR&@ule@k7NSF0)-?%(L67pV33t&)g-s!=O<-u>c+BE20Y8'`;o-tJ&`[V^qC6a[3[7\Es +YtEpQX)c&'0<0m]*33gVRXa=k]9%tOUR/UE4mA2[1Mq9#\QRYE/9anjkHf"/\H8[GS$%VTHkr!P#PDa# +I#k),i[jF=4Yf#oW0m%-@m$hlqAWe@1X`2a2XHRIQJj9=gG6[X`&.qBCWeEp_7%S_YZ=6,H:P>go[#9& +j5_A)9\O[GH1&4%eSrtG-MfXhr9@7!P2D]V'kdBMen4kV*585?s+Pf@V*3e?ZcC0kH[ki"F768-0^W)\ +?^gdo/3YS4,pW:QI/A&!:=!:;m`Y`0hBg\gV:9P9bq2N0\cUVm6b]MP)C3&W+7%DM(;:cp5P:DJD\N#8 +&"Su&o"KNQf)GD?Q+-j%4UDjrV,PCBO9\NS3Y&AHlM(3hQ9OB!ZM_+Nhg;_;S6k>?0%H\[%`>#oqbk)!)B42$mqNhLl;9-2Yn?YD +b0N>[JEK;S-LVL/,9X.hMJ.K]pNUWF;+5QI2Knc1a&672;a^-ZDRA,-k!/I@pJM(hX1!T3USKl%qjX[B +nsB@o,L+!e5ejeKq=aV\n=d$pF1S>i,'G!9<=44_gpK)DgC!ttpN^`AXL]>^dc&t.gu=@4=lQkVE5uMM +M"gk-akGI*45r&Q6PHMF%amVO'qUj?Q0@Bo@jrbGg[k\gT[mD'j(.&,(U^c(nDFn\"/q6is)I\@XUoLN +PA@SY6ag%O[FgEp[e-pb[C-s^6_9f?obXmoP9ZEBGN%dRRRA$l`U/`lj,B)`4b:6%>_2ZMI](-oMX3c! +SD68DYasVM(,q;klAF.A<9$`oK7(;jel_h=rIgj&]P%LTN7[R4SnFp8-H\SP[js)UH.G-B")%sOn;#$g +H-cB%h;=WB0-b^s2b%S!"ajE17pa!+]V4n!%ro5@,"K,?AgR!A`N^WZr,8\"Dp:o"bH4o=r-^0Xa_RNT2q-MaWUu>FM&iesM%r2Cos1C3>cQ1[0WEFKr +2QckQ9qSaoj2gX3`Q*k*UY)$e*Pld(#hH)9eg=\BIl9kd="^V'Wk`F=ZKK=O#8>hA7Ro\ +Dnk_al1p7lSQXRG4Angrm8+]N%`i^,7jd[X=-];e@8Ei5!Yk/\r[d>ai_.&WWQ0[]=3s#NDp?FUPE!%` +jVY;l;uprO0:^Q0LZ4JUHAio-N?jFHd7!?q%+RZ&kq=7!T]Sc1&=PQ7GtnfBa/ecra5-'fO$GM$l3V*< +;Z:_Q5D"'PZKMm,/%60d[[B>H1`X;6q/]dT2YK"FKYjSX,`\@sN/=1Y0V0B'B?RImSUA6E0_855E%1&'9IJ%mik.D]oHb2Kq)fHiR"JRUmofSXGT9>@39 +MAj)V3)1\\47sW&=sJ71j2,8+m#H*#[8f_AU@k6#I2JT^LU#IgprWS#rtc0I_UCNr.dEG/b5Y,eQfQ;p +lU]=Y.UiPf="eEaJ/[M^/s+/jH2F5b&^?91a8J74/`blnL*A((%:XBFWc1Y;T5Cb)iEXnaSD#l1loZrGp3+d(LGA[gBMCJ>mlK=U^D,YYc#IY=l2gpKg#KhCE="p?\Tn6*%N= +B(_K<9:.#C",lATCEja#pEk(>HV%#%bg&17pKi;h=mAorh&A@)`XIq-gTWV6g3Q*b0[gVj3jT_ZVoL78)Us;E`RL%,O`H5)u$\OJnjR?P!SqG1/"_/(P2VZ@[!d`.tAt +8\;X4:Qj.n2=7-oj#fsa9;g[L>ued+4l%1cpDnG_`a-ZE4J:?-b1BdpCGWRa,15P)Y8HuW4[h)X-/+S& +Za'132UZ\._lP,05S*6L`Y1+(Iic<2$_1LerM&F8s)p4%^uZ;TggX;Q?&)19\Ic<6"[(-*nUqj9O0 +H^qVmS^T:ajP7t:kU4gt-q;S*AbE-/>P2ke=Kt!IX5VkO\elq +9V^Q23E!RM3%hop:3]Mp-+rA!kH+=I(c*H.4N;2pjRgZ!l/se`@H;]m3@i5+RBt>fA'FD?ejnmcj8hEL +#BRmDAWc(Y5[5)N@mW5;$&dOl8*)]'?-h^6_E*`4B-kl6bZW'Z(iraSGt__bH&Oa62:KWLl#K15's5[* +A")NDq)p%I/E[:?A2,lReMfLlC&PiTb)rOf;9m=T1a:p>aS5,ofON82KQY>6kB0"3o,m\@EI,CF@;kK_ +_Z"sn4s(XKn*.b0R3%ej1?VFKI(oqRJ!ig.ZIkM/t1Rm,_GXYEJt1H"mYkK6=$c!;1mmtoi+ +k;bd,q*u;/(G`bk9c\N(pmnSC!pi?g<_'VJo]X%_#2F6Dl_<1eb`c3%(![3`erE\.EU;-;IP`ZNuSt.p-1C#'`/+XF;b*b21dF)mb?]['?)NH%A7NbIdd>Y_WnEBj8 +FUNEi,VUJ%4_<7/)1;1GO`7:OYf8NEH\WIF`<5tk-H;!thPqO.buk5]@.l:hfeK%_/)$(l/$Em:8/Re- +`]57eFKihWGdUjif*d'BB&\GCmIk,T7bG6GOM5>%(eMm&ZT:gU%e>"k83?o]S_T58mcdsQ,!d2\$ge.AJ +7d+=&i?qBO%"H)``l_Ouk`4265Rj:LP`tOokls0(*_P4n"8Rlh8%uhfGl5q!mGqN3M]"n0,LJOJ)nQ*F +nQ*b']N;g%b]gsiKnYF@\532p'[\4>Lj`>L"5u21N>:9V52UU[??9X&%*ada27KQkb$`5t*h(iJ2Fbq# +);Cst+X<\(a\7'$Sol!:[H]#n>]3n0MHSPQDFAY)aY'&W8'2_t]_Un&c#%4iIGrNb`N2EY_duHfec'^[j-i,kP,W3+Ul=(?@;*eD-pC +51%K9Pc8u;L??L*MB>N4^Qn%fRbO'Eefk3,BPnfLCMoeRBl9FWC2V*`(n04OgPPGtc9Z/^;k@+@gnq/1 +Mlf-!2e40Bc#'o9)19bmV]m?oE86lZ[ZlnNRN)"7CMnUe=\,A:1q1\9E^mpV,Vta(H4c4Wr,-S&m_'4\ +Fc!"mL+GRZ=_9Yh-'cR@(Y5J6OtEp7D:/$iW.%f9WK"QBo.Vh)iSHMO(m2uQil_6mMa;hC.?i31DbH)1 +/91EK5r!2,]PUX6crjrW4PZLGrp$#K4G#[Y=aue+PaMDfnU?1\i(D^p^M,B_T]Fbc=o69/R'eSeSCQLP +&Gc@=[E?O&I$Bc_NfELU3;/,1#&ttFg*;LNI`RobZPUKnls]0F0cYn(?+sW^t3%7f)G/e^S((F@-kK=n\bhhNbnqAiiSp)u.YsUuI +=mV5+:_l,:\(D;)Q@THth`/n*qs@r@lk[2b/*+U>Ka1'>HQ_`q^Bo4RK/'JNmUTf23EOTH*Yb&S\(GQ^ +VT.#l]E'*d0?bIOYP$*7"5SI7n`4g$_r&8MBK?]@#E>g%`g#L/K +`m,mO`#&@?b^?;XHS!pn8(>_1ITk=`O;lRH=_219*-+2j[H^fP$0;!]kQEl5]s,PfjN+ZqHkg#=RH?J9 +mG?aI,lBP/XYZTENqfR04*a05=/?q17qmEn7`%+oq>K530c)q]SbAq%jQR>KT9j@`L;0DQo/\BBk6,c#&e1D7:,LH4ng)9%PfPjZadkd$iVrk)7Z +IS\9(kdS1J,=j/B2*E1Xmmgt8 +jK0>A%am.*.!"rOp=I>@&sB\mhc/0sGE;WQZd1ePN.ep?R$#a"-hj?U4HfS)Bb'AF71,`$e;Jcr7RHj. +,,T";^')b1AgDKE-tXa#IhOUN;L`7I6J$+COjI<<].KV:con-\1[p?47RDDjM5')Lp$.%P8E^fk@9F#+ +?]5a&jrU;:J=4Ma]4&];=q$d\OHP87$!kh1,GDTX0?*L@KPiMXILD&"BLOhiSrp=u$1Hk6n7XOa7RFR$ +0I%Vj#0\5\MZGW@\]'(:g>$/QZ.!0tN.ePT0I*8q%%*0=('E\=Ur>Si('@uI%%*0=('A.oFGg1kkL!bS +N.fo]L]NDsbgB.WDmn5aVOHNC1(5'E1Gt(-C9P,PlMZN(3('A.oUr>UtQ,Q"`Oi;hg$#)-@]YJ!L +)4&6lGm.n:@*V&A%#E\J+KW9H8/L0TOjGZabXb9_JbC(o,j7C9BM$pf1(mL"f"RKi5[7`[[O1AJFa6Hn +YS]bt_L"1c;F\MBXiq&DgJ'?VKU%\hq?*=M1(nK.,E7Y8]>NYFSgF-tYQG?_H/QpUK#YS*_Gb_4q"ts- +eV@HK"M\r&hK[LV35112s2O/2g_$LhWN[=hRZN[$qNQ<77Ei$U3$9FhB&)RYau;&Y8'XZ8<<\Q]s+$@R +E%fMAh.5uN.auHH[h2WaS:`c/\2ua4HtA+:G8U?BeT2TL6`>(AY9)787ZUT-sD[Z#1NM4N.fq\J<>s*fY99$E.Kgo-[^7"^!O=^kAh_JJc:GI3nebB/Qc7`&g3-8(jK"g@LqVnbZF']fT!epXZc;=Eoc]Tf:F5T2mH$0R +3*Po20A9"[j\BpILS?e0L-a&K(Ck=\1coWg=OiHM$#esWe2(_#6:Sg41!LOKWf/VVTk9a71R,ia)$s4l +f7>`5Ik#diApploN?^\nGiL_C=*aab7`+%#g,3Fe4HI/0Og".*>eiXfOmnqdf11,E:)/1%>4j^a)74jM +^VI`2\.:>-H>D]MS[#,,Mb$jT.Q/*F%hUeZ@P0_"Y3\B#\hb;]?u,a:pC)Nu!=jO8i<>t3$K%$!bK*6< +XgZg7b=u6/HPq)4[?mrh3EAi/T%Ul/pI5?;r,XO;XN*KBGq%N;R#/tn2]@9t92HH\&pijd;RA+69"#IY-j9d*1`W%q"k?P+n7or0sqp4Dh\bbqWg5W9Aq1k?&FSo +g4pop?]I"Q5$?khF("?4j;$/s7F"3d;Bk"p^tPX[VfjNG)nQfbTjrl0?dPq$M ++rue%nEP!LHXV/p +ng"+O=*3J=PLha+,ePjB2.\%l&[(gLZ(E?P>"!FoAU"%rtUBoDnc`jI:c;o*p=sZ!@7D]7A +6pH3s?a4X!54B49X,M^uCY?jdCGGAm1"<,_*/'Z67=IR^O3a+Srjgp!DrRFWo9Nn)9fjc,#9tB>[2J+c +gfhUNq=/D>XNXX6h1h&D6X5AV!jPd^a_^#nMLq)C?npNFVgh;pd!H.GF_-lNQ?lN/rB?gBY1\c!LlqU( +Vttc/XdmG:;@d@*jV&?4Vbm.8ll!@i`pk_"#AX",qH42O.O*E.d"eVC==,81dW7fG:iT&^G]pOa7`X5C1 +@GI=SEZkc(R`Cg'_hQ:sh7Fb5]X`#6#C!K5:-5isjZ`=CWV=/#'AGQd +N"M;rC_p./ld2)/F%G@+&%n>'Gl(XUq0Vj8"V:#HAmn,h`95P*O9-8Y\*EU:Y7.ig,NRd0 +`>9XmB2C^,!AWKJPl9C";XPW0@V4mCjo1An5Hce]h+l9HIO*UWib.$H>A?k(-%/q\.rSNX8U'StC6^s8Htja#'#!4CB1.Z6=g,$6:^;R`#(b*snEq,2Oqt)=pJ">\S>IA>iKd1%W8*gUC%$/OA.bC^o49@WCpEo7a8? +Ve=4VeN)D/cY=g<1VNu+gk*%dO5cduF"Vr\cn2tXkTQ!.Bbf%Q=C[<0>BToHFW@h5D[L-6RM]],+J6Vg +]s,M46U%Me8UNOAMj`7k&DI)rXtXcjkV$"[4/NVbH&*]Dd9:'LISQuF^UJ%9Vq>'B'C8SYh2='qA/.K0 +]O[[\g2SI>lH:"6&bG%bc:=2p2gBU"@>iT*rL]APL@=4rnuJ9Xp*e6?B"Ws1C/pdI(]Fa-I&+H`/`Crg +MTRKE;dVIZE^Zgsa5^J2kAi=QQY0aH]lrYrp?X'$(WP\tl&1E)hok&`QScsHl1@uSU5Zn_3Uq1e?)iLAff +nnHAE-!QUbW2Ou30LVCI9FHbMt&`T:k +6t<;PXJ$Qek,I;'dGG&:_`P%l0`XpKK#f)=^O??HMp[E- +g%M;`X:7R3^4jc%@K-)7;TOE=H-/Qelb/@]lXJt+An\Dh^_#MXMXXF"afIsX4tthOREaU.Gcl6NIs +D:@!qAU)^c7XKQ;=.oPaMr/Tl4s0ePEY@WMNP9$$.WW0F^]$[.`V:p2^ef4t>&EXJE!hT4dan)YSa>@L +S0jiI@R)L"l$;doV1I&G=dVHstC!^C9_1%YH?+c^]CJs4[k:k("DSeW^-H##KTODHYJO+5!n7#J$4WT`:A4](Y*XDZ4kqilR/0V`0AilUT9[6GCJI\\uMZl)b*fA[L*[MWnjHg-Kj[,$=9Q._Z0!XMm*nB.^i3YW`,[ +Kg3nV'RIDq!qs.WgT>XNA<`2=2^N)'8H*\7ZGI_k@;]o);.U`UUCT/(>nokH4uQeG_Vrt%2*&eU=B?8" +G.R,NpZP!q27es5l2h[Jd)3GfTPSo0JEiOYr)R=JI]X\m^2TN,s+@&I!_B[nBlkBe!hHIi*'U#S$9)cHW2L! +`7l3glbiN$I33cF*cb30DE6KHk0I9k>>[6]oY%pJY-^FUoWQ0`R1qG2'R&\X"WEOVb;)!e +qEl9`*/EDpa6D7Rj>aj,2kAuJ1sU)ekL6H&Z%D"XB]%*TqP5)hNmj]rOQ/;E`rQsrl'(u^4Z#nR*2i+m +=BOsc"Ae8Cq2s_0qJ$0],KK$Z\P`#[XMJm-;jVQq[b1n+S0n&,Nl.d:EVLl$S1]XlNmeBHb/;@D:[f(l +h`4HPq6Tp"CRUU8=V>Q\[Mm3N6Q/J>L`?Y%*63),3QLBT>XUGH\nhDnLXfCpj7GfHjRN&KRH&B\+jg,( +`H8brJOcIR&q2I70+E9CNeas-#GQk1a*_>&0XpR5kbf!]^`N#!#H@ +0D*auS@@A6WOKt1l"rI&Y/h"NY#qA6h(iNQs46pu[REtUa_UK<(f`D&hM*VD4Y$n0Te@VSNS^5YKu`M_ +bg:(EAF&kka[Yf,8JuGUDWW,.a'h+Hm*#L3onbgng09=Lp!rn8qfmqd9s+>U*cUT948/"C +s&l#8ZhG+dpl9Gme4b"LNLt6m[\FpV6H8@VN(k5Kpg%-9FIoPo57<-:."a?/?X).]lrcpKdK=>S5goB? +[N=";*jXt]ro]V[ncZ?ZH];k*UrK)t@mA1IAMhO7qXTRprKX-`l(<\$7"3+CRB4g-W(51S.=GSJM3Jc8GDs%/%&DUgS.4" +&-oRT$a!uq*bEt/Ws@A,LkuZ>C]D&9LjNJUb6Yh.AINuN=%K7F!c`$&[<>-Mk4;EL\-lp5atigj6!%6k +$j*G-4VGjdXq74CWV3Xc(A&X'4HqGpPq#ZWl,;beZp6AHr.=rbM/'Xpu_HfcNJXNpQTVmVZLqdE2V;J`8'@%qR?TYN4`%2$5G6^r2;1UNj( +.*:G_pZO5`^0hIoo>QLqY$dN>75<3$u02"L59WF<7+#O3kF"ehf3Ms +2!R]1%@P81e'+e0ME9kLqT*8H0M=9=6b"A&2?_dJZm!1QP$d^UmK$*efpteU_L"Wh_J2U\3[N8!WM090 +Ca6>[I`S"_A8MTn-uFbS6>\."%k4NB#E^MM;]@d'DP59D-SHu%c+%kae"lB=(kM;&;oE%t8$qFYI/"FV +HTBRr9H2u^.^^Et\8+IBs7D(?G&;O84eJq\=1@=1ef0^9if-+UrZaKM]^R1F`WYk]efX[`;/U2p23EYf +TIKLc]QJ^&PMG4r;lB+8?ugg:AcL?\\0^'C\(R(,B]KW=&;Zescdjf4#3FccuAer]B9c/>3Bmo4NK"K3ACYPoSZ$78o=`Kpn8&E#KV=ZAo'QHJX\JL*qE[s\A/tff\NKH-15fI_oQ]buVH[)5Ls,D. +8gf6I7A!$,TUkejQPD-qDVJ\l1-]XL[@3_E`mKtD([lP2)\tL*SWO_4`\!K[EVi&X]2iW&PP]*12U,0" +][+6gL`YXdk2"]V``g,9Z0DP+9A&eT-=na8`_G/Pn!d.BBu<.Q;bAu_*h2#%TP*"::$8dF4R-J9J<-3W +NS=iMdJO$ApXCZU"TE+28:"/e>$&UTTtq_o0[H1TcU(]7%A'?,T).uFTkAIofI"qR,$VlOO8Uo/<=b\: +Z)UfIL#H$JN$ikh)tXtg.(sTo5cKoJT9In>2DgS`G?In>4j,mF-<$Legj +;P6Za,o--?']=jH$KUkkonG*2fi7X!Q/s!%dGHoqB_;%X%G/mk0[Ag?eKCIAF[Keho"$"$B=r,Xo@biI +Tm>@u:Y&JAT`kO@;i^:73?t$ATX(R)4=i&`.?N9dKGSl#Qd:u:JTU^3Z-GV\VmkrWm2Gq_'uXdu*'euSh*F$`<'p?FLtR5s7m(.8(6kG6(_fdB;\^I5(]B? +g1A`:JUGIm:7EbnG/?1^kd9$Up;Mu]5Q%lk]YDd3$?7\*@Wh<=nHci&ObJh]]V@uBI^s)dlj9_ZTc8(@ +&`C#DcI\XJGu%R,Htcfhkm5Ie;lAq0):PI(;;?-H/H5I"$WRjNdm4b!=`&&Jrd;V;WYNo2%qO__DJuDj +S[,Ph/>73s0WY0D]&=9f9h.u\ +RN23I_o3LkAHoO^>[-1n>aLm8lVc1p[&-crgN58.Yj#C,2dE*HmI`tIs1(Ynl\=_7C4'&8@&FsH]$t@^ +e#_n'?.V\lIcTdT)m6c"7+UYOPSF5i%G/SW)eQ?XiacO(g>7T<8\*R@L5S$5Mq-^ra)]H9?%_%3Y2ORV +Rjd05BhXaMCLq@7F5o*fPiQ\V-1hJQW=5J>ZreaTU+3u7l`a"+4W,"r>W6srs3Eef>!2' +s+[4:?eNI2D7$OLjqlc("ftmMdMLTDp?U,9eR/5u\Sq3/^V$gg"b0BZI`#W.AImhY;d8eYLDO6u>NiT3[Ggi+ +kM'&(jpLGLb&('GL]Yb"/+/Dq]5k%MHl!rdFf![7J!r#JAuQNk`S+Ye[I\=35EODA^52K(i-RpTnp$bW +A$aB`o_1eFX+<0S?DA>/.qT;6gf;7h#Scn!Xcr@^3-+pqI9QP`--\@?Y\#/ADW*p>Y-p7#i4T@GojmpU +n<5&E1LC4[>F56>G>cAV-b,_T.e@Xg?ET&q_49LfjgVWrH=\u;cLBnt!g8QibZb@2W5#rIo_o]e9d/Z6 +1<0ds>X_C9dX,!6.*FCgX])>`p3i5+)nA:f:Dc(r]aloK@E-fPZeCNd6d(h@D:&lLhsOKj209@dfC/2+\[4[Ssb#bY5Ur5E5!*pDPXZ=,jD/O+9?& +&2BqLps0%V_"Sm]a21p46Z6L4iND%pi-H6"[qb"pNHp?SMN]oDIui*S1r%8/>:HTlnePF6f1VAu_mOIp +>.,U"PMboi2?E\^518F)n;n,D_UVNLid339)lQ!2a7Rjga@#ps]b)Z)*#MQJqBpPNhA\Nq?Z"#R_-*j7 ++[,:GqN@&mj^dn,BI.rlj1Xqo[i)jPf&@Cl*m'g".EYq%$[:_@hrkqs"[)gEZ!NANR9VI]DT&]g[7NO' +1q+fKX>'hBb*lNRUj\F]Q3#N[8P=EMSo)pHXL7T3Li03G;2bs_r%Bh<GosPL$\agi1^RRTmZ> +UZMbrX>DkD#\II)gq:GK(]sXL[;u +7@K_-piXi@a>GncC2UP0OQM:e[a2lO9,c]P,ghOW]8(I.'-VWLA%GKl+PJXRN.)ms>mcJnH$"b>PrfQ3X01.(*,!QQa;:Eu +G)Q++FN%=bm#=Pqp0aMd^[:QYR\mCurU=%X(!OSO8K=5F3J2;^Gd$r'@=HrU:S07FO.'HPg7H'Y0TMfI +\'%.OMc1UfDt0(KhD,X^-f/&%e]I"pT%UVX%eXbemGXH=[!-W7BmQ'm)9+gIQ$5VtgbiE,DpV[!A5AqE +dYCa`WCO(`/5eUpY0%98<8!@]Oqc[/eWf2je/ ++QpaGE7DOPS4R>'*R=$h^X2QJE^_Hf9:GW[dBgQ95DhNN)BlGSO$$n0R%Z7kHTIcH1O*>OtlSj+4>>VD]^gZF3,EO^Q*ZRV_cS^,u$Co?^fcO=d-1>b43e%WoLF><&^0LqP5/qUcRr8 +W=tpK'OW(>%?'[t^5B,Z9J`6Ce.r:>khVe%:KH`)T)ElBo"`leTpa4i7LDArYt%Lcs!fk1!?n5`F_4p!=+AG\%%jf((AAV>H!g- +(7*Dt1MO%Sm]-MsB4]O[X/*FSmTV8WPPol0VgGbE/?MhsioqL?[U^+N^@"R-h7qloCYmDc0p_m.GA*krM9AQZ@@<[qqAE6hp37n\$&2:X3rINne+%Gk0PeEJjjlGf!poFY[aVYmu,4C%I-*$&\iDGf!poF[CI"P!g&f7$<5B@-aUk +4J08j%(=?TX_29K%(>&fY_(cJ[M]LWs;42S[A_3H/(/ +QM/'r%+Z#OO4=Mo!IMX]&:6r>^;XHEe,,sF+?Q#C#$Y^l`GatAQle"R;]"@-L>f%_J]AfrcN>MZGOa#)Jb3`P?Mk-]8"LKC\gS +=TK?ipYqmYB!#UYoP;mh>7ARfBJCXMTZ&&F(WNkaOQ6@%^0,mrX*/IA$4O*6TI5Pp\N]/741_1g[8"Z2 +<5uBhE84o/U-b--&AkU3T1'(X`ng6KMgrK\kU.b6O9SJ+(NIV7NpjCM*>!4%L.Pfc,W^pk[YVAck-6ki +jqp,8Oa6*)-@kmqqX_*9N74aLS!ZXV\hl_`B\?QAi`X1jMaRIPjqm3ObmEGL.e*^ei'1*9=RSDEqt]4M ++4aVY7`GA<^/^f,NA6`Zq8FPas'(F&<42tJ;=mc!T=*8O*5T9'"NRB\%GO2l9;;U1>Lj>31c +pJ7B#h1jIXV7ii]2+G.KY`%2$A7=5OXa)&Z:7rH?PWi^*9%H+c^XkJ`FajD_R,\(Zo"3N*4^ic1Hp`LN +P_OG.S!.'*ERtcJ'$":/JjL6`h=5mV)CaP_3YUaJZh,N$kA,O7`EKh[X>mVpUtO7l)iVq@gaD>HaPe^$ +:+an18d/rrp1t&9NZu8P`F/n:>B(M[7Onrnc"P'.Oqic)\IJ,.s+R#5kl9OBYb[1H7lF9aZ* +Gm+AlrD?8@':;X/cnMmuRH9Vlhs_F?I>bW1\Xn62R:"u&*oZ+-7?"8SSWtp2_,Y7$=Y'%Cd[#:nkad0= +j`%*ALY_6hRMPi]\ibH,=id/VI,LY2X`i:76lD2jo"1k5,6sHFg@.nXbZdk*(seth&DKWRG9Mi5dn*4. +_NXnrodp3E/B;LRK&]h`m&JCr'@OKbQ=EueQEQN7hmbpId<)b1CM'$uH)HqXZbVp[]tCFs`j,6e +3lni=WL5pU>S$WDG"0rO1o#nfRE$@GDULBWRrpj%WP#bgT)Zf52ekM4^aQP^HK08AjjrSa$n8j6+#.LS8cp-1V`. +T7;OM8u8F'7I7Jqq)bl[h-Op,;DAMHr1L6[?1O".[uRQ'+Qb+tD/WSK6-_,eWJ!AhQl&jRrf^B_)E3tP +(S,^\Bf)%0p\$Q':bdjH>EAIn,9a8raR=m8+uW)i&B5>oqT5uiDP[tXZl-U].of#E.(<+1?@gQR.OSs0 +I>([Y1S2+>hu$Z;'u0KK4jOP<<0]C]N2c::Z=O/JmQ^G:NO'P`U!epY`BTJ++2r9]ZX'-`lhU51kK$km +E6\F_?ha/Q?0[fhE_<>90!S&^kKWO)Cr4<+F@r.Y0"$"g^(7.=N.Ae-b%DZ($dpYu>M9:R4inqP:.\!$ +VE_Z8VB<>/.*QQ,i^GgF\0EU$iK.U6O2Z$)5M\.V,%om+[IqqDCQLNbJBmB#]9j",uM_U"#p?J!;@G$$KY4<;JYfYu!"V&rKD8+O" +#5/YsocLGT@!_ekDcV0!52ZsYIm;:>\*hW\h72Q8Ap0b/6=]g[nsU_76pH3s?a4WU?8]S_=kYBB`g8Vf +5BNW.[,.+^aZ0@-?6h>N%;rn:S:&O4u8Gs/AZBd)%*Nj.qqlHor@%,LG%Rleoj;[7.L +DUGie5OrA?:4?aJ+J1mh?9r?u*hi6h09=J+A-Xh^g-G_FrQ +Ark;bjNGe-/n@fSNb=J\']&S_NU,>YQ"Ns +8hjVVj/`Xkr8TcNM!)OXSrZJu2])*WQ"#'kr=IuG'"3ukBDt6ID!B/dLl*guaV,@CL"D)8C;]\dLbU(8 +:F.X_VWC$8Y!CGXY<[`D7at_k#e[p5m.YdQ\PeAbJKk"["cT[%gT/h/m=743;:269/Jmd.*LXcH$`4`q +q@Z6a&)[Yq-2s)G@e&=3j`uZchX'8BiA+s#,TPd)VZoY)prBYC3QM#V'3-d4/S5-N?t)TUqd?2(\<8#-qmnWti6>s"s[0^%[WZn^YOJ<"^,?8ck%CTG#I-^6XMFn\T,f +/B"):_I:.EIaa?M0nZ&(Lp4MJWmeun`49Q\e`P-VC!nECf/*_\'e?($&P\=a9C%op`O)D?hnXF@1"*l( +dJX(t2GpaT*$EF'll]8bBnqgBYid$fZ8t>G/C*?lot>$998!/0FkU95m^U5D/XX.oH:IOI,-I:`rj-pI +'(Qcu!s9@VB^?/&?&)#u?+$UkRbjPI%\GZZ[J#^V29)V'a3m`uH(4Y=H8uIhXcn46[dWdTKMKPI?Lln/Y=LiuY*qm95''L4]2O#9`HcqW!]ibOuc/ +(%^9uo3"Yk_9,;A.-B8g2u3B!I,DF#FXH%M78I90N*B)0;+&W9Ei#/]3*gm'*!_n3`,T0T(WV*'2E233 +GDP;+Wd[&SUL]C;8ho1+3,]=C=ObQ7:uO_\#Pl!ME?(,$&KC!3dEA3j> +33ZPDj.K;/3"@FW"NS>QM`;2]_2.+_3QE9ia6AbokTMpjEu_993)5fF_H8>!#WWCSA%]3GiuP3LZ89Ne +Ul@>DC;5SbTJB)XS0n&,Ne=7KEUY;p*.0+n%^N3nj.K;/GRF9h*!['=hV-)P1*&Er+dJ5NTGEJGN[&Qa +@WEGfM@mlpV3p1#aIFWL`1BC;;s\0sMXf8Qg.*H`[`aq#RO=$?gH,q8pZeTrA_/Fl'Iq(D +<6]"H?)_Z3T+R2aUiZ.\LNHsEOf7@cIRZ0,%hDH44M))o[U/`$IF^9$BLR1+VNW#M`VfFN +Un4iO!@l'1O(I:;@#nrs(f;=:`b?'N@T"`5e62a(s7GS0TP(TgE((+DbIYFP0m:9[\P`i*(mSRYL_"^2 +WiVG0hJdg+O6h6KUJiOBiL^&kF^:^H\/.eVS8W`VT)Zf[^ThYC4%:?0H9CF"jpd[6n:gO_&$f3m'uqCnJj\jrmm[/;0aI22W?a0[?B6;@mD:@31rDb8R>MK"T(Hh'?bXbj7+M? +3,N(taTG7J`d;"NDWsB>&Mbl4dr'^r[ZXh+FJBlJE5Z$A98EHdPb70C,T4#Q/Qp%-_UGf5CtV+#<9>'Z +`tRZI3n]st'4N^`Fs3Hs>Dk$^N6@n`KeX%b-jQffi5CG-\0DC.d'M*"-EG9SMMr2:XfC97LkfH#Sf=p%H5k]ZlrE;O>_ba0qt'\ +rE?qcN>aZX7;jN&O0n<>nH:AL+k1t'2MA(WTa_UG2KV>SV@DKYdY@7^d"ZN*->a$R@RN#^+UX)fjjj3h +qC4"Ze>s+QhM\`ae:Nf@7-*&50t!_BQLP^YF_ur86#?mm@RN#^&IjW7D3MG$I]2s8ak@oVeI:`CSf.uNhLp@IIJs,94*VI"4W +rQMKNr<,$I1>F:%E)n?(a"*M#QX0E5a4J(G]YDP9pVJO#ftVI,k:OD5:Tt`MQPDYGc3HoOc6[7oM+cDD +kru/sg$kFQTYa@Pj!+o-:$>$lN1a>76P\:ZbG*5h0f,%?eG`)p[XM;3d=GoB*[fuQmLQ)@KtV%Hps$pR[ro#23HB.Kh1@f6s&HWW)A/R.r8UO*q\ +R_aBM('hiTh>ePU_>VqDp) +Mse6:P.74qI]@[Al,hc&8XV4O)5huoTs_,-"\p8#p$3,K$>N6se9NTWbn2I0Fkt]WW6TBo'bPA.]oiVB +9KZO(ehFhWr^lQf`=Qs)l!2(t,Mg%O\o+YlHIfuDURj_L?=8e]gHX$5:TBi2#P7$g#OQWdh-N&d^>#+h +5L?pHT'#(C]&qBg\+lnq39aDO]9=>sIR:/22i+CPF==cc*U&f5o>PNMpWPMA;D*@V$1I*?(;>@o\eU]_ +W1l*6+n%$)oR7(BR5Xu,0f:?!k$QZX70XtcTEA5U\c?C#XmcOW'GrJP^g=VeJAkelJ@>nke+"^f`_-XIfa&.a,:0BoQ^6AC&i>USl\pf&#f8MKt>,Z*6t-V**cbI(d0Nc-Zgr%D8@M +:\Tqf5dZ!"1lM<_>9fo/,UtD'/oQlAb?JQ,_*:#XqX>*@o[PQiYo:\rI#<\A*%fHErA>u`=*m/@J/3=js8,gB0gKbM: +44XRT],dY,BXbT%luoH5ic[CQF*R#:f8dh$*,8T5&im6JR.p*6luj1V]:H1mG'oa%42-Pp]=NBuQMRuD +DT>*7dd\YMcFurQ1 +o9rKCT#'N`La:^A7+&&KLXh/31)I)'[,P@#,#&a]NU2We`.U#uEi7p:ddRL@iXaBdG +`.&gk$[P9IPl.q+%ro:?cW+/`G;Z%-dA]tPBGSU$Z*q\+?EBGJ$k<DEj,LE6()2u6#\6^?2"QB46k +pg:Cs#!JAG'%MMEnZ"='s$^K3>GY\0A+s>j<3\menCd\7B)>"STW-BAX3o]Ap&.bdQJ(J?DC>=.KA=u6 +qqnOqClVdBpIUnf+&9om_G/#N9X&o?QGrqB#nl7Lo,Od$TCt*22$99(+1lnJ"?3pi!W'OAl`O;GBb)27PgDPK[kT"o3ZNE +\lVu8oi/-!e;X)tHNu@^\>t.U@gVb($.XRoXs1PD[^UN$bkZ`>hG\b2\SXb'%rVSQ%EMa$>>'m&s2`6C +>iC7k+#sNg9J_1?eU['=5,fIIZq*uNC8h'N)d@dcKULd>q3c+?qX[QATXf0r>XX(-\e)UMIV +4''YsefgKn@tL8"0'sub".H/#c[%_f<<@m0F-&B$@iYD"I(VKOT)ZgrT"Kor'.PLE4benBEL/MkE]k1X +J\js5)*@mWs54I0fAs'!qp0X::^q)Ujl&Tb+S<*0UkQ?4KfV-H=N-X&)3WniAX;;$HKh=juK<&Vl +nQh.P9&KDU3am.Rpj7L!XB2P/'UfuGCMp*<+jI,\)]Plb!m7G'L70:6gi/IQ?../mZ"G#&;HZWrmmR@X +ja3b;%/u1M&61f.Au(;9_M?H(#$/GSr1Nqnqr=+alF0<*6ZnX?ge\G.1cJB%OlhC&)/3ue"r*LFWW>\4 +6bORUDFCY<&Qber(_k@(:njK7?ZeD*k0-oAC[WB45@kJXMMEm;jKj.!i*;(lRDqb_E%2'?UN +9tFJgl:[h]EAXi/Tc0"L2pMdg2cKkdbmgpldbI[f8"(BZCMq)?gi-ULbmi3R6?D2DT]X6W8"N>!GE1Ea +r0TR8RN#tu2)fOZC2QaL?73d4rfX;_bssOA\B#Hp=eTHD54%8XK/"A`D>2ZjE!Fh*C#iPOWp<>Y2J;B4 +[h_.hM:s=KmB0SuN.dnSY(9i4&.cd<4HeD[hWrUT?X4Y@c:"XX&Wf01FB2hO7>njrH!&MA3(qaW3J.]' +Q^lqZDlt]uO.'HPg7H(D0NLS^9Mkdq>boDfPGd@$/iX>Ylb'XpX.']'4I:&s*9uFH'n-L5i==r?:!jOPBApAdkE"L\r]'Z?EJp"10kQu#*>;Im%Y5eXj3&'@g)'G:?6ecQ@UT;IpkR"nG.!-<%hWmPmVkm +fisEk_&@G>NHB +cYnK6(2jW+NS&G,TDuoY5,nY&Bo&-h&*-jA\HGoOK)_N6Nk"MA-*8qGihacARPd+\Qh`k(W/f1>IKs5. +89M6BnpN]E!`Tj@G_0X3I'<-AI[+Q7l$RVBIpJ8rB[kc-3K!D>mi=Na&%:oUUGT`r)GlrY:s2>++l$Sl +1rLFsSKg(ak^:*To5#S.3J*f*`I/:$1F3VNpB(06dh(gCkVP[A1Tn&+C'Dk`2t=S1)Ocg-S$V>4VYA[g +!Cq4$6EiCBJ*/rhBo'+6dh$/RV]j?hT8&/3=.M9W!ffEskJ)"#%3[7Uc,<^_25&j3.fSS(7Mm!7@Bi[& +((=/s.F^sM$VN=6MA]E;9%J0*AC%h]dop&2>Q':"/_]Nu\7J2'IHCSg/=l4.PnJ#t1t0pCQ!e,Md!'_L +oa4,mebW-#+K[)m!QBqk)N_EDmPr[.OT@3(a.mpc("\;t3:YLDSJpkedsV&f5H@knWL<=DbPq)&g#nr0 +ir7_1/(p("JMa.9'"MAoF3(p@U_%cb]]^_E"1m[:-WL8;nDdq7TR5_DC.s[3:<9'DE-_^/U_Gc`F`J$&ti('A$T(i7;(`(!9b2dHYg5si3:dY&O6RncM; +XV"QobZUoF][]apgNPD_/ft!.nMQB7qjEQjF+b'rm2reSCs7?T* +;X_U'[k%a),oII#@^d1*m=3V^46#dE2;DZFFc*D%&:6r>^*Q@l=8#8pfWq+jNZ@nJ@WWO'3_6S',Ti.Q +$lh4?#sA5rKG\M,MDf:XohesUXNaTsh%%kRuYB-MHalP)*" +hH;#5^,eoW!SJt.I(NJi#W(o?4!bR2HK8g+O?QMNLsh*%H"^u%eXg,o,V>Z9b<6M@^Q._uH6Cg9M$L(= +iLeTiB^*3'2)tA2fNQe"SZD(d^;\efhV+nq/L'&5nCc+Ah8)]FfX9iN9VhXp2q)C3:XtqKZn]r=l]FT- +jl8MP%&WjFeU[QUN4!l>qt-]i^4rT?j15;-qHS;ZI@c[;W/3<;2^H8![X'37lA51lj&WpOhlLu83hZ=" +l,).9OtL!2C5AhB':^%"IAA$\h@FrY'GRjoqb0cTT9[`o4H'2=c-he,T!.t7eUZG4_qHorCfm$Y.V?M4XaeZJt*&N*Ilr +'Ss-FD1$hm$)_!."4BkaDBq_S%4JX4VS`a*pnshlh5\pf^*4)'0"F\hl<.K/>RfoFc:HK9J5miUSip2` +#,2G=Oug'[Ybf5.ONR5@C6j>WQ#c>BK3?_]s(!Herk%Q(%7d`\([""_^[]gZ1a1D)V$A9J8;"X +5?FV=g-Dfh#%kQ34AWL+d.HUA`(isS+8X!SD\;"t=D.8KF5Fl)(u\BKYK3<[3\.8b"BVR*B4Z`AD:tup +1q1NkM-E":\_YdtkNq5D$R4l,LdWrLifCDE[U5=,J"e',:-A_V&bG1>PPLLf-k +XRe9$$e/\tj&gYC*SgChFG6u7QcPd1F!uB;7]FNOWqj0-am)*.1EoSYI9q@\YQ*d#5j=8aYDkK$2d^J$ +rIt1L>RecV<5QNth/kIK`ir>qZA;P]l?>[S;j39jAt0Gn%Vr[9Z=i!scrtRpZ[CbF*A,;)KKd"&o@0DUIXJF_'kCd9+5#Hcqigo"*4@,o$#l)D(uLOauDO +;WGc[oAZ[=o'tB\?2#(7.?6"I$.S1,;.#/\q3pK.g"f4CMBtmePH+_pPq"J^bmm7j=cE_m8?najG6Rr? +C-^c0I>([Y1S2+>hu$Zl!5J8>p@PLLM9]4!oZ1QKQ^=fu&6&UM2'BZ-@5X_)AqmDAP]>`rc??8nPb)7N +I?*NmKPUR:Z@9h+#B,bB7dOmtRT%KlqB[JG$ +I37iSg228)\fI-LFLhrh5Q7PYqMt#Be>CVG75Z4T6W=/F?-8W@[E9!_Z!)$(BDbQU4P5qEKp??u-7RoSf=)r2QKIk]&K^Gu^j:-/2;qk:c1,BD)"eCe8sB%(9\^ +0C1'2emeOojhoN6gtu<*5,(s3>/d>.ocJd_T`TH$s`kTOGRa;%4pC\CZ5K +Z@V99D`lq-JM'CKT%!\PTQ+l*UG9?"bDXU24F;b$U3NBiN9nSoepLn53BeOpa3(D#d,eH^%rj0jlYXD1 +Nqtu,LIH&&N/u;NfD)2S^N%J)Hcn-Obl?#@HK8g+I_R,/q]/]>ir'AQI`Mk:FR#OD_(D+8omah'Pgt<^ +IYZI(]N(P>7WYL4jS(a;Ur*!Lf)+G):*o(sG."607h +Wr_a>LXgq7KRFQtdA$UNmrsG,^E\^7*EE:%r=5k/q>Ce=f8/^P\;OrUH\?-UC=QItp6[/^n$#iP;tER% +Tre7RpH-7'hpKo3aQ/h0^SC8]IRh)erJl1pqZcrdZ6$R5]X9&H:f[\hq]3ohmWVe%L\Eks]&*hLpnmNd +AMkUFhdJAbhX@[T%e"Vk?et0a1qMQV%C?&lQ;%ApQHmYuPgt;/^Vb@^]\a=lG\LVUT:Y*Ub%a1j^SCVF +rJpRk\c/RHR;e=[c?D/uDatiuPuWR7D-I00^GkE<(b^rss7rRArm*g\ReXMYs7`/KK)b,R^qSbRcY'Tk +0;a"8[C5RcE5+>1G?4auIp)1@>,/!GEQ +rtk^@g5CI4rM&1Xp1&.'U(&rJ4hEAj](gL+/fOIYo5]\GI^dot)t11VD&:OqVK4]C7DCp6DG.lBo_[u5 +KtpnEK\>4(OX6C)h_b5U``"00(= +)g_fH5-\09.D?+.AfP/7nEfG--a`9_B5P\lHcp4%N:`0V>165&+o!lTA`f[G##HgtCV]s*"1GPn1qrD3F#Y#%or]B"I5PMa!U.cFaji"bU@X[_UE`]S2s)\R]H +4a6XuGrUrt2G9pIq$`k,^LOuboE0uVAA]S0J0i0pRIEYIa20JrMOfVX6])nN)-;]$7Gpa8m$PUUrAp5k +6;m1&d4_E+N9@Hl:?d5KbOoBV-'B@`<(St&jEl! +9!LXC@]*Y48W'&C#m)Q!kG>[&]8)*(Ut@DA+DGm3VWDj`*WTG[EF*su>3SpVYrf3R8[5l9,Mp$TB-G6C +;4C.7@6m8<*=EaI'KZ\tJJ>_snK]Q,HVn%*h:0Bljj[#mgliGa/iPXk48'/[fVOo^8[6_i47E/.(%Ml5 +m$NnYUtA[LVb=9n+h&T5XtkHh'K[(:,UjKjjil%thj>*>;`b4^/9&FqO7#p4(Y,+QfGW$O+iV`WNX%Re +/4"YSZ.$Z:g"NK"Z"#)'6].Fh*=F<>kA@gF]@9iD'KbFi+=VIPj<:>OC$P07Hcl]/d&tA6.<_e/BLoD[ +]R]/b85n:As8K?r#BJ.2%ULus_ga+O4U(;Gi&MSai*R+'Qgp5;4GJ"k]$E9S5AP+[EXhsgI4eqo)n&]! +`=jck]0e23*Sfq/A`,*"AJ^9R2/=5jJn5mi5jX,WHl88rXEB0As%[]'7W63tF`/qQJ&2t0jOe_MZ9J"o +W#[#E&p?%B\7#Pj8_QWE1`f"Ba/"AM;j=Z(1Adg-1j%+fV&ml9k5o`&:gsr>O1Br3ZP@i&>8q8+H=TOJ +%Mk%:T-J_brIC*8D2n_S5\S0Z]jRG@]^##nN;O9c;(h"=*a^+ +o_WI0"Fc)o+eE+9DnPj-d9:YWqIO%NnUtL<)](0!SZ9)8o&Xi_4+(Y1Sr>E%I*n]dBUOr/V@Zu>?*Yj5]j<8BZ9pHU1k_g#+J[;h"LmAIuMf(bU")dGtI[n`N/SVLKTC!e;b*M*K5ic80R:Y>O +AcoE.U1QOs;$MP`^?5**+hAOsT!/ccS%IE,7,qnT5-b`*e<>T"'VfAGLb9TO(8?_%Z4N")4,XlKMl'^7 +/fM#*1pj\D$dhCd$8Do#O^!XSeLX,^-FXQB5*R''U5QKbM!eqW=GVj6l='@rq.*Ch9X^dUlLTJ5mbHN. +7c!7d#`",4QpN;2;)*nO$W4/r=]gK;A[RbVW*#;g=bqUF6DRXaL^Gsb>):=UAR6,r^6]V098(J?7"p*P +Z4IbO#TOVS]/@GlFK6pFKr>tnBk8I!?>%/-7)dfH/4;a3+_%K#i`oV/$W/po7?N;&i&"4;;6_@D/4='m +q?RI+TcF7OU11SCZ4IcI0Zl^]Ci?/@1?[i6Dj#h<;Gc+V+gSfQa?juGX0N;ic:JcqgH/=LU5PIELkshQ +UfLTR0'Z.H,/2)L6l0X2OoDC47(5Z^#n,/"`@XKdf,n.O0!_S4Zk,6)Yf$,S3A"$.NaNq-I*GfG]C/''mkWc7$j)G[h+g]>^X'g6S8sq>6KBp.4jiHi76fHljAJYg;@F7$;#`&;:gn4b-E=(*^jL=GT;$_*[4b;\F#YKd``6NZ4-j[,"Q9#t^!\cYPh?'FRm4pTmqY +"_oigmdi,q'VgA<;N)A88hjM*>`-oq6to!r&bdP&6nZGl+]9gA7$M/f&@iLMKh1ZdKnrWc(*])SZk0b^ +UP4M%N#!U00j9B.+&;:f3L(o8o85]j3U6&i>*#2?[(*^jL=GT;$_*XAiHZ^bA$W0GL=GS6A?m1[IK7t_L +(bA(<@^$D8MSVUn;@F,3)^A+`^mY$^&\SQ%d:kFicslB81gg$^#EH[h.4doo,d)]=D:S7HcC@?6V&X +h*Q52it`j9mKs>aXlke"aEm(d,,YQILa]TD+X.BC6=aS%(*]Ng,7BVW[h'.^Kda@;P>T;G/U&p$7)ZGD +)-XZ9CFW&GbT*BECoo>`t[K$]uUA&>cWD9+)BgIUa=/Z4OQq7Z3AO>K7158H&@&9M^/Yjp<@B +;-@ciJtB9SKrC@g-O\>P<1;Q&ISn9(U-]52/4;HkbO\nJ^0#Uh5lCt5W?ERECtghqn$kk8Gq +KrA+UFu_bB66r+"(*_t,Z3cd"]._QbmcXAUo9AJ#\aTCjU.;!*>-TEOC]0amroH]$ +27dQ:W>.I`\]\6Y&*7<5LZ"(+:Hac4^*GB$Cn8lGD?(8D2V1]1+38V`S:`\uj2Gb#8a$A!j`'q$(3a`&EA(gu(HqP+ +!VLB(B6#%eYCBsK4O=KYXS@?YprMl!M[!BEulXoHb?t8*ZK@u)qXYg(76kADEUU36mFRLZ_ +fkfGrce=f#i)YBkNWMC=MeO>I/edP_rP^r6"+>S)FOa +j2(*gX>LQNUuu?W>[jINK,<3h_P))63jQ.WT\/EK5E'dfNC)KI37e%J%S^][r;6NW4*`]Jh`pdB$LGP- +lrMBG!r0h+@1KjJeh?&q_CtVDKdSpQn:*-Mpoa)ZgRm"i>5-`i&oq5O/%@$9JZarGVWRVVE1_OJ^$VkD +7s<:nGGljq=#1$X,2B"O;k#fN`^RY*U)1F[?-O)QM\b/bJuFls[UJ#]Q`Y?spXU#A5&9/r>>=q)BER'p +pA.T]D=I>#[TBQgla3qQI3d>##EWmT\b0\GJT0oepi>R`=#pL?Y76tV-2_9JhKDs^&6uU2/6Uc>btBdE +];oP?AM:d0mO!4ma=8IlH+5d[5a[mb#9Br@G,h"MjNbr^7B_@,[/nQ_]< +C,4kjrd3"MNMgVMOQ(IA/m?=8C:sV]pLuPmo*3o,RX7\-RENoG2kh$$;=-Qgjoo,.m;TtVGVp/S7JA^P +RapPp#H0b\fnJtO]C&_)3p,+FdIlGm>mnqN2,CYq3>p7?3UQFp2g92pBX(WfL%i>"5F?M]2iGkn@p_LO +=)2:6FK?e^as=qB+jNc<96@V[I_ghdgORIE.3jWu'"fGcHPDQjrqRBAK`5@/&)18?B)?rd@h"E!lT@=Y:V8$"MSp.YJUV7k&EXH[Z14rrmf7b^fqcSbC@An0](RNG$&*g +i"bSj=2Xpi0b565G:c>=Z28qj]&^asH)g26U)C>6*bi>ErICWnAa(AWKH\=G*@!tSI:8uu`GX%DG:D*K +Ao*^_HZkQ]h9HqqYAG=LP[Z&`(K.?p*h?31_]tOhC;='+V:`#:f\TdXnM\a18W!t%Hr5dWpk[p-9Lr&4 +nO:6JZfaqFJW(^8?on6dj2mL[6Z7mc%mJkedp=u;QRd.JC8%3H#Ca'!FH"s@`))2HYWY+K+o9% +Y_No3'g%`MXcTlN@`-?GKbPI@qcQh`[SN6!2c90i.5iYh5Z2F60:,RQ'_.P`&[r=?fnL8[2J,EOZ]MZ"Ze7DB\>3nC,%sUGXGQ\Li@tgMAQ6(\riLT$D@k%(VGK[fs#e-SZ +/;a)HJ`LgtL@*"!_S*`F%TC\RS%%Z3qLY'iYVL5[KV0C/-%q1?5j/;:Q9,R6V/=:To["3uX.L.r89,lR%\4o=onHU:WLED4Str=aX8nQrgHn?6`"*SDY]lgK7ffHs`#XHm'[n%J==fSA9N2HS=`QZS-KGm#phDGX-bPcXPTo6DGnEgE5)k6mEHs6$"S&aS&Wmam8UBqZ8& +!=T(rQCdso6hbqY@rl^Pd_f8W4EfMg2/$0a*Z2BZb.)>*q8^3jqpOlcs.V$C\s][$KcbTDoO%C4pS?>Z +g;CqE4hMCpBZh/cCl3rGGq3J9J+'N1L1g&,B";g0ULm"josY[mc#Gkj]J4m7d:ctJeP%gViF&6bSA]X> +/@K748Mb/i`-_Jl6^oDW]3QlcC\Yo#oEorD$07#Ir+Vh/:@GKH)eQ7h7X39p<^91e+P=Y*NE/-\g-9$A +C,N+Y1WL-o_`#-aeHWTjJ]g76V'e2pi,nM&D/A.)f0!"A?#?u6]V03)-[bHDctEThM#0I]f(;Ygm$.8m +9XFG!bUGY5RX%5B>R"GTA]r?6!#p=iWj:PiM10.Apfs)+BKdMt'39 +3Tf!ZMkQ/DESK15Qu*[`dK]P&>IqAI?:O2Sget&N$2lS +;S3bB(AmjOd`/Un&##kMNrZ +mZ+Z[b&4W84;BA563n,(FP#]b
C*Ar:J'' +Nqk\sHh-\Ka?d]^Fe;@b3l<..+7\BXRV=)KAI8hJeoRg6oZ0LqP>:@c?sPC,mA_)7i7>6e>!qMqO?31US1+9;@gC@ +,9=QQTNQcqm)ml8PJ0-G +rGq.cN`^KPLFdk67]dN,IX+-79.KJ4Q_JhVIF8oG7o^X<=!oEArD]!,H_PZ"4'Q-9TNAS/p,BYGA`q@^ +:gXFj:O"\ih3T#E"8J?dTE71s;6;`cD`OO1W[%5aD'hUsri!1;%-QRO]60%5#3fEG?YgGL+a(OiiE;L8 +e-XrmS^1qI_gP%IQMhVI;"%*1jTp1CZ,9Z\D+,Q*6SQEECZkc)S6L6&D$sX1nnm3+&`SQ5ZV02422hlW +"(s0>ZRA?7C)`e!c^WM0KXZ=@ag8]Io80Xln=-Td>_Eb]*rE"c^6?<\A+1GMg(P)CN[+R1%$/c*f^0ef +HcCdgen1[IeS&m,.!/$$57poCR5G[Be9Y=rgS77c=a3Na@h29.Fa?#1U(D67lX2Va!SIRn,>Mpds'amF +i'C\CJf,LUiG(X1]?RMSSF?ecc98i*eqN#o5393kG2Sb&C#J>Mm[]0h(m4#"<5X&5YC$B.,DpKd[#djt +=O.*21Yi6T7J$e.p4K]NS@N'8TCWo*AV*kKbU/qNWuo'F74,9g+9QTMZ>AS>U"_k%TSbZ&9^)AomPF_[ +WH9ju+QOck:iHMKE`*Kl#'e,g6m)?^3?TlfdgS1SIphWGJS@t" +8gH@=IJJh4S%EGPkJDh@?*,]:?sdifI(]W%1;dJ)3%*9/._8;Q%s+e;1Cn8PB.mm\p;(!;GnVKK%2M51 +>-sK8::"@l*#&T@/+%s"rdhPj_=+UU_Z*pN0@o.^Eko;D$C+oooS'61hXTr_f*$K*TUgVsZ.uf'dS[bI +"MfF=::Bgq<5hdsL__R@iCGKM0*b8(q;+ujWVidf&3R$5a=E@Re>D_&EAVDNBq\lph"QCmqC3r8*kh1L +`N_$K`=k=_XX2/0"*+'r*TUDp@_#9PKh84+i`bnO'ob-L#a%^DVuR9OU)/3Q:$38rMS$k'K>OdN>i(uB +B(e:Zl5d>klneW]JA:Zg"8bZ='p2;Wd=?tUb=uj/d3*$s,9:2!>riD]=uSFE>[Qec4.h5qeNJfdA^otj +pE%>^l`9M9ZU]Ig]4HIjSoQZLqALe@\t[*@oMhuo>B!2B5WnAc9*fUdqJDeO=```kTFF+0Be)XJ'^44rF6?P1=7G[H_* +bHhm\K(_=kPWNh^O_6MF0[=T#kl8Dlmq_HN5[XI,igtt$HJ"'fHiD'\mA$]:&)R>\l4.]-dP#j&I1@Ht +eaD=u`R>i:2CdQL*%mRC-YCfq7jss0&I>jL3Q=2Y%9SXk7nOaqV9#4_O$Q9VtH>Y5mU>nF7nk5V-*sAImZD&*&D$E1W"aQ8D ++kLP*'bmcJ=('4rHS<)>QEp->2000iN;]g]f,5=4Xm*N0SnfCl=Fbrh?W<5Nre>7$M#fU2:%0Hn=KJ +c`YFs]IbOSre7f=kl!IWEtk`\I,0@XZe\`!D2S@tei[j6H.k6iG`tSNrJlCL5a$Tl'>>/>b*X%Bs.3;,eOK!.L=&'TE*Umg?\,kl03cS,$r +pSRj,B^JTV0H7L9qqq(*AlG%(UurO7%eTq<.R"%?rAK:`hCnnjUTh=-M\/mGFfiTD-/P7qMtKP&&A@GY +)3\aI>0KK_qrQUR[?r%+^KR^s91"R4Z.[Z+4h0fmYs'fN[&t21MmT,ml\kZTEi!]7-F=UeC9b_MUU?[0 +OUDeW(/#?F/uHURcIU?O^%&e0`S>Wc[9-3F]e;lS +MtJR17_2R%kKm\K;O5!E[2;E'>)\hPb^4QAR.p%!@\a18)!@'.$64Q'(b)62_,V;WJTOL9k0iF-0s!0F +1#GL6>0M9_C,'`Q[9/d77kL&%nC`,q=<_LbQS+L_LUQ>sI6M20"qQ\;S`!irZ;N,mI%L\ti8YhP2XL_& +6Lh.Q$sGa-3oVZBQ87n)EK'Y3$Lra081PcZge[aeZ=/o<'s4\bc2tkaWt.Z+]$=BO87m2[q2IAAmMK2E +T/__A4;^#+>njq6#42oUn&+[=gqi+Yo@FIKI5t_lCLX[\ED>rLkiC!V1kpt.Ce;e0/(+IJ5,7YWC)nB% +1r]u5NiR-C3p$MhXmD/jnH?Q^bEk16B;](<`+RQEHdMn0+(M@9/#7YZT2s@_a5HP89Y2e.2l:T= +NZ")Lrl!WhhLgU(;`n_+d5'[^*D3b$cjI>>)c0"*n6)@<(:<5gE?Jp)Y&u8E3l/XPgZ^u];'6P3Zfc&) +e0KKQYp7_!rdYdMp48G_E.^_7F4Hb'8(9TNW+TQT9tM^dfh1-n>LkWhLHWWq$cut6g?WS&jkNm7D*hQl +q@bl?5?N7]bQdb@E>fMsmql@Vnn`]IQ$V59(gk>X/nuPW<@HpI,2.L]/a`qYr6c?U"[;_o*',B#BA@$C +Z/GdKndd=X^h4s(:4B#W%,Ol*>m,;"',[ +[6qPI4EP78CZZ.EX,u>L[3mi)-+_+&S!NteBS@Ne//?oC=]UM9L\Tun00f6FD-/Bu +fn[p*R.7$m29?AG_]H5)Z,N:G][M%=QIqh;.-Ms@CR]?*FqSXb1>ClZE6G0>)3XF3jQEuHVFdbslGLqb +J\q?1_Z#LB=u^`Jr\%/oo9Zj35I[I04nuS4PS21s/skEng6qQRmhWuaDj'Ti0la9di=1hYj\JS)fbr+#C.neab$d +X[T%Hr3]Q;lP:>`L;'hL9g_6pTGFKfZf&8dmqrFGdXaqJP[9trI.(2%Z]D;M$!S$*)NuNfdGHZu +98>tmBPCqO,N99qeK,R'(8!Ufe7_a>n53jmc':fBTqML%N.^l +lL5RQmrT@+kJO[P2W%b!='LJeGe:)F%AU&Q6;ne$j6VO^*IHIi,P$g\_DhTLf>FHN+l;[6DsbZf&:. +ZP`4+k:&cHpf&5TcR0J^:HJ!g`mGKoG]r@TSu:64bGehi4^4M]RV9FMFD/Zq&)lcu2#tc[Z5! +4F#GqCbDIX6,tFCo@.E%b\YROH>%Z,RD%[TmBJ8J^""\GI%$7H:PfMkXiYc*?9T&pOmi/#<:JOD:#GW= +U\g^E*pH7MQCd`j%S3_kF#UP0,2`(J>!II=8QEG_Zr;F]YeTB6,j4Q5iC`K"2ge*K@]sq=cskgg8iU;p +"c9[8NeojDU*d8GcM*?Q>UY#C9V,<"EqL:&hlH.Vo^JS^*^!gVkl?/U*MA0T"+l-M'-l`L`4:+sqo:>p +V2!hgfEZfSA>@H2LSqo,Pr8!?7ugP,&F%PQ"YFb'^D)(kQQ0kLVo7!"KtIUOHB^(R'O:$WZ[qICf6Y3% +2gEf'://%%EeWP?p"+OX\)pa"D>Rcq,)6`%jCmll]&e9uQ%S_Z>]Ym?cL+A0>5B(1[LTH1r,V^]mu[O\ +kDS#gLVBGVd!G'oB,@-4\]u&%]#,oJ,l,mgNRla=_/!m>IEtnASh43n3YR)C&mk:74i&8[&)W;QIUqd6 +j:WtMB3Ldh_:e*#'glI(:!8"J)(9#"ANVmO&&H`Ka>5rt)RA!QZhR/f&9*e`1o>WF@6EipNF2)9""cYt?TJWf +SIZ-XK09Lp56_JJH+Kl\r`+DIrKH +,=(97]dmrWd*[^BCsF89Mh)]4bVnb?_(u\e>'Ng^V&3bD:.Sn?S%,rtj97;Hk(s?lF)IVP_="WrWC5XV +aZEqo;R,=T;&?ek,2'OR_4I'2\b>*T(aV2^)UBWEIp"uq-N-[A*f+/?]5(lfHg`\s'Bb]!Z.`O0V1h/- +]/SZ*88l:g]E:G;gAg4^IWIVV15p\/b4icR?b&@6+5m&h^(&'h24cKYHF#t>iRD7Wm4r8u +?&WP\B.a`FN6kKZYnn8(6F2lm+j#PghjAYKqkfeE(oCR@hqO`&"<"^'X5qYS>D-KjYV.uE"aS5a7'8E) +Mm^:a*S]SQ?)&t1*4*CBbF8"^6TTDTk8YGR"dRDoX.'c:/ipBrK>!P5@?u +1S0J%68tJr2ek +&iOJ&fVRP3c5LPrV&`=[ZaA)>ei!))SLF`Do!b*s>sE9-C%Y\6NUtSPRE6$o0r\)s[RR?^4.T6e?>I\& +`A=9GI6TmNe$oTl-ThW82Jns%0n20`qSm_%28ER_Ot,hT\L@&_cV8[6(L+G5b!gTa7a.&Tr)3N!]_A!7 +iN##e6]Z?CL]1J'8hhBW'n61XdHVthPgCj(k]Hc!;s-f;1kjIqmH]u8s"!\RY6a$D]UlWW"*bNO;uE9e +'=gCRSeu]]iJIKO+PraXrr(Z5WdgT7$A&p9J',JF2<5cZ7pC7Z&R44=@\/'c;b*qBn2&7:VoKiFJ)ZJ! +eSO_Ugk@n#kNI`4c4/)pjkp$P`q$uII0IuR^%c2e:.kPCDFMs3ZHsf'Z +7uo[Af$OF?"T#TR>;n<%48O'Grs#Mbke`O3XK`(L5P%<<1q`/'f51A?0j:%sQ8OkB@-/>D=995gZkJdB +6u^*p`#UP19bA@fKXbL;D4R\@2%B/0aMV2h@TaiUla\.Y]mcX'phW6EM[i897=kWnnDD%%E_UaU.^CQEE=G:Wq-uRA\69iU>hq&o\Uq/9GJ]'>3Gi1.Lb2.U/%8n +3fjehOECZu.ZO"V(_oNQs/:);)u!/R^]QE;8^BJ]n;5E%UFmQi0.0@gm*;&OSCr,Ch(I=^jJ4CX)g3KWRj3KXF_=&#="hF$'&P#hYSZqK? +d]^!b8AE1uMlOGJ]fk[?q,_poG4H^IK.Lg2\&WoH)GT+f)hO2qgjtW15`$sfWGg!$.!SV=JP*SJ&c'UX +hg@*U*;XE2ih>X7NtR65DeaWZ#.60B;Ht+d3e1S-AtTdg/1HD/%&0pT22M*D`Q[P4XEg3%AR.ZPPnd0C +UKa3!r>f#a`CE9ji^cSn5c/0s$1iZTcV=grn1Qa>G;3B`m]*W+,JXa[Ll("cRod4\#.o])C=6&ZEZPdj +aS"H,H/fccR;,r^Sb)Si'TF,M&DWe`5.me;BCRQi5N_8L8gC.D!jW +-[gX,3)#B=Qq,28]/>.hEG$$c"6$$hq1V6? +hU5LfLTO]6dWr[+oKV[C_i8i\`c9K`;o_a`m/bDQZus@oZVm1Z8o1N%N8)B27V4N\,f1+3rlP',kkAhHUf0p3;3a5GImLD9n2#ETVk-mlZ>@RGa=" +fo'.]:>@D`miI%iINS`);;F%;O_7NKqZ/OPn+A5I7+U,U&SKLO3ph_i.^I3d9-2jYA*b3MBA;:5*SDrV +AY%l+R9[&o^]g`@LKUMh*BQ8n0LIU8=QRg_X@r3oGBm_$)3K8a1K@\#*U7bZBgL=:F(+$3UH)4Rf-qf.fEl/tqHk7r.iaX>1K80TIOAGFQu@Z'JWE +?A-k7>&&ZI9O[=2k1#j0Gc/7ET^j;J^5HLn(\JSaHrCs)GO:S8R&JiP=o/j&(XF!JjXY^RmrbK7SqVktnC0-O)Yjf?IBnNcuYX*5FWhY0s_>YW?&cT@5D%chi +7ttjJ$IkKuX_0/*j]54MTUs7+[9"(EIE+inb[TX/#5;cl'?ou5J))kYS].O*kbti&p#UtM[<,C;XBP^h +*fs-tB[Tfk-b.!*79":aR?n5OUm,c,V9I[%nj,4c8OKX/h\:KRiXaW1CS3BW\V4Bar6/(`mr?OPe'/iC +:s1-J5U?/'OC8u(@cON9M\GeE&Eg<1lRLiFLpmcFakYGl3K#FYQfsUZjH`krs0&bg;kd0,<])+NWg`HB +=k2lgU-$4h`D"$CItNDm0`jU]$FD9tB,oi0?n@Ncb7A7ZgT#o"/air^5!Yl1Pc +!ca]mUWJB#cEjD`n4,lS3?"th:]g#/jNk.".9frE1*X8h?RCGp#]XbEG[dh)f(m->R^"X!7J'$^pnQBZ +Mo(DCi1K-1TNs`XdU@.ct2"`"t:>B77TiCN+X)O +0Ah[l\)Hm?NKjSn:;n4ar:]ng6*s";QPOL'cmV)]%JT8b0!Xn-NVGo&?k4pW)AWr +bnd?_Q(Z+3>1jW1^P+ojgM^ljZO:m/H4MFA'ruIHrh&+$8BZh]GMJUO:JO;YrZ6V?X&g<*>>1Je2^(TAmM:KJOpqG)^gC2#hY8mL<2;-u24B_@*\X\,gNK]+GQ?]@J>&-bPb]nI!:6Bc7 +n%Qp4f\;THJ7qYVqu'/!-dl=59Ul*d`lU6Rg6I0-OP +;1N;i_iM"L6B5a9K>d2/,7f9IX0O`ikq1sLH&E'f]6+`!?ffrYHc4UYrc53ed;[WVU^*qbl#HC#?YrPHfXe7/FIqIhUSZ9X=j +6]HHVWGWQdZrj53_q#!N#;,(IXuokr:-**o%c7N!R#n]-K0NOV]Ehb]Z"pV&>`1a9FP,^&@eY=+QQ:Lq +LYP.]0chp$mAdN&N_Zu/46n^rMtO(JA@1po$sKa-hKs"B^9FpfW6eub-dkpA5'OCONZt+H3`Cs1%*R11?qG&eSPMS1 +_-qDm=6G6_Z@tjDR>clkr1=03ST3h!k<6_:pN.3I:tro7o!G&be"4f`l?b9Na8c9"H**%dL8X!-'i9:PNKk +Pc>s3\Vmh9&S$P/9C!edofmFigk4YPNJ";-inZa/.q$C-miZI+k48^-\?DDIclN]FN$1p]X)0-63"Lul +4t`QaGCp,V5Udd^l&\_([rpL"gWDZKTEYn,aF-8YW?FGdUT55GP?i,gTB19K@p8;tAtZ`3Q14.Jo)`@g +aEcPUF)4uPS+J)crl,]+(!bK&RLAg,P1$L],94Dm^ +*GI0)q/#5CBLFAs-4A2=mKdU!-U'\R*0+p7MWc!LD$g$E'm>)8ZM+0VOm6Fp7:5j([%MU>):[&Z:W1$h +"OmXEHrd_-Fj`"S,8>P'2GXnD4#oste)?-&qIJ)+4]!.c)!eQ`>."96?>;+Vf5n$s@W"5N;EYTc<VXNt]Xo],\qfH7W',M*r+DdV +:5m%$n``>I5L7c)hdY&TGgbRhpd+RUe[('P(!a3C$WcM1>l9f$%G2urjOnh[D]a6[?l.Kf)mDf4=iu"O +WD+;'X]O-,QKG/3$aL0rdl-%sfs$Jt/aF(+dL.i%K.c+!RXa9*:DA7< +ijL8KG8O'[p!jX$X*THR%Feb@<2f>Gcs5K.BX@kg,>nU=@\V>=>QLY[L^dZg4Dq\)Ud$sNELE/S^;A>1 +a)2Ku?/Z)nTFI"+;sUC6jW9#-3,O:TV443f_nVEjgt:_LrO*KSf^NbWZm9 +X*!J-i7WuM!1/$I2Pl=>4<*C4,.>6SlXFJFH;RgmFs1??HAEN[8%:&.^!&gDlpF3ZI@\c@c7HSerZ^KS +9KmeY#N4P"q18$mn2;HICWT)uOPsb&BXV$pTEhlo$"5I[\udrcF0r[B^DjgObe5DFXV2bgM+ekt#oft+ +&MA>kHSBAXU7->b40V&#eeS-]^,%NFrsW3PYmMHF_^T$@`o75p?3WplgUb4T'=3Qp$0.GJHtt\[Fu8^3 +f"_=]B!!_9$rfZTq&%N"Ekko'7+aL=B!`X0c:X&]ltCM"ToKfh,1+)qCi-Bf/2BYb%afd_AKQ$YJL=Ju +XMAcT4R7!CL3J&pF2^mFR5M?\a_rJR[j0NX^'nnKifXDs0]E&'5__J3op*'a0uCkt+4#Y@=L$'jPpa-d +2emoE$h?^^798IU9l0pn5Im1r +7),W+JS+knXfLS;>D8ZrHWpXi6oF:t).m,^SgUhmO8QA/*\ju/;Erb%hsc\lG1SQ!KcIVlZZ1[pqU3Bn +/"X$GOfKRQ2n[0Dt_(j#PE7BE*T/ua`,q$Q"b1N+!@=33)52G53oG\;$ +jnV*EOb9CnUAU-=n4/N;[^L1*aZH-c]t>#9?\O9S;27*q +D+M^(gQ"6k)f#k97@T?k`/kF +]e8':2e55f*V`5VXYGH`.1s39eS]/dIZIi*a.E%Vn;un8nAhP!LmPaP`VG3WK>!+2MR0uh#FoC^8d09- +$7:@198q(QrGohBL#+mL.d-)tW'dL)NgcLq,LP2l4NFAj:L;XNlE`S_T.]2U3>\lOs93l4Ylf#Zr1H!F#7XN +8G,XQoj1Hd?R8.oUR/\\nM.E9E99B*5=OI:_.jJ;?cSZ5oh-$%n%KH&i`*!kpk3f&Jkr>)gPU5/lgR"O +=C9naq'Z)-PUWlgO),cmW?@RqFY%sJcofluk&J16d(:=1<7E@K%t5b$GJZ_^Ml__&7I,Lb%24"P#FE^7 +#I]46J;GLFT@TI@W!Q@Bg_+\h:.<\oo&\CI$W[[2*;d.Bm?@BmT6lFItu?Z +Adc@Wi*eIiPl=1caaNY,Fhk[j+S9S0525g)V]g?YJ\c9oXAnD_#kRq_GXSX(,HX0)SN>BbVVgW'kAU-d +qF\ABi9Y`nk-oj$3UV,H`-0!/n' +^Tob6E6u\WJZ*HX4*W*SC[E8I&`6/$kVssBZ4O.Q*$`5ILdSTM^t;pSVX98S\+2>Z0-2#1fLSGS^FHRVSVe)-0L,>i&^7*ll,YRj:)e[`8B>-T?9I.t +=X`dV*nXX2rE?SN$N"E,acpJ8T?_)c`rH5adO_]Xr5@A#CoqYY*Te)qadoRT]P5J8?&:K;\$YUY4LRi5 +ONMWQ%jrI#0Kb#0Bb0AfGu"#7^;W*pplL7qZkaL:$(p@(i.HDpQP"9fj4m-aiJ1!3l-.tV +F5_eJV3b1Q5$29?B_bTCV_@)D++tU]eBYUf6\[84@j18RZqU5Nr,3 +n"[")X3A8<(>)*$XEa&>\_-AKNEY-8cej4skFYo!?d/6\C*.8Q>6-3'3MC3)h\UX1Vonm>_Q"!^F4I3Y +;Q"C5oNI<@pVMkdG=ds/>"S$B8ME2I@>(#:(bq;0Dd4'K9Gn!t@^;^e4phYD`RWVaaFM!(6_K)@MBTI\c5WE2ZB/Z\9Z5:$u`"C9:_%B%Or6F5=dG"EQ +GqQjJehF,&L4ftt5^7535WIY,76Jc@OeWM0F<:FQ8XITS"q0#a1Yhjr")$s"n"6Cjg]R?Gi]%>RKF!94 +0QopQoQR`?a2\cek*poW-:H"`O&j8oeOq:?GBSr&RU*Lu@["<[qpW?:ep'20_A8DFp/eTD[JZo2r;E;@AqEBLB!_,^Dg[C-G**`&,P +qgHpUTAQ("mD[#G;!d/j9tmQljDQNbr(]`\#NC+_+N0c-IPAg$M(NBE>rR+dkYci,o#;uZ$13Q=&fh5i +>!'dMBY'1(8=>k@k>C"4eMpgs?qhNV@fXo%?[qpf'0@Z&QTlB9G>VAqhIt_HMlj*fn\'5*F1b*=nV3W=L[UoP +gZ@E:<67@u`O<-)[FfBXVpJnbEAXB.Lf0-W4s-o#`O9BE>`^qP=JF/OM0dUd?7iU-heU;n[<1GrJM%gK +6b5D6Af\NC,,F6>&9O,:T_P/ +:lB=8C'@+(MifWk`CMk$Z*jA`csgcf]2OWi8P4)FZs>8lFFPq-6LL`ej3"#1He/_6(e:'!N0mdb=5@Rq +mX1fG,;#k531&,njS756lb#8rl'Ag*@\cq;Z*e^M\(u0r/[dS1+19bLh/Y.r;Muiq(0e'E(SA+9`=c%+ +]l\hLg!*"ZU)2o^`,Gn::TJAf&%_/1>OT+/,\=2H,\9;oOufE=pp>ob4pGYlIdJ*;m%[i$m2P')]4U^5DeupB.Ii(79,-+Ika$*c#J^$jZncg>!c]jp +DE*8H0dq4P*u[Pg$/euOK3H=J$MbQCo*f=REj5)CBJjN;^NI?>d9$eBd-p/QfpBVYcA7$AZ65Su#R +M:8DWn5Z0dmZ)0dVsO_JP*hSCO='VuM][R=>NqS"Et3%uaGTcWR3TqYhpKf.G/fVTNj44US(/+\Z(Hr5 +eU0%$Rpbng,$IpBB-@M.L@j"%+HIS[:@@JdOETl%7CHigB=i'MSTqSa5NOs=N?%N:;%\[u)`8OO9b<=q +V/O#Irk_r#&1-NEat*hlR2Y)?4u\K[mhe98G"Do"ki;"\.AL+u*&E@cIjGgSDs9RT]JQd"($V')d\#t[ +ZlT+GLm/q?p*!m]Z[X9H@dq+`13.1_WXF]ng>U:uS=bP*$lppmSeQ"B6n!@oCX]Tp#Z7q)C[TRL)Xl5D +jm<%BN?t1HIJ75Z*ij1gT6QgjB`k&)Xm!7X7BL7BBM(Xa&@j?`DRt1;UXani/>A_;nnA--aeoTVY3uZ*%fYrcp6BB_)KJd=H +rS>dL1@EIBX2ahd;ncWsO*]n\Vp$jJ53j(*P3=_\,U[/a7aCP"beSQQrA0OqPHsB\4\O*kI!+[Kp&6/[ +"n:SFT"7e4gomfD>>sD$H@$J0?:hKB]DFEcg`7r75pp@emmjgjk0(9_E`305S&&;PH)F^-ZZVG#;9.bg +HJe'te#mWFe[SPd<,#4%P(fTN*mSaP@3V5Jcegg6Io&339m=#06*2YhTMp,qPk@mSmUcF#?#TD)n.B)&&t*=,<%ubD3'UKhI*@+g2^cVS +aAZjTNL0\1]"Iom,TB)AkK5!`&;3A"(>I,4aI__&+G>%PN,+Bl&f>AO.O=J)^8^E-ESC['t]JKt"ET[[HcQYT4e9/ENEjh"/5QtE%Gp7B(fub7O0O5s-*%o +@#&e],+1j*0a_j,iI%<\FO1DY!\lq'+V-n?G0l[:5Oc]&+6\ +?+EueG4>@6G5JD`G8@6`Q,ho-NfqG6,?/HDYKZ@9fZlW`_nF/4G,T=.nQlq+KC:Nt]j)Yh +Ff=8R?e">d5"nfg2u[QI`VrJebMQOt415;F`dPV)ml7XXLbu0APqm:>.bsK>8c?bVf1DQGG"B7Dq)+5' +=ilCj=oA-S)Xbp?#k/@fjIfal3X5p_[Ii'n+PN*XB6ctNd#,#[2Jeji+VuuW+RnZYjB*Mfke5n4gn(Q* +j+\:s0HN\oSi=Rnq@O]P3*.NX+,Njohi,/Zj]-12)ofpm8"jdt$Y3.]_dqRZ<&XKOh0pQX<92K.qe\k_'K*crXAd\c;!BnJ@tX/!<)45=9: +Tp'PqH;FRd!?+;pD#7m_+2;WfWno8,(13=IM]NU:S*FpSF^NMlNjS;o6$+M[MgApo^d$LXHi9o?B_Ri2iWW`f:QF$ +?gh!*&\_H1%i[/7LXbWQb[9@q@D,aO%-stRd\cX9B-u*Un+%f$cD3V<*5C?GdXU<&UjD[[(DItF09$SY ++%&+X?OroIFW8[H1Xeh]=^>4iITc'NL=Rk<0V`YOUK)Ym\Uf"Z8;c`/1:.@*f'OD7;a6LGZkIC[Y^=0\ +Fiu/IJ/DF>5'6&gfV9&sm.!JI%o41a2njA4$T?e%P8O%-2;EBACOr8"9Gc,/j\u%Kb.se0E2RaQqqKK^ +0O!5OZ\fjQZQHgG+g8AI\$[B?3.1%PH!Y>p3Kmq5p22+cGG3'I^BV&u37`+S9fs0^B+IN&fo1k_W\*uf/F?0UR*"?XUc6n]]h%e8g_b-T +U7gI,O,tsE59aHfl+aRM2%k@T;fj0oJ1:1GCh+4RFs>!-qFjSk6@&F?@J?0@"[k+&PBS4`]Cn]b[.,"W ++V))cjcO^j;1DSN5el6f*4+\t:E6)j@>V2FBnVNENHOO^K9pD)i"F$)KFT$.rd0&RQPSZ7n(qG_AC#Q^ +MQODg=+1;-_VK>Z\@A>$nlY"#2s5h`&k2E.R#(I$1YK!Z2FZqOV3t)8_U:u8M6Xbl"-/gqg(sEU?Q=-M +&V\%u;`^QMQ]*@%YNa9[-D-uj+lj&+SQ-P3Z&d^/kSS=qV+A +M&_5=;nM!V@k&Z6A!>Hcrn=o`-P5rW#GS-EoITa6'8Xon@DGoYSV7D6/ +HcDW>:+heA'9*aIhB)ZQ94.fUMJ9`9L7:ijNG:+";BHf/HGAqCU:FW!I??Lj>,$VT.Iqh3W2IoBNMF<] +Aq0Y]jKG%6)[suDqd!(<88G!bVYPt9ZMaoHJS$l)gSCJKOhUdhQ+u+A2uN7;48KMS)*,@fKFrF+hK(XH +6_Wr#3i+.>fOu"^ZO6BfVRM!(%n*!oG;S3m38%P\S`jSqr7k%^XN_CZB\CZ*K/SmbgQ?strRZmj]N*K:ElZ`k*:bZCJ)[i +fo$LRgsE(M&*+_;PTUX;Y=i1>A-;%+"ka\QFhsWS&'+-_Im2uE"A`s=r/o;L)AQ.Ze(XjSUF8b&c^FC@ +'.3?BVbcOF8C&tpl-dN:79/+7kl9fjmfp>FIpZHJ?%=C.j5qYHNHuek +],?PAi]Ym40iHpK[obCE!dT1Rk1q-t;>C2:@s3qs0Go.j]UOHc*9Q&WrQ1"PCn5s;Z>`npJ[+i%#=u[2 +7a*j9f"`@oeEEk%a&7T^%'uDNp3[]GlLKA_rCq'4W?gZ35=qg9j] +B(3gGjPh3W@-6:(-j$K_)k]E`GOr1NeWl;>_L)8mhTa[plO6frbo>[pBraalF`MIn'oK]jnnnsCFVCM&SGW1iWDY!&/>MA+3 +aLf"?6/ITIJ\2rp&:$4us.@NI!PWhiX\o$fQZFj3d$;MWBTRn8^hbPDN]UB +Ea8']pufo6\QUP(@A.M4h+n%mH$<[X9M]oi' +XbcM6(.6s'eRHV3OVU;RN3jGKeRFBu8*eq`+r;L7/X@flfG59fUk??a/9/`@j>TVf+i!)Q1$%.]gE?M_ +H4%2$=,VV^C.4_LZl#``[2AIPoNpV78;eXpk%ZX3h-;POG%rbmh+3Io4/p3O$YhEWRRg3tn)e@>N_Z^m +&^k-a9561ZNEQa92pu0V]=D)RGrO/\aLdQM6f-)<@c2]L0s1q"Z$5$:GrJf_,mLhYlA3erTZ#AiTS9`i +k#-M_*1pCCPf_M$jP<@bJYpk)7&e.k/AT)7m:rmokKUVa[2AHLYki:p +bui1o1[0ScV7`9P-$,/JJbf_reej2M@,B.qR6(X&UXnr1NUZEco)uJ/raam1=Bm#DGt9jSh-=*;]UKLe +n!&#P'RsJ#XY>pQ:kBm/?>/0-)_9o,imbQ\V"kN@FbDtujFmVo\5Z%F2WN_%o*:'EiTN5kVN?"C3FRh] +3jWk\je47YWC^U4WSG.UZ2'YR>#>)=mj;8808.-OjitC<qm='FIapNA1YTGrJ!<1@HPoNLC!\.,%8C2t&*j0_n@PUeTRCkKUWL=pK=3 +L=7gSFs%UifNg@a1@N3o8^4me7lTl@kYL?sA8BedcD<_n&QR\INLUGSo\@(ul_lM6]Gp1$&P[SaIjsjK +g6B[TO\JD8RkiB9o54a_jsE;%<;$1M8*8Vm#D]_d(RTAZ`*ueNIuI0XKk)&WI<>VOX'P-J1)gjIB.a<= +4@V!1=q^F)i2**%$Fp:lnK\:l^i3&&Gl'3l5>4';^u'uG2C\-YD>&N2.`:?=^Tl?H^\8iF5%!Mrpr!>[ +*<*_N`J@bZAi?p"&2R6mY:etf1teoa\k^LgN.n1d@0Qr1TUEb#qCb16PS +oGt$*c*9T2S/ZaVrnke<<*QtSXO_B#dq%pN)gSTTBo9d7Srq"a;VfE8T]jq"oIMpRe)gQ=R +hgmsHX;+-pgXP#?QRKHXkA1gpA`SW']53WTe@PJB-3rJBqi*`<_2GSAGgjT;&LDb.S?o.De(pV8qMaI$/_aqd<,t7H?IDAgT`6MBDX]eh]3`?<-'s.->a@^6eDR0bBjf7dFR@:tt]iYBbL? +c`r4CjE[nB:j(dq(6RdY;BBc2+"KO!XR/ZCk@>O.IORH#qd`-VVr*Ptk@9Waj?X&%n"QrYK%nuu +H&`L!=2h)sT_hs]bk`>1HkVH$#Q"%Q,0un4o)(9;L)E^'cW@LnX>f\irSt4+(1$fKd5C_9(i+oJo:_ZW +]7'cLSj.=]^Z!tho4MZVhVZCq/$&t(>]D6QQs\?HZ=P] +r5%JaSrWsoh;rp]Dr`nc`i;NW$*4&3D3*'Q7M0h.@7P,T_$K?"GWlsiSt6J4=(cm,g0`q19Z.=tPSbf2(F6g+F +&:&/P<9_ioS$D(Zm)L&jmZtS8,uUn"OOhndc.X +HR$l.KTOhf=@A&.l<@VQTcQ:*Nb,16>#rB-E[/oZj3,O&CmBc:G[\Z\>r"=J?i([[`3Kl-c8Fl+7r1p! +N1j1Xbnt!86FlX/f!T]@ah@:=Olni*A)qp,\1p`\Xb`Crr7StbI$Xca)gRITs,&qJL0ESo.5p=K>(,"Y +l)\(\Y-]V^hZT`fEus5[o4M\4F2Gn`A"J-(niY7<(p>J0fu1Sc7\3hNcc!1?n>Bt"iO;T&`+hanks<\6RbU_MW&DV4[TRSr]Y[2rg22AquL@"aVe2Ok(IsD?[/],ubrASgGe&Qi[&kpH"+ur@b\: +LftbaPn7M6@a&thG#>E0`-*BOH+q*c?-$A:96nPO;P*ElfHJXdK1K9X`\\l^NO>NP\"#OD`ZI/1*#Ao3 +npDFZXJom,flNa5p!k_c!:3;J6l5&lir,mhT)pEXsj>h*qd+WH'c^)@i,L;-R`j$;08DjEoqYUtgkPj>.2tl3"Se$H2 +M9,9so&?$aWJ)=MZ.0e=5%[9`bFI^Gk`]#:k!"rU6)FLS`#EluBRukC+V!1F[9a_UJP$ +XGb!&/A%&-Nlo8cKDH#S4K'&fn%l>;c(eHb.MrehZ;XbT)0rC62G32T8Ff9"^QI^RXCo[/IW"5W:+9dE +@o,Ke;CYF$&%n:<3"CWR"QWJo]hE4qLr%UuQVE?HiGf>`sNBGg,%a[B)+m)ACP\cdIo),(ei2\nI +R^)u%a-nnQ4DVm[T#buDCb]g0Gq`0p_*_^fFr30h>c29hrVtIY'H;n;2CJ4]`3VAO]`#VC:Fi@R]LN7K +=Z2km?W?"1G5qP^[tT7`M2GtrIrI)o*CFG1j.D141Jr.+oseMVT3q91;/>ZH5%5#:n,"U.+J7Q_5-,=s +&[quime""P(f?aop0bge0"2)&l@GO.&+i'Yi8]13"5N;)?\j[1"bn4-0ZY>X5`^^e>;--\cS%e@BEU*_ +4>IWb&"L?dCmH+AXADF#!o^Vr!9(D,R?[IBm-)F,(sWQ>]+CR9:MU(djt@rbkBUtK+9h2\g5&KHp*"Td +$[9*Brisrg%MKi\:h&Ab6:[_,(+sQL]LoO@4F8,[4p-%qpd]\NS%sS>N"Y$L6,Tp;(':R.;b-FhDXKXd +iQV:g,.8#^GWGcf*q@*)0oc_l*`V2pe-f#Fa/qhs3J$0_*PEl"Z/^)e[g0$"`iF/c".DKJ$ll7,UCU4^ +hmrJOei[Uu4r\a_J%lPJ'"Hs`'5.U8#'BXp$N+DK7/o?1"c>Jqe3@+j?QI2NRXR8C:h^R\\Vjf8i5E'S +^bJ-O"g>7Bqe3*%Dt9=XHdN'$8@/>V@U2DBiGoZe'm[TOOd>*RSS0m^3jV)"6!as`gH,+D6u/?EI\u

_3.066!Nt9Re3NCJ +KXlp5B'\)7R((1M2/d9epVm5?TG97hpoL8p7=P\iVbQi'_9j$3MKd^&8?nQ$%LGIYfAWDuge`.[_1qE3 +b\1,5%8rnUhZf-r+O"01SXe*Fp9(YKX7!K=SI_gBETMJBoW]fCjL?$T%./&UcW01kQJ'Ea\En<_G^tgM +g=1<^_JQ%Y%tF'a^u0-YLH7)sm#ESA@])u?RhfNOecN9KJKYVr/$FWn+]W[NiHF&-a%Xl^r,>K6)m=bGkA6D>3JJPDT@Ved7Ts6L`dQh +$aq19Vlt&0PT(c1,J];)"eIl=*$I$q[-31]gu,Zkq_&u-MO;a;8_Fsba[Ff"=#u5E1C>3gPWb1J6.t9f +G#FVS.1;Z1or["q-qg(p^"=bqK0;ZV=YZ\Gm&ZA*:>&3Q"3Smrqa?[>l;c$AOm-gX5c%dmo[)8/gQ(!CZ/K"PB0%,']`@X?]g^GO]L:A&UpY/:ZdLnAqWVK$rX/X`/dY +8e*>D+mm".XB6JHoIiIHP)?/C\IbpTYWN+d +s17pL#@0[Ym#jE]pXY?4B]HG2%^\j\0ogiu3_-rZ3&W(e,APK]ncdcdTgmL7;`rQ>Xf\S5O/EH:i1!D@ +-,26"H834slsY\#.(%Eg@1EWZ2EC1OA_f,p<\EX'C&\fg*umqNNbG9NkRr#4onhRoYaH^s/k/?&1':7l +):R4S1m?8CRZ@X2FK48Fb-//XfK/m-jaqBppJR%CWsejF*c8uUm-b-!31;Fg1Ies\8s9`(1aSF8Ai"B/ +jZ),o_*?j+XtFFnMd;8..]_I;F]WOD[+MPXZM/Aun-HDRc@%4MfP^0fTO0eTXhMAiLTa*LCe+;IBsA[E +!Rao]_1U(gl<6kP>&6.!1hj5NTL#6i +G([/V(>BjN+YED?=%R88#C$AenqR]4[2.t8lYWIulrqVsS*$Kbp.T7DJncmFfUf(ZZOB +fU`oh[+Lu.!GSfUg?lKfqa"_iT%X9IY-ch&Ybp(\Nh@0B?13I_qk4pNVe\"_LHVWUG(YI1(>D^/>?]5a +;.jXK?*R%e(>BRE*a25f"'Y&$h1QQu'E=11]/[<;Jo8qE5@`UBd:,:DWI85.8*[':+@i6HS20bS/L.[U +mcd_Vj;9tX`HG&Vmt"Yl[+Ofn(H"5#;N'oh4U!G">e@1E+:ghtm=sGU[+GUh+ENREeeWUqI:/V",A*Ku +IYCqbBMjB#r#Jo>oSsqon2kOV=9j3Cc!+9c*!P"6Kq>eAF[Xo5rUq+2'2eGA&cK[$g!5Z_%U%oqSpU,P +3Zc2!.bl#()A)dRnGC"JE"&0_:+?p"nsckjmMXC!^N&%'pORah_EAh+/jT&4*7EhCLJ3XJr3gA`qB!EY +-U!0tg('r8Gj'86o7leCa-5q)OrJ&,G(f?9f)T#h]*,/!&cA._ZW5Y?@>>OAXg/>mK/j[0Y_$A"m\Y<' +P5Yr@[QJ`)UQ(D!oCdFL7P+CoDh$f!1OpRm4r\O"p=V&5Q4ik-iX.>g55uQ4eDu(_CU^VnR=jH"EXpmu^firmH^p*sHG4lBYY.Q+KQFA-lYe:t>sHfu@k[YC +5=(4Q]i0hiGt;NiC8=u)Nd&6Aa75nT=njR*8K34qc4]tADtQAf#5:J4&GJEY$m2&O^b^@uet:,Yc,J\Q +ak?M?B7jLspV@md#UcuW`+h$FPEI`-U)-e/,.].^W8I.-#VfpIi8!R`I"RcB09t&cL=7D4b?,PSJ)5*nap`] +2VLqY>!"kO2%Y2e@INU#h*rB,325$V7\C5AXm=XI:O-a'.dsj-NMdI"1ZXq[Dqi[NTIGN:M(2Q[K0.sY +9nKPWBULitE.!CP.n4hqf`kIA[dQ,S))K1])2S"X3IFjh1sG7L[mpi(:`ss& +2u8J&[*gR`2+_#aLj,0W7W5P`Q_]*58'jmL^ZFmI53qDsUgtX0dQ3gg(Eh)$fPMYC$)Me*8A=Oe?D7ZI +k%#FOU]gjW:%'AQ$$J1sY=56Q@R_R&o8HYipG%h>=)p2D%mI`E3ht!-RA'q/jdC5_ +TTP9"JZsGse!#S/k^D_^j%5VanF=3\rt1B64>^a/L_Shr,";N/7*7e.E(6AY0<4MS]79j*;)i7GmPHo( +^g<$!oljfe*U40,"&t?Zf*=)r2P1M:pO1rN&DcsR4n$9k?2c)B?ph82%sS_g*ip]ZM]N.:&%6Y6m(nu/ +6OuAp(HpCkniq/HrW_N]q8&(<49Ig+dui4ToDI9/!4FtPZ[!@71!op*DBDjgLR9[^L +$TobuIGj#=Yb#Tj?5Q^WXasY"4BnCXH4fpFY4m<^PAfn[*'3*DI2daiLQ8W[0,(A-d=QdZ4V5<2&V')5 +;'t%lFjTmRJKc01_*MSiC +)6NXDlYMd9h\\ZIL'q1-Z4=go!)gg%[uNY>FOV]lg/ZH[Nl^YW^ls$'4!d_ILLF8q&KBoC&?k;IE'XVF +*-rP7X(V[oZIQa;pnb*cdK5kkgr6HlE+7H&EG%eg]*i&UXM(aDlV52[kNtXU.E6OcFB!h&5Kj!VX)g:m +f[$'"Xk`ft'<'sHW#n3-ks[ricb3A/;6P9]/+PO(@N>UmMU'&[M3N)oj62:W_4&dK66H$cfGNj10_1VQ +2b++bL3cCscCFu:mfFspW*P`$k!BYjBU/T>o4IL=4LGV +Nnl&S8BYi2="WU!d'qKW-i,bhp,)$kO=ma7$]0,L +iN[]7+s+BYE`gWD(^&A42bMaU_-V!NqrUb658PHkudAP_%O:0DD=/nGVU-H_o4am#&/6G/OG?$(;5*j-\` +pOr/+V"NJ[r*?hWgL]>\AW=?$/BA?2T=;H]&qt+6<7Dd>*9ca1mjDL/ppbF4"CPc@\>bpO!Qj +.1hNo-6?.@#>.$NiU=R@M)Y]3E2@b$Y0A<)E:6pqM4p=I3]*Oc/o'eR(@apnM-<%2HJ[EVA`R)ds1q"B +e1FN:bW,qH"_u*6+9#Ks''#AheItq9OOI:*)H`X=bb28(`B[=mqQ]4*@Vr>I]>_a@i1b^?g<6EUeVEK4?RuI?)uKIn+s#rsKcf[BCR@77Ngmlof!A*IdcEd@(h9cjK`=,W1ne7,4HC9_ORcDtgI]Qu*lO=TssbABCDMq-'Z;N')MBCXhQ +>o?hIDZ5f&X<)*qC,*.qMcHi!C,+"'C,,F6>-+l.:ITuI7uPp>hkJZr?CR96e7(_O/M.`O8;dYO=,R7. +DJ?@@;d(aJHR<%6rLq1O2!RMj(?9(,M\/mG7Er3h@MR<@[9/]1"5b_FV@Lc$X2R'e\$]HmI4 +Nea7*`N&Gk.p$#e>)]!(+j3DbregA-u=k4?;jcs.Edqm8=6u0d6/@]()>+(XW +/fN`Y[Q_*:Rp9a$Gdui7^p-elUtlWqS-\k4q072E>4:YoUeW_gEH4AZf7_FeigN0EENd7L%9am61I/^$ +!GXrK[XPTHMA7q(*[09B1Y\RX>%`mMdS@EIfuk%g*7OSG%?t$FSsB6k/$1\2Qi;P0cgD>?K5Z8"MDG7q +ls?iCj.+r;p1LUJ]#N-SV7Ss33)guYqfNqd,1]#1SoP[:mBnm#ftgl$\%U&nLh_222^%MCMJ +YNf9e,M8I%[gM+cb*Ef#fq*]]<>]-HFPU0@eHBhoPl8=>s=T+XJ6UNq@S.-uDSkO6nH7b*Fr^?"C8>JG6mZ +P5=B$#M@3aO1c>ei:9k3#;>IurV_.Y`aZllqRI[Der1?NAj5@0fX&NP^@IU((*2;5.M2G+.Rp@,(3Na/ +?i%56o;$&n'erl8iVTG$:OB:7Hk5aR82\n3.,Che-m-&uQM-#5qN_4T(NP8A[,L:`?!"ljdRkJ48+-7$ +<.FYG5U2u*Waj'JETE0l9<+fDlSg#;[*m^_07=]E(cYQjVLT26VR.enEUQ,5u4_!t[H!7->7N6#3g67lg1H2V8aUWZq)fl: +npQI-pq`,^ZpER;6"c.)D7o(`ei%k_p2>G-@!;96htQs$"8PpK"7,uYNugF@;aa0H'@%6A]D',TObGsd +jSuuPb0OOd]/]!P'jf-]9Ci,R5I,(ZYENs0J4D;J8q*c*HI0(Q'ntM6<6&-\D.PtI0U7eBhCK'Xa;JDh$6O +.Z3p8\DcZhc+pt]Ll.t$\XL=bUDI6IU^V31Lijnk2@eTM2)lf:"N:&/?:^-MZKP3n)E.54K7a\r(%U\o +p,o3Y0fogP$"XlE'4$^(:nm;OY)J/>rEM:DgXE)"K=jk8i%NH)KA/LX]18H%XIPe4i!(7[1Jf78G9Kk] +hq%5Y9,"4q5ps#_l`#*0fI-b3lO:j"Vh8>O;/p)'mg2a"J$E;hW-;+O>e(Dg1a\4d!`dY,cS)jt#"4M. +)Qs(SJISp$X24EfH^NGSH&\VEpJPjc..n\7X&28C.n#o\^87[+7OYV7'IbQL;tE(439HGe40_&MQ,#WUY-\eK/LKhZKaYMbYm%l6#N,O#jC;-l"7]%>0Cq+P +S:hRam&3:W%b/NRaCJ=!MLb`B?1gtpYs&l`2l[o2F,.D)*&P$[WGWJo9[q;g.]U&mZ4tuB('1Vea<&d0*q"=,hiFQG1GVMH+h]$]G8,AL3.5a/cM%r\2R;9>-FW#TpT(AB`O +)QF`J$`e$.d&aqth*N\SR_)qD)W4P[FBkkY4B08fINY_6[mU%%$]\A,gB,R_8Vn15b1&s25OSVY_1KWmV`lR'86#>"fAhZBo?o$$#E? +pF>j0!jQW3cgeTsI@k;'P,qGKIp%9T'*m[1MpZ2)'-Ys19CpjnC&nMWNs8;+q;@A'/Ia,`OjVD-<@Y]( +#2d0ikkX$tMM%e+QM'tn9=@`X\*F]3Lh])4(8F_oa^%XChHH<+m/ +K]cQDjeQ5@T4]PLbqNZY2H +j%6Q$n_Mn,h+CZ#h,,E^g>X@P,O-_3LGAGVNn-0>C0Z3a5[j`LQ*r#Gg3HRdc)9bgH=fTPRHeoaC?3pW +;,>3G'QqkG:TG"BSZ]t6@.quC]/")Aeft$Vpmn29Rl0p8a56'9MK3m=)R0,6QOY/r]J^`3#? +K1\bs!NGeUHQB)ZS5`GppE>"1s3&cB`*B_X3G3H2(^rkqZJ +WF9e`oaNak2np2N%X]\qTYs>u-+Cmcd[-4(90/.-)\(()DUh&,a(XhehE5mV(eCt.)\mW!4SM""XtWq] +RDeP8I.Y9o'H/#WSU,?Qr/QYUNo,iK1U]&Kg&o@X4a"Ndk=81(BnfikMcIB+bSPE,\9bU3fPdWL_E^?U +].'A1X46[FeX/XoT(q-ShIWJUgE7D[G!+uFh`Y(!jc%b0WQO&9B4[(lGojnqU&N^Y^H3:YCJbUZj+!T\ ++7`q^^Dl0FddJ.$Mj9lpVcZ&7omKh%W`&Z;RZIkTq0Fqu5F]j%)"m%$L6m'5b/nNhr/Q)FNo/+*gCV,h +R2DZDoo+V>[LG%GUZbZLg\#,X`jT7$WJr5Ddm/43\n,-$SuHq2]sFXrQU0?aK9uo6]CS=d['cpR&^=fl +#0![__7_Yo3C!4s/'R6bLu3b(^RH*86Mt_UYPuK/Ag0Q0<+.-K->$k+F%4\.<7Ki4/`MGQj;1)G(&&mg!NStgDMRcCl)ff\[Ktb +^>-G'I)`)J[dXfrY#O!oFtb/s_9?=c.dQY_1]E/Pb\2TRGPf&#LS?fPO%l!8XI.;4o_.E5BD2(28TS+: +*!XBnaY&)AAK=!92XDg!"8eRoJ]7/pj^E[\gFGMTE,JdE\YD[G/0tf7]l.`S1$'#5lLQi&:5>1d2:IC +RE^+2\UL5]emgqMM0F$:HG4d!3B5@%1\,G"CRPp]@@rE8k0^&Jb$"E8jAD>k83`qqK>l8QfuNqrJc +p_>ANaXAc]:rVRm>N9@aJ'L4G:-M[AL+RCdMh6hf#/E?gGuV0e4nSWiDI*]`LhaU/gKAW>c0VS=m#3(l+Be35&-@!F8,q;&ke;>,l-Eg;1E:E=(P0- +Bkq+o`SmSXcn^B;3\nr"?KcG)(QQ'c%pEnIg,g+O@aT)Un1U#3abZ +$PN&Ois^jJHL5n"bBU15lrJ*#W5[4-%NcF#k4@A:Mk?trA7'EB?,aI/_oMFe%Ngt,Z8oR45[=jAUU]ib +W$QO]fqT&&b6jlN)tga6@g"c=bY*4@r5!ZG_aj+%AIbotCR;q/osJKDIjB>aF81jcoPjT%0K:'OmL3** +HLDg:/p*)2:-+!c"l +4H"?N_hTKqF82-]rc%Xog@=B\OCSTQk`^-YFaeW5`;A&eM?KcJpC.&ZT$/!O +cK*bKeG,\>IblmUPm\L%h`tZ_a,2\J +K;N7\(u7"CMe.%W.,(;4E[@Q?:OTQSSQ#18=9MNTU?hab.sU]4Y*QhtFabS2JOi*[[^;q:@4p-"7tu&h +4V&T`Y]tP-"m#'@)Huqs=(dQ"fr)!1]>TN27mnB2S?Hkb64/sBEcb2Sjop$6-KN:N\D"s/3hPR\#FBUX +>1Q8[*7b=omrqDgb@CO6gV;b")>qY#Q7Ma`.`kP`YdYFhBYL,95OkP^j9Im"/^"q\N.!]UfKs([JnHO\ +gCC7IZ;Z$&0/HCG87%>G`/H;?>]a:9a>(N`l^=u0Ce*&ZBR#%4[;*Xnc6N+4[3VAXSk=jS;:VkgGY>X$ +:ckg@E`<+^,-gH,reMPblW[Dfo!cMCfg5#.'#0X`7An"+hIH`,=meR5)_j!0gE"M,Y,7h8/K3L6W>[[5 +o]i*02gVL[jiMRDs6&=Ao9==b5XST'EB%h@S&g4:'Rk0Y0BH$4Mm=hbJWGSLp04,?.Y%Ie*^`7eF#HO! +M&\X9nLFK`q^IUNbNt!.H`f6k5#43s*?W5@*aErm@V&&FQYt+pU>,4=`&UUN^!b-M>7HhU:Xj)0*1!Mt +RdHcNe*@mM_XcZ4V8D_grPUdu0qYnVFLeB0pF:PrL.Gf$V#=c1OI#L?-3CA8%=du5)!H?nu= +\J$J\/\k7E'8],-Gggf'D/#N:ReN-Aik\eWq:%.66;-q9S5r[5.UStUdQ&W)]%iM!Gk9l25`BA9Nu#E5 +L;!8TC!4bG/_hjO?i4/KRS"%'@I7Dpgmr=p5o%SX!YRSNGF\hedX4 ++[+)?m"I:R_V9+/Z]2go.D)9WmZaEtVIajG.O)l_/7EZQ=GZnC:Zd$J'.h,>.'FU3ZH\L/lS$2lD\LWcNg8?<-XZpp2:DN$2OK0Ps\%q--uXr`1(@7[r&F +nsYmaHT>cH7g$O!1Ykf%QKQYVk<&6jN&Fl*JKlBr^X;RbnocWq`'j`koiZuHTJJNPX/cdjF\5p,*,c7i&n1HfpS6l +$RZ%#"H"bUW;M*@9tL'=s(6R?[.q`-T^dSVf"HVNfm4rhbP?$?#:YL&LPhB=i!@MJ\84M&.j^k8]NkU"^B[)E9+1H3Vg3>n52U^uh9 +h0EX"c'`1%pY0\^f*89GmT-*jA5NgR-XnClSbKs'GVT11..W]a^OST?Y"`!._3tt\",Q'>OPVGq2%$p@ZocXJ_4NKPe85>@-ueS +)Tl>=7]]$@pKii-K](:K4.bJT4,hS5NOW;fMRLpGh-,g$:1U8EO"g86f2f0CB`)PKc?NE?1gnoas+?du +TQ&k63cCA;EK,Bfs#0RQPT.<2B]p6V?dBdObl!+"p3@+3aQ]>`eU?O>#@;cgE5XdBPmM_27PAV(Y@_(r +kX2MTDQVLg3sQN!l:X7;,6-^cr\gI<()l^2gN-eV1tKggH_I2$0t+2.PukjdqN&k7K^@CComH-C +R@#"ZaL"'&R[^t[N?Wk_#3u)Fb3_I=hUuEua'[-=l1FD'?09qh_,Y%pa,$Vid#R\NFiq%WUXkY' +fc[>8@GA@T36U/:jg=WT9"/HdaP/XtZR6uMG$,?VPODn_FSFfY_>fH"[WSEi4K*OJ/lq[Ykl+2Hhp!-@ +CXrsD.,gimKH=VLPp>U[N83du^=n:j93,_?/Q00'Y"Q)j*$]LCAHTR4NB)o)T"7YR&YZINd>mS4p+q$P +dY9*LcS("cM:TA.H#SdilkT`27t'CL*S$^/T:OL-+7-#3ClY$>S;8mQ]RE*VnpcqT*Xk](skm +99q%i8"IC2kfRRm.HGS^^Eg*ER55CqQHHm52t+pTG$+X9IMT1M@24.HDbA5=LfNPRCIE&0?@61_:>:>^ +O+]Drr+b?Mm2jJ\0?TN/=R2l6r_a!Y1V"m[)Q2phCcRVtaOcWDc^>!:?J46'r5Kn:9[SsH/7_0WOmU/D)jt1hj]5\O1TIj'j@!#6n&RmJ^/gd8.`mVh]?D7Y:,L5]OG=8K9lN5U&8ujGZJ9Jda.e+[n$3O +0<=_::=_#II?WtP5"P&00OK,Aa/X7+AnRC!:Wk4.D)Wr>O_5 +]G`%r4+^5Y+Ct8h"&hr:3ofGb_:NPp;X!e":iWqC2;W;]h1HUoXP/gJ1\e137hT***>L*!)f.Gs*A%q?*?@q0VE!Xh/);ilZ[Q5%,pZ^F +Yn*Gc$n8?&l##jjkk+/#[,MaQ/4Smr7*_Jg=MD89qg+idCc1k^=_f]JnYNQ'!JuP?FFL +?k[gd=gj@g7.F=?@1I*4Y-p)K1N*".BWS4I_Aq^-Of:s"eI6"E.ahr?:>FUN@$*H?S2Vhk5]bp*NuAh +TP7OAhWsA'97iIYY&33hp/Us.8*I1P88O$?/SE^:q(B!%%"T;r.-Xqb<$>M_7"8XZW:J7eAabm:oSojg`8D.9,hXEWpdAUT]GVc.m/ +gcNT#hX0uRrnAam43<`2bIU1:F\>)H1`%kDKdPJI`\c?Xl8c-^OHV\tW6IhN%T8&Vk%1BGMl> +&u0'GRH*P681-,iUFH*;i$3?5:U-.;Z";OR=>H_Mj,Niaa+C>7D"s%Y,V._oR,Yib@eC7,h%K8nml%Q@ +2>Y.dH"CGRWm;nECKj9f.N]X'@MZdl@cUlNL\fa]_F5eC8P&6S`+4)ET!Im=ro'$l)q%-9ZD>c,1HB/^?%+6(Y.S:9gabOSPntnLZG/I"@ZoDL'iJTAE,)Gt\";55]Fshp6/M3V,WtpeDFZU3 +f=bW(dJn:`9P/l-`S@L"HR7]V%(A^g#]1Ghjf7b!.86IWkU$WW*E9B;[l=YD3h!'>=3H"@Qbpq +:XSSr`\HqnN-jht,1/s6l$@Vq_46msk?2J,P`6hf!rB +_o9;Xp]aC_#HZ\*]DgQGs85%>^]!pl(6ZdW~> +endstream +endobj +7 0 obj + 138709 +endobj +3 0 obj + << + /Parent null + /Type /Pages + /MediaBox [0.0000 0.0000 800.00 541.00] + /Resources 8 0 R + /Kids [5 0 R] + /Count 1 + >> +endobj +9 0 obj + [/PDF /Text /ImageC] +endobj +10 0 obj + << + /S /Transparency + /CS /DeviceRGB + /I true + /K false + >> +endobj +11 0 obj + << + /Alpha1 + << + /ca 1.0000 + /CA 1.0000 + /BM /Normal + /AIS false + >> + >> +endobj +8 0 obj + << + /ProcSet 9 0 R + /ExtGState 11 0 R + >> +endobj +xref +0 12 +0000000000 65535 f +0000000015 00000 n +0000000315 00000 n +0000139453 00000 n +0000000445 00000 n +0000000521 00000 n +0000000609 00000 n +0000139428 00000 n +0000139907 00000 n +0000139623 00000 n +0000139662 00000 n +0000139764 00000 n +trailer +<< + /Size 12 + /Root 2 0 R + /Info 1 0 R +>> +startxref +139980 +%%EOF diff --git a/images/events/event_man_arch.png b/images/events/event_man_arch.png new file mode 100644 index 0000000..6920a18 Binary files /dev/null and b/images/events/event_man_arch.png differ diff --git a/images/sat-rs-structure/satrs-structure.graphml b/images/sat-rs-structure/satrs-structure.graphml new file mode 100644 index 0000000..359b5e1 --- /dev/null +++ b/images/sat-rs-structure/satrs-structure.graphml @@ -0,0 +1,274 @@ + + + + + + + + + + + + + + + + + + + + + + + spacepackets + + + + + + + + + + + cfdp + + + + + + + + + + + sat-rs core + + + + + + + + + + + HAL + + + + + + + + + + + TCP/IP + + + + + + + + + + + PUS + + + + + + + + + + + Events + + + + + + + + + + + Power + + + + + + + + + + + Modes + + + + + + + + + + + CCSDS + + + + + + + + + + + ECSS + + + + + + + + + + + Thermal + + + + + + + + + + + Housekeeping + + + + + + + + + + + sat-rs MIB + + + + + + + + + + + Parameters + + + + + + + + + + + Pool + + + + + + + + + + + TMTC + + + + + + + + + + + FDIR + + + + + + + + + + + sat-rs Book + + + + + + + + + + + Subsystem + + + + + + + + + + + Actions + + + + + + + + + + + Health + + + + + + + + + + + sat-rs Example + + + + + + + + + diff --git a/images/sat-rs-structure/satrs-structure.pdf b/images/sat-rs-structure/satrs-structure.pdf new file mode 100644 index 0000000..b5596c8 --- /dev/null +++ b/images/sat-rs-structure/satrs-structure.pdf @@ -0,0 +1,607 @@ +%PDF-1.4 +%âãÏÓ +1 0 obj + << + /Title () + /Author () + /Subject () + /Keywords () + /Creator (yExport 1.5) + /Producer (org.freehep.graphicsio.pdf.YPDFGraphics2D 1.5) + /CreationDate (D:20240129115539+01'00') + /ModDate (D:20240129115539+01'00') + /Trapped /False + >> +endobj +2 0 obj + << + /Type /Catalog + /Pages 3 0 R + /ViewerPreferences 4 0 R + /OpenAction [5 0 R /Fit] + >> +endobj +4 0 obj + << + /FitWindow true + /CenterWindow false + >> +endobj +5 0 obj + << + /Parent 3 0 R + /Type /Page + /Contents 6 0 R + >> +endobj +6 0 obj + << + /Length 7 0 R + /Filter [/ASCII85Decode /FlateDecode] + >> +stream +GauF[bHal/DYSN;a2iOfS98U1f!= +RtII8p?rM\oeCm\q;md^YJss1^_p*lS)&qPUOsiH_9m#Q9^5NV,E+^N*Jl +8q&0LBDj^"iuc=<4Ub5JJpdP2a7K?-l[S`2fFOVocO[[kkRK:B!:],iKhj% +Ldo7**tfH;*ap-_!Id4NBNTbrOjEl.&l@_sqc=lT"]Vq8=5^YUM)BjU:Dh@KmRI3uh!K"^*_Uf%Fi9f-[Ehd'ni7%huWD>pDG-p4V- +5Bo*ri)T;blTkWUs%83ao.''CA[>!?bRV!1nKt,Zh)Dln%_(*e!X`4 +\bq$GHf-T-rtF":`ukh-p)@S?E(7GK^ej4\GKq@$1A7C?d[6Y@^F?H9-!m^;oDO'YjE+7.T_[qWEC2U< ++'Z*h-7XSXc)3]T/=t=[e'q*>GA4MakRcVdQ1V,OTR(_/f-ig]V)F"@p^NaOj8#'ESS/Yt78:+[ +[qEkMeZ_7Wdd$G:EZrYDi+Y!.2\U?2`cmWRCr*H=cqn(6/#8ldII,P"FA^'IfVc.1KRtsI^%_qDcSk(^ +Brf*ENtS6X*p''_I0_r/=1mei"3M)=T%<;d#=H^ac5/\7FPLQ-/5rD/CN__S380K=qt7L)`XEmP2#;`g +P"s5!3N\]3Ldg&p-L.+d`-7mY$k:M)I%`CiXO`s*a73b'S<;W>BurAf3)t"HKZl!m@.0+.8d#+KGh+f+ +'pRCnV%Zgt^.**s$`1Qar6j@Qjb?B0NMP%'VtEp`_t>Qo-u1YT4L,B"_IslmHNQmi/^p=deDCp?Fir4@ +`Ukc2UG[h>?+>Vhn'g30M#J2o>$)Rpl`k>BXB00n;[pL)_9It?O//tK\rs09S19EQHjlR-Z,gEdS]9?C +X(3@iEb!(,4fE?1N/o'F0?\e+9`()sI8D"Oo*:Q(EhAC;dG((O9o4L'2KW_]_m6Mi6EBJASIUgK$iFZb +[MEV3>g@PkF3o*r0NinUUBO88IhsOKUP[PjGrP-MFBHORhgYfpTX09&'E*(M:?2-<6gl<-Yc"n+%anQ> +[-2p@f]\$2"6g9LQFc!?7l-7`q9gMYUD>.kR;$lbqV]R=)nmqkmCNe;r?_T`LAGdEj=M=4LB,O$d! +XA@2)cf"%=DoXk%bM.m)eO!<,%PC=J-!6s/_.&/CS%lf0-(2"Ar<6JUn70,`%!;7%%NZV)rH@@KVqLAl +C[I_%CfsPgY>DYSja$BXU&EuL7Cua*&$Js&\\+a1D+]*VAifrR%Rn_@*\_)s?;HU]"pPV[d[&UJCh5E.bi3Dh\^;hlWT_ +^G.[:I*ic,0Z1?-rlu$V\).$M*P+tu^roO#U"`(Ff$T+"[9)?@-2nP= +kNUNY-/1]TUEqXHLi1kOP;98k@Dn*`iBXE`U("/<>Ep9UIh,^f*8NL+=8u^tP\R("GZTp]S!o%3gF%/_ +.c13DojDKYduY*oS>g6#XE@h*Q!qpA2)n!34XDSR4]3u/7IJU#d#mJ;%iQK0/n8mU$^5i)oEkO'`GFFlVrH1iqUJ^@H +5YmS3Kn +5-]H9:N*U`RT4*N*?cdgNc[ii/R0MK@h6/Ho:aKh'og_\%&u2O6h@H%,d33a^&5$Y==d.D5e7BjO[-eq"K$AUDSfDN+ +[cW3#OfalZbpld:8RJ5g8+4S\<><.B(cpW$/TEA;GNIJjX-UkeoWB72rNIT-X>h5*P+5/:Y+1+9Y$V$#Np#-o +r;<\"grAdp +2_Yd#l-VKi@@%/b1=>3-WjQpHUOUqB*^\6CpkR-IFl+"l^31S,N_Q,nDJcn +_qoc:g[r(:i:[\.oj.8bhWr3Hlk-:nIqY',raD.)8H7$sgASojs!]2`Bp1^Tqd>cEOaYjINN:J#kOHJ*nFGuOjIc+V*ebi>X\E@W9OY\ZU0Z%dWgA7Pg@7`L/,DR;?Z#1:";1@/'W+^PP)B_4M%H_77M +F48bkrA%no@-h-LbuT/;TI^V#GP\rDCkd+.TaV>YWb"!SBKBE#S!?35A8pO0'Rus6*jHs"8R^5]*M]%3 +jl)tmF;\SY%YQ$Sb^NR*?_7S\HeDr*kXPn3FkW1\W$ +XkdP<&"p%3fhN?cCB[[9bbNK'l+=jn#J5Hh72`jc:qAD;^u(Zm"^IHn^9)JaLM*Xq-hDulNlcD2Oa-W7b_%":,^fRp?2Hs2O_T@+3V/f"N#X1S8DMiLd+MM0]K%c +Lh\=m!]r?th&\Fk`$QcJjqeRRqN";U.6]ggl`e[p]7]Y,$Ih4ZrD:u4AmG4qTJGcb3i]p/+l(^W]a;]; +mrjc!lW;2ZhBR'=pD8Wq_e@Q"O`j4`7>GdGL,%[UN2XB;f\'WZf4,SVLLJT"\CX)dl:LDj(n//@d5nPr +br8AV1U?S)37cbY/$oJ/Ke`#,m]f+IJ5-ci$9a#rXWA2K(=mAehTQa9MXMlp*uQ4a_hCs4UY=AU?%J^_ +'#!%6?1pRWETiL3Dbj>)gkcUHiASIHS'bPENTCbp7ppfgN8uP0gMQ(kr%.`^L7X+T:hOl-%A:P)HEl+Z +@AECG(<9h]3f%#m4H\Xs_ru(8[s2!A)sI)V*qE#t2APs6NouTToRDYhHh%"A5Q)Vq`XI#I$]fb +0CP%!8Gru8dqDMB(B9XeLZCN^U/GG27DZo5qE82MYsHWf&c=i&_,`";S;lo.liFLe.pJW9B[8G>?Xi2. +R;F.d&ZgFb[je"q'9%9dG>mZ^NudC%4`]TtGKfiD^R8LPq0.70nmCM4jZVV7k5FsQcUXa2NDshR%e3h% +8\Z_^W^f!r\YITG+j&cZ%u.40\3G**-['4uk4tKbXT!s5CGanJO]"8#*F*eJQ)IiUK%)ib +q;MgcM[+_RX;L%U#KOBKc8Q8;T9&q]OjL>?%+X8rcAqPr@N<+`o-PZ3f6>uoKAL86815l=/;\,NN;Ig) +(B%;`WSSjlZ1lj]gde-jl)f2O^1H*!UgIC\).,$7s3Fa@>ST8]IO6NsEN&)jb(p&$3:LM&qSC7^jD +/J9$?b5CG=Q'(rP2M`OP<=^OEB7:?=@&61#H'kBHC5f%N%&A=)SFN[i3bm2?l`fJm=XGAcXTQlr`cpA) +(m=(Vr)fW6I>83^N`PQ3FeVIRFDO+@%"VF+#=Yum!8>OTFW0Y]@_W/HJ:Oa +6^889k7pKM3F)^4MH5W\>/6Bp*mBq*7r1.@hd[I!f_iOF6qQ^ZH*Kc5GJ.1*\tih65UCl'4'aD;g0n[> +c(31@1c@_D0`_^cj(QCBYXLaWrtA`t?aYO54rZZ6( +")"C-L\XF9DY:^)S\CVg%Db[6mab&HkAT?@crqVMZ]+ed*ml,eED*$P][Ln/bB*/8&i]Z.Xg +_hea_No#ZD*k\t4XS7s6DiAgsgE'O/brV^\ODB]*iUskJA9rW+p18:coK+bgoI%BB#F=8$hCp%Q(A=#' +*^okU.N7ECr^[>PBUAF#NoAA;Yk=cVdF\p4C8_\dc3g=k7QocH7M9Q=g5T"XZ]_;nd&NZ%agkKc&lC?% +L:$\#(GR6s3J4(e\gS$cF(=S2?T<+%[k__Zho[a?:=s/f@HHeLV7"UKVHqMMo-X@5f'2uVH&s8[Iu!(3 +N\b3\0S]e#F4c\)(hgIn25S2dk4WKPf+5)jt9HSI9-WF&ot6RO9J1 +_E;Jm372V_c6YndHjlQRYoI=W,]IquY>pO;0r.&(TT'W2Ipsru#Fd>j.$>N0T3Q$>X.?g%gh`)uEoFHH +F^GLuShr9mU[RCOn^!1@JapB,j6FR;$J.LH.l/sd4!If#m2BbF6Yk]fb\@^Kq"SZPNF+&T5&nrQqC/C& +#5s$8k(C6X14"qIs"d#2Go+7\#@gQik'!_H<\sFJ(k!H,o_3cKN +p1A;Tcg"Jj=rVPJnZ.D/'/q(WV>:\2Bd_):UjZnpa3h1E(I$U>27KEto)-El@t4Ll%n=_O:J-tU,h=d= +;/@AWYWZ\ob!n0Kgp8@WQU[I+@FAnU]61"fP4Xs@__$W+K:!7E%]#W(1A1"%5Kp'=0"N^pfR8S]f:s!o +EkQ3tW$s\AqP,Cp[DP[k%Q;R7_TJQY/_a(NTlna`TeB9/9*Tg$ZDcOB99ePpN`h7"gm:"QFKiHXdt0uQ +Vc;kQ0V[[C6s6<@I>OkO3Zg[qik[/;h[d`l=i*XplumpT&iKQuZk@,6ZXjfPRl6=A"ekE3%\EUd/E'6( +jK`++KJHNg93=qrXROfud]`?#,#)/)Ek`i]L:LU(KZ0M!-8AJtq\$n7],tB(Y^?a>h8>R-4RFe/6F.9< +K]k(+krki%En0L2pP.j?3kn!V' +Kf3N;&fOL>&g])@o-Hd@q@`,T\\jafX:3b"B9da)?77`qX#7N\9?N"9Rp&,A99dC/47jh(k.:<%hf_A\ +h5Sth4)5U%9pB5`-!VtIQ`0JLkY&S\$g`VFdsB[Bs*aBu%Gg*a,A:f4cV##rBd7A_()DgBS>KO=lBDP( +Yj8BWRsW(iodi9pgK%UMO!/;F0!7/VV]j`ZWuV<1,lTSq9m,6aHVOEG*"ZmaRH\CBcdFKY)*GL*Rb)tV+;[Y-'St;\erQ.!8k$!/ak.#/DF98I`cKeV'>IqIYdC\;MQOm^A^rk%>=X363Q/GXM&oVK(UOmi$oUh+gV. +(3S40t5@%TG+GRnVjIAW%hap-lP)VM@g,4=IXWCA(gA?*\&e/=d&jYaCRjUGM-^$Wn#gNc,H0rP<%N3 +m+?fqH6!mq.*aTI^\'XclYlVj,QC5TLj\)40&;;72gn!^hWZ,nB4+l6ME3sHkfpHgNJemc,.m1:V;@RA +9$b5 +rCfWA)>*2X3G#'>qlh+2i_FR=N@a?5+3]d^*b[o0"bT> +oGKZYJSE`)SarF?]mu^!L`(3NZpYUZlU[CdrR/1,QQ^j&7Ang"P/kmc*?5XpVaWVQ51/4,q6i]scSItZ +cS:a2+5faApu*9A8[5\FhiZeUoK-4LF%;;9P*31qXth0DI9l05;*rfTQD&j\Z*89GTVc;=TQ`U:4SqE; +em+:-dM3-2"VUhMiI*]O?RYa"?LOY^HEq/%nO;a+SG>ljBCq&=6"D6Qh"!(f,ec#u) +bm/"eNAT43'8p1#^+f6#ni3,mA1FHKA\l)?0p,q$1Qum2pT%s,H2+0p7'j +htt?<55P/5om?_E00XILr$=/FqSE,s1hu&(5*U?OlZmp;Bfe.sn9g;]U#gNY@=O7+@3kDuB[D3f(Od.H +fCS+aHe7_m7le>2A472=4X+53L!1TfiqN\*mh<_pr^RISN3`pD'@rXCV0'aBJX09a$0gl^r==i#JS6Irsb"Oom<>+RLMnI_\8FX-`U!6pK@+2AZkd/6[W.Q_D#rs@K>](haG-K3jQN)[j$6FTCEGgO>)/'T*0 +n^adB>Mu%^3^X``RS]8E6,YC-^6:HF@"a(`H6Y^6^;R;er]A9J#A.aSLYaSr@]2=#^!g]?j9$Ht1b'76 +^[g'/d;`;CPX33-TgNa--I3CD@j<"WIofV/d<2?0Z?>%% +rCr:o/gKA#+48`2!V4INd':MSC^$_?^_Fk:^%#p(Zts_sT/TBeT<4cO/_'bH95aQL1JcEq(%=k&",fqm\b>F3PYQ^^Y=&-07K2JVS#DM +-0"l'k#[LHpsOJriJjLd*Nm+*;GeT^-'N9*r.3W$4MEd/-g-EZXkAE':O2\T+;mY;`Soj3Cn>XbPTk!Skh@uUI]>:EAB#SE'"^BQp9:>B6e[u6hY8S>7-\0$0'M0iX +0!&*\Gp:\O"c1LDt2V?H[NPKCjE&jHTo3j#_BC)g`/i!kRA=2M[aCX +ZZn"oJ-t1%nm.n*TlB1DZJOs'OjeLZALN"ME(<>[K$+r."hI//r"GHHp.pYA'Dm%SL#(Lr08^I8<]Lj+ +`kKZNZ%$GcGG(q+EGO;k_2)CN!XtOUJ<$>6Y_U\s,OXMu-In"@Uj*QiFF=rE=7N=1)9(GWU"DjJp@6YN +5(;M5B^OUX,KEQcEkGDRdM$o3-]5kG?bJt*V[QGUPqt8 +^C4l"[EpBEdS?2i.!Ig)WCRo/I00pBCM-Lji]1,b`h9tFd!Hb(W5\tgYDDn7@K +Z=`BJ2d9\*M]t&$,BCiAq +@q>#qhYQ>P:CtSka[J9lZ=&KL78noXK)*4]r84%f=HN&8[0N?R]6T_ct,49>oR(Gfgt?hjU`=pR76 +AHseqP<)1$C$(;@I_2k!h9reSj4&T&PNr;%V/X92l=FVNQmN&R+p6LZ+R>_7^Y/*iVj/6;Y:q&rnQFtk +;1"YN#V#[h@8<+cW6"fGXRl74RMbi]'5)Ru1W(aOBEQ@W6]!taN?2jnHXk(LE5^Njj#q9N[o5%n-IYH*=@J4Mb)M-grW1&aYke&!%]uMh5B08rhQp=$c`lX7.Xsgo +(Ks\qTO^8[;0faCXq4Xf&mSU;$30L6jXba/b%RFaZpu&!=P]PMePRSGAh_\O\E=Yj>[a.lVLEjaqd:LS +!u_-S-1Nh#5Q:t+>%nJd/7\,cqHs=q:F\SOPFB^qJ']NWb[kP2dJ)l1nC91a3Ia5)'))nCgY+;E0!o0p +HaV=(A="#Pbsq]b%NX]1BfFU_N.7R._'_Dd_rq'8PIkAoq1)-oFV]A2Pq-@SG^_bRE%p/OR31FCah4<@ +r`!bQ.\8qd2KD8jkSZau?4+(J-f'UXCbkY+(Ne)]4uSU1j/B19BA5Got&``0S)&(E'QZgcW#b +okR`\.O1B?p;s5U3cq@Fc7A72dFjU^BYtS2-u@tfFJ5eDX2Sh]q5(&*K"iN*jP&.'4pBX`;s&+6I;[T- +25-`-bgku/"u?BsH@]OY_Hm))#r+m +9?qlp]B(h<89@^a\nF+ZF'/0?c'?c[8kS]"!19&#Vhon-\=6lW1Y;^;'q#50MmhG38>HSPLG4L@:hnVq/ni*!3 +VXIi5^oms-d%d8l7GmPm`KM]V%ea7=XWX:]\%1C5QPI&1!NE@q4m7iT(c=qac,kUaqX-d@Nj!FmNBN_L +].]'1R%8B-VI/$6ZruhYI+NCLbS'!?)R;5N4UMlC,)jqES$#!Nh2;d2[-<,#e +g_a&'@-3@NB^U%*)Z%>4rHngQ>j^q=rVGiC6@<^cE!"(Q(mk**mrge`qSLj/[*N*.Z*$i9guilDe\h1f +AgOaS4Hme)PE\'pkrZF2PbJog3[&Ee+6c_,aZ?QMb$4AthF+GrZB_PRIZZRd125AGAo_GN"7oEJ;lj]j +5O3M`re`?db,X.E5g!"ScCp?r]PlYerpL\>\QYn(`F+FY`hpD;U![:53+G]q@?6(`<\6#r/>*A[+F(HUQ2[p!"VE9PY*)h.QO)efpHG#Z\)7\8h9.U1]N%)h_\gTb( +Xb8m!UCUi;HTK.Ce[dFUIrh2&?^06fmh$'X*.^,k7sjo91,>[fV$5"@YHouYq!FT.3.dqGYc3)6YHJfG +ReQ_'-fq`/?c.@.Lqb^0qpO!;KO8kM%ZOb!T@`ASY$2:dn_6:ud5l[S&%l$g*IT+"5d&YDVhl5jVkT"U.8.N#@.m&11^BHUSaEPA(G>_2[:SIqQms-n+TQH9RUJjQf8jse=#F: +rVi\'4f4d;\YVCi1N=eth`rmt@I@h+RtE^9qJK,,^P_EOkRma$kjMtL@qFjl.rYFZ2fUD;HrmetkuT[e +:9$gE]YBIe)Lk[1eTZWUqGgBa4V@p:qL5I`5h#W^<Mc4G_)(2oMjGDl"m +r)Y]KXjeRm>5c]PZU(?pX,iK&I[gnJWY\XOM>NqgP+gb('miF#8+?C`BbR)!SV)9Lf^I!>G2=!a)4PcI +LVVU5oARS!H^/BR=rI*//[;k`J[tB:@-^k4B/O')%^YV0f%*L+BA=afnWl*%@.Ai\Fba?J/Os +m*=rFc)$V&d#d$B=Jqgt8%+?Mih+jZA,\7C,J`BU`dOn`9_/l3#%m3]ZMr<(XO&_@Y%P2cObl-ep"([- +c7b_q!%0,^YpdM&+lTN`5B1rh31T<61=>Ohs6)Rg;%EWU$2>]jE7^0F&^8+UleS[tb".$DLrrP!h8dGs +RrQH`$P:Vi)c$ig/'m%si]@ff+5Md%$fu]@C;QW]RHl!O;KLg>*K`qC(!GeW=nLF3(+ha^&]'nl@C6IDIk('870l9$` +fLHa[R;98)/LKR`HWR:+lJF1X+4f(@4c;=kL;I8^W"j?N1fbe";4IAt\?9oLA,VH0:?R0KWu1*YcDoGt +KkaA':FEo9ZPQT?-c$ssHA,D=62Ia'B2oC`X@8*IZ7Yn*s3@_CZhLoL'B@H9 +EDO!Hs%WCcNE(gIiso%c*+>W86J6S#-"WXRZ`#=[G?&qNB"DePg-M#$YB>ScAFeU)ToB9d:@"4#j\j2Wf1Eq%I0IsYhJ`kHO9)#8+,q(j#j"CBgZpZFcG5Q>qOAi0'.Wj2Ic\F?l@<6_oZGLB== +M5J93[u42G)u"=gN]Q^mpW9a)\@Xf68'IGuF1SU3&DfE?Frs:ZZm;sFE(Go`>kd+H:>X*^,TUf^p +M2T"*9h8lDGNu=Gs4,41ZI@nW7=UZ]Q(2,EADZ][GS\YEH4TE4rDuf%.BGg$'WMN::;;'YGj/PIbdtQ1 +nIB9aSXikPm<8baPr6LJ:5Lc_m.&S^`6J8t7i^hud@(nOR^e%)ODH>r&5TB21N=IE-JiN#EC(pIcs+$N +Iei6/@;=u;q:mX<_i,%gZEGRAj-12ou&lIZ#b<7 +0"]-n9\aTI\$V?IEBFn?SDsuq[WdaE'nF3llb\+O\i;lLY5QZ9RC@9pj"p-O(<8N-)/r90pG^4ara"]3 +s&F'OH6f*=(ZO*"26 +IRi-.]qKT!3[lT?pcPJkSE"[7D^FYNQQ."6.-N6VNqYKSnbuhWGlLca]_=rZ]q%qf(N*QZeKZ="pl]>k +43&/g3Z#gu]ufdtr[,[(q?A?Yrqg*R,+.eHB&;CX(t6[@.=8VFcZ/7Q0kOhiDf+?eSr+cLqG??hm9H!o +3k5!Au0@[47&?I,OYaoN6'c5/`=3_cK=lj>f?;B$)SVoOi2agRg^]+WLjj)V..d^']0ng?U-Z?LFXQ28;-?ADUjZ2knO*oM\msf#@ +H,\#:XaK?,Fkq!2Fh5A&1c21]1fI?CiiQjI1S7Z@9t-QjY&kCD$?N9u4rRd.Ras!l.SX*8X%F>l$XZn/SA\Hn-JOQ]8de:QX,:Di.W)[a[RpE\Y>rPiI'+se +`H0jK#b.AbV=AF-De7H[XX^I"3q-.Gn]hKD.P89W20kD^bh&lSfN4?(?,N.:]2-aQKu_]gWks[bI9!Nn +7]\NEn#K(0%\R!pa+m*M<3K<`7$DFe;=.#Y.ZSmV3B6V5`OBY`BrphGX%I0q6eSL!EAXgJXjSQjWmVD6tY# +Wd&X8ZG,,je#":/4o5hRLi!="M)NRPiNLuo7%Q6@9.Zj&YIsbo.?`/pugT'(olOj]GRuC +3ktCohE4pTc[XkXFA$OY4;7=CX(1kt'754SbFP2J8;Qm95'SL5Jd:NKgZljpq1c1kbCXqLp[dHEJnfqG +\:KBNS)m1"8$:;J3QIIEIDLVE_7;LMK8#YE9\p:f1rG<;6Ml#ia9^B9oJSTq+sdk/(4$l'o$I5BU5@2_@\UAk[@fc@8ka+aDRgTd%lG"3^,>)]!Lk +mfD0?Lgt;fZm)S[#X/^9*W>`E?ZAOCQXrq[j;UN"eKP?H4.a)RQ>`Wd0:q&qh=#+>YGP&&Q9BP=d=%)8 +8G$etQ"Qjj$X\O)lFXR(^qQj2#W):Z*-s!OZ"]t:k&s):cgQJkZfqqRfbWUfZPKf1:j?Q1Tdol@ZBdCQ +EH`Jh08$/-:hbr7+/=-;FB]uP4@Pb&1u-",ICfeS%WIJ?V*+C&VH`884)K#bD>JJD,[^*][0mIP6$@M9 +AYD";534u:F:lPOLoEIXrfEH^)3ls7;^/O=V9/1[XZV/K(Z*NYKENF@16AMo"tmPeOS'ekUMZYr5*c(P +GiG4%A67PCjZHf\pc'8>qYS,!S#9gJ3Kna3\T/e>E2&rjh'4))3o8JVVFtHEMWetZZiF`S$ZukEHXS5!MKF`Uj,P+;nW.5EN7;,WYI%(d&$N,m7!AY>69(f1hY*5+PO&[EH9@OU3,-qc$!9s"3%U#1C#Q8 +QEnd#&q:Sb#'jg:'OD!_)eoV'U+nEd\?][DfM5\JV;%k!i_mmFM%hmRF?#]Mjg:k +d2QjtUIUTjZd^E];Z^7b<(%k4eh)m1.JI")Mpknr5.S%>*DG_`X^@`@=Ju+2*654%>,$28`bDA+lPdh; +kgQYR[VM!sY>DtrGWXt)\&RCKH#MfXXjT2?+Z)"cldrbgW@2-7)TRtUS:fcrA*_N"%R&UBE@T<.]>g9* +Tet%=`C&[!1p1heFCE)MdKi?/mKF +Y,$''CCsEQjNok$Y5-I)Wm)cJ;e73'LdCB#j#D4A;fTR7CE1lmOiFYOgm?$c$7b(LSE8H23^ruXNO*nZ +:9Bc<6Pn?i0=k!?[,dOR2" +NDhnr`U_c5`s@ZGd-E;lBcL,JP/aIL.WdLBS^_`/338+t3e\qqNlE6U]leB*Q,Z+kph1YY2NBrQq +dP$+50s/8r$9SE;X/<'0f4COc[5c\Y-$7Dq6brHoRPaA)NqQ`!$R>Cc-Ul,Y%n8])p4`oq#tb@>7"`1Ego9@+ +=5>mZCr^D9>]F<#`bkr`[q?6*3RtL-c]K1TTD[/nAAO6K2_HbY3D0/&Rjkg9g0& +ofG_W[>>rcLnP\9f?Wk\Z'U@!_)rcl/9^;"JVMH,S;0VZ[](k@lD$j9b(U7_[1R=,4$W8IWks\!VVue` +O0K#V#7p2Pn">KnMJ]&0qjtKlp!qa-H.jVaq7mV%;^E/S_!'&T6ALAn[9+WL"s6]Q+<"7@E&<%,`:7GpIG7W'ePn?)4CpgU=lC*b/;Pf +fP0M2(mQ]&0KVpV+L6`&bh14'29G[;MS,rh;*t3Geh*1do`fU%mFN#&;+$OPnd&*c`[H(>f@lfPo8,8A +6AakiZ*YL"Hb#G0cQYI][>`N**d&KSg!7AN+1@mCrD]RUH^^^pSXR,k^B=5'DqH(m3p,VV9riKhZ +r*`&t[8d(ob\3uY[r(dee^%KfPa0CY]JA%2fkYsVg-p;3lnch7SIq.T4#(C`Vbh;uiVP^8rj,aI97&>( +9\7cSh2%NAZ-QeqmrdpBt\@%NONN^>&Y,0u!!X''%0l%u`1=lt_"%1QF1R`V;\W'LCk&#=,m'9nfaUpUk2Fg>ZYr$)G +2$L+1LG!qq)chf!4G/G&=>T]pGp]0R*isr]b-@h>kUsu6`R%H0l'-<8`8(OUBI)HaBaoMmq7s*3;-B'3 +UATikc]!7MGg8m#%Bg\`>)QSp7fQZEHac9l"qnOWL2-(-"_unL^o/%AVA"&QD;<DT3pc>fRofBi.'4:#86MdfQt/2"qKqkBB+a+"m0g0?K4?1mi([DE +[aLH.4+DB6B.-i2PDcAZ9TRflqg^X^'Qg=U]6.O]W5!<64;DV;`VEj$mbU>ZSR&PoV\Z`BbVH0N:R9@F +WL806.rTNqSCHU\ZhSce$DgLVQ*n94F!"D#ZG,JsDJ+%uko#5[)hcqr6Cqh(d_<-AfhXn7D[C;9:*%4:l$FC,kgZhM.Kq8"sG:tF$rgFtglB*7%a4VnJ +R.Xt\Ok+4ugl9dPRCdD-h9:,?IPip%W#btFr>!msBX@J)H"Q3cJQ@Y,#sAV&hXi,5(\\(GX,RG?gSlZS +B%m+m]QJOhf\X+/?YbjDE;/>Fg2Y4qh6RtV:97p<';M>(S%?"7RWU*0Z_FG.(-W"!15'h\BC;*`>3 +[P0:FLq"p`9Z[(Rb3P(94<*tU54R#-rj>OlFgq/;gP`CO[2aDLN[q]!?B[J)nhH7g0`K;[2L/ls2^e"2 +rAATC%c(-9$5%3>=Nd[]&\p2t7ZH$t@`C>Z+$27O_MjWJ*r.s$pH(&YYQ"Sq[ital],^JB<kSSH,g70 +*LsDh)egB86^dn2a&j;;>:4JPi9c(fe6P1P];6_[UWggL#A5!mOU4CLTePIEN:L%Z4%[$hZQ7,hS..ie +e5,pNFQk<%aBJ;gs5ceVm<)TD+(UnOhV2,OiI!&RJOop-5(f&]kIg?n=?9643V9HS,pU +@;3;XG5gbNdN6:K-_lKtFseQAk^Pc/D55SVK3$hK@_Y7*Q"tO0U.E^)o,SXBTm9GfJ(\%4VrPbKZRf\C +R:>&Jb;K2Vk!g>V)>e?sH,f^8/roLI^:3\,k05<+,oDGVK8*V3MKW(RUGT^Ar'QL-Cgm/:*P3,774b"e +dN4H#fUAAj[&>m,,V4\n<='n[eh$/oeC2PR2?%31%tRU3`%lY.Obgs@;d9,JQ$2cidGFj%d68=^2cKda +(d0WVKRXJ$&rbf31'c"B@aR!fR+^k3,1nN^lW`he(Y8>C<"o%A:N"#(3;U[_oVe+@+&&ZZPFQ]&fe+W5 +/._/HSoKAF89nOUWXM?LCY7>Vo1SnBEgE`?>%MDC7TN0rUqA+^KMnuM^*Mg:\.;"WGO/5Q!oGWlT%?R` +03EO\Pd@U/96j*6<@Gm=T7*@Z2+.G'tc8U3dJg`tp+\phta +rm(!gXM6R(2NDVo*M@^SP&eOL6coc<5q_9E+dDO6s$4^70;o=/f83HuH-,K4%fVVR;+`jPTl=/rD0.DN +RJ4=KPUdC7Xc-!?c-1UmOE.,?#V")YkX`k&elS4/=8s089^_^R7Ll:hB`I263JEE::,Z[ZFLQND,A"Be +AMV,DO?G-"QH'^c,Klnb19t18)WR1Tng#Hs+X9'"ahl-_4>O3UCP@]u`^qF#doSJl +!pb?LA>BY.OjMf2'pOj/>UCjDKfAm"7W`-rA;'0fLh"RuOnVBadFl9rOePq`AV>9MKG;59^1:b[T"1Ot +Nf7qdj2_LBI$YH.&B`(PIjcPI8OGt5NrUrPr!o&*hO].+,Wmj'aE5-T.4CAqWQRgMjCP`>#MS(bg>ar_ +o=C*I_EECj87>5V,9qF4J$cO+NK%/p"*^kXQ1lVRi3W"Eq*4Ck>cB0V`Ju?,WK5kgl81c7eZKK]fn0mo +n>-R7HeG"XQZ[/Mgj8=_$9._K%Por4Z^=ISkl:lbF@]LTb4aUc7/$7Yn2d0^^9X,JZ:?Mnm-g&[h>DJ# +[D%^dFGSS(W2.Li.#=hCrd)nWa2-O(&c=h;0p4b+U[Ee!b)0q><#f-d&mJ#I&?V#!4irnq\T]j_X3o4^ +XS[7V>M%lCN[[)!Bo8Del&ULg4jm*tpN^Zeub5]JNXC$GG!0D83LC&Q@_s`dRai0mBGmN3Q?6G+iL0rVTdoJq*s)!r_#4qB"aY`='6KY9TD[6HF7A=FZ?.-BZ,`6;PrZJL+'4[k5dE(6et1N +@%;B%pQ.5f]mk^RjCijPmFHQ:[=kFUIX+&b#@g9iY0&^aD+L2ImoiUL:_D]2S;']8`D'"q1^aN`QWUE4In-tY7k_*L7h;#0m/8"gkCCtAX#939(5aiR +pab[-$^4j*'i9/L)u4[ufD65#Fr5Muh2Q^7rT8$a,r/F[X8V\"kp?hVSF]31ZBT4aZ4:/:8J*d`c:amG +#B#I%%4GM\0Inc[bJqE&AA,N"F1CO"kg+*3QT58q$uJKQCV@ICQ3#mK9]Ro6n(nJlL^AEitMaF^,Vl\.d"<&"6UWNUI!.2f_[oMV5s#S3B61@ +o0sgo6PbR'fd4jb_U=<$elP\m=#G$l=nAk<8*eO%``6IB$FkLHr"@R8NA`#_Ar-E/ae.\,R6(,ZPk3_F +Jhku1#Yn1&FbT19,kA+eETB@eKiBO(DB`X4:?:)-395,M)ZgEXK(j1O?Z>fSu^X7X6aFik(: +=6Ja/s+uk]s2&@h3YL''p)L.L*2OC-nC?Sc9ep8!U7pQ0qt(!cl;pNLSj0`VFs+nsEBkuLh*hf%`RhW= +f#Bn?K/u/P^hl)HqI]fq_R.UFSQk/F&QRKd>+7-Gcckj-4E>KpUYD4d4kO"2890*j'=Ik@)7s5cV +bJSt$Uk/:.):n^%[U,,-+[nnG[edULX_tsm,$MUH1p=9j=9QbXf3r?_5WekLrLVc4V'FkWBWUA]0qtP^ +*"u?coo',:%5jJcNG84rN"MC$Q_I2FX_;o^KfY>sIF5I7r6MR6&a9ME=5o7aHi?""/\0`30-M=L6qdL` +kiiue^-pf&Z!8Y-FS?Na6s[:aXKI2P=6ho]`Ik%;W=h)W-(^[eZE7b"s,b#]>1WgC#O>+[&p6g%j`m*W +&Zk%SA^ONNSo+$EWnVmk-0c7Y_3\iWm=7US^+LqPMA1Gl0@>sL$YY5ZU5Q7'$,%cpU:KY69;V&\1KE^` +Xlsnuoj)'5=6jtC,987f02]dU]0kEWWRU$`3"O?UFCO0;0H07]UF%ab$SZ +[c"/i8an%kep"K]M(\?F.Qp`+&qTtmNiTleq\-\sju@_pn[ElPn[ElPof%7)N@'%0l4u1'$.238Z\@MU +/mk=_ff![fMT_=t$=?-PARe87Rsp&h-"!CXV#Xjh1?U]18R&_0=t^>l?!GPupYeb9/'+%gQcbZ)oaC/A +2RH6$,&X%nYu.MK;8#mUGNd\[]q+m.5!$>Xn`pRlc/!mYHB85bgQht'#$oOG`92L6s!RO>fEaQTm5)Th +?$Gsd8Q*6WDO\E1rPanIkBT? +l)NpYeN$RC>4n`Sp,[M!cZm/g`cmL=R>&BC&JqqV#A=Sn<$7E2s/Vcg]Ia(.7#' +d>lg;l+:fQZctL)A)'6Lmk<)_a7qLN,^W9k1[;e@kNe'"3;`@kkb?Jt[7h$=G=M2i(t4R76A#E0nLSfP +c"t'E\j;BH)>?_["MnaXh' +@QgJXl%h8FB*Vf$Nh]t1:H2L#Rmg!J6r5)!3nL-5cTl,r_O2dCph#*Ulea;b7"RHj8+RelilILLU0M`2 +Z,3Td^l&$aGX&HopKold@XEa(lK54fCd;GsPmF_;9WXu#eIC8N;rK<)CU*_MoogOa9m9;P4S^;XUU+JN +,!4o'Eq8,OKB3`PSA1c;ektH9:m'jNrU5:Hfq)Y=,-[V$61;GR?eS%"ZTd=>67.IGT%agVlGLGp-ZO#b +O`EQ##_?S][A+)hrP,D\YL?@sOe[n;IHEko.DF\P69(h1KK_#1\UcdEmd]A@\_: +laFCDgn)r>[YOP*$5*sQ24&"88q*BrpR-)`*Ygmc[ZH&XZ%Yk(+qNF2[#J#E65QK+#%&3%\\KVDM)hAl +ZJ2'rk*M:($c&;KL@(S3SN+2kl^&d-+Y +`X^I66(%n`aN^m`LAGF;c8^pIVr'6jlK?uZ1tJ^mqu?BGc*LjFDtPo&P@MYd$?<'oHLil;E(S=gj"iZO +UsV-PJ?"CE:p*'R!S2ZLaj2e8%iSGn!__aI`pqXEX2&hG31TV6VKmF:&X9*6'Ti,b[\<-ge +#L8h/$\/-h1Nf0h@tIat'lo[KSd%QQhp(p):6KTt9XjWGC.O,Z`S#EkCf0PW>Yr0$gA9Uuh!fK]TO:b6P'(LVV:%D`X#$\9fL.HslJBI,P`_a3=:SQM7^R +eFFGH5?`Ll9h&<81Po"U*4=_qdINbUcd<).Pa9@8W,ubL*G]9MM9;5Qb<'_rn!I;8hX%E,EQYL+/W]^E +Y2qUkM581opL>0p+*8O,KZlZHp5L=QSaZrDHq+TM0DJ;M@7'C=#\WPBFhO +/Ucc&k(m!bD'?Bm$288F`Mt+n%Nq&h:9T2bN>9cZ^6!hl3lQ,UHC,5Y\P#`JC ++)CfMp_)1;NUJ"?WY5leMl**Wa +_fS1Q71^Z7MX'eNh*c[sQ5eH;jsO+ThaKsSAeZ"ckc.g(B-h^`PX0+]'B@"<1H`k=fqM*Q)NsM48O9@: +H5N\).VJ-a/_!K'hO<6r6f#BA*Y'cY/<9+^3EfR^q)PcIqFb"Y9:4BcH-s*#FhR/`_+:<0Cs(!`BZKPY +8\K4c'Go,07:F6R;JE(iVq!>3=Ia/Y6Zd52jr\VCjm%hho;p+o.%ctcf!cPn+r0r+am5"i'fr$&f?@MF +.+bJ&o!QVlf;Kc=CS8(ncTrF`n]#;;uQ'c^M,[!2OqW`,1c-h@EA]+ufa2#TO:nsil\XC:ml.\Z=[&)EltfX725+*_,>!h;1o^I,/YoB[g-nSB5L+iML$&fg;; +WQpsm(dDs;pr%r:I/XWrPlpLNpsEfF2(pQDfX4a+pV1YX/QReakGTdFROEGeY^J)$>$\n1SuRccX(sG\ +G4F-YrD,"G]Bd5oB;JW:/Z/C#dUI>eCs('\.DCtX8s])W:0.%D\FEW8D4q`Gh//CrMHrC;QD(rD/Z)`* +]X7kIYQigkrLlj@?ZTP!\lW(ZO_Z\le7)DiP#IjO?WsPZ09d6Y7"k&*%IJRXZ.aG\NESF4[@e;9[.6PA +eNkXZ(39tZ(45Q^:D7ksH`kS[h@RfltrRYPYF]#dlc_2-+EKb%@HG?gh/ZpZE +ldqsI>"j-oKGJq-T4a5Gb:00k>BhSVa&C%coOiD%<^]o1;uDP$0Nnt^9gGm^W$NNK>*Ii6[>6S.`/;rB +)N!Wll_`d`8t?4SXCoT@jjrO`r5,-bo_^'H>HMf;fhAPcX&fn>$7(+P[[N2TL99%tm15%3_36I_hhBY@ +d5@V&:.3(*U-"ga;Vo>)eMTsUZ&Z<"b +;JqtFKUl:BXj-6/4.Xa81LZ!WeeeW_4G=OlG"FWep,UcQhprW/7#:q3NP6Vf0Xg,kh88bgg97`Loldp+ +YrGcW[9E]HUL;+C@ooInVnFS3L8<:)N%d%Ke#)S&J=CO-(Y4Rd,LJps=-_L/95?iVYE4+1dG)1_g,A@i +9'_Ea]2ih?rR@i)#06U+-#2p=\l<^X^0A-(0hA/+2J`#!e+QdrVsS$0gD\RVm"ikA)npVYrOMoS%iH>H +SX=&dB\h]EBH8-3lJt=7[ak1mn[m_@iGl6\Dkuh+Xu[$6fs$Z/D0QJGnJ\!R1q`V>`:[i+^DkpR^?JpG +JuLK5@)hImFM[X4T^[&0,#*tX@Qp:'Qg$hCD:c%',9fa+;tA$FZo)2*F[C-iDm=HAA.feon=CE'N[p\3 +\gTJl`8=^dD1bJ#hmDYU0E(HcpMT?&6+)TBqu&Y-/B:1m>)IhU?4ik)KRj&]*@-$k7!kG,eU)&P*F +aV+l=Hf@f2O7s=bGlM?iB"EO-\ng-q5>lODX6#DId5QG7PGYX.?)feK^Y>nG^FQ".P@# +[:1KCc)tFDml(+SJ$n)q&ip_FpLs2rlOSG'kTKkE.ZHgC2gn8;S63=HgobBO1R+qD0mhS"@>3da\ +\`d,m]1(G3^RALmargT,YL+'S(.ZO9*]heT'.16WEmZjfIUr0iU<2RHnZ&=TBCpSLtVf(I#UsLAMM%g:HEggfP-jQ +_=nIf7^-9uJ)^'IpE%*>kn3BcYeRD/Q/ft)e#7IZN+a^+NZ,d;"H)Z"*aUG@]Tn&[+:o(GQP\sh=mV3* +Ok54;c77\nX;GP(nM0.90Nk*.KtE3L;HCtNPa<8$-@)#>1\YPWVJZ[dGBmEGpL!p#/5GXsdd7P009N!` +jg9'(PBe4D_lS`NLe8="$;SEobtb2*/E^%Fi23(EJtP`pNE>o'"<+"YibWhd]5mT]Hb:bV>;>^2M"FI< +Zn`+N)g<],BVY'G-gicmF@J4Td@Dh;o86AkRs8bhfD4Qrgem$S.5.21qpdE'?][DhSb;R+?R!qQ@MI\1 +W%0nZRP!n(NT&ckS.8#@SR\'8o&B,Qo+WP[NJ.RCI?P=M_[=#qp#eR05LdKZ?2Ej^2;Xl;dM^[pVQp=' +^g$WFRJ0g4igCeXq*2*jL\KsF+)S7iX)3iqMY4im.Mo2E.*dMamil'A"s2j"5A64DWi@Td`j[@sL+HrR +qgSYnH?5)O.\ld."4i$QGNb>1s4)\c;r/aZ_#$3jpYd/88'K2nQm:-3N&`B3Rio/s[Q*6]R1T"2*<'S\3mX=,s*rDPtcsYK@m,af=G;ooT\0,&,-:B9CIdr,ZiLK9>@nHmh)h`BJNP(t@T)fmORGVfrP\65kQr +_2B#eqEOR;90'_H2srlWS^.CBp\#W/m@3j!)R_efM;*Wc3U?%ZR:#$rC;cenS,u8bb8T?!=)j2h;NKME +%M`'9(a?HDTfMhXERm$jTOij>nduooat6M$N:YjSmdXY<2<-i_T(!V;4n0HJ5HS1tE4Y12/m>jZ<4GoJ +I/Xf"o7'iPLf&q\c2-<&K@s7]Ac[,+=OD!0Wft;4X"(8G4c>LgMub18$7J!QZZ]sE[)19dMe>5UO>hYf +Q&FQJ-6/-Jice8'6qC=]0$N$$Lj5c@Ze(.<,ki5E=*5>T?lp;*ZrZW/n%129[L-9GdmZ3^cS&:W0f2l: +.,eM"'U>"7E/cnE\:_C5<.Z`_EOqVoY?Z>c%TS(!;n9WSd-C:Bi-FX/:)hH"Up7@0CEcA:V;-CO4ZXND_kX4Rr#/-_aF_)?X1XqKZ5W,Q7[qM)?2,!Nd +$%Q)H;X=oA*F+a4qg76^CNdk$ru.errs^DnT1<%^cB$O,[=<( +CV,f`,-=tI>a31#XD=&*^'E&-oH!lQ55dMTW9940DFkSDZu)-iV<#Q:rDlo4?=l0+]LZJ]Gl&!c1nrPA(UAjj5(B)P?FrbTfNAorjPj@]c607g,QiCd/% +&F_!Gg\C=cC3SLE^JmQL1hUqO/A6q`9rGB[DlDes^O:rBodoI+>$\V8K>Nr\bh=XKX25E_[kD1/j,Yj% +l&j#Q5\7(@Q`+fjad8rD)o@q,`L--@lB%fgKqTY/Zpq0on0Nb)QO(5lmra#73i%*t*NF6=;ft_7.sc&F +I96HteQUet83s*49@WP/DNTpnL!o?]e?I>p[T>Q)7!5O=^-m+E9YQ1++`42"b!rH!p^ZThA\hBUP):`d +p"PfE?0"YO[SQZ2Jc*-?PJ4Da02]>^Uf/M>*FGOac"$.=^Yqh!1uV),\!ZnNFHQN,Mq^\LBj-NM;2[D[ +^6!^K@@%G48Gp_Xq\#%rq%AiNf.g!gc8AE_^'.?HFA1;1j\&,paHr".O=tV>(KE)fma8(9RbQuKQHH=F +%<_#B&%QMok8'>r&>;(lH^j"Y.pC2d6^ZU9A[ajW'5^.NR:pU)]?.%R?VROYk*?VE5I/$lh]LnU\OVTO +F3]TjEa,t@FJS\fZXY,\3T*L&,V?Jbg-"mF+O34jD,7'#3Q:NA"2n]MiN`O +8K,Z#BiEcqV,l)liihG(g9WB5%&g,ZQ8FKn8D\:YB?ADone/_**P[c4\ds.TGqjqK0W`ccRH?[ll$r&g +HTkD7X,8g3Zas3-31t]5],ndQo&l/bFlo_lKQ%ZU=SDdO8fGC>BL +Wk1_KSBpUA!S-,>lQ=%j\pJ$QJt\G8I)7hA+oT"h'g9,.*S<$7+XeBI6n`a@Z=hG-(r28+=# +55LaTUIhWUApj?I6h]%u>^H4S2grHS(>-9FI&,L@mI#IEp9s,,kB=HA5jNIhT5N^7E2b1FDs+PLhn=8S +bN8i'Z:E3Z,9;fRB1MYD(0^<4(E`8m42^GO=@t?PF"l+8K"R +pi\g,2i>e:.Yi<1"Z&A*rWB;G\jfNXWIgo?VK3]kHVr1u$94;6E\8F$lT:0rK4?7!a4H+59NdD:nTjk" +k]"VU8&I9BRLSA_`MW49=Rb*TA>ZmQJ1F>#pV%W\"jO$`s*efAR7B.:eo/g8=Y%YoBqs/r3 +H^E<.@IN)$V\c]0$/hEZ7>UXri, +.s^M>]2`OLE_&YS.RU.ZZ07JA\TuVqX`O$F(H*oUF[""(I(S`uB-n6q'@2#.0$uu+DKMu/p^bg+I!o(/ +s-ODji5&>nQJ(s'.-V03#+@Y'a"t+RYu3m=l*ktftHgo]k>>qE2B+J*t% +N(UAKk0t:"#,F:9bU-NH/'C%\Yo_1.Vj:qbhW($$#FBF6Q_.Zn1TGNd]T&OkS`a7`;0\7=]K(!BY:G5d +1P$L?s7UQb.Fj<"rH(oun*jS2ospMPe""cpS]J2Y2t)7J/RZogC6-r2r2`jB=6e'.U'l.1%8G@WW]/pO +A&g"GK/_,uUSURmofS.2H8m-YI2JJc4/oYfR/uJ88T.TkX,s*U>'QFOTF6cOK>L#=5<#L4f?GHQne%#kQSn4h/%p@I6q>akGbSU5bK +(S(,'NE*\hZhSb&3&eEp<&M:-5p"p;EA=nqc+18F^,D,o@`DPkE;a6`KX[P:rWPh- +,nb54X]?FBEYp+9Fs2lE##bDW5B.JeS]ZdiLM'IcWk+ErWUoCQ2C=tqC`%/Me)uH` +\MV5C[t].klBPup:a$hql.TAMfVqB5;,QAF?dBD3kSTuUEI:O)IrWob4E40OcZ;U(LI$f+?u7#piel6eUJ,.dhd)b4nd@h>?*Y-8$V`T<6X^(C0Br(&a6%"SRgXXDe7P'ABCiV +rV;?MJe>3G@EOa]jiCK0EFnbjo_AFl2=$C/pIrN9Z=[.TY(?e>9T!*N>b$Hd*Te`/cABhQS9*[.=aWp:r[$T*rS0ndablLHkB?<;:Q+qd.St%gg:W?P(355]ALVWd=b70YS1S*g +fc#3;YMtr0(S%1`^7tTg51/SMHBKfqA&Eg;NK2(G0`cC9&W.gl+en)I'dJDc^5PO*PTa*7:rK5\rc"*5 +P\*-O&W$6-U,NUSf;6rAHc8$/GAHu/&WNLC58eO1 +YL4a,&"V##1e4J8^6`tg`]$-JoK^i1JMFq39BgQTj>o=b\"CJj!jmt_7ujoKV_c3^k]HK'3ZU6Y6Ds"? +odlZZ3)Jr5N,pH6oK\_cO=n_&mPm6Jq;*`pc3RS=FP\ORf&f]/UXRaTQSrXSFJKU3S[ZJq\^Z^H;CXma +L69:"kttYG>u-VYf6]i^Wtj(IC8&"XI_;0#>Q-8Bh@2:PgTApN([6$P'+CEQaNl_u:=h.qdi"?rFLpVD +F?9!LKhduI6`6tDU!jlHD4N;i8>l&;S9;DQCT5d6P')'nl1Dbt)r--Fr3G3@?2#\BdriiR7Ji]]VmCSE +g9(BfXS\dAW>2M`WA1L'DuE"nY+&eXg'\l]Wg$SU7;OS:fDc:=T2l0Q=[=sol-L^8*+LV8<,>S$07t5. +m&LdoUX.C!EUh97c5#>>OQ\s +\0oe%))o\/+tRWFmpA2l@=$su0iouK9l2/F:D3EEh)ACodeO0E1;)c&dk\Su;9HS7\P^GHSL:3D)e4%]tN.'28Xc/][kt">-caF*O9', +-HNA+3q+(aft"=rlhoFse<)?N)O%a]c;TU:IH%X=U*eqA`RMa@hK!F8r<%P(;"6X`%W-nnBYS'I4pU5@ +XjiN0Kj>?PeUmN5\=XA6b?!S4Dl8_.;.&D(;HQqaG#\1JY=!.,^BsAuOn\Z<]L?EPrMfaV3;P#(rpb_H +o^B%ZXSt@$?2!`lSVnOGcVO5;]p-O)a<7K9r@=$Uqc@@3?[:IEC-FQ1_+3hW0&.C(=!H?2>`$i` +jpk%l=qe9D:ZP+NC=iB#a)3lBMUG"s@/mk$X7S19[.-d-d'+jD?#P3-Ii]K.`)IKiXb)CgfZ[m/s,R+6 +VQubnA&l*fW[Q+\`r%`>a*][blu*aHht=NCS`nY%*"7X`8Q`4et6aL_V$h&(u8 +YPJF#ZNoCV-\k5_bV)NgIi\IVp),hMa:=CIGlJkkj=KBq+I^N!_"dZD08tbc[,g9#BV2;nSS)RZGfD1$ +TXK)nOR8a)ouHkh$9<[!M#`Ci^f7`P'":TnanG"d\:5OFho>XIa=='S(:lSOq>ns(K,#=75>E!\ND`@9VMVHRQ.$Hcd5oo324 +%:=i9_:H]XJ'W%\T+I?=")\"?K:>Ud1GdV[!Dd;?M#+<"9'72,^DJ5F@r$mu0LJT>N'0KSJRTBHj:MqA +bTeg#5#48)"4T;4oi+.0Y?M>qT>@:M"r#n9*di\;7(*3B9&qH>33R1W@k'<\F#%_,IrI5na->J\s,&!" +YIF&s;D+*Gr/4q?,oZLkKMTosruJ`NogJ=J`=p3H't?dF]Dal%?E1sTiPFZbo7sEBmIn@m0G\O//M._o +jDCJ(AP9FBQ67Qi(;7ceTNtB>@&KjDQ,L +Esq(&)L$5tka2(/*mY9U/FO..m/=I8$/Y7O]Ho0g2uG^lL=gEZTj)2T*R1>f"6;FG4B)pm=eOu0n759$ +rGn#^?dH+ooiCZm0sN;%nPt7-#9`7M,[."kX^L!XneYCA;cCUL'Y+ErZupVk?8@AX/)O,8V/Vd +Ac;r*5I@S08eQ20G]f*[XmsYnZ%*NH)O1mV9DB8In@)L +V30n]a_93GAs6n9WR>[,cnoZAbBJGTnF7G-4lkM8A)'m)m[T!%Dj)6t*6/6)N%ks+qU?sBn4f$ZaE+t7 +?$gMN7tiAQNM##6jcQ2U+4d!K3r#R/U(:>r4hg($ceR8I&m'OZVCKfu5do[lN#(EjDrf!>mF<,V\0,0p +YZB?rbQX(q>CM/7^\:Z14JJ>2XCgb&aIn(AEEK$\5bRWNPC=XW\JcuH1P44P)']C`7[&"+F`n&FQ-:N$ +*m4bu$RhuOe[J/A4Y<#Eh_*%T/KDt75n:F`%\J74qVCn'.MC>"[>iB=JKu%?gm^cV!bib!HZ9#JXcc;* +G1hc-!=ebPh4'fb>u\p2InJ/q4&X`T_TJt0J?gD71:M0HP'net-rS532n5+rkZThtEH4RD]di^[UfcR_^9[;S< +))02Zmku"WN&aR-Z)o7;JZn='=_Ba]-r\e(SbY7Hm4\*&1EN&VjR1[s7rRA_Uh6^hUMOJVYKZMcSPmR- +(E03'0]=LPdRVMKP0"$ASB4,K*G^[!MR:ijIea6%jml/&Rh!'#n6qYV_If!Y_bpFe]__hU-SM:cf^*WL +KI?\?YW.S"gKX[kNr!]rhf25nF&i5\n)phKqS!o@qJ>U^Nq[BC2L`GFb2+1a0oC!`kl4^HJTkGP/Et#< +!B4AmIkgRjIK?/@8:l$(G]J%<,O1A=kI+8pj$';bE)!9X9^98W*Q +)(?IL0Q[MJ8[L9EoBL[/SST,[NW+pBZV;LgD7%?\5UQrEDu4M&V)[8M0+2=#Nu;##hXQ2GH/hJA:UX]p +^ODS8TAhZ^8*k"Hp(B?[rh,5?X?i.4_R?YGpk;V8NYk>hF0oI[3Sd0P>Y40=VXOPbCb4R4_auRd_*F@FLr0@41jh:&7K@$UqA5!O3#r]puND[f?R5HJS +7cdM'r'3Sq(@"ih_C^7*M5Vl#EcHcAiba_?'Y;e\GdAP_B +r,luN<:p>(C,02dHr:&s'9[Wg;C6V(`nL6t2QEGmpV(p7p`lV>U]1j+_)i/&WW"u6r4h+."!bb>3dVY' +rT.5XoP>Tt[,3QZDo,S*kh!N,1["`6GML/sgu(E4Y2J%U=tAJ@fa'\>6gom%VZMeq8 +j*XK-[nf'1b(.S2]iF/`XK,P>$XZiE11Vq`C$iJF\%:E`L:ag3(G&0u(S]s"iZ$j*8Un[oZ>g,h/Z7.K +#(3`d[&WGTIs4CYDoB9l64@I5TFA!ZoYatX3t9D3h!nLVQSQX6ZJ48r,k$&EU?>e$uiVoAr.mDHF#1n06kC2"1,`4&tUDZNbQS4f"oB.\![Y,WAq0(eXdk/(+ol<%%bDrI/6 +>qsI9n]%$K*+DBI^,Oq6;=@cTQ&XOBP"VS<$a$(kAC;AZ(_[\ao>X +,ME]M@R/8sE[&s?H#'C3IK^e/=ir,)j1YRrXa3U8>+>0Ja%3d?P8Vb'Gg@lrg-s^?1U\QNMaf-fPF`.o +;oX3D,\FToQIe7]b0h,HDD.!B+smBSne&Y/bePg<4r>:\j4hNnbZWH;fZD2fU.`qM?T/`:@,Ri#i\&jK +)<&W+I>g>B44iJ]g$PMTJ" +G&1*;2P$,(R<_W0?DAQ&_0pI*[0ufMX]rI7*_EJiHb*Gp^#lSA!cGekILgI+#ii!%N0q^JjTRl[?0h@U +GhQaB?KBQ]?[+M&q=K[^I*0XEM)iBK$:aN&oQf/qbaCbEXha&+d+s^"40/.ZRH)3_en"/<4,3L>q-cbV +^<";E1sJ4LCmV^4?*j/0pmSPLD,(Vd +fi!bha(ICaCZ5epHBu*lfdHAYF,k!@8iEqhH11b_eNX!Q.0nV[_;cb0Ui?f8dT^a8h&&A9kbD9M?c(I0 +7-8?"q%C5?6Y*RB2SPP#9=+%k/.M(,)qiAr?7f1EA81ShW'nn47,=S?<]`sNgXD.hY6i[En1X$!Cd`4# +M7T3-?-hVM1`"h=$Vh,_`V*UNpT/5L4=D$!!K!c2fh]66tUgD +p_'"B8+Ir]j+ZPe(tk68\EZXjn,=Wbgsb\66smfH:-,7OBf]rNp!0C?fjskqH>[VlZgo^!7q`>-&hj+C +dk')*VdpD27R$@o_)3q$q2QQO$0:jZ%pNIUqIY$'okJc[lohWLrT0lgh7lI@f(!P&UFFO!M4^!3,+m8J$7PRPhp:d?&I>.o&qp:__,Zp[T^a7CHGKO +1Angtbl_0;]p6@a]'@Gq'+TMqft)sRf6fVX%NG!/pR85.,hCdm"shLMUUScTPs2V\iU;oboD!Z.^AU4> +>YF^Xa'ql9[LB,KSrf]a4P;rX-@%MOomp6^Zb!'HNBNi'B%QG,[?=WBeUtc@fpd+Y00XMJh=tHRZ(6mF[RVZWi@P[!1c3BoB@1 +mX3D5j41>]]\_/Q^dY>lrUfQ`DlWe"_gWZ:34lg:p;!Sd.l_qF;Yq!kTD]#hYM"+5O775`Y=X)NJ*hA[ +I-R?ng!u\f7i]7te'q.Z$BIt`iO_8!X7$gPK(&F*gNS#@RBMsY>\Xc^1S@Z`Om7Y!,()/_o/41&(Fh;2qE=^Y7j2F$e]*9;L=OKuH+>Oim8; +5!srD).ED+#7Yaq$d2`*04j>4\LU_:ne[GZi>-\tm.9S'lJ&sDLX$0;$TX_?\9fUB%c.(H-O[no*^n89\>6'geE?A"J\mmW&"]iOhu*8mDYpuS#OQhQpV+6L$Af.S=i\9V/eC0iH.USN +QY)FW?;cdV*d`m[M:,s"0c`9EkPBjWWH;Dt]&ZM@&+1"^H'/C1Rd@[M_rp@6,,;ecdJN[Fn7J!C/VW). +NWHM5P2T'>sdVn]%C8"\IOi!Q"J +GK@F;P*kcTKRJNH:;_+BHbsS\CMYb:@5eI*BmO.l=W;.;ML]7fGTE\::+^0/"ZGKiD)$m +^U-20NFm#@>-3:-oVJJ*I'lVebF..EoD2&sC/;TU8DP:BUshrHk7?aS7]/^ZUZms6l$@VeZ8'pHS]V?iT6fpTE+4"O=( +endstream +endobj +7 0 obj + 40412 +endobj +3 0 obj + << + /Parent null + /Type /Pages + /MediaBox [0.0000 0.0000 537.00 194.00] + /Resources 8 0 R + /Kids [5 0 R] + /Count 1 + >> +endobj +9 0 obj + [/PDF /Text /ImageC] +endobj +10 0 obj + << + /S /Transparency + /CS /DeviceRGB + /I true + /K false + >> +endobj +11 0 obj + << + /Alpha1 + << + /ca 1.0000 + /CA 1.0000 + /BM /Normal + /AIS false + >> + >> +endobj +8 0 obj + << + /ProcSet 9 0 R + /ExtGState 11 0 R + >> +endobj +xref +0 12 +0000000000 65535 f +0000000015 00000 n +0000000315 00000 n +0000041155 00000 n +0000000445 00000 n +0000000521 00000 n +0000000609 00000 n +0000041131 00000 n +0000041609 00000 n +0000041325 00000 n +0000041364 00000 n +0000041466 00000 n +trailer +<< + /Size 12 + /Root 2 0 R + /Info 1 0 R +>> +startxref +41682 +%%EOF diff --git a/satrs-book/README.md b/satrs-book/README.md new file mode 100644 index 0000000..c6fcf81 --- /dev/null +++ b/satrs-book/README.md @@ -0,0 +1,12 @@ +sat-rs book +========= + +High-level documentation of the [sat-rs project](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/). + +## Building + +If you have not done so, install `mdbook` using `cargo install mdbook --locked`. + +```sh +mdbook build +``` diff --git a/satrs-book/src/events.md b/satrs-book/src/events.md index 083e76a..95c5df2 100644 --- a/satrs-book/src/events.md +++ b/satrs-book/src/events.md @@ -13,4 +13,4 @@ event components recommended by this framework do not really need this service. The following images shows how the flow of events could look like in a system where components can generate events, and where other system components might be interested in those events: -![Event flow](images/event_man_arch.png) +![Event flow](../../images/events/event_man_arch.png) diff --git a/satrs-book/src/images/event_man_arch.png b/satrs-book/src/images/event_man_arch.png deleted file mode 100644 index 61c8d72..0000000 Binary files a/satrs-book/src/images/event_man_arch.png and /dev/null differ diff --git a/satrs-core/Cargo.toml b/satrs-core/Cargo.toml index 6fa8535..d16f0a0 100644 --- a/satrs-core/Cargo.toml +++ b/satrs-core/Cargo.toml @@ -73,11 +73,10 @@ features = ["all"] optional = true [dependencies.spacepackets] -version = "0.7.0-beta.2" +version = "0.7.0-beta.4" default-features = false # git = "https://egit.irs.uni-stuttgart.de/rust/spacepackets.git" -# rev = "79d26e1a6" -# branch = "" +# rev = "297cfad22637d3b07a1b27abe56d9a607b5b82a7" [dependencies.cobs] git = "https://github.com/robamu/cobs.rs.git" @@ -91,6 +90,7 @@ zerocopy = "0.7" once_cell = "1.13" serde_json = "1" rand = "0.8" +tempfile = "3" [dev-dependencies.postcard] version = "1" diff --git a/satrs-core/images/event_man_arch.graphml b/satrs-core/images/event_man_arch.graphml deleted file mode 100644 index 1336793..0000000 --- a/satrs-core/images/event_man_arch.graphml +++ /dev/null @@ -1,259 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - Example Event Flow - - - - - - - - - - - Event Manager - - - - - - - - - - - Event -Creator 0 - - - - - - - - - - - Event -Creator 2 - - - - - - - - - - - Event -Creator 1 - - - - - - - - - - - Event -Creator 3 - - - - - - - - - - - PUS Service 5 -Event Reporting - - - - - - - - - - - - PUS Service 19 -Event Action - - - - - - - - - - - Telemetry -Sink - - - - - - - - - - - Subscriptions - -1. Event Creator 0 subscribes - for event 0 -2. Event Creator 1 subscribes - for event group 2 -3. PUS Service 5 handler - subscribes for all events -4. PUS Service 19 handler - subscribes for all events - - - - - - - - - - - event 1 -(group 1) - - - - - - - - - - - - - event 0 -(group 0) - - - - - - - - - - - event 2 -(group 3) - - - - - - - - - - - - - event 3 (group 2) -event 4 (group 2) - - - - - - - - - - - <<all events>> - - - - - - - - - - - <<all events>> - - - - - - - - - - - - - event 1 -event 2 - - - - - - - - - - - group 2 - - - - - - - - - - - enabled Events -as PUS 5 TM - - - - - - - - - diff --git a/satrs-core/images/event_man_arch.png b/satrs-core/images/event_man_arch.png deleted file mode 100644 index 61c8d72..0000000 Binary files a/satrs-core/images/event_man_arch.png and /dev/null differ diff --git a/satrs-core/src/cfdp/dest.rs b/satrs-core/src/cfdp/dest.rs index b66365a..1899abd 100644 --- a/satrs-core/src/cfdp/dest.rs +++ b/satrs-core/src/cfdp/dest.rs @@ -1,47 +1,32 @@ -use core::str::{from_utf8, Utf8Error}; -use std::{ - fs::{metadata, File}, - io::{BufReader, Read, Seek, SeekFrom, Write}, - path::{Path, PathBuf}, -}; - use crate::cfdp::user::TransactionFinishedParams; +use core::str::{from_utf8, Utf8Error}; +use std::path::{Path, PathBuf}; use super::{ - user::{CfdpUser, MetadataReceivedParams}, - PacketInfo, PacketTarget, State, TransactionId, TransactionStep, CRC_32, + filestore::{FilestoreError, VirtualFilestore}, + user::{CfdpUser, FileSegmentRecvdParams, MetadataReceivedParams}, + CheckTimer, CheckTimerCreator, EntityType, LocalEntityConfig, PacketInfo, PacketTarget, + RemoteEntityConfig, RemoteEntityConfigProvider, State, TimerContext, TransactionId, + TransactionStep, }; +use alloc::boxed::Box; use smallvec::SmallVec; use spacepackets::{ cfdp::{ pdu::{ eof::EofPdu, file_data::FileDataPdu, - finished::{DeliveryCode, FileStatus, FinishedPdu}, - metadata::{MetadataGenericParams, MetadataPdu}, - CommonPduConfig, FileDirectiveType, PduError, PduHeader, + finished::{DeliveryCode, FileStatus, FinishedPduCreator}, + metadata::{MetadataGenericParams, MetadataPduReader}, + CfdpPdu, CommonPduConfig, FileDirectiveType, PduError, PduHeader, WritablePduPacket, }, - tlv::{msg_to_user::MsgToUserTlv, EntityIdTlv, TlvType}, - ConditionCode, PduType, TransmissionMode, + tlv::{msg_to_user::MsgToUserTlv, EntityIdTlv, GenericTlv, TlvType}, + ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode, }, util::UnsignedByteField, }; use thiserror::Error; -pub struct DestinationHandler { - id: UnsignedByteField, - step: TransactionStep, - state: State, - tparams: TransactionParams, - packets_to_send_ctx: PacketsToSendContext, -} - -#[derive(Debug, Default)] -struct PacketsToSendContext { - packet_available: bool, - directive: Option, -} - #[derive(Debug)] struct FileProperties { src_file_name: [u8; u8::MAX as usize], @@ -51,28 +36,45 @@ struct FileProperties { dest_path_buf: PathBuf, } +#[derive(Debug, PartialEq, Eq, Copy, Clone)] +enum CompletionDisposition { + Completed = 0, + Cancelled = 1, +} + #[derive(Debug)] struct TransferState { transaction_id: Option, - progress: usize, + metadata_params: MetadataGenericParams, + progress: u64, + metadata_only: bool, condition_code: ConditionCode, delivery_code: DeliveryCode, file_status: FileStatus, - metadata_params: MetadataGenericParams, + completion_disposition: CompletionDisposition, + checksum: u32, + current_check_count: u32, + current_check_timer: Option>, } impl Default for TransferState { fn default() -> Self { Self { transaction_id: None, + metadata_params: Default::default(), progress: Default::default(), + metadata_only: false, condition_code: ConditionCode::NoError, delivery_code: DeliveryCode::Incomplete, file_status: FileStatus::Unreported, - metadata_params: Default::default(), + completion_disposition: CompletionDisposition::Completed, + checksum: 0, + current_check_count: 0, + current_check_timer: None, } } } + #[derive(Debug)] struct TransactionParams { tstate: TransferState, @@ -81,6 +83,13 @@ struct TransactionParams { cksum_buf: [u8; 1024], msgs_to_user_size: usize, msgs_to_user_buf: [u8; 1024], + remote_cfg: Option, +} + +impl TransactionParams { + fn transmission_mode(&self) -> TransmissionMode { + self.pdu_conf.trans_mode + } } impl Default for FileProperties { @@ -96,8 +105,8 @@ impl Default for FileProperties { } impl TransactionParams { - fn file_size(&self) -> usize { - self.tstate.metadata_params.file_size as usize + fn file_size(&self) -> u64 { + self.tstate.metadata_params.file_size } fn metadata_params(&self) -> &MetadataGenericParams { @@ -114,6 +123,7 @@ impl Default for TransactionParams { msgs_to_user_buf: [0; 1024], tstate: Default::default(), file_properties: Default::default(), + remote_cfg: None, } } } @@ -122,6 +132,7 @@ impl TransactionParams { fn reset(&mut self) { self.tstate.condition_code = ConditionCode::NoError; self.tstate.delivery_code = DeliveryCode::Incomplete; + self.tstate.file_status = FileStatus::Unreported; } } @@ -141,36 +152,137 @@ pub enum DestError { EmptySrcFileField, #[error("empty dest file field")] EmptyDestFileField, + #[error("packets to be sent are still left")] + PacketToSendLeft, #[error("pdu error {0}")] Pdu(#[from] PduError), #[error("io error {0}")] Io(#[from] std::io::Error), + #[error("file store error {0}")] + Filestore(#[from] FilestoreError), #[error("path conversion error {0}")] PathConversion(#[from] Utf8Error), #[error("error building dest path from source file name and dest folder")] - PathConcatError, + PathConcat, + #[error("no remote entity configuration found for {0:?}")] + NoRemoteCfgFound(UnsignedByteField), +} + +pub trait CfdpPacketSender: Send { + fn send_pdu( + &mut self, + pdu_type: PduType, + file_directive_type: Option, + raw_pdu: &[u8], + ) -> Result<(), PduError>; +} + +/// This is the primary CFDP destination handler. It models the CFDP destination entity, which is +/// primarily responsible for receiving files sent from another CFDP entity. It performs the +/// reception side of File Copy Operations. +/// +/// The [DestinationHandler::state_machine] function is the primary function to drive the +/// destination handler. It can be used to insert packets into the destination +/// handler and driving the state machine, which might generate new +/// packets to be sent to the remote entity. Please note that the destination handler can also +/// only process Metadata, EOF and Prompt PDUs in addition to ACK PDUs where the acknowledged +/// PDU is the Finished PDU. +/// +/// All generated packets are sent via the [CfdpPacketSender] trait, which is implemented by the +/// user and passed as a constructor parameter. The number of generated packets is returned +/// by the state machine call. +pub struct DestinationHandler { + local_cfg: LocalEntityConfig, + step: TransactionStep, + state: State, + tparams: TransactionParams, + packet_buf: alloc::vec::Vec, + packet_sender: Box, + vfs: Box, + remote_cfg_table: Box, + check_timer_creator: Box, } impl DestinationHandler { - pub fn new(id: impl Into) -> Self { + /// Constructs a new destination handler. + /// + /// # Arguments + /// + /// * `local_cfg` - The local CFDP entity configuration, consisting of the local entity ID, + /// the indication configuration, and the fault handlers. + /// * `max_packet_len` - The maximum expected generated packet size in bytes. Each time a + /// packet is sent, it will be buffered inside an internal buffer. The length of this buffer + /// will be determined by this parameter. This parameter can either be a known upper bound, + /// or it can specifically be determined by the largest packet size parameter of all remote + /// entity configurations in the passed `remote_cfg_table`. + /// * `packet_sender` - All generated packets are sent via this abstraction. + /// * `vfs` - Virtual filestore implementation to decouple the CFDP implementation from the + /// underlying filestore/filesystem. This allows to use this handler for embedded systems + /// where a standard runtime might not be available. + /// * `remote_cfg_table` - A table of all expected remote entities this entity will communicate + /// with. It contains various configuration parameters required for file transfers. + /// * `check_timer_creator` - This is used by the CFDP handler to generate timers required + /// by various tasks. + pub fn new( + local_cfg: LocalEntityConfig, + max_packet_len: usize, + packet_sender: Box, + vfs: Box, + remote_cfg_table: Box, + check_timer_creator: Box, + ) -> Self { Self { - id: id.into(), + local_cfg, step: TransactionStep::Idle, state: State::Idle, tparams: Default::default(), - packets_to_send_ctx: Default::default(), + packet_buf: alloc::vec![0; max_packet_len], + packet_sender, + vfs, + remote_cfg_table, + check_timer_creator, } } - pub fn state_machine(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { + /// This is the core function to drive the destination handler. It is also used to insert + /// packets into the destination handler. + /// + /// The state machine should either be called if a packet with the appropriate destination ID + /// is received, or periodically in IDLE periods to perform all CFDP related tasks, for example + /// checking for timeouts or missed file segments. + pub fn state_machine( + &mut self, + cfdp_user: &mut impl CfdpUser, + packet_to_insert: Option<&PacketInfo>, + ) -> Result { + if let Some(packet) = packet_to_insert { + self.insert_packet(cfdp_user, packet)?; + } match self.state { State::Idle => todo!(), - State::BusyClass1Nacked => self.fsm_nacked(cfdp_user), - State::BusyClass2Acked => todo!("acknowledged mode not implemented yet"), + State::Busy => self.fsm_busy(cfdp_user), + State::Suspended => todo!(), } } - pub fn insert_packet(&mut self, packet_info: &PacketInfo) -> Result<(), DestError> { + /// Returns [None] if the state machine is IDLE, and the transmission mode of the current + /// request otherwise. + pub fn transmission_mode(&self) -> Option { + if self.state == State::Idle { + return None; + } + Some(self.tparams.transmission_mode()) + } + + pub fn transaction_id(&self) -> Option { + self.tstate().transaction_id + } + + fn insert_packet( + &mut self, + cfdp_user: &mut impl CfdpUser, + packet_info: &PacketInfo, + ) -> Result<(), DestError> { if packet_info.target() != PacketTarget::DestEntity { // Unwrap is okay here, a PacketInfo for a file data PDU should always have the // destination as the target. @@ -184,68 +296,23 @@ impl DestinationHandler { return Err(DestError::DirectiveExpected); } self.handle_file_directive( + cfdp_user, packet_info.pdu_directive.unwrap(), packet_info.raw_packet, ) } - PduType::FileData => self.handle_file_data(packet_info.raw_packet), + PduType::FileData => self.handle_file_data(cfdp_user, packet_info.raw_packet), } } - pub fn packet_to_send_ready(&self) -> bool { - self.packets_to_send_ctx.packet_available - } - - pub fn get_next_packet_to_send( - &self, - buf: &mut [u8], - ) -> Result, DestError> { - if !self.packet_to_send_ready() { - return Ok(None); - } - let directive = self.packets_to_send_ctx.directive.unwrap(); - let written_size = match directive { - FileDirectiveType::FinishedPdu => { - let pdu_header = PduHeader::new_no_file_data(self.tparams.pdu_conf, 0); - let finished_pdu = if self.tparams.tstate.condition_code == ConditionCode::NoError - || self.tparams.tstate.condition_code == ConditionCode::UnsupportedChecksumType - { - FinishedPdu::new_default( - pdu_header, - self.tparams.tstate.delivery_code, - self.tparams.tstate.file_status, - ) - } else { - // TODO: Are there cases where this ID is actually the source entity ID? - let entity_id = EntityIdTlv::new(self.id); - FinishedPdu::new_with_error( - pdu_header, - self.tparams.tstate.condition_code, - self.tparams.tstate.delivery_code, - self.tparams.tstate.file_status, - entity_id, - ) - }; - finished_pdu.write_to_bytes(buf)? - } - FileDirectiveType::AckPdu => todo!(), - FileDirectiveType::NakPdu => todo!(), - FileDirectiveType::KeepAlivePdu => todo!(), - _ => { - // This should never happen and is considered an internal impl error - panic!("invalid file directive {directive:?} for dest handler send packet"); - } - }; - Ok(Some((directive, written_size))) - } - - pub fn handle_file_directive( + fn handle_file_directive( &mut self, + cfdp_user: &mut impl CfdpUser, pdu_directive: FileDirectiveType, raw_packet: &[u8], ) -> Result<(), DestError> { match pdu_directive { - FileDirectiveType::EofPdu => self.handle_eof_pdu(raw_packet)?, + FileDirectiveType::EofPdu => self.handle_eof_pdu(cfdp_user, raw_packet)?, FileDirectiveType::FinishedPdu | FileDirectiveType::NakPdu | FileDirectiveType::KeepAlivePdu => { @@ -262,30 +329,47 @@ impl DestinationHandler { Ok(()) } - pub fn handle_metadata_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + fn handle_metadata_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { if self.state != State::Idle { return Err(DestError::RecvdMetadataButIsBusy); } - let metadata_pdu = MetadataPdu::from_bytes(raw_packet)?; + let metadata_pdu = MetadataPduReader::from_bytes(raw_packet)?; self.tparams.reset(); self.tparams.tstate.metadata_params = *metadata_pdu.metadata_params(); + let remote_cfg = self + .remote_cfg_table + .get_remote_config(metadata_pdu.source_id().value()); + if remote_cfg.is_none() { + return Err(DestError::NoRemoteCfgFound(metadata_pdu.dest_id())); + } + self.tparams.remote_cfg = Some(*remote_cfg.unwrap()); + + // TODO: Support for metadata only PDUs. let src_name = metadata_pdu.src_file_name(); - if src_name.is_empty() { + let dest_name = metadata_pdu.dest_file_name(); + if src_name.is_empty() && dest_name.is_empty() { + self.tparams.tstate.metadata_only = true; + } + if !self.tparams.tstate.metadata_only && src_name.is_empty() { return Err(DestError::EmptySrcFileField); } - self.tparams.file_properties.src_file_name[..src_name.len_value()] - .copy_from_slice(src_name.value()); - self.tparams.file_properties.src_file_name_len = src_name.len_value(); - let dest_name = metadata_pdu.dest_file_name(); - if dest_name.is_empty() { + if !self.tparams.tstate.metadata_only && dest_name.is_empty() { return Err(DestError::EmptyDestFileField); } - self.tparams.file_properties.dest_file_name[..dest_name.len_value()] - .copy_from_slice(dest_name.value()); - self.tparams.file_properties.dest_file_name_len = dest_name.len_value(); - self.tparams.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); - self.tparams.msgs_to_user_size = 0; - if metadata_pdu.options().is_some() { + if !self.tparams.tstate.metadata_only { + self.tparams.file_properties.src_file_name[..src_name.len_value()] + .copy_from_slice(src_name.value()); + self.tparams.file_properties.src_file_name_len = src_name.len_value(); + if dest_name.is_empty() { + return Err(DestError::EmptyDestFileField); + } + self.tparams.file_properties.dest_file_name[..dest_name.len_value()] + .copy_from_slice(dest_name.value()); + self.tparams.file_properties.dest_file_name_len = dest_name.len_value(); + self.tparams.pdu_conf = *metadata_pdu.pdu_header().common_pdu_conf(); + self.tparams.msgs_to_user_size = 0; + } + if !metadata_pdu.options().is_empty() { for option_tlv in metadata_pdu.options_iter().unwrap() { if option_tlv.is_standard_tlv() && option_tlv.tlv_type().unwrap() == TlvType::MsgToUser @@ -297,82 +381,198 @@ impl DestinationHandler { } } } - if self.tparams.pdu_conf.trans_mode == TransmissionMode::Unacknowledged { - self.state = State::BusyClass1Nacked; - } else { - self.state = State::BusyClass2Acked; - } + self.state = State::Busy; self.step = TransactionStep::TransactionStart; Ok(()) } - pub fn handle_file_data(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { - if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus { + fn handle_file_data( + &mut self, + user: &mut impl CfdpUser, + raw_packet: &[u8], + ) -> Result<(), DestError> { + if self.state == State::Idle + || (self.step != TransactionStep::ReceivingFileDataPdus + && self.step != TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling) + { return Err(DestError::WrongStateForFileDataAndEof); } let fd_pdu = FileDataPdu::from_bytes(raw_packet)?; - let mut dest_file = File::options() - .write(true) - .open(&self.tparams.file_properties.dest_path_buf)?; - dest_file.seek(SeekFrom::Start(fd_pdu.offset()))?; - dest_file.write_all(fd_pdu.file_data())?; + if self.local_cfg.indication_cfg.file_segment_recv { + user.file_segment_recvd_indication(&FileSegmentRecvdParams { + id: self.tstate().transaction_id.unwrap(), + offset: fd_pdu.offset(), + length: fd_pdu.file_data().len(), + segment_metadata: fd_pdu.segment_metadata(), + }); + } + if let Err(e) = self.vfs.write_data( + self.tparams.file_properties.dest_path_buf.to_str().unwrap(), + fd_pdu.offset(), + fd_pdu.file_data(), + ) { + self.declare_fault(ConditionCode::FilestoreRejection); + return Err(e.into()); + } + self.tstate_mut().progress += fd_pdu.file_data().len() as u64; Ok(()) } - #[allow(clippy::needless_if)] - pub fn handle_eof_pdu(&mut self, raw_packet: &[u8]) -> Result<(), DestError> { + fn handle_eof_pdu( + &mut self, + cfdp_user: &mut impl CfdpUser, + raw_packet: &[u8], + ) -> Result<(), DestError> { if self.state == State::Idle || self.step != TransactionStep::ReceivingFileDataPdus { return Err(DestError::WrongStateForFileDataAndEof); } let eof_pdu = EofPdu::from_bytes(raw_packet)?; - let checksum = eof_pdu.file_checksum(); - // For a standard disk based file system, which is assumed to be used for now, the file - // will always be retained. This might change in the future. - self.tparams.tstate.file_status = FileStatus::Retained; - if self.checksum_check(checksum)? { - self.tparams.tstate.condition_code = ConditionCode::NoError; - self.tparams.tstate.delivery_code = DeliveryCode::Complete; - } else { - self.tparams.tstate.condition_code = ConditionCode::FileChecksumFailure; + if self.local_cfg.indication_cfg.eof_recv { + // Unwrap is okay here, application logic ensures that transaction ID is valid here. + cfdp_user.eof_recvd_indication(self.tparams.tstate.transaction_id.as_ref().unwrap()); } - // TODO: Check progress, and implement transfer completion timer as specified in the - // standard. This timer protects against out of order arrival of packets. - if self.tparams.tstate.progress != self.tparams.file_size() {} - if self.state == State::BusyClass1Nacked { - self.step = TransactionStep::TransferCompletion; + let regular_transfer_finish = if eof_pdu.condition_code() == ConditionCode::NoError { + self.handle_no_error_eof_pdu(&eof_pdu)? } else { - self.step = TransactionStep::SendingAckPdu; + todo!("implement cancel request handling"); + }; + if regular_transfer_finish { + self.file_transfer_complete_transition(); } Ok(()) } + /// Returns whether the transfer can be completed regularly. + fn handle_no_error_eof_pdu(&mut self, eof_pdu: &EofPdu) -> Result { + // CFDP 4.6.1.2.9: Declare file size error if progress exceeds file size + if self.tparams.tstate.progress > eof_pdu.file_size() + && self.declare_fault(ConditionCode::FileSizeError) != FaultHandlerCode::IgnoreError + { + return Ok(false); + } else if (self.tparams.tstate.progress < eof_pdu.file_size()) + && self.tparams.transmission_mode() == TransmissionMode::Acknowledged + { + // CFDP 4.6.4.3.1: The end offset of the last received file segment and the file + // size as stated in the EOF PDU is not the same, so we need to add that segment to + // the lost segments for the deferred lost segment detection procedure. + // TODO: Proper lost segment handling. + // self._params.acked_params.lost_seg_tracker.add_lost_segment( + // (self._params.fp.progress, self._params.fp.file_size_eof) + // ) + } + + self.tparams.tstate.checksum = eof_pdu.file_checksum(); + if self.tparams.transmission_mode() == TransmissionMode::Unacknowledged + && !self.checksum_verify(self.tparams.tstate.checksum) + { + if self.declare_fault(ConditionCode::FileChecksumFailure) + != FaultHandlerCode::IgnoreError + { + return Ok(false); + } + self.start_check_limit_handling(); + return Ok(false); + } + Ok(true) + } + + fn file_transfer_complete_transition(&mut self) { + if self.tparams.transmission_mode() == TransmissionMode::Unacknowledged { + self.step = TransactionStep::TransferCompletion; + } else { + // TODO: Prepare ACK PDU somehow. + self.step = TransactionStep::SendingAckPdu; + } + } + + fn checksum_verify(&mut self, checksum: u32) -> bool { + let mut file_delivery_complete = false; + if self.tparams.metadata_params().checksum_type == ChecksumType::NullChecksum + || self.tparams.tstate.metadata_only + { + file_delivery_complete = true; + self.tparams.tstate.delivery_code = DeliveryCode::Complete; + self.tparams.tstate.condition_code = ConditionCode::NoError; + } else { + match self.vfs.checksum_verify( + self.tparams.file_properties.dest_path_buf.to_str().unwrap(), + self.tparams.metadata_params().checksum_type, + checksum, + &mut self.tparams.cksum_buf, + ) { + Ok(checksum_success) => { + file_delivery_complete = checksum_success; + } + Err(e) => match e { + FilestoreError::ChecksumTypeNotImplemented(_) => { + self.declare_fault(ConditionCode::UnsupportedChecksumType); + // For this case, the applicable algorithm shall be the the null checksum, + // which is always succesful. + file_delivery_complete = true; + } + _ => { + self.declare_fault(ConditionCode::FilestoreRejection); + // Treat this equivalent to a failed checksum procedure. + } + }, + }; + } + file_delivery_complete + } + + fn start_check_limit_handling(&mut self) { + self.step = TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling; + self.tparams.tstate.current_check_timer = Some( + self.check_timer_creator + .get_check_timer_provider(TimerContext::CheckLimit { + local_id: self.local_cfg.id, + remote_id: self.tparams.remote_cfg.unwrap().entity_id, + entity_type: EntityType::Receiving, + }), + ); + self.tparams.tstate.current_check_count = 0; + } + + fn check_limit_handling(&mut self) { + if self.tparams.tstate.current_check_timer.is_none() { + return; + } + let check_timer = self.tparams.tstate.current_check_timer.as_ref().unwrap(); + if check_timer.has_expired() { + if self.checksum_verify(self.tparams.tstate.checksum) { + self.file_transfer_complete_transition(); + return; + } + if self.tparams.tstate.current_check_count + 1 + >= self.tparams.remote_cfg.unwrap().check_limit + { + self.declare_fault(ConditionCode::CheckLimitReached); + } else { + self.tparams.tstate.current_check_count += 1; + self.tparams + .tstate + .current_check_timer + .as_mut() + .unwrap() + .reset(); + } + } + } + pub fn handle_prompt_pdu(&mut self, _raw_packet: &[u8]) -> Result<(), DestError> { todo!(); } - fn checksum_check(&mut self, expected_checksum: u32) -> Result { - let mut digest = CRC_32.digest(); - let file_to_check = File::open(&self.tparams.file_properties.dest_path_buf)?; - let mut buf_reader = BufReader::new(file_to_check); - loop { - let bytes_read = buf_reader.read(&mut self.tparams.cksum_buf)?; - if bytes_read == 0 { - break; - } - digest.update(&self.tparams.cksum_buf[0..bytes_read]); - } - if digest.finalize() == expected_checksum { - return Ok(true); - } - Ok(false) - } - - fn fsm_nacked(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { + fn fsm_busy(&mut self, cfdp_user: &mut impl CfdpUser) -> Result { + let mut sent_packets = 0; if self.step == TransactionStep::TransactionStart { self.transaction_start(cfdp_user)?; } + if self.step == TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling { + self.check_limit_handling(); + } if self.step == TransactionStep::TransferCompletion { - self.transfer_completion(cfdp_user)?; + sent_packets += self.transfer_completion(cfdp_user)?; } if self.step == TransactionStep::SendingAckPdu { todo!("no support for acknowledged mode yet"); @@ -380,7 +580,7 @@ impl DestinationHandler { if self.step == TransactionStep::SendingFinishedPdu { self.reset(); } - Ok(()) + Ok(sent_packets) } /// Get the step, which denotes the exact step of a pending CFDP transaction when applicable. @@ -424,7 +624,7 @@ impl DestinationHandler { let metadata_recvd_params = MetadataReceivedParams { id, source_id, - file_size: self.tparams.tstate.metadata_params.file_size, + file_size: self.tparams.file_size(), src_file_name: src_name, dest_file_name: dest_name, msgs_to_user: &msgs_to_user[..num_msgs_to_user], @@ -432,101 +632,258 @@ impl DestinationHandler { self.tparams.tstate.transaction_id = Some(id); cfdp_user.metadata_recvd_indication(&metadata_recvd_params); - if dest_path.exists() { - let dest_metadata = metadata(dest_path)?; - if dest_metadata.is_dir() { - // Create new destination path by concatenating the last part of the source source - // name and the destination folder. For example, for a source file of /tmp/hello.txt - // and a destination name of /home/test, the resulting file name should be - // /home/test/hello.txt - let source_path = Path::new(from_utf8( - &self.tparams.file_properties.src_file_name - [..self.tparams.file_properties.src_file_name_len], - )?); - - let source_name = source_path.file_name(); - if source_name.is_none() { - return Err(DestError::PathConcatError); - } - let source_name = source_name.unwrap(); - self.tparams.file_properties.dest_path_buf.push(source_name); + // TODO: This is the only remaining function which uses std.. the easiest way would + // probably be to use a static pre-allocated dest path buffer to store any concatenated + // paths. + if dest_path.exists() && self.vfs.is_dir(dest_path.to_str().unwrap()) { + // Create new destination path by concatenating the last part of the source source + // name and the destination folder. For example, for a source file of /tmp/hello.txt + // and a destination name of /home/test, the resulting file name should be + // /home/test/hello.txt + let source_path = Path::new(from_utf8( + &self.tparams.file_properties.src_file_name + [..self.tparams.file_properties.src_file_name_len], + )?); + let source_name = source_path.file_name(); + if source_name.is_none() { + return Err(DestError::PathConcat); } + let source_name = source_name.unwrap(); + self.tparams.file_properties.dest_path_buf.push(source_name); } - // This function does exactly what we require: Create a new file if it does not exist yet - // and trucate an existing one. - File::create(&self.tparams.file_properties.dest_path_buf)?; + let dest_path_str = self.tparams.file_properties.dest_path_buf.to_str().unwrap(); + if self.vfs.exists(dest_path_str) { + self.vfs.truncate_file(dest_path_str)?; + } else { + self.vfs.create_file(dest_path_str)?; + } + self.tparams.tstate.file_status = FileStatus::Retained; self.step = TransactionStep::ReceivingFileDataPdus; Ok(()) } - fn transfer_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { - let transaction_finished_params = TransactionFinishedParams { - id: self.tparams.tstate.transaction_id.unwrap(), - condition_code: self.tparams.tstate.condition_code, - delivery_code: self.tparams.tstate.delivery_code, - file_status: self.tparams.tstate.file_status, - }; - cfdp_user.transaction_finished_indication(&transaction_finished_params); - // This function should never be called with metadata parameters not set - if self.tparams.metadata_params().closure_requested { - self.prepare_finished_pdu()?; + fn transfer_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result { + let mut sent_packets = 0; + self.notice_of_completion(cfdp_user)?; + if self.tparams.transmission_mode() == TransmissionMode::Acknowledged + || self.tparams.metadata_params().closure_requested + { + sent_packets += self.send_finished_pdu()?; self.step = TransactionStep::SendingFinishedPdu; } else { self.reset(); - self.state = State::Idle; - self.step = TransactionStep::Idle; } + Ok(sent_packets) + } + + fn notice_of_completion(&mut self, cfdp_user: &mut impl CfdpUser) -> Result<(), DestError> { + if self.tstate().completion_disposition == CompletionDisposition::Completed { + // TODO: Execute any filestore requests + } else if self + .tparams + .remote_cfg + .as_ref() + .unwrap() + .disposition_on_cancellation + && self.tstate().delivery_code == DeliveryCode::Incomplete + { + self.vfs + .remove_file(self.tparams.file_properties.dest_path_buf.to_str().unwrap())?; + self.tstate_mut().file_status = FileStatus::DiscardDeliberately; + } + let tstate = self.tstate(); + let transaction_finished_params = TransactionFinishedParams { + id: tstate.transaction_id.unwrap(), + condition_code: tstate.condition_code, + delivery_code: tstate.delivery_code, + file_status: tstate.file_status, + }; + cfdp_user.transaction_finished_indication(&transaction_finished_params); Ok(()) } + fn declare_fault(&mut self, condition_code: ConditionCode) -> FaultHandlerCode { + // Cache those, because they might be reset when abandoning the transaction. + let transaction_id = self.tstate().transaction_id.unwrap(); + let progress = self.tstate().progress; + let fh_code = self + .local_cfg + .default_fault_handler + .get_fault_handler(condition_code); + match fh_code { + FaultHandlerCode::NoticeOfCancellation => { + self.notice_of_cancellation(condition_code); + } + FaultHandlerCode::NoticeOfSuspension => self.notice_of_suspension(), + FaultHandlerCode::IgnoreError => (), + FaultHandlerCode::AbandonTransaction => self.abandon_transaction(), + } + self.local_cfg + .default_fault_handler + .report_fault(transaction_id, condition_code, progress) + } + + fn notice_of_cancellation(&mut self, condition_code: ConditionCode) { + self.step = TransactionStep::TransferCompletion; + self.tstate_mut().condition_code = condition_code; + self.tstate_mut().completion_disposition = CompletionDisposition::Cancelled; + } + + fn notice_of_suspension(&mut self) { + // TODO: Implement suspension handling. + } + fn abandon_transaction(&mut self) { + self.reset(); + } + fn reset(&mut self) { self.step = TransactionStep::Idle; self.state = State::Idle; - self.packets_to_send_ctx.packet_available = false; + // self.packets_to_send_ctx.packet_available = false; self.tparams.reset(); } - fn prepare_finished_pdu(&mut self) -> Result<(), DestError> { - self.packets_to_send_ctx.packet_available = true; - self.packets_to_send_ctx.directive = Some(FileDirectiveType::FinishedPdu); - self.step = TransactionStep::SendingFinishedPdu; - Ok(()) + fn send_finished_pdu(&mut self) -> Result { + let tstate = self.tstate(); + + let pdu_header = PduHeader::new_no_file_data(self.tparams.pdu_conf, 0); + let finished_pdu = if tstate.condition_code == ConditionCode::NoError + || tstate.condition_code == ConditionCode::UnsupportedChecksumType + { + FinishedPduCreator::new_default(pdu_header, tstate.delivery_code, tstate.file_status) + } else { + // TODO: Are there cases where this ID is actually the source entity ID? + let entity_id = EntityIdTlv::new(self.local_cfg.id); + FinishedPduCreator::new_with_error( + pdu_header, + tstate.condition_code, + tstate.delivery_code, + tstate.file_status, + entity_id, + ) + }; + finished_pdu.write_to_bytes(&mut self.packet_buf)?; + self.packet_sender.send_pdu( + finished_pdu.pdu_type(), + finished_pdu.file_directive_type(), + &self.packet_buf[0..finished_pdu.len_written()], + )?; + Ok(1) + } + + fn tstate(&self) -> &TransferState { + &self.tparams.tstate + } + + fn tstate_mut(&mut self) -> &mut TransferState { + &mut self.tparams.tstate } } #[cfg(test)] mod tests { - use core::sync::atomic::{AtomicU8, Ordering}; + use core::{cell::Cell, sync::atomic::AtomicBool}; #[allow(unused_imports)] use std::println; - use std::{env::temp_dir, fs}; + use std::{fs, sync::Mutex}; - use alloc::{format, string::String}; + use alloc::{collections::VecDeque, string::String, sync::Arc, vec::Vec}; use rand::Rng; use spacepackets::{ - cfdp::{lv::Lv, ChecksumType}, + cfdp::{ + lv::Lv, + pdu::{finished::FinishedPduReader, metadata::MetadataPduCreator, WritablePduPacket}, + ChecksumType, TransmissionMode, + }, util::{UbfU16, UnsignedByteFieldU16}, }; + use crate::cfdp::{ + filestore::NativeFilestore, user::OwnedMetadataRecvdParams, CheckTimer, CheckTimerCreator, + DefaultFaultHandler, IndicationConfig, RemoteEntityConfig, StdRemoteEntityConfigProvider, + UserFaultHandler, CRC_32, + }; + use super::*; const LOCAL_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(1); const REMOTE_ID: UnsignedByteFieldU16 = UnsignedByteFieldU16::new(2); - const SRC_NAME: &str = "__cfdp__source-file"; - const DEST_NAME: &str = "__cfdp__dest-file"; + pub struct FileSegmentRecvdParamsNoSegMetadata { + pub id: TransactionId, + pub offset: u64, + pub length: usize, + } - static ATOMIC_COUNTER: AtomicU8 = AtomicU8::new(0); + struct SentPdu { + pdu_type: PduType, + file_directive_type: Option, + raw_pdu: Vec, + } + type SharedPduPacketQueue = Arc>>; + #[derive(Default, Clone)] + struct TestCfdpSender { + packet_queue: SharedPduPacketQueue, + } + + impl CfdpPacketSender for TestCfdpSender { + fn send_pdu( + &mut self, + pdu_type: PduType, + file_directive_type: Option, + raw_pdu: &[u8], + ) -> Result<(), PduError> { + self.packet_queue.lock().unwrap().push_back(SentPdu { + pdu_type, + file_directive_type, + raw_pdu: raw_pdu.to_vec(), + }); + Ok(()) + } + } + + impl TestCfdpSender { + pub fn retrieve_next_pdu(&self) -> Option { + self.packet_queue.lock().unwrap().pop_front() + } + pub fn queue_empty(&self) -> bool { + self.packet_queue.lock().unwrap().is_empty() + } + } #[derive(Default)] struct TestCfdpUser { next_expected_seq_num: u64, expected_full_src_name: String, expected_full_dest_name: String, - expected_file_size: usize, + expected_file_size: u64, + transaction_indication_call_count: u32, + eof_recvd_call_count: u32, + finished_indic_queue: VecDeque, + metadata_recv_queue: VecDeque, + file_seg_recvd_queue: VecDeque, } impl TestCfdpUser { + fn new( + next_expected_seq_num: u64, + expected_full_src_name: String, + expected_full_dest_name: String, + expected_file_size: u64, + ) -> Self { + Self { + next_expected_seq_num, + expected_full_src_name, + expected_full_dest_name, + expected_file_size, + transaction_indication_call_count: 0, + eof_recvd_call_count: 0, + finished_indic_queue: VecDeque::new(), + metadata_recv_queue: VecDeque::new(), + file_seg_recvd_queue: VecDeque::new(), + } + } + fn generic_id_check(&self, id: &crate::cfdp::TransactionId) { assert_eq!(id.source_id, LOCAL_ID.into()); assert_eq!(id.seq_num().value(), self.next_expected_seq_num); @@ -536,6 +893,7 @@ mod tests { impl CfdpUser for TestCfdpUser { fn transaction_indication(&mut self, id: &crate::cfdp::TransactionId) { self.generic_id_check(id); + self.transaction_indication_call_count += 1; } fn eof_sent_indication(&mut self, id: &crate::cfdp::TransactionId) { @@ -547,6 +905,7 @@ mod tests { finished_params: &crate::cfdp::user::TransactionFinishedParams, ) { self.generic_id_check(&finished_params.id); + self.finished_indic_queue.push_back(*finished_params); } fn metadata_recvd_indication( @@ -564,13 +923,21 @@ mod tests { ); assert_eq!(md_recvd_params.msgs_to_user.len(), 0); assert_eq!(md_recvd_params.source_id, LOCAL_ID.into()); - assert_eq!(md_recvd_params.file_size as usize, self.expected_file_size); + assert_eq!(md_recvd_params.file_size, self.expected_file_size); + self.metadata_recv_queue.push_back(md_recvd_params.into()); } fn file_segment_recvd_indication( &mut self, - _segment_recvd_params: &crate::cfdp::user::FileSegmentRecvdParams, + segment_recvd_params: &crate::cfdp::user::FileSegmentRecvdParams, ) { + self.generic_id_check(&segment_recvd_params.id); + self.file_seg_recvd_queue + .push_back(FileSegmentRecvdParamsNoSegMetadata { + id: segment_recvd_params.id, + offset: segment_recvd_params.offset, + length: segment_recvd_params.length, + }) } fn report_indication(&mut self, _id: &crate::cfdp::TransactionId) {} @@ -605,31 +972,333 @@ mod tests { fn eof_recvd_indication(&mut self, id: &crate::cfdp::TransactionId) { self.generic_id_check(id); + self.eof_recvd_call_count += 1; } } - fn init_check(handler: &DestinationHandler) { - assert_eq!(handler.state(), State::Idle); - assert_eq!(handler.step(), TransactionStep::Idle); + #[derive(Default, Clone)] + struct TestFaultHandler { + notice_of_suspension_queue: Arc>>, + notice_of_cancellation_queue: Arc>>, + abandoned_queue: Arc>>, + ignored_queue: Arc>>, + } + + impl UserFaultHandler for TestFaultHandler { + fn notice_of_suspension_cb( + &mut self, + transaction_id: TransactionId, + cond: ConditionCode, + progress: u64, + ) { + self.notice_of_suspension_queue.lock().unwrap().push_back(( + transaction_id, + cond, + progress, + )) + } + + fn notice_of_cancellation_cb( + &mut self, + transaction_id: TransactionId, + cond: ConditionCode, + progress: u64, + ) { + self.notice_of_cancellation_queue + .lock() + .unwrap() + .push_back((transaction_id, cond, progress)) + } + + fn abandoned_cb( + &mut self, + transaction_id: TransactionId, + cond: ConditionCode, + progress: u64, + ) { + self.abandoned_queue + .lock() + .unwrap() + .push_back((transaction_id, cond, progress)) + } + + fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64) { + self.ignored_queue + .lock() + .unwrap() + .push_back((transaction_id, cond, progress)) + } + } + + impl TestFaultHandler { + fn suspension_queue_empty(&self) -> bool { + self.notice_of_suspension_queue.lock().unwrap().is_empty() + } + fn cancellation_queue_empty(&self) -> bool { + self.notice_of_cancellation_queue.lock().unwrap().is_empty() + } + fn ignored_queue_empty(&self) -> bool { + self.ignored_queue.lock().unwrap().is_empty() + } + fn abandoned_queue_empty(&self) -> bool { + self.abandoned_queue.lock().unwrap().is_empty() + } + fn all_queues_empty(&self) -> bool { + self.suspension_queue_empty() + && self.cancellation_queue_empty() + && self.ignored_queue_empty() + && self.abandoned_queue_empty() + } + } + + #[derive(Debug)] + struct TestCheckTimer { + counter: Cell, + expired: Arc, + } + + impl CheckTimer for TestCheckTimer { + fn has_expired(&self) -> bool { + self.expired.load(core::sync::atomic::Ordering::Relaxed) + } + fn reset(&mut self) { + self.counter.set(0); + } + } + + impl TestCheckTimer { + pub fn new(expired_flag: Arc) -> Self { + Self { + counter: Cell::new(0), + expired: expired_flag, + } + } + } + + struct TestCheckTimerCreator { + check_limit_expired_flag: Arc, + } + + impl TestCheckTimerCreator { + pub fn new(expired_flag: Arc) -> Self { + Self { + check_limit_expired_flag: expired_flag, + } + } + } + + impl CheckTimerCreator for TestCheckTimerCreator { + fn get_check_timer_provider(&self, timer_context: TimerContext) -> Box { + match timer_context { + TimerContext::CheckLimit { .. } => { + Box::new(TestCheckTimer::new(self.check_limit_expired_flag.clone())) + } + _ => { + panic!("invalid check timer creator, can only be used for check limit handling") + } + } + } + } + + struct DestHandlerTester { + check_timer_expired: Arc, + pdu_sender: TestCfdpSender, + handler: DestinationHandler, + src_path: PathBuf, + dest_path: PathBuf, + check_dest_file: bool, + check_handler_idle_at_drop: bool, + expected_file_size: u64, + closure_requested: bool, + pdu_header: PduHeader, + expected_full_data: Vec, + buf: [u8; 512], + } + + impl DestHandlerTester { + fn new(fault_handler: TestFaultHandler, closure_requested: bool) -> Self { + let check_timer_expired = Arc::new(AtomicBool::new(false)); + let test_sender = TestCfdpSender::default(); + let dest_handler = default_dest_handler( + fault_handler, + test_sender.clone(), + check_timer_expired.clone(), + ); + let (src_path, dest_path) = init_full_filenames(); + assert!(!Path::exists(&dest_path)); + let handler = Self { + check_timer_expired, + pdu_sender: test_sender, + handler: dest_handler, + src_path, + closure_requested, + dest_path, + check_dest_file: false, + check_handler_idle_at_drop: false, + expected_file_size: 0, + pdu_header: create_pdu_header(UbfU16::new(0)), + expected_full_data: Vec::new(), + buf: [0; 512], + }; + handler.state_check(State::Idle, TransactionStep::Idle); + handler + } + + fn dest_path(&self) -> &PathBuf { + &self.dest_path + } + + #[allow(dead_code)] + fn indication_cfg_mut(&mut self) -> &mut IndicationConfig { + &mut self.handler.local_cfg.indication_cfg + } + + fn indication_cfg(&mut self) -> &IndicationConfig { + &self.handler.local_cfg.indication_cfg + } + + fn set_check_timer_expired(&mut self) { + self.check_timer_expired + .store(true, core::sync::atomic::Ordering::Relaxed); + } + + fn test_user_from_cached_paths(&self, expected_file_size: u64) -> TestCfdpUser { + TestCfdpUser::new( + 0, + self.src_path.to_string_lossy().into(), + self.dest_path.to_string_lossy().into(), + expected_file_size, + ) + } + + fn generic_transfer_init( + &mut self, + user: &mut TestCfdpUser, + file_size: u64, + ) -> Result { + self.expected_file_size = file_size; + let metadata_pdu = create_metadata_pdu( + &self.pdu_header, + self.src_path.as_path(), + self.dest_path.as_path(), + file_size, + self.closure_requested, + ); + let packet_info = create_packet_info(&metadata_pdu, &mut self.buf); + self.handler.state_machine(user, Some(&packet_info))?; + assert_eq!(user.metadata_recv_queue.len(), 1); + assert_eq!( + self.handler.transmission_mode().unwrap(), + TransmissionMode::Unacknowledged + ); + Ok(self.handler.transaction_id().unwrap()) + } + + fn generic_file_data_insert( + &mut self, + user: &mut TestCfdpUser, + offset: u64, + file_data_chunk: &[u8], + ) -> Result { + let filedata_pdu = + FileDataPdu::new_no_seg_metadata(self.pdu_header, offset, file_data_chunk); + filedata_pdu + .write_to_bytes(&mut self.buf) + .expect("writing file data PDU failed"); + let packet_info = PacketInfo::new(&self.buf).expect("creating packet info failed"); + let result = self.handler.state_machine(user, Some(&packet_info)); + if self.indication_cfg().file_segment_recv { + assert!(!user.file_seg_recvd_queue.is_empty()); + assert_eq!(user.file_seg_recvd_queue.back().unwrap().offset, offset); + assert_eq!( + user.file_seg_recvd_queue.back().unwrap().length, + file_data_chunk.len() + ); + } + result + } + + fn generic_eof_no_error( + &mut self, + user: &mut TestCfdpUser, + expected_full_data: Vec, + ) -> Result { + self.expected_full_data = expected_full_data; + let eof_pdu = create_no_error_eof(&self.expected_full_data, &self.pdu_header); + let packet_info = create_packet_info(&eof_pdu, &mut self.buf); + self.check_handler_idle_at_drop = true; + self.check_dest_file = true; + let result = self.handler.state_machine(user, Some(&packet_info)); + if self.indication_cfg().eof_recv { + assert_eq!(user.eof_recvd_call_count, 1); + } + result + } + + fn state_check(&self, state: State, step: TransactionStep) { + assert_eq!(self.handler.state(), state); + assert_eq!(self.handler.step(), step); + } + } + + impl Drop for DestHandlerTester { + fn drop(&mut self) { + if self.check_handler_idle_at_drop { + self.state_check(State::Idle, TransactionStep::Idle); + } + if self.check_dest_file { + assert!(Path::exists(&self.dest_path)); + let read_content = fs::read(&self.dest_path).expect("reading back string failed"); + assert_eq!(read_content.len() as u64, self.expected_file_size); + assert_eq!(read_content, self.expected_full_data); + assert!(fs::remove_file(self.dest_path.as_path()).is_ok()); + } + } } fn init_full_filenames() -> (PathBuf, PathBuf) { - let mut file_path = temp_dir(); - let mut src_path = file_path.clone(); - // Atomic counter used to allow concurrent tests. - let unique_counter = ATOMIC_COUNTER.fetch_add(1, Ordering::Relaxed); - // Create unique test filenames. - let src_name_unique = format!("{SRC_NAME}{}.txt", unique_counter); - let dest_name_unique = format!("{DEST_NAME}{}.txt", unique_counter); - src_path.push(src_name_unique); - file_path.push(dest_name_unique); - (src_path, file_path) + ( + tempfile::TempPath::from_path("/tmp/test.txt").to_path_buf(), + tempfile::NamedTempFile::new() + .unwrap() + .into_temp_path() + .to_path_buf(), + ) } - #[test] - fn test_basic() { - let dest_handler = DestinationHandler::new(REMOTE_ID); - init_check(&dest_handler); + fn basic_remote_cfg_table() -> StdRemoteEntityConfigProvider { + let mut table = StdRemoteEntityConfigProvider::default(); + let remote_entity_cfg = RemoteEntityConfig::new_with_default_values( + UnsignedByteFieldU16::new(1).into(), + 1024, + 1024, + true, + true, + TransmissionMode::Unacknowledged, + ChecksumType::Crc32, + ); + table.add_config(&remote_entity_cfg); + table + } + + fn default_dest_handler( + test_fault_handler: TestFaultHandler, + test_packet_sender: TestCfdpSender, + check_timer_expired: Arc, + ) -> DestinationHandler { + let local_entity_cfg = LocalEntityConfig { + id: REMOTE_ID.into(), + indication_cfg: IndicationConfig::default(), + default_fault_handler: DefaultFaultHandler::new(Box::new(test_fault_handler)), + }; + DestinationHandler::new( + local_entity_cfg, + 2048, + Box::new(test_packet_sender), + Box::::default(), + Box::new(basic_remote_cfg_table()), + Box::new(TestCheckTimerCreator::new(check_timer_expired)), + ) } fn create_pdu_header(seq_num: impl Into) -> PduHeader { @@ -644,228 +1313,288 @@ mod tests { src_name: &'filename Path, dest_name: &'filename Path, file_size: u64, - ) -> MetadataPdu<'filename, 'filename, 'static> { - let metadata_params = MetadataGenericParams::new(false, ChecksumType::Crc32, file_size); - MetadataPdu::new( + closure_requested: bool, + ) -> MetadataPduCreator<'filename, 'filename, 'static> { + let checksum_type = if file_size == 0 { + ChecksumType::NullChecksum + } else { + ChecksumType::Crc32 + }; + let metadata_params = + MetadataGenericParams::new(closure_requested, checksum_type, file_size); + MetadataPduCreator::new_no_opts( *pdu_header, metadata_params, Lv::new_from_str(src_name.as_os_str().to_str().unwrap()).unwrap(), Lv::new_from_str(dest_name.as_os_str().to_str().unwrap()).unwrap(), - None, ) } - fn insert_metadata_pdu( - metadata_pdu: &MetadataPdu, - buf: &mut [u8], - dest_handler: &mut DestinationHandler, - ) { - let written_len = metadata_pdu + fn create_packet_info<'a>( + pdu: &'a impl WritablePduPacket, + buf: &'a mut [u8], + ) -> PacketInfo<'a> { + let written_len = pdu .write_to_bytes(buf) .expect("writing metadata PDU failed"); - let packet_info = - PacketInfo::new(&buf[..written_len]).expect("generating packet info failed"); - let insert_result = dest_handler.insert_packet(&packet_info); - if let Err(e) = insert_result { - panic!("insert result error: {e}"); - } + PacketInfo::new(&buf[..written_len]).expect("generating packet info failed") } - fn insert_eof_pdu( - file_data: &[u8], - pdu_header: &PduHeader, - buf: &mut [u8], - dest_handler: &mut DestinationHandler, - ) { - let mut digest = CRC_32.digest(); - digest.update(file_data); - let crc32 = digest.finalize(); - let eof_pdu = EofPdu::new_no_error(*pdu_header, crc32, file_data.len() as u64); - let result = eof_pdu.write_to_bytes(buf); - assert!(result.is_ok()); - let packet_info = PacketInfo::new(&buf).expect("generating packet info failed"); - let result = dest_handler.insert_packet(&packet_info); - assert!(result.is_ok()); - } - - #[test] - fn test_empty_file_transfer() { - let (src_name, dest_name) = init_full_filenames(); - assert!(!Path::exists(&dest_name)); - let mut buf: [u8; 512] = [0; 512]; - let mut test_user = TestCfdpUser { - next_expected_seq_num: 0, - expected_full_src_name: src_name.to_string_lossy().into(), - expected_full_dest_name: dest_name.to_string_lossy().into(), - expected_file_size: 0, + fn create_no_error_eof(file_data: &[u8], pdu_header: &PduHeader) -> EofPdu { + let crc32 = if !file_data.is_empty() { + let mut digest = CRC_32.digest(); + digest.update(file_data); + digest.finalize() + } else { + 0 }; - // We treat the destination handler like it is a remote entity. - let mut dest_handler = DestinationHandler::new(REMOTE_ID); - init_check(&dest_handler); - - let seq_num = UbfU16::new(0); - let pdu_header = create_pdu_header(seq_num); - let metadata_pdu = - create_metadata_pdu(&pdu_header, src_name.as_path(), dest_name.as_path(), 0); - insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler); - let result = dest_handler.state_machine(&mut test_user); - if let Err(e) = result { - panic!("dest handler fsm error: {e}"); - } - assert_ne!(dest_handler.state(), State::Idle); - assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus); - - insert_eof_pdu(&[], &pdu_header, &mut buf, &mut dest_handler); - let result = dest_handler.state_machine(&mut test_user); - assert!(result.is_ok()); - assert_eq!(dest_handler.state(), State::Idle); - assert_eq!(dest_handler.step(), TransactionStep::Idle); - assert!(Path::exists(&dest_name)); - let read_content = fs::read(&dest_name).expect("reading back string failed"); - assert_eq!(read_content.len(), 0); - assert!(fs::remove_file(dest_name).is_ok()); + EofPdu::new_no_error(*pdu_header, crc32, file_data.len() as u64) } #[test] - fn test_small_file_transfer() { - let (src_name, dest_name) = init_full_filenames(); - assert!(!Path::exists(&dest_name)); + fn test_basic() { + let fault_handler = TestFaultHandler::default(); + let test_sender = TestCfdpSender::default(); + let dest_handler = default_dest_handler(fault_handler.clone(), test_sender, Arc::default()); + assert!(dest_handler.transmission_mode().is_none()); + assert!(fault_handler.all_queues_empty()); + } + + #[test] + fn test_empty_file_transfer_not_acked_no_closure() { + let fault_handler = TestFaultHandler::default(); + let mut test_obj = DestHandlerTester::new(fault_handler.clone(), false); + let mut test_user = test_obj.test_user_from_cached_paths(0); + test_obj + .generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + test_obj.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + test_obj + .generic_eof_no_error(&mut test_user, Vec::new()) + .expect("EOF no error insertion failed"); + assert!(fault_handler.all_queues_empty()); + assert!(test_obj.pdu_sender.queue_empty()); + test_obj.state_check(State::Idle, TransactionStep::Idle); + } + + #[test] + fn test_small_file_transfer_not_acked() { let file_data_str = "Hello World!"; let file_data = file_data_str.as_bytes(); - let mut buf: [u8; 512] = [0; 512]; - let mut test_user = TestCfdpUser { - next_expected_seq_num: 0, - expected_full_src_name: src_name.to_string_lossy().into(), - expected_full_dest_name: dest_name.to_string_lossy().into(), - expected_file_size: file_data.len(), - }; - // We treat the destination handler like it is a remote entity. - let mut dest_handler = DestinationHandler::new(REMOTE_ID); - init_check(&dest_handler); + let file_size = file_data.len() as u64; + let fault_handler = TestFaultHandler::default(); - let seq_num = UbfU16::new(0); - let pdu_header = create_pdu_header(seq_num); - let metadata_pdu = create_metadata_pdu( - &pdu_header, - src_name.as_path(), - dest_name.as_path(), - file_data.len() as u64, - ); - insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler); - let result = dest_handler.state_machine(&mut test_user); - if let Err(e) = result { - panic!("dest handler fsm error: {e}"); - } - assert_ne!(dest_handler.state(), State::Idle); - assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus); - - let offset = 0; - let filedata_pdu = FileDataPdu::new_no_seg_metadata(pdu_header, offset, file_data); - filedata_pdu - .write_to_bytes(&mut buf) - .expect("writing file data PDU failed"); - let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); - let result = dest_handler.insert_packet(&packet_info); - if let Err(e) = result { - panic!("destination handler packet insertion error: {e}"); - } - let result = dest_handler.state_machine(&mut test_user); - assert!(result.is_ok()); - - insert_eof_pdu(file_data, &pdu_header, &mut buf, &mut dest_handler); - let result = dest_handler.state_machine(&mut test_user); - assert!(result.is_ok()); - assert_eq!(dest_handler.state(), State::Idle); - assert_eq!(dest_handler.step(), TransactionStep::Idle); - - assert!(Path::exists(&dest_name)); - let read_content = fs::read_to_string(&dest_name).expect("reading back string failed"); - assert_eq!(read_content, file_data_str); - assert!(fs::remove_file(dest_name).is_ok()); + let mut test_obj = DestHandlerTester::new(fault_handler.clone(), false); + let mut test_user = test_obj.test_user_from_cached_paths(file_size); + test_obj + .generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + test_obj.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + test_obj + .generic_file_data_insert(&mut test_user, 0, file_data) + .expect("file data insertion failed"); + test_obj + .generic_eof_no_error(&mut test_user, file_data.to_vec()) + .expect("EOF no error insertion failed"); + assert!(fault_handler.all_queues_empty()); + assert!(test_obj.pdu_sender.queue_empty()); + test_obj.state_check(State::Idle, TransactionStep::Idle); } #[test] - fn test_segmented_file_transfer() { - let (src_name, dest_name) = init_full_filenames(); - assert!(!Path::exists(&dest_name)); + fn test_segmented_file_transfer_not_acked() { let mut rng = rand::thread_rng(); let mut random_data = [0u8; 512]; rng.fill(&mut random_data); - let mut buf: [u8; 512] = [0; 512]; - let mut test_user = TestCfdpUser { - next_expected_seq_num: 0, - expected_full_src_name: src_name.to_string_lossy().into(), - expected_full_dest_name: dest_name.to_string_lossy().into(), - expected_file_size: random_data.len(), - }; - - // We treat the destination handler like it is a remote entity. - let mut dest_handler = DestinationHandler::new(REMOTE_ID); - init_check(&dest_handler); - - let seq_num = UbfU16::new(0); - let pdu_header = create_pdu_header(seq_num); - let metadata_pdu = create_metadata_pdu( - &pdu_header, - src_name.as_path(), - dest_name.as_path(), - random_data.len() as u64, - ); - insert_metadata_pdu(&metadata_pdu, &mut buf, &mut dest_handler); - let result = dest_handler.state_machine(&mut test_user); - if let Err(e) = result { - panic!("dest handler fsm error: {e}"); - } - assert_ne!(dest_handler.state(), State::Idle); - assert_eq!(dest_handler.step(), TransactionStep::ReceivingFileDataPdus); - - // First file data PDU - let mut offset: usize = 0; + let file_size = random_data.len() as u64; let segment_len = 256; - let filedata_pdu = FileDataPdu::new_no_seg_metadata( - pdu_header, - offset as u64, - &random_data[0..segment_len], + let fault_handler = TestFaultHandler::default(); + + let mut test_obj = DestHandlerTester::new(fault_handler.clone(), false); + let mut test_user = test_obj.test_user_from_cached_paths(file_size); + test_obj + .generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + test_obj.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + test_obj + .generic_file_data_insert(&mut test_user, 0, &random_data[0..segment_len]) + .expect("file data insertion failed"); + test_obj + .generic_file_data_insert( + &mut test_user, + segment_len as u64, + &random_data[segment_len..], + ) + .expect("file data insertion failed"); + test_obj + .generic_eof_no_error(&mut test_user, random_data.to_vec()) + .expect("EOF no error insertion failed"); + assert!(fault_handler.all_queues_empty()); + assert!(test_obj.pdu_sender.queue_empty()); + test_obj.state_check(State::Idle, TransactionStep::Idle); + } + + #[test] + fn test_check_limit_handling_transfer_success() { + let mut rng = rand::thread_rng(); + let mut random_data = [0u8; 512]; + rng.fill(&mut random_data); + let file_size = random_data.len() as u64; + let segment_len = 256; + let fault_handler = TestFaultHandler::default(); + + let mut test_obj = DestHandlerTester::new(fault_handler.clone(), false); + let mut test_user = test_obj.test_user_from_cached_paths(file_size); + let transaction_id = test_obj + .generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + + test_obj.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + test_obj + .generic_file_data_insert(&mut test_user, 0, &random_data[0..segment_len]) + .expect("file data insertion 0 failed"); + test_obj + .generic_eof_no_error(&mut test_user, random_data.to_vec()) + .expect("EOF no error insertion failed"); + test_obj.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, ); - filedata_pdu - .write_to_bytes(&mut buf) - .expect("writing file data PDU failed"); - let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); - let result = dest_handler.insert_packet(&packet_info); - if let Err(e) = result { - panic!("destination handler packet insertion error: {e}"); - } - let result = dest_handler.state_machine(&mut test_user); - assert!(result.is_ok()); + test_obj + .generic_file_data_insert( + &mut test_user, + segment_len as u64, + &random_data[segment_len..], + ) + .expect("file data insertion 1 failed"); + test_obj.set_check_timer_expired(); + test_obj + .handler + .state_machine(&mut test_user, None) + .expect("fsm failure"); - // Second file data PDU - offset += segment_len; - let filedata_pdu = FileDataPdu::new_no_seg_metadata( - pdu_header, - offset as u64, - &random_data[segment_len..], + let ignored_queue = fault_handler.ignored_queue.lock().unwrap(); + assert_eq!(ignored_queue.len(), 1); + let cancelled = *ignored_queue.front().unwrap(); + assert_eq!(cancelled.0, transaction_id); + assert_eq!(cancelled.1, ConditionCode::FileChecksumFailure); + assert_eq!(cancelled.2, segment_len as u64); + assert!(test_obj.pdu_sender.queue_empty()); + test_obj.state_check(State::Idle, TransactionStep::Idle); + } + + #[test] + fn test_check_limit_handling_limit_reached() { + let mut rng = rand::thread_rng(); + let mut random_data = [0u8; 512]; + rng.fill(&mut random_data); + let file_size = random_data.len() as u64; + let segment_len = 256; + + let fault_handler = TestFaultHandler::default(); + let mut test_obj = DestHandlerTester::new(fault_handler.clone(), false); + let mut test_user = test_obj.test_user_from_cached_paths(file_size); + let transaction_id = test_obj + .generic_transfer_init(&mut test_user, file_size) + .expect("transfer init failed"); + + test_obj.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + test_obj + .generic_file_data_insert(&mut test_user, 0, &random_data[0..segment_len]) + .expect("file data insertion 0 failed"); + test_obj + .generic_eof_no_error(&mut test_user, random_data.to_vec()) + .expect("EOF no error insertion failed"); + test_obj.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, ); - filedata_pdu - .write_to_bytes(&mut buf) - .expect("writing file data PDU failed"); - let packet_info = PacketInfo::new(&buf).expect("creating packet info failed"); - let result = dest_handler.insert_packet(&packet_info); - if let Err(e) = result { - panic!("destination handler packet insertion error: {e}"); - } - let result = dest_handler.state_machine(&mut test_user); - assert!(result.is_ok()); + test_obj.set_check_timer_expired(); + test_obj + .handler + .state_machine(&mut test_user, None) + .expect("fsm error"); + test_obj.state_check( + State::Busy, + TransactionStep::ReceivingFileDataPdusWithCheckLimitHandling, + ); + test_obj.set_check_timer_expired(); + test_obj + .handler + .state_machine(&mut test_user, None) + .expect("fsm error"); + test_obj.state_check(State::Idle, TransactionStep::Idle); - insert_eof_pdu(&random_data, &pdu_header, &mut buf, &mut dest_handler); - let result = dest_handler.state_machine(&mut test_user); - assert!(result.is_ok()); - assert_eq!(dest_handler.state(), State::Idle); - assert_eq!(dest_handler.step(), TransactionStep::Idle); + assert!(fault_handler + .notice_of_suspension_queue + .lock() + .unwrap() + .is_empty()); - // Clean up - assert!(Path::exists(&dest_name)); - let read_content = fs::read(&dest_name).expect("reading back string failed"); - assert_eq!(read_content, random_data); - assert!(fs::remove_file(dest_name).is_ok()); + let ignored_queue = fault_handler.ignored_queue.lock().unwrap(); + assert_eq!(ignored_queue.len(), 1); + let cancelled = *ignored_queue.front().unwrap(); + assert_eq!(cancelled.0, transaction_id); + assert_eq!(cancelled.1, ConditionCode::FileChecksumFailure); + assert_eq!(cancelled.2, segment_len as u64); + + let cancelled_queue = fault_handler.notice_of_cancellation_queue.lock().unwrap(); + assert_eq!(cancelled_queue.len(), 1); + let cancelled = *cancelled_queue.front().unwrap(); + assert_eq!(cancelled.0, transaction_id); + assert_eq!(cancelled.1, ConditionCode::CheckLimitReached); + assert_eq!(cancelled.2, segment_len as u64); + + drop(cancelled_queue); + + assert!(test_obj.pdu_sender.queue_empty()); + + // Check that the broken file exists. + test_obj.check_dest_file = false; + assert!(Path::exists(test_obj.dest_path())); + let read_content = fs::read(test_obj.dest_path()).expect("reading back string failed"); + assert_eq!(read_content.len(), segment_len); + assert_eq!(read_content, &random_data[0..segment_len]); + assert!(fs::remove_file(test_obj.dest_path().as_path()).is_ok()); + } + + fn check_finished_pdu_success(sent_pdu: &SentPdu) { + assert_eq!(sent_pdu.pdu_type, PduType::FileDirective); + assert_eq!( + sent_pdu.file_directive_type, + Some(FileDirectiveType::FinishedPdu) + ); + let finished_pdu = FinishedPduReader::from_bytes(&sent_pdu.raw_pdu).unwrap(); + assert_eq!(finished_pdu.file_status(), FileStatus::Retained); + assert_eq!(finished_pdu.condition_code(), ConditionCode::NoError); + assert_eq!(finished_pdu.delivery_code(), DeliveryCode::Complete); + assert!(finished_pdu.fault_location().is_none()); + assert_eq!(finished_pdu.fs_responses_raw(), &[]); + } + + #[test] + fn test_file_transfer_with_closure() { + let fault_handler = TestFaultHandler::default(); + let mut test_obj = DestHandlerTester::new(fault_handler.clone(), true); + let mut test_user = test_obj.test_user_from_cached_paths(0); + test_obj + .generic_transfer_init(&mut test_user, 0) + .expect("transfer init failed"); + test_obj.state_check(State::Busy, TransactionStep::ReceivingFileDataPdus); + let sent_packets = test_obj + .generic_eof_no_error(&mut test_user, Vec::new()) + .expect("EOF no error insertion failed"); + assert_eq!(sent_packets, 1); + assert!(fault_handler.all_queues_empty()); + // The Finished PDU was sent, so the state machine is done. + test_obj.state_check(State::Idle, TransactionStep::Idle); + assert!(!test_obj.pdu_sender.queue_empty()); + let sent_pdu = test_obj.pdu_sender.retrieve_next_pdu().unwrap(); + check_finished_pdu_success(&sent_pdu); + } + + #[test] + fn test_file_transfer_with_closure_check_limit_reached() { + // TODO: Implement test. } } diff --git a/satrs-core/src/cfdp/filestore.rs b/satrs-core/src/cfdp/filestore.rs new file mode 100644 index 0000000..72fdb4e --- /dev/null +++ b/satrs-core/src/cfdp/filestore.rs @@ -0,0 +1,370 @@ +use alloc::string::{String, ToString}; +use core::fmt::Display; +use crc::{Crc, CRC_32_CKSUM}; +use spacepackets::cfdp::ChecksumType; +use spacepackets::ByteConversionError; +#[cfg(feature = "std")] +use std::error::Error; +#[cfg(feature = "std")] +pub use stdmod::*; + +pub const CRC_32: Crc = Crc::::new(&CRC_32_CKSUM); + +#[derive(Debug, Clone)] +pub enum FilestoreError { + FileDoesNotExist, + FileAlreadyExists, + DirDoesNotExist, + Permission, + IsNotFile, + IsNotDirectory, + ByteConversion(ByteConversionError), + Io { + raw_errno: Option, + string: String, + }, + ChecksumTypeNotImplemented(ChecksumType), +} + +impl From for FilestoreError { + fn from(value: ByteConversionError) -> Self { + Self::ByteConversion(value) + } +} + +impl Display for FilestoreError { + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + match self { + FilestoreError::FileDoesNotExist => { + write!(f, "file does not exist") + } + FilestoreError::FileAlreadyExists => { + write!(f, "file already exists") + } + FilestoreError::DirDoesNotExist => { + write!(f, "directory does not exist") + } + FilestoreError::Permission => { + write!(f, "permission error") + } + FilestoreError::IsNotFile => { + write!(f, "is not a file") + } + FilestoreError::IsNotDirectory => { + write!(f, "is not a directory") + } + FilestoreError::ByteConversion(e) => { + write!(f, "filestore error: {e}") + } + FilestoreError::Io { raw_errno, string } => { + write!( + f, + "filestore generic IO error with raw errno {:?}: {}", + raw_errno, string + ) + } + FilestoreError::ChecksumTypeNotImplemented(checksum_type) => { + write!(f, "checksum {:?} not implemented", checksum_type) + } + } + } +} + +impl Error for FilestoreError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match self { + FilestoreError::ByteConversion(e) => Some(e), + _ => None, + } + } +} + +#[cfg(feature = "std")] +impl From for FilestoreError { + fn from(value: std::io::Error) -> Self { + Self::Io { + raw_errno: value.raw_os_error(), + string: value.to_string(), + } + } +} + +pub trait VirtualFilestore { + fn create_file(&self, file_path: &str) -> Result<(), FilestoreError>; + + fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError>; + + /// Truncating a file means deleting all its data so the resulting file is empty. + /// This can be more efficient than removing and re-creating a file. + fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError>; + + fn remove_dir(&self, file_path: &str, all: bool) -> Result<(), FilestoreError>; + + fn read_data( + &self, + file_path: &str, + offset: u64, + read_len: u64, + buf: &mut [u8], + ) -> Result<(), FilestoreError>; + + fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError>; + + fn filename_from_full_path<'a>(&self, path: &'a str) -> Option<&'a str>; + + fn is_file(&self, path: &str) -> bool; + + fn is_dir(&self, path: &str) -> bool { + !self.is_file(path) + } + + fn exists(&self, path: &str) -> bool; + + /// This special function is the CFDP specific abstraction to verify the checksum of a file. + /// This allows to keep OS specific details like reading the whole file in the most efficient + /// manner inside the file system abstraction. + fn checksum_verify( + &self, + file_path: &str, + checksum_type: ChecksumType, + expected_checksum: u32, + verification_buf: &mut [u8], + ) -> Result; +} + +#[cfg(feature = "std")] +pub mod stdmod { + use super::*; + use std::{ + fs::{self, File, OpenOptions}, + io::{BufReader, Read, Seek, SeekFrom, Write}, + path::Path, + }; + + #[derive(Default)] + pub struct NativeFilestore {} + + impl VirtualFilestore for NativeFilestore { + fn create_file(&self, file_path: &str) -> Result<(), FilestoreError> { + if self.exists(file_path) { + return Err(FilestoreError::FileAlreadyExists); + } + File::create(file_path)?; + Ok(()) + } + + fn remove_file(&self, file_path: &str) -> Result<(), FilestoreError> { + if !self.exists(file_path) { + return Ok(()); + } + if !self.is_file(file_path) { + return Err(FilestoreError::IsNotFile); + } + fs::remove_file(file_path)?; + Ok(()) + } + + fn truncate_file(&self, file_path: &str) -> Result<(), FilestoreError> { + if !self.exists(file_path) { + return Ok(()); + } + if !self.is_file(file_path) { + return Err(FilestoreError::IsNotFile); + } + OpenOptions::new() + .write(true) + .truncate(true) + .open(file_path)?; + Ok(()) + } + + fn remove_dir(&self, dir_path: &str, all: bool) -> Result<(), FilestoreError> { + if !self.exists(dir_path) { + return Err(FilestoreError::DirDoesNotExist); + } + if !self.is_dir(dir_path) { + return Err(FilestoreError::IsNotDirectory); + } + if !all { + fs::remove_dir(dir_path)?; + return Ok(()); + } + fs::remove_dir_all(dir_path)?; + Ok(()) + } + + fn read_data( + &self, + file_name: &str, + offset: u64, + read_len: u64, + buf: &mut [u8], + ) -> Result<(), FilestoreError> { + if buf.len() < read_len as usize { + return Err(ByteConversionError::ToSliceTooSmall { + found: buf.len(), + expected: read_len as usize, + } + .into()); + } + if !self.exists(file_name) { + return Err(FilestoreError::FileDoesNotExist); + } + if !self.is_file(file_name) { + return Err(FilestoreError::IsNotFile); + } + let mut file = File::open(file_name)?; + file.seek(SeekFrom::Start(offset))?; + file.read_exact(&mut buf[0..read_len as usize])?; + Ok(()) + } + + fn write_data(&self, file: &str, offset: u64, buf: &[u8]) -> Result<(), FilestoreError> { + if !self.exists(file) { + return Err(FilestoreError::FileDoesNotExist); + } + if !self.is_file(file) { + return Err(FilestoreError::IsNotFile); + } + let mut file = OpenOptions::new().write(true).open(file)?; + file.seek(SeekFrom::Start(offset))?; + file.write_all(buf)?; + Ok(()) + } + + fn filename_from_full_path<'a>(&self, path: &'a str) -> Option<&'a str> { + // Convert the path string to a Path + let path = Path::new(path); + + // Extract the file name using the file_name() method + path.file_name().and_then(|name| name.to_str()) + } + + fn is_file(&self, path: &str) -> bool { + let path = Path::new(path); + path.is_file() + } + + fn is_dir(&self, path: &str) -> bool { + let path = Path::new(path); + path.is_dir() + } + + fn exists(&self, path: &str) -> bool { + let path = Path::new(path); + if !path.exists() { + return false; + } + true + } + + fn checksum_verify( + &self, + file_path: &str, + checksum_type: ChecksumType, + expected_checksum: u32, + verification_buf: &mut [u8], + ) -> Result { + match checksum_type { + ChecksumType::Modular => { + todo!(); + } + ChecksumType::Crc32 => { + let mut digest = CRC_32.digest(); + let file_to_check = File::open(file_path)?; + let mut buf_reader = BufReader::new(file_to_check); + loop { + let bytes_read = buf_reader.read(verification_buf)?; + if bytes_read == 0 { + break; + } + digest.update(&verification_buf[0..bytes_read]); + } + if digest.finalize() == expected_checksum { + return Ok(true); + } + Ok(false) + } + ChecksumType::NullChecksum => Ok(true), + _ => Err(FilestoreError::ChecksumTypeNotImplemented(checksum_type)), + } + } + } +} + +#[cfg(test)] +mod tests { + use std::{fs, path::Path, println}; + + use super::*; + use tempfile::tempdir; + + const NATIVE_FS: NativeFilestore = NativeFilestore {}; + + #[test] + fn test_basic_native_filestore_create() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + let result = + NATIVE_FS.create_file(file_path.to_str().expect("getting str for file failed")); + assert!(result.is_ok()); + let path = Path::new(&file_path); + assert!(path.exists()); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap())); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap())); + fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed"); + } + + #[test] + fn test_basic_native_fs_exists() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + assert!(!NATIVE_FS.exists(file_path.to_str().unwrap())); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap())); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap())); + fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed"); + } + + #[test] + fn test_basic_native_fs_write() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + assert!(!NATIVE_FS.exists(file_path.to_str().unwrap())); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap())); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap())); + println!("{}", file_path.to_str().unwrap()); + let write_data = "hello world\n"; + NATIVE_FS + .write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes()) + .expect("writing to file failed"); + let read_back = fs::read_to_string(file_path).expect("reading back data failed"); + assert_eq!(read_back, write_data); + fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed"); + } + + #[test] + fn test_basic_native_fs_read() { + let tmpdir = tempdir().expect("creating tmpdir failed"); + let file_path = tmpdir.path().join("test.txt"); + assert!(!NATIVE_FS.exists(file_path.to_str().unwrap())); + NATIVE_FS + .create_file(file_path.to_str().expect("getting str for file failed")) + .unwrap(); + assert!(NATIVE_FS.exists(file_path.to_str().unwrap())); + assert!(NATIVE_FS.is_file(file_path.to_str().unwrap())); + println!("{}", file_path.to_str().unwrap()); + let write_data = "hello world\n"; + NATIVE_FS + .write_data(file_path.to_str().unwrap(), 0, write_data.as_bytes()) + .expect("writing to file failed"); + let read_back = fs::read_to_string(file_path).expect("reading back data failed"); + assert_eq!(read_back, write_data); + fs::remove_dir_all(tmpdir).expect("clearing tmpdir failed"); + } +} diff --git a/satrs-core/src/cfdp/mod.rs b/satrs-core/src/cfdp/mod.rs index dc6e87a..c3bda16 100644 --- a/satrs-core/src/cfdp/mod.rs +++ b/satrs-core/src/cfdp/mod.rs @@ -1,8 +1,13 @@ +//! This module contains the implementation of the CFDP high level classes as specified in the +//! CCSDS 727.0-B-5. +use core::{cell::RefCell, fmt::Debug, hash::Hash}; + use crc::{Crc, CRC_32_CKSUM}; +use hashbrown::HashMap; use spacepackets::{ cfdp::{ pdu::{FileDirectiveType, PduError, PduHeader}, - ChecksumType, PduType, TransmissionMode, + ChecksumType, ConditionCode, FaultHandlerCode, PduType, TransmissionMode, }, util::UnsignedByteField, }; @@ -14,6 +19,8 @@ use serde::{Deserialize, Serialize}; #[cfg(feature = "std")] pub mod dest; +#[cfg(feature = "alloc")] +pub mod filestore; #[cfg(feature = "std")] pub mod source; pub mod user; @@ -24,7 +31,27 @@ pub enum EntityType { Receiving, } -/// Generic abstraction for a check timer which has different functionality depending on whether +pub enum TimerContext { + CheckLimit { + local_id: UnsignedByteField, + remote_id: UnsignedByteField, + entity_type: EntityType, + }, + NakActivity { + expiry_time_seconds: f32, + }, + PositiveAck { + expiry_time_seconds: f32, + }, +} + +/// Generic abstraction for a check timer which is used by 3 mechanisms of the CFDP protocol. +/// +/// ## 1. Check limit handling +/// +/// The first mechanism is the check limit handling for unacknowledged transfers as specified +/// in 4.6.3.2 and 4.6.3.3 of the CFDP standard. +/// For this mechanism, the timer has different functionality depending on whether /// the using entity is the sending entity or the receiving entity for the unacknowledged /// transmission mode. /// @@ -35,30 +62,40 @@ pub enum EntityType { /// For the receiving entity, this timer determines the expiry period for incrementing a check /// counter after an EOF PDU is received for an incomplete file transfer. This allows out-of-order /// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. -pub trait CheckTimerProvider { +/// +/// ## 2. NAK activity limit +/// +/// The timer will be used to perform the NAK activity check as specified in 4.6.4.7 of the CFDP +/// standard. The expiration period will be provided by the NAK timer expiration limit of the +/// remote entity configuration. +/// +/// ## 3. Positive ACK procedures +/// +/// The timer will be used to perform the Positive Acknowledgement Procedures as specified in +/// 4.7. 1of the CFDP standard. The expiration period will be provided by the Positive ACK timer +/// interval of the remote entity configuration. +pub trait CheckTimer: Debug { fn has_expired(&self) -> bool; + fn reset(&mut self); } /// A generic trait which allows CFDP entities to create check timers which are required to /// implement special procedures in unacknowledged transmission mode, as specified in 4.6.3.2 -/// and 4.6.3.3. The [CheckTimerProvider] provides more information about the purpose of the -/// check timer. +/// and 4.6.3.3. The [CheckTimer] documentation provides more information about the purpose of the +/// check timer in the context of CFDP. /// -/// This trait also allows the creation of different check timers depending on -/// the ID of the local entity, the ID of the remote entity for a given transaction, and the -/// type of entity. +/// This trait also allows the creation of different check timers depending on context and purpose +/// of the timer, the runtime environment (e.g. standard clock timer vs. timer using a RTC) or +/// other factors. #[cfg(feature = "alloc")] pub trait CheckTimerCreator { - fn get_check_timer_provider( - local_id: &UnsignedByteField, - remote_id: &UnsignedByteField, - entity_type: EntityType, - ) -> Box; + fn get_check_timer_provider(&self, timer_context: TimerContext) -> Box; } -/// Simple implementation of the [CheckTimerProvider] trait assuming a standard runtime. +/// Simple implementation of the [CheckTimerCreator] trait assuming a standard runtime. /// It also assumes that a second accuracy of the check timer period is sufficient. #[cfg(feature = "std")] +#[derive(Debug)] pub struct StdCheckTimer { expiry_time_seconds: u64, start_time: std::time::Instant, @@ -75,7 +112,7 @@ impl StdCheckTimer { } #[cfg(feature = "std")] -impl CheckTimerProvider for StdCheckTimer { +impl CheckTimer for StdCheckTimer { fn has_expired(&self) -> bool { let elapsed_time = self.start_time.elapsed(); if elapsed_time.as_secs() > self.expiry_time_seconds { @@ -83,24 +120,322 @@ impl CheckTimerProvider for StdCheckTimer { } false } + + fn reset(&mut self) { + self.start_time = std::time::Instant::now(); + } } -#[derive(Debug)] +/// This structure models the remote entity configuration information as specified in chapter 8.3 +/// of the CFDP standard. + +/// Some of the fields which were not considered necessary for the Rust implementation +/// were omitted. Some other fields which are not contained inside the standard but are considered +/// necessary for the Rust implementation are included. +/// +/// ## Notes on Positive Acknowledgment Procedures +/// +/// The `positive_ack_timer_interval_seconds` and `positive_ack_timer_expiration_limit` will +/// be used for positive acknowledgement procedures as specified in CFDP chapter 4.7. The sending +/// entity will start the timer for any PDUs where an acknowledgment is required (e.g. EOF PDU). +/// Once the expected ACK response has not been received for that interval, as counter will be +/// incremented and the timer will be reset. Once the counter exceeds the +/// `positive_ack_timer_expiration_limit`, a Positive ACK Limit Reached fault will be declared. +/// +/// ## Notes on Deferred Lost Segment Procedures +/// +/// This procedure will be active if an EOF (No Error) PDU is received in acknowledged mode. After +/// issuing the NAK sequence which has the whole file scope, a timer will be started. The timer is +/// reset when missing segments or missing metadata is received. The timer will be deactivated if +/// all missing data is received. If the timer expires, a new NAK sequence will be issued and a +/// counter will be incremented, which can lead to a NAK Limit Reached fault being declared. +/// +/// ## Fields +/// +/// * `entity_id` - The ID of the remote entity. +/// * `max_packet_len` - This determines of all PDUs generated for that remote entity in addition +/// to the `max_file_segment_len` attribute which also determines the size of file data PDUs. +/// * `max_file_segment_len` The maximum file segment length which determines the maximum size +/// of file data PDUs in addition to the `max_packet_len` attribute. If this field is set +/// to None, the maximum file segment length will be derived from the maximum packet length. +/// If this has some value which is smaller than the segment value derived from +/// `max_packet_len`, this value will be picked. +/// * `closure_requested_by_default` - If the closure requested field is not supplied as part of +/// the Put Request, it will be determined from this field in the remote configuration. +/// * `crc_on_transmission_by_default` - If the CRC option is not supplied as part of the Put +/// Request, it will be determined from this field in the remote configuration. +/// * `default_transmission_mode` - If the transmission mode is not supplied as part of the +/// Put Request, it will be determined from this field in the remote configuration. +/// * `disposition_on_cancellation` - Determines whether an incomplete received file is discard on +/// transaction cancellation. Defaults to False. +/// * `default_crc_type` - Default checksum type used to calculate for all file transmissions to +/// this remote entity. +/// * `check_limit` - This timer determines the expiry period for incrementing a check counter +/// after an EOF PDU is received for an incomplete file transfer. This allows out-of-order +/// reception of file data PDUs and EOF PDUs. Also see 4.6.3.3 of the CFDP standard. Defaults to +/// 2, so the check limit timer may expire twice. +/// * `positive_ack_timer_interval_seconds`- See the notes on the Positive Acknowledgment +/// Procedures inside the class documentation. Expected as floating point seconds. Defaults to +/// 10 seconds. +/// * `positive_ack_timer_expiration_limit` - See the notes on the Positive Acknowledgment +/// Procedures inside the class documentation. Defaults to 2, so the timer may expire twice. +/// * `immediate_nak_mode` - Specifies whether a NAK sequence should be issued immediately when a +/// file data gap or lost metadata is detected in the acknowledged mode. Defaults to True. +/// * `nak_timer_interval_seconds` - See the notes on the Deferred Lost Segment Procedure inside +/// the class documentation. Expected as floating point seconds. Defaults to 10 seconds. +/// * `nak_timer_expiration_limit` - See the notes on the Deferred Lost Segment Procedure inside +/// the class documentation. Defaults to 2, so the timer may expire two times. +#[derive(Debug, Copy, Clone)] pub struct RemoteEntityConfig { pub entity_id: UnsignedByteField, + pub max_packet_len: usize, pub max_file_segment_len: usize, - pub closure_requeted_by_default: bool, + pub closure_requested_by_default: bool, pub crc_on_transmission_by_default: bool, pub default_transmission_mode: TransmissionMode, pub default_crc_type: ChecksumType, + pub positive_ack_timer_interval_seconds: f32, + pub positive_ack_timer_expiration_limit: u32, pub check_limit: u32, + pub disposition_on_cancellation: bool, + pub immediate_nak_mode: bool, + pub nak_timer_interval_seconds: f32, + pub nak_timer_expiration_limit: u32, +} + +impl RemoteEntityConfig { + pub fn new_with_default_values( + entity_id: UnsignedByteField, + max_file_segment_len: usize, + max_packet_len: usize, + closure_requested_by_default: bool, + crc_on_transmission_by_default: bool, + default_transmission_mode: TransmissionMode, + default_crc_type: ChecksumType, + ) -> Self { + Self { + entity_id, + max_file_segment_len, + max_packet_len, + closure_requested_by_default, + crc_on_transmission_by_default, + default_transmission_mode, + default_crc_type, + check_limit: 2, + positive_ack_timer_interval_seconds: 10.0, + positive_ack_timer_expiration_limit: 2, + disposition_on_cancellation: false, + immediate_nak_mode: true, + nak_timer_interval_seconds: 10.0, + nak_timer_expiration_limit: 2, + } + } } pub trait RemoteEntityConfigProvider { - fn get_remote_config(&self, remote_id: &UnsignedByteField) -> Option<&RemoteEntityConfig>; + /// Retrieve the remote entity configuration for the given remote ID. + fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig>; + fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig>; + /// Add a new remote configuration. Return [true] if the configuration was + /// inserted successfully, and [false] if a configuration already exists. + fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool; + /// Remote a configuration. Returns [true] if the configuration was removed successfully, + /// and [false] if no configuration exists for the given remote ID. + fn remove_config(&mut self, remote_id: u64) -> bool; } -#[derive(Debug, PartialEq, Eq, Copy, Clone)] +#[cfg(feature = "std")] +#[derive(Default)] +pub struct StdRemoteEntityConfigProvider { + remote_cfg_table: HashMap, +} + +#[cfg(feature = "std")] +impl RemoteEntityConfigProvider for StdRemoteEntityConfigProvider { + fn get_remote_config(&self, remote_id: u64) -> Option<&RemoteEntityConfig> { + self.remote_cfg_table.get(&remote_id) + } + fn get_remote_config_mut(&mut self, remote_id: u64) -> Option<&mut RemoteEntityConfig> { + self.remote_cfg_table.get_mut(&remote_id) + } + fn add_config(&mut self, cfg: &RemoteEntityConfig) -> bool { + self.remote_cfg_table + .insert(cfg.entity_id.value(), *cfg) + .is_some() + } + fn remove_config(&mut self, remote_id: u64) -> bool { + self.remote_cfg_table.remove(&remote_id).is_some() + } +} + +/// This trait introduces some callbacks which will be called when a particular CFDP fault +/// handler is called. +/// +/// It is passed into the CFDP handlers as part of the [DefaultFaultHandler] and the local entity +/// configuration and provides a way to specify custom user error handlers. This allows to +/// implement some CFDP features like fault handler logging, which would not be possible +/// generically otherwise. +/// +/// For each error reported by the [DefaultFaultHandler], the appropriate fault handler callback +/// will be called depending on the [FaultHandlerCode]. +pub trait UserFaultHandler { + fn notice_of_suspension_cb( + &mut self, + transaction_id: TransactionId, + cond: ConditionCode, + progress: u64, + ); + + fn notice_of_cancellation_cb( + &mut self, + transaction_id: TransactionId, + cond: ConditionCode, + progress: u64, + ); + + fn abandoned_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64); + + fn ignore_cb(&mut self, transaction_id: TransactionId, cond: ConditionCode, progress: u64); +} + +/// This structure is used to implement the fault handling as specified in chapter 4.8 of the CFDP +/// standard. +/// +/// It does so by mapping each applicable [spacepackets::cfdp::ConditionCode] to a fault handler +/// which is denoted by the four [spacepackets::cfdp::FaultHandlerCode]s. This code is used +/// to select the error handling inside the CFDP handler itself in addition to dispatching to a +/// user-provided callback function provided by the [UserFaultHandler]. +/// +/// Some note on the provided default settings: +/// +/// - Checksum failures will be ignored by default. This is because for unacknowledged transfers, +/// cancelling the transfer immediately would interfere with the check limit mechanism specified +/// in chapter 4.6.3.3. +/// - Unsupported checksum types will also be ignored by default. Even if the checksum type is +/// not supported the file transfer might still have worked properly. +/// +/// For all other faults, the default fault handling operation will be to cancel the transaction. +/// These defaults can be overriden by using the [Self::set_fault_handler] method. +/// Please note that in any case, fault handler overrides can be specified by the sending CFDP +/// entity. +pub struct DefaultFaultHandler { + handler_array: [FaultHandlerCode; 10], + // Could also change the user fault handler trait to have non mutable methods, but that limits + // flexbility on the user side.. + user_fault_handler: RefCell>, +} + +impl DefaultFaultHandler { + fn condition_code_to_array_index(conditon_code: ConditionCode) -> Option { + Some(match conditon_code { + ConditionCode::PositiveAckLimitReached => 0, + ConditionCode::KeepAliveLimitReached => 1, + ConditionCode::InvalidTransmissionMode => 2, + ConditionCode::FilestoreRejection => 3, + ConditionCode::FileChecksumFailure => 4, + ConditionCode::FileSizeError => 5, + ConditionCode::NakLimitReached => 6, + ConditionCode::InactivityDetected => 7, + ConditionCode::CheckLimitReached => 8, + ConditionCode::UnsupportedChecksumType => 9, + _ => return None, + }) + } + + pub fn set_fault_handler( + &mut self, + condition_code: ConditionCode, + fault_handler: FaultHandlerCode, + ) { + let array_idx = Self::condition_code_to_array_index(condition_code); + if array_idx.is_none() { + return; + } + self.handler_array[array_idx.unwrap()] = fault_handler; + } + + pub fn new(user_fault_handler: Box) -> Self { + let mut init_array = [FaultHandlerCode::NoticeOfCancellation; 10]; + init_array + [Self::condition_code_to_array_index(ConditionCode::FileChecksumFailure).unwrap()] = + FaultHandlerCode::IgnoreError; + init_array[Self::condition_code_to_array_index(ConditionCode::UnsupportedChecksumType) + .unwrap()] = FaultHandlerCode::IgnoreError; + Self { + handler_array: init_array, + user_fault_handler: RefCell::new(user_fault_handler), + } + } + + pub fn get_fault_handler(&self, condition_code: ConditionCode) -> FaultHandlerCode { + let array_idx = Self::condition_code_to_array_index(condition_code); + if array_idx.is_none() { + return FaultHandlerCode::IgnoreError; + } + self.handler_array[array_idx.unwrap()] + } + + pub fn report_fault( + &self, + transaction_id: TransactionId, + condition: ConditionCode, + progress: u64, + ) -> FaultHandlerCode { + let array_idx = Self::condition_code_to_array_index(condition); + if array_idx.is_none() { + return FaultHandlerCode::IgnoreError; + } + let fh_code = self.handler_array[array_idx.unwrap()]; + let mut handler_mut = self.user_fault_handler.borrow_mut(); + match fh_code { + FaultHandlerCode::NoticeOfCancellation => { + handler_mut.notice_of_cancellation_cb(transaction_id, condition, progress); + } + FaultHandlerCode::NoticeOfSuspension => { + handler_mut.notice_of_suspension_cb(transaction_id, condition, progress); + } + FaultHandlerCode::IgnoreError => { + handler_mut.ignore_cb(transaction_id, condition, progress); + } + FaultHandlerCode::AbandonTransaction => { + handler_mut.abandoned_cb(transaction_id, condition, progress); + } + } + fh_code + } +} + +pub struct IndicationConfig { + pub eof_sent: bool, + pub eof_recv: bool, + pub file_segment_recv: bool, + pub transaction_finished: bool, + pub suspended: bool, + pub resumed: bool, +} + +impl Default for IndicationConfig { + fn default() -> Self { + Self { + eof_sent: true, + eof_recv: true, + file_segment_recv: true, + transaction_finished: true, + suspended: true, + resumed: true, + } + } +} + +pub struct LocalEntityConfig { + pub id: UnsignedByteField, + pub indication_cfg: IndicationConfig, + pub default_fault_handler: DefaultFaultHandler, +} + +/// The CFDP transaction ID of a CFDP transaction consists of the source entity ID and the sequence +/// number of that transfer which is also determined by the CFDP source entity. +#[derive(Debug, Eq, Copy, Clone)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub struct TransactionId { source_id: UnsignedByteField, @@ -121,23 +456,38 @@ impl TransactionId { } } +impl Hash for TransactionId { + fn hash(&self, state: &mut H) { + self.source_id.value().hash(state); + self.seq_num.value().hash(state); + } +} + +impl PartialEq for TransactionId { + fn eq(&self, other: &Self) -> bool { + self.source_id.value() == other.source_id.value() + && self.seq_num.value() == other.seq_num.value() + } +} + #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum TransactionStep { Idle = 0, TransactionStart = 1, ReceivingFileDataPdus = 2, - SendingAckPdu = 3, - TransferCompletion = 4, - SendingFinishedPdu = 5, + ReceivingFileDataPdusWithCheckLimitHandling = 3, + SendingAckPdu = 4, + TransferCompletion = 5, + SendingFinishedPdu = 6, } #[derive(Debug, Copy, Clone, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] pub enum State { Idle = 0, - BusyClass1Nacked = 2, - BusyClass2Acked = 3, + Busy = 1, + Suspended = 2, } pub const CRC_32: Crc = Crc::::new(&CRC_32_CKSUM); @@ -248,8 +598,8 @@ mod tests { pdu::{ eof::EofPdu, file_data::FileDataPdu, - metadata::{MetadataGenericParams, MetadataPdu}, - CommonPduConfig, FileDirectiveType, PduHeader, + metadata::{MetadataGenericParams, MetadataPduCreator}, + CommonPduConfig, FileDirectiveType, PduHeader, WritablePduPacket, }, PduType, }; @@ -272,7 +622,8 @@ mod tests { let dest_file_name = "hello-dest.txt"; let src_lv = Lv::new_from_str(src_file_name).unwrap(); let dest_lv = Lv::new_from_str(dest_file_name).unwrap(); - let metadata_pdu = MetadataPdu::new(pdu_header, metadata_params, src_lv, dest_lv, None); + let metadata_pdu = + MetadataPduCreator::new_no_opts(pdu_header, metadata_params, src_lv, dest_lv); metadata_pdu .write_to_bytes(&mut buf) .expect("writing metadata PDU failed"); diff --git a/satrs-core/src/cfdp/user.rs b/satrs-core/src/cfdp/user.rs index 9047bbd..65ce909 100644 --- a/satrs-core/src/cfdp/user.rs +++ b/satrs-core/src/cfdp/user.rs @@ -1,10 +1,10 @@ use spacepackets::{ cfdp::{ pdu::{ - file_data::RecordContinuationState, + file_data::SegmentMetadata, finished::{DeliveryCode, FileStatus}, }, - tlv::msg_to_user::MsgToUserTlv, + tlv::{msg_to_user::MsgToUserTlv, WritableTlv}, ConditionCode, }, util::UnsignedByteField, @@ -30,13 +30,44 @@ pub struct MetadataReceivedParams<'src_file, 'dest_file, 'msgs_to_user> { pub msgs_to_user: &'msgs_to_user [MsgToUserTlv<'msgs_to_user>], } +#[cfg(feature = "alloc")] +#[derive(Debug)] +pub struct OwnedMetadataRecvdParams { + pub id: TransactionId, + pub source_id: UnsignedByteField, + pub file_size: u64, + pub src_file_name: alloc::string::String, + pub dest_file_name: alloc::string::String, + pub msgs_to_user: alloc::vec::Vec>, +} + +#[cfg(feature = "alloc")] +impl From> for OwnedMetadataRecvdParams { + fn from(value: MetadataReceivedParams) -> Self { + Self::from(&value) + } +} + +#[cfg(feature = "alloc")] +impl From<&MetadataReceivedParams<'_, '_, '_>> for OwnedMetadataRecvdParams { + fn from(value: &MetadataReceivedParams) -> Self { + Self { + id: value.id, + source_id: value.source_id, + file_size: value.file_size, + src_file_name: value.src_file_name.into(), + dest_file_name: value.dest_file_name.into(), + msgs_to_user: value.msgs_to_user.iter().map(|tlv| tlv.to_vec()).collect(), + } + } +} + #[derive(Debug)] pub struct FileSegmentRecvdParams<'seg_meta> { pub id: TransactionId, pub offset: u64, pub length: usize, - pub rec_cont_state: Option, - pub segment_metadata: Option<&'seg_meta [u8]>, + pub segment_metadata: Option<&'seg_meta SegmentMetadata<'seg_meta>>, } pub trait CfdpUser { diff --git a/satrs-core/src/encoding/ccsds.rs b/satrs-core/src/encoding/ccsds.rs index ecd4ff5..37694d7 100644 --- a/satrs-core/src/encoding/ccsds.rs +++ b/satrs-core/src/encoding/ccsds.rs @@ -113,7 +113,7 @@ pub fn parse_buffer_for_ccsds_space_packets( #[cfg(test)] mod tests { use spacepackets::{ - ecss::{tc::PusTcCreator, SerializablePusPacket}, + ecss::{tc::PusTcCreator, WritablePusPacket}, PacketId, SpHeader, }; diff --git a/satrs-core/src/event_man.rs b/satrs-core/src/event_man.rs index f465969..8cb549f 100644 --- a/satrs-core/src/event_man.rs +++ b/satrs-core/src/event_man.rs @@ -2,23 +2,13 @@ //! //! This module provides components to perform event routing. The most important component for this //! task is the [EventManager]. It receives all events and then routes them to event subscribers -//! where appropriate. -#![cfg_attr(feature = "doc-images", -cfg_attr(all(), -doc = ::embed_doc_image::embed_image!("event_man_arch", "images/event_man_arch.png" -)))] -#![cfg_attr( - not(feature = "doc-images"), - doc = "**Doc images not enabled**. Compile with feature `doc-images` and Rust version >= 1.54 \ - to enable." -)] -//! One common use case for satellite systems is to offer a light-weight publish-subscribe mechanism -//! and IPC mechanism for software and hardware events which are also packaged as telemetry (TM) or -//! can trigger a system response. +//! where appropriate. One common use case for satellite systems is to offer a light-weight +//! publish-subscribe mechanism and IPC mechanism for software and hardware events which are also +//! packaged as telemetry (TM) or can trigger a system response. //! -//! The following graph shows how the event flow for such a setup could look like: -//! -//! ![Event flow][event_man_arch] +//! It is recommended to read the +//! [sat-rs book chapter](https://absatsw.irs.uni-stuttgart.de/projects/sat-rs/book/events.html) +//! about events first: //! //! The event manager has a listener table abstracted by the [ListenerTable], which maps //! listener groups identified by [ListenerKey]s to a [sender ID][ChannelId]. diff --git a/satrs-core/src/hal/std/tcp_spacepackets_server.rs b/satrs-core/src/hal/std/tcp_spacepackets_server.rs index c124a47..6ade0d1 100644 --- a/satrs-core/src/hal/std/tcp_spacepackets_server.rs +++ b/satrs-core/src/hal/std/tcp_spacepackets_server.rs @@ -173,7 +173,7 @@ mod tests { use alloc::{boxed::Box, sync::Arc}; use hashbrown::HashSet; use spacepackets::{ - ecss::{tc::PusTcCreator, SerializablePusPacket}, + ecss::{tc::PusTcCreator, WritablePusPacket}, PacketId, SpHeader, }; diff --git a/satrs-core/src/hal/std/udp_server.rs b/satrs-core/src/hal/std/udp_server.rs index 28e0328..6f2cb5b 100644 --- a/satrs-core/src/hal/std/udp_server.rs +++ b/satrs-core/src/hal/std/udp_server.rs @@ -19,7 +19,7 @@ use std::vec::Vec; /// /// ``` /// use std::net::{IpAddr, Ipv4Addr, SocketAddr, UdpSocket}; -/// use spacepackets::ecss::SerializablePusPacket; +/// use spacepackets::ecss::WritablePusPacket; /// use satrs_core::hal::std::udp_server::UdpTcServer; /// use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore}; /// use spacepackets::SpHeader; @@ -144,7 +144,7 @@ mod tests { use crate::hal::std::udp_server::{ReceiveResult, UdpTcServer}; use crate::tmtc::ReceivesTcCore; use spacepackets::ecss::tc::PusTcCreator; - use spacepackets::ecss::SerializablePusPacket; + use spacepackets::ecss::WritablePusPacket; use spacepackets::SpHeader; use std::boxed::Box; use std::collections::VecDeque; diff --git a/satrs-core/src/lib.rs b/satrs-core/src/lib.rs index 22699e0..061f728 100644 --- a/satrs-core/src/lib.rs +++ b/satrs-core/src/lib.rs @@ -20,6 +20,8 @@ extern crate downcast_rs; #[cfg(any(feature = "std", test))] extern crate std; +#[cfg(feature = "alloc")] +#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] pub mod cfdp; pub mod encoding; pub mod error; diff --git a/satrs-core/src/pus/event.rs b/satrs-core/src/pus/event.rs index 165493d..9424b24 100644 --- a/satrs-core/src/pus/event.rs +++ b/satrs-core/src/pus/event.rs @@ -147,7 +147,7 @@ impl EventReporterBase { Ok(PusTmCreator::new( &mut sp_header, sec_header, - Some(&buf[0..current_idx]), + &buf[0..current_idx], true, )) } diff --git a/satrs-core/src/pus/scheduler.rs b/satrs-core/src/pus/scheduler.rs index ce98095..392eb87 100644 --- a/satrs-core/src/pus/scheduler.rs +++ b/satrs-core/src/pus/scheduler.rs @@ -626,7 +626,7 @@ mod tests { use crate::pool::{LocalPool, PoolCfg, PoolProvider, StoreAddr, StoreError}; use alloc::collections::btree_map::Range; use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader}; - use spacepackets::ecss::SerializablePusPacket; + use spacepackets::ecss::WritablePusPacket; use spacepackets::time::{cds, TimeWriter, UnixTimestamp}; use spacepackets::SpHeader; use std::time::Duration; @@ -857,7 +857,7 @@ mod tests { let mut sp_header = SpHeader::tc_unseg(apid_to_set, 105, 0).unwrap(); let mut sec_header = PusTcSecondaryHeader::new_simple(17, 1); sec_header.source_id = src_id_to_set; - let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, None, true); + let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true); let req_id = RequestId::from_tc(&ping_tc); assert_eq!(req_id.source_id(), src_id_to_set); assert_eq!(req_id.apid(), apid_to_set); diff --git a/satrs-core/src/pus/test.rs b/satrs-core/src/pus/test.rs index fd5c05a..c205178 100644 --- a/satrs-core/src/pus/test.rs +++ b/satrs-core/src/pus/test.rs @@ -72,7 +72,7 @@ impl PusServiceHandler for PusService17TestHandler { // Sequence count will be handled centrally in TM funnel. let mut reply_header = SpHeader::tm_unseg(self.psb.tm_apid, 0, 0).unwrap(); let tc_header = PusTmSecondaryHeader::new_simple(17, 2, &time_stamp); - let ping_reply = PusTmCreator::new(&mut reply_header, tc_header, None, true); + let ping_reply = PusTmCreator::new(&mut reply_header, tc_header, &[], true); let result = self .psb .tm_sender @@ -118,7 +118,7 @@ mod tests { use crate::tmtc::tm_helper::SharedTmStore; use spacepackets::ecss::tc::{PusTcCreator, PusTcSecondaryHeader}; use spacepackets::ecss::tm::PusTmReader; - use spacepackets::ecss::{PusPacket, SerializablePusPacket}; + use spacepackets::ecss::{PusPacket, WritablePusPacket}; use spacepackets::{SequenceFlags, SpHeader}; use std::boxed::Box; use std::sync::{mpsc, RwLock}; @@ -154,7 +154,7 @@ mod tests { // Create a ping TC, verify acceptance. let mut sp_header = SpHeader::tc(TEST_APID, SequenceFlags::Unsegmented, 0, 0).unwrap(); let sec_header = PusTcSecondaryHeader::new_simple(17, 1); - let ping_tc = PusTcCreator::new(&mut sp_header, sec_header, None, true); + let ping_tc = PusTcCreator::new_no_app_data(&mut sp_header, sec_header, true); let token = verification_handler.add_tc(&ping_tc); let token = verification_handler .acceptance_success(token, None) diff --git a/satrs-core/src/pus/verification.rs b/satrs-core/src/pus/verification.rs index 47c2689..ff38b89 100644 --- a/satrs-core/src/pus/verification.rs +++ b/satrs-core/src/pus/verification.rs @@ -39,7 +39,7 @@ //! //! let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); //! let tc_header = PusTcSecondaryHeader::new_simple(17, 1); -//! let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true); +//! let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true); //! let init_token = reporter.add_tc(&pus_tc_0); //! //! // Complete success sequence for a telecommand @@ -87,7 +87,7 @@ use delegate::delegate; use serde::{Deserialize, Serialize}; use spacepackets::ecss::tc::IsPusTelecommand; use spacepackets::ecss::tm::{PusTmCreator, PusTmSecondaryHeader}; -use spacepackets::ecss::{EcssEnumeration, PusError, SerializablePusPacket}; +use spacepackets::ecss::{EcssEnumeration, PusError, WritablePusPacket}; use spacepackets::{CcsdsPacket, PacketId, PacketSequenceCtrl}; use spacepackets::{SpHeader, MAX_APID}; @@ -353,7 +353,7 @@ impl<'src_data, State, SuccessOrFailure> VerificationSendable<'src_data, State, } pub fn len_packed(&self) -> usize { - self.pus_tm.as_ref().unwrap().len_packed() + self.pus_tm.as_ref().unwrap().len_written() } pub fn pus_tm(&self) -> &PusTmCreator<'src_data> { @@ -877,7 +877,7 @@ impl VerificationReporterCore { PusTmCreator::new( sp_header, tm_sec_header, - Some(&src_data_buf[0..source_data_len]), + &src_data_buf[0..source_data_len], true, ) } @@ -1438,6 +1438,7 @@ mod tests { fn base_tc_init(app_data: Option<&[u8]>) -> (PusTcCreator, RequestId) { let mut sph = SpHeader::tc_unseg(TEST_APID, 0x34, 0).unwrap(); let tc_header = PusTcSecondaryHeader::new_simple(17, 1); + let app_data = app_data.unwrap_or(&[]); let pus_tc = PusTcCreator::new(&mut sph, tc_header, app_data, true); let req_id = RequestId::new(&pus_tc); (pus_tc, req_id) @@ -2162,7 +2163,7 @@ mod tests { let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); let tc_header = PusTcSecondaryHeader::new_simple(17, 1); - let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true); + let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true); let init_token = reporter.add_tc(&pus_tc_0); // Complete success sequence for a telecommand diff --git a/satrs-core/src/tmtc/ccsds_distrib.rs b/satrs-core/src/tmtc/ccsds_distrib.rs index 58fb8c6..ea9bf30 100644 --- a/satrs-core/src/tmtc/ccsds_distrib.rs +++ b/satrs-core/src/tmtc/ccsds_distrib.rs @@ -21,7 +21,7 @@ //! use satrs_core::tmtc::ccsds_distrib::{CcsdsPacketHandler, CcsdsDistributor}; //! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore}; //! use spacepackets::{CcsdsPacket, SpHeader}; -//! use spacepackets::ecss::SerializablePusPacket; +//! use spacepackets::ecss::WritablePusPacket; //! use spacepackets::ecss::tc::{PusTc, PusTcCreator}; //! //! #[derive (Default)] @@ -226,7 +226,7 @@ pub(crate) mod tests { use super::*; use crate::tmtc::ccsds_distrib::{CcsdsDistributor, CcsdsPacketHandler}; use spacepackets::ecss::tc::PusTcCreator; - use spacepackets::ecss::SerializablePusPacket; + use spacepackets::ecss::WritablePusPacket; use spacepackets::CcsdsPacket; use std::collections::VecDeque; use std::sync::{Arc, Mutex}; @@ -244,9 +244,10 @@ pub(crate) mod tests { &buf[0..size] } + type SharedPacketQueue = Arc)>>>; pub struct BasicApidHandlerSharedQueue { - pub known_packet_queue: Arc)>>>, - pub unknown_packet_queue: Arc)>>>, + pub known_packet_queue: SharedPacketQueue, + pub unknown_packet_queue: SharedPacketQueue, } #[derive(Default)] @@ -268,11 +269,11 @@ pub(crate) mod tests { ) -> Result<(), Self::Error> { let mut vec = Vec::new(); vec.extend_from_slice(tc_raw); - Ok(self - .known_packet_queue + self.known_packet_queue .lock() .unwrap() - .push_back((sp_header.apid(), vec))) + .push_back((sp_header.apid(), vec)); + Ok(()) } fn handle_unknown_apid( @@ -282,11 +283,11 @@ pub(crate) mod tests { ) -> Result<(), Self::Error> { let mut vec = Vec::new(); vec.extend_from_slice(tc_raw); - Ok(self - .unknown_packet_queue + self.unknown_packet_queue .lock() .unwrap() - .push_back((sp_header.apid(), vec))) + .push_back((sp_header.apid(), vec)); + Ok(()) } } diff --git a/satrs-core/src/tmtc/pus_distrib.rs b/satrs-core/src/tmtc/pus_distrib.rs index a400484..e4eefb1 100644 --- a/satrs-core/src/tmtc/pus_distrib.rs +++ b/satrs-core/src/tmtc/pus_distrib.rs @@ -18,7 +18,7 @@ //! # Example //! //! ```rust -//! use spacepackets::ecss::SerializablePusPacket; +//! use spacepackets::ecss::WritablePusPacket; //! use satrs_core::tmtc::pus_distrib::{PusDistributor, PusServiceProvider}; //! use satrs_core::tmtc::{ReceivesTc, ReceivesTcCore}; //! use spacepackets::SpHeader; diff --git a/satrs-core/src/tmtc/tm_helper.rs b/satrs-core/src/tmtc/tm_helper.rs index 06ba532..6dca7ee 100644 --- a/satrs-core/src/tmtc/tm_helper.rs +++ b/satrs-core/src/tmtc/tm_helper.rs @@ -11,7 +11,7 @@ pub mod std_mod { use crate::pool::{ShareablePoolProvider, SharedPool, StoreAddr}; use crate::pus::EcssTmtcError; use spacepackets::ecss::tm::PusTmCreator; - use spacepackets::ecss::SerializablePusPacket; + use spacepackets::ecss::WritablePusPacket; use std::sync::{Arc, RwLock}; #[derive(Clone)] @@ -32,7 +32,7 @@ pub mod std_mod { pub fn add_pus_tm(&self, pus_tm: &PusTmCreator) -> Result { let mut pg = self.pool.write().map_err(|_| EcssTmtcError::StoreLock)?; - let (addr, buf) = pg.free_element(pus_tm.len_packed())?; + let (addr, buf) = pg.free_element(pus_tm.len_written())?; pus_tm .write_to_bytes(buf) .expect("writing PUS TM to store failed"); @@ -59,7 +59,7 @@ impl PusTmWithCdsShortHelper { &'a mut self, service: u8, subservice: u8, - source_data: Option<&'a [u8]>, + source_data: &'a [u8], seq_count: u16, ) -> PusTmCreator { let time_stamp = TimeProvider::from_now_with_u16_days().unwrap(); @@ -71,7 +71,7 @@ impl PusTmWithCdsShortHelper { &'a mut self, service: u8, subservice: u8, - source_data: Option<&'a [u8]>, + source_data: &'a [u8], stamper: &TimeProvider, seq_count: u16, ) -> PusTmCreator { @@ -83,7 +83,7 @@ impl PusTmWithCdsShortHelper { &'a self, service: u8, subservice: u8, - source_data: Option<&'a [u8]>, + source_data: &'a [u8], seq_count: u16, ) -> PusTmCreator { let mut reply_header = SpHeader::tm_unseg(self.apid, seq_count, 0).unwrap(); diff --git a/satrs-core/tests/hk_helpers.rs b/satrs-core/tests/hk_helpers.rs index 07264ef..8791b1e 100644 --- a/satrs-core/tests/hk_helpers.rs +++ b/satrs-core/tests/hk_helpers.rs @@ -1,7 +1,7 @@ #![allow(dead_code)] use core::mem::size_of; use serde::{Deserialize, Serialize}; -use spacepackets::ecss::{Ptc, RealPfc, UnsignedPfc}; +use spacepackets::ecss::{PfcReal, PfcUnsigned, Ptc}; use spacepackets::time::cds::TimeProvider; use spacepackets::time::{CcsdsTimeProvider, TimeWriter}; @@ -64,7 +64,7 @@ impl TestMgmHkWithIndividualValidity { curr_idx += 1; buf[curr_idx] = Ptc::Real as u8; curr_idx += 1; - buf[curr_idx] = RealPfc::Float as u8; + buf[curr_idx] = PfcReal::Float as u8; curr_idx += 1; buf[curr_idx..curr_idx + size_of::()].copy_from_slice(&self.temp.val.to_be_bytes()); curr_idx += size_of::(); @@ -75,7 +75,7 @@ impl TestMgmHkWithIndividualValidity { curr_idx += 1; buf[curr_idx] = Ptc::UnsignedInt as u8; curr_idx += 1; - buf[curr_idx] = UnsignedPfc::TwoBytes as u8; + buf[curr_idx] = PfcUnsigned::TwoBytes as u8; curr_idx += 1; buf[curr_idx] = 3; curr_idx += 1; @@ -100,7 +100,7 @@ impl TestMgmHkWithGroupValidity { curr_idx += 1; buf[curr_idx] = Ptc::Real as u8; curr_idx += 1; - buf[curr_idx] = RealPfc::Float as u8; + buf[curr_idx] = PfcReal::Float as u8; curr_idx += 1; buf[curr_idx..curr_idx + size_of::()].copy_from_slice(&self.temp.to_be_bytes()); curr_idx += size_of::(); @@ -109,7 +109,7 @@ impl TestMgmHkWithGroupValidity { curr_idx += 1; buf[curr_idx] = Ptc::UnsignedInt as u8; curr_idx += 1; - buf[curr_idx] = UnsignedPfc::TwoBytes as u8; + buf[curr_idx] = PfcUnsigned::TwoBytes as u8; curr_idx += 1; buf[curr_idx] = 3; for val in self.mgm_vals { diff --git a/satrs-core/tests/pus_verification.rs b/satrs-core/tests/pus_verification.rs index 097eba8..9c9fc63 100644 --- a/satrs-core/tests/pus_verification.rs +++ b/satrs-core/tests/pus_verification.rs @@ -9,7 +9,7 @@ pub mod crossbeam_test { use satrs_core::tmtc::tm_helper::SharedTmStore; use spacepackets::ecss::tc::{PusTcCreator, PusTcReader, PusTcSecondaryHeader}; use spacepackets::ecss::tm::PusTmReader; - use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, SerializablePusPacket}; + use spacepackets::ecss::{EcssEnumU16, EcssEnumU8, PusPacket, WritablePusPacket}; use spacepackets::SpHeader; use std::sync::{Arc, RwLock}; use std::thread; @@ -54,17 +54,17 @@ pub mod crossbeam_test { let mut tc_guard = shared_tc_pool_0.write().unwrap(); let mut sph = SpHeader::tc_unseg(TEST_APID, 0, 0).unwrap(); let tc_header = PusTcSecondaryHeader::new_simple(17, 1); - let pus_tc_0 = PusTcCreator::new(&mut sph, tc_header, None, true); + let pus_tc_0 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true); req_id_0 = RequestId::new(&pus_tc_0); - let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap(); - pus_tc_0.write_to_bytes(&mut buf).unwrap(); + let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap(); + pus_tc_0.write_to_bytes(buf).unwrap(); tx_tc_0.send(addr).unwrap(); let mut sph = SpHeader::tc_unseg(TEST_APID, 1, 0).unwrap(); let tc_header = PusTcSecondaryHeader::new_simple(5, 1); - let pus_tc_1 = PusTcCreator::new(&mut sph, tc_header, None, true); + let pus_tc_1 = PusTcCreator::new_no_app_data(&mut sph, tc_header, true); req_id_1 = RequestId::new(&pus_tc_1); - let (addr, mut buf) = tc_guard.free_element(pus_tc_0.len_packed()).unwrap(); - pus_tc_1.write_to_bytes(&mut buf).unwrap(); + let (addr, buf) = tc_guard.free_element(pus_tc_0.len_written()).unwrap(); + pus_tc_1.write_to_bytes(buf).unwrap(); tx_tc_1.send(addr).unwrap(); } let verif_sender_0 = thread::spawn(move || { @@ -81,16 +81,14 @@ pub mod crossbeam_test { tc_buf[0..tc_len].copy_from_slice(buf); } let (_tc, _) = PusTcReader::new(&tc_buf[0..tc_len]).unwrap(); - let accepted_token; let token = reporter_with_sender_0.add_tc_with_req_id(req_id_0); - accepted_token = reporter_with_sender_0 + let accepted_token = reporter_with_sender_0 .acceptance_success(token, Some(&FIXED_STAMP)) .expect("Acceptance success failed"); // Do some start handling here - let started_token; - started_token = reporter_with_sender_0 + let started_token = reporter_with_sender_0 .start_success(accepted_token, Some(&FIXED_STAMP)) .expect("Start success failed"); // Do some step handling here @@ -158,8 +156,7 @@ pub mod crossbeam_test { RequestId::from_bytes(&pus_tm.source_data()[0..RequestId::SIZE_AS_BYTES]) .expect("reading request ID from PUS TM source data failed"); if !verif_map.contains_key(&req_id) { - let mut content = Vec::new(); - content.push(pus_tm.subservice()); + let content = vec![pus_tm.subservice()]; verif_map.insert(req_id, content); } else { let content = verif_map.get_mut(&req_id).unwrap(); diff --git a/satrs-core/tests/tcp_servers.rs b/satrs-core/tests/tcp_servers.rs index 251eead..e5297c3 100644 --- a/satrs-core/tests/tcp_servers.rs +++ b/satrs-core/tests/tcp_servers.rs @@ -28,7 +28,7 @@ use satrs_core::{ tmtc::{ReceivesTcCore, TmPacketSourceCore}, }; use spacepackets::{ - ecss::{tc::PusTcCreator, SerializablePusPacket}, + ecss::{tc::PusTcCreator, WritablePusPacket}, PacketId, SpHeader, }; use std::{boxed::Box, collections::VecDeque, sync::Arc, vec::Vec}; diff --git a/satrs-example/Cargo.toml b/satrs-example/Cargo.toml index cf3cade..a300bfa 100644 --- a/satrs-example/Cargo.toml +++ b/satrs-example/Cargo.toml @@ -28,5 +28,5 @@ num-derive = "0.3" path = "../satrs-core" [dependencies.satrs-mib] -version = "0.1.0-alpha.1" -# path = "../satrs-mib" +# version = "0.1.0-alpha.1" +path = "../satrs-mib" diff --git a/satrs-example/src/bin/simpleclient.rs b/satrs-example/src/bin/simpleclient.rs index 6f6de79..4d0dab7 100644 --- a/satrs-example/src/bin/simpleclient.rs +++ b/satrs-example/src/bin/simpleclient.rs @@ -2,7 +2,7 @@ use satrs_core::pus::verification::RequestId; use satrs_core::spacepackets::ecss::tc::PusTcCreator; use satrs_core::spacepackets::ecss::tm::PusTmReader; use satrs_core::{ - spacepackets::ecss::{PusPacket, SerializablePusPacket}, + spacepackets::ecss::{PusPacket, WritablePusPacket}, spacepackets::SpHeader, }; use satrs_example::{OBSW_SERVER_ADDR, SERVER_PORT}; diff --git a/satrs-example/src/main.rs b/satrs-example/src/main.rs index f8d6b65..0bbc395 100644 --- a/satrs-example/src/main.rs +++ b/satrs-example/src/main.rs @@ -472,7 +472,7 @@ fn main() { let pus_tm = PusTmCreator::new( &mut sp_header, sec_header, - Some(&buf), + &buf, true, ); let addr = aocs_tm_store diff --git a/satrs-mib/Cargo.toml b/satrs-mib/Cargo.toml index e901ebe..b64813c 100644 --- a/satrs-mib/Cargo.toml +++ b/satrs-mib/Cargo.toml @@ -23,7 +23,8 @@ version = "1" optional = true [dependencies.satrs-core] -version = "0.1.0-alpha.1" +path = "../satrs-core" +# version = "0.1.0-alpha.1" # git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git" # branch = "main" # rev = "35e1f7a983f6535c5571186e361fe101d4306b89" diff --git a/satrs-mib/codegen/Cargo.toml b/satrs-mib/codegen/Cargo.toml index 9a3887c..1ad3810 100644 --- a/satrs-mib/codegen/Cargo.toml +++ b/satrs-mib/codegen/Cargo.toml @@ -20,7 +20,8 @@ quote = "1" proc-macro2 = "1" [dependencies.satrs-core] -version = "0.1.0-alpha.1" +path = "../../satrs-core" +# version = "0.1.0-alpha.1" # git = "https://egit.irs.uni-stuttgart.de/rust/sat-rs.git" # branch = "main" # rev = "35e1f7a983f6535c5571186e361fe101d4306b89"