From b6d6f588e101bf9bd7ce2b3676d9ad0a10f53a36 Mon Sep 17 00:00:00 2001 From: Tu Nguyen Date: Thu, 19 Sep 2024 11:48:23 +0700 Subject: [PATCH] receive can frame from beginning --- src/event_handler/can_handler.rs | 78 +++++++++++++++-------------- src/event_handler/debug.rs | 81 +++++++++++++++++++++---------- src/main.rs | 1 + ui/app.slint | 6 ++- ui/debug_page.slint | 56 +++++++++++++++++++-- ui/images/pause.png | Bin 0 -> 6734 bytes ui/images/play.png | Bin 0 -> 2382 bytes 7 files changed, 152 insertions(+), 70 deletions(-) create mode 100644 ui/images/pause.png create mode 100644 ui/images/play.png diff --git a/src/event_handler/can_handler.rs b/src/event_handler/can_handler.rs index cb2bfc6..522596d 100644 --- a/src/event_handler/can_handler.rs +++ b/src/event_handler/can_handler.rs @@ -24,6 +24,7 @@ pub struct CanHandler<'a> { pub mspc_rx: &'a Arc>>, pub can_tx: Sender, pub bitrate: String, + pub dbc: Option, } static mut NEW_DBC_CHECK: bool = false; @@ -31,20 +32,17 @@ use super::{EVEN_COLOR, ODD_COLOR}; impl<'a> CanHandler<'a> { pub fn process_can_messages(&mut self) { - if let Ok(dbc) = self.mspc_rx.lock().unwrap().try_recv() { - #[cfg(target_os = "linux")] - { - let can_if = CanInterface::open(self.iface).unwrap(); - let _ = can_if.bring_down(); - let _ = can_if.set_bitrate(self.bitrate().unwrap(), None); - let _ = can_if.bring_up(); - let can_socket = self.open_can_socket(); - self.process_ui_events(dbc, can_socket, can_if); - } - #[cfg(target_os = "windows")] - self.process_ui_events(dbc); + #[cfg(target_os = "linux")] + { + let can_if = CanInterface::open(self.iface).unwrap(); + let _ = can_if.bring_down(); + let _ = can_if.set_bitrate(self.bitrate().unwrap(), None); + let _ = can_if.bring_up(); + let can_socket = self.open_can_socket(); + self.process_ui_events(can_socket, can_if); } - sleep(Duration::from_millis(10)); + #[cfg(target_os = "windows")] + self.process_ui_events(dbc); } #[cfg(target_os = "linux")] fn open_can_socket(&self) -> CanSocket { @@ -65,7 +63,7 @@ impl<'a> CanHandler<'a> { } } #[cfg(target_os = "linux")] - fn process_ui_events(&self, dbc: DBC, can_socket: CanSocket, can_if: CanInterface) { + fn process_ui_events(&mut self, can_socket: CanSocket, can_if: CanInterface) { let mut start_bus_load = Instant::now(); let mut total_bits = 0; loop { @@ -88,11 +86,7 @@ impl<'a> CanHandler<'a> { }; let _ = self.ui_handle.upgrade_in_event_loop(move |ui| unsafe { if ui.get_is_new_dbc() { - if ui.get_is_first_open() { - ui.set_is_first_open(false); - } else { - NEW_DBC_CHECK = true; - } + NEW_DBC_CHECK = true; ui.set_is_new_dbc(false); } ui.set_state(bus_state.into()); @@ -103,33 +97,37 @@ impl<'a> CanHandler<'a> { }); unsafe { if NEW_DBC_CHECK { - NEW_DBC_CHECK = false; - break; + if let Ok(dbc) = self.mspc_rx.lock().unwrap().try_recv() { + self.dbc = Some(dbc); + NEW_DBC_CHECK = false; + } } } if let Ok(frame) = can_socket.read_frame() { let _ = self.can_tx.send(frame); total_bits += (frame.len() + 6) * 8; // Data length + overhead (approximation) let frame_id = frame.raw_id() & !0x80000000; - for message in dbc.messages() { - if frame_id == (message.message_id().raw() & !0x80000000) { - let padding_data = Self::pad_to_8_bytes(frame.data()); - let hex_string = Self::array_to_hex_string(frame.data()); - let signal_data = message.parse_from_can(&padding_data); - let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { - let is_filter = ui.get_is_filter(); - let messages: ModelRc = if !is_filter { - ui.get_messages() - } else { - ui.get_filter_messages() - }; - Self::update_ui_with_signals( - &messages, - frame_id, - signal_data, - hex_string, - ); - }); + if let Some(dbc) = &self.dbc { + for message in dbc.messages() { + if frame_id == (message.message_id().raw() & !0x80000000) { + let padding_data = Self::pad_to_8_bytes(frame.data()); + let hex_string = Self::array_to_hex_string(frame.data()); + let signal_data = message.parse_from_can(&padding_data); + let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { + let is_filter = ui.get_is_filter(); + let messages: ModelRc = if !is_filter { + ui.get_messages() + } else { + ui.get_filter_messages() + }; + Self::update_ui_with_signals( + &messages, + frame_id, + signal_data, + hex_string, + ); + }); + } } } } else { diff --git a/src/event_handler/debug.rs b/src/event_handler/debug.rs index 544d191..f510d9c 100644 --- a/src/event_handler/debug.rs +++ b/src/event_handler/debug.rs @@ -1,6 +1,12 @@ -use std::{collections::HashMap, rc::Rc, sync::mpsc::Receiver, time::Duration}; +use std::{ + collections::HashMap, + rc::Rc, + sync::mpsc::{self, Receiver}, + time::Duration, +}; use crate::slint_generatedAppWindow::{raw_can, AppWindow}; +use chrono::Local; use slint::{Model, SharedString, VecModel, Weak}; use socketcan::{CanFrame, EmbeddedFrame, Frame}; @@ -14,32 +20,57 @@ pub struct DebugHandler<'a> { impl<'a> DebugHandler<'a> { pub fn run(&mut self) { - if let Ok(frame) = self.can_rx.recv() { - let frame_id = frame.raw_id() & !0x80000000; - if frame_id >= self.filter.0 && frame_id <= self.filter.1 { - let bitrate = self.bitrate().unwrap(); - let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { - ui.set_bitrate(bitrate as i32); - let raw_data = ui.get_raw_data(); - let mut vec_data = Vec::default(); - for data in raw_data.iter() { - vec_data.push(data); - } - if vec_data.len() > MAX_LEN { - vec_data.remove(0); + let (tx, rx) = mpsc::channel(); + let mut debug_enable = true; + loop { + if let Ok(en) = rx.try_recv() { + debug_enable = en; + } + if debug_enable { + if let Ok(frame) = self.can_rx.try_recv() { + let frame_id = frame.raw_id() & !0x80000000; + if frame_id >= self.filter.0 && frame_id <= self.filter.1 { + let bitrate = self.bitrate().unwrap(); + let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { + ui.set_bitrate(bitrate as i32); + let raw_data = ui.get_raw_data(); + let mut vec_data = Vec::default(); + for data in raw_data.iter() { + vec_data.push(data); + } + if vec_data.len() > MAX_LEN { + vec_data.remove(MAX_LEN); + } + vec_data.insert( + 0, + raw_can { + time: SharedString::from(format!( + "{:?}", + Local::now().to_string().replace('"', "") + )), + data: SharedString::from(format!("{:?}", frame.data())), + id: SharedString::from(format!("0x{:08X}", frame_id)), + len: frame.len() as i32, + }, + ); + let message_vec: Rc> = + Rc::new(VecModel::from(vec_data)); + ui.set_raw_data(message_vec.into()); + }); } - vec_data.push(raw_can { - data: SharedString::from(format!("{:?}", frame.data())), - id: SharedString::from(frame_id.to_string()), - len: frame.len() as i32, - }); - vec_data.reverse(); - let message_vec: Rc> = Rc::new(VecModel::from(vec_data)); - ui.set_raw_data(message_vec.into()); - }); + } else { + std::thread::sleep(Duration::from_millis(1)); + } + } else { + std::thread::sleep(Duration::from_millis(50)); } - } else { - std::thread::sleep(Duration::from_millis(1)); + let tx_clone = tx.clone(); + let _ = self.ui_handle.upgrade_in_event_loop(move |ui| { + let enable = ui.get_is_debug_en(); + if enable != debug_enable { + let _ = tx_clone.send(enable); + } + }); } } diff --git a/src/main.rs b/src/main.rs index fe7035a..7abacc1 100644 --- a/src/main.rs +++ b/src/main.rs @@ -101,6 +101,7 @@ async fn main() -> io::Result<()> { ui_handle: &ui_handle, mspc_rx: &rx, bitrate: bitrate.to_string(), + dbc: None, can_tx, }; loop { diff --git a/ui/app.slint b/ui/app.slint index e16b0ef..91af0e2 100644 --- a/ui/app.slint +++ b/ui/app.slint @@ -12,8 +12,8 @@ import { raw_can, debugPage } from "debug_page.slint"; export component AppWindow inherits Window { in property is_filter: false; in property is_new_dbc: false; - in property is_first_open: true; in property is_init: false; + out property is_debug_en: false; in property init_string: "Please select CAN device to start"; in property can_sockets; in property <[CanData]> messages; @@ -114,10 +114,14 @@ export component AppWindow inherits Window { } if root.active-page == 2: debugPage { + en: is_debug_en; state: state; bus_load: bus_load; bitrate: bitrate; raw_data: raw_data; + change_state(en) => { + is_debug_en = en; + } } } } diff --git a/ui/debug_page.slint b/ui/debug_page.slint index 18f2df1..76ebf3d 100644 --- a/ui/debug_page.slint +++ b/ui/debug_page.slint @@ -2,11 +2,43 @@ import { ListView, Button } from "std-widgets.slint"; export struct raw_can { + time: string, id: string, len: int, data: string } + + +export component StartPauseButton inherits Rectangle { + callback clicked(); + in-out property en: false; + border-radius: 5px; + width: 30px; + height: 30px; + states [ + pressed when touch-area.pressed : { + } + + hover when touch-area.has-hover: { + background: #ffffff.darker(0.4); + } + ] + image := Image { + source: root.en ? @image-url("images/play.png") : @image-url("images/pause.png"); + width: parent.width * 80%; + height: parent.height * 80%; + colorize: white; + } + touch-area := TouchArea { + pointer-event(event) => { + if (event.button == PointerEventButton.left && event.kind == PointerEventKind.down) { + clicked(); + } + } + } +} export component debugPage inherits Rectangle { + in-out property en; in property state; in property bitrate; in property bus_load; @@ -15,8 +47,17 @@ export component debugPage inherits Rectangle { {id: "181FF1FA", len: 4, data: "00 01 02 03 04 05 06 07"}, {id: "181FF1FA", len: 4, data: "00 01 02 03 04 05 06 07"}, {id: "181FF1FA", len: 4, data: "00 01 02 03 04 05 06 07"}]; + callback change_state(bool); + VerticalLayout { HorizontalLayout { + play_button:= StartPauseButton { + en: en; + clicked() => { + en = !en; + change_state(en); + } + } Rectangle {} Rectangle { max-height: 30px; @@ -63,21 +104,28 @@ export component debugPage inherits Rectangle { for raw in raw_data: Rectangle { HorizontalLayout { Rectangle { - width: parent.width * 30%; + width: parent.width * 40%; + Text { + text: raw.time; + color: white; + } + } + Rectangle { + width: parent.width * 10%; Text { text: raw.id; color: white; } } Rectangle { - width: parent.width * 20%; + width: parent.width * 10%; Text { text: raw.len; color: white; } } Rectangle { - width: parent.width * 50%; + width: parent.width * 40%; Text { text: raw.data; color: white; @@ -135,4 +183,4 @@ export component debugPage inherits Rectangle { } } } -} \ No newline at end of file +} diff --git a/ui/images/pause.png b/ui/images/pause.png new file mode 100644 index 0000000000000000000000000000000000000000..8f2eeeb1fda3891060ad22a4f466c4147f20f8ff GIT binary patch literal 6734 zcmeHLi93|v+s9-K*$u|NXN+Bz?E5yv7(><&gDe?Y6S6OnEn`V#9fs`LNp>|#c4dod zGDN9Jrbh4d{=UE8d%b_d`##t8T<3Gn^|{YE_x(KQbI!Rl5jN&5O#DnVG&C%h7A8m< z8agVZqX7Y^#b~YMGim_{GqOa1K%mtv+f8bf5o6&TMneP6{QJ_jNsUfY8~MUbox<%y ze8Z!>uA^z9qM{V8UJDHK_QIeQLazIjeb(Wp_Iil0Mw$Nk^M}g+|L6ZB@P8))?^S~|cPdLRRck?AZm3o9Et2PYSpn}-*|$1fl#BrGB-CN3cgImg!_(`E zw-4Ia&;M#bU{LV25X|+k@QBE$=o>LNW8>m)B_t*#r=+H(XJp>a%D$76o0ng3w-8%Y zTvB?k?EZs? zdhv4l)y(YN{KDeW^6NJ%t842UZ#TEzz5hV`xJ}yG{j|6L`OCrAZ{H7(ejNWiA^-YK zIo%A{U8SMn9=9|xL_zrWhvPgQh;a-DU!L@CNpYMdj;FQOD#|&@T36)tva**T+q}2r zJC_!D0~%VnrCG&==BJw?Z%0hY*;OV4ifHVf>)zl3LqSv2)bZR{DS^r8% zz9~!S4q0vgnpriH`uLm=Thh0zj{5?iqocp)#HqcsHVAt;*uE{&8|G`nUx=vw#^hME zhY#>Zt~??M62R`F|6&^5Ka4|CU*RrTyEp;%)Aa&ZSi#uWSw+b#@^3$u|I%Zbf=g_FPSi z9apbu6$XljJq)ZN@|Tx!C0;jN*^4*=SH`dJsY3mT+~pOb(zm}?2%Zn$p}igExr2a8 zTdyt{<-4d~Ooq>85YuH|ZxJOb$nvVI9mpQX^V(+?Mfi>#=6GRWrV`)_4!H~S8oMIB zOfZg1`dqjclk#4E*yZzG5pz*m-7gbv8?U_6J*?Nwny(-ionoU%-d|#+gc_o*dNkL* za=4|*(%`q{nyW&Sv%#O%hw>j51CCYFi{Z0gtiGaoqtDjRUv{kI`D2YaV!9K}T+tp_ z*~hGyqnvkTe6&)0il5Kv&XhQ)r;Js3hIKtfRG=T5*c~C4v`d(Dsees=jRn=DcA}oz z^|KG(rEo5O87REt$;hp47cq~57$sz{T1MnQc+U>rF|}*3GRd0b0NKB-+MRW^-@3&Y zsEv=AZBqy6Y3zU;L~^W?%2P#xs(0;Ilf8Z}m0YS<__U~eP@j4S73%K>(&ukagxLtB z4Mm+EKpfU`C}O{*_CM{T82$gm@W=X?@Hr$A%!V}d!tTSoCiMa3J+kmcw%Pto2EdZt z(R?2*d&mm`6y?VR1f=o(63X)DqIE{(t!$V~+b^-@Og3`>L% z+;#UrJ-G}pA@#~z>zrLlEdXE!Ua+QD*7>xR$wvY;LYK1($>bF_1=2$0lK?nxpr zaVKx3^5W&b(&=FIv1*^q3G3|D?Phzxn%5;~8^pp4pP1nkj(9!>qB5Ns-zK_*-p z8}ZqMV6A72esKP7zWC@{T>DJ_Xg=qb5Xy0d!wjZlg04@`qqASFms`~1ZtkxtAh=*J z4F!Lym+KN%a5X@8(qk4&%wUhyauTyf^efxfY)3G!m|&lp5(3Y4+BvrbbGj9Troa;< z_%M?$Fpi}WZhLLK38{pMx8|v-)R^lI<}wEsVv1Sofnp)Ep%^EBX{@NWF~(603r)qK zBDt{8i*GPPD-c!f;<`P2junKtdD(vSyB#ZOcYS#klb~IiS%-3#@r>CZzzyeO(lrnc^JJpkc&DT!uZq z0J)&ZSglyDZ5CmJ27N8PD5mxBR7t^mMq>pZAE^G$8#=1kVlLQxBY9UYxhLBn2jX_l zu=*gPRo~ZgQA_R|%@|dMAGm=sLGmFu@w%;V z>UIgY&Po{GjW-*=*wa`k-{E1Z#Thwn)=*U0b062?F)uwM;-T|$$o2M=ropc`4$AlE zy3Bomo@||!FCMcVJ8yS=hKQ9R;~kf+c*r3(Ldi{#IJmaqr?|7vdjhlp`zHKkm9iym zT|rAmii1FMkUAM@^hr0{=c48WjjW-F_)&-Zwt0Ss*sJqByAq`JZT~0NFKqod=5s1H z{-Qn;6E({5^l7nSC+!n{OhH#xJZKtnU?QEo^MU5@ghLP<)B1omJcd1{$o3a;$nM4>b4Bc7kVMn5o?s;Lm`_Jwwi+SS z6eXC`{~iFm=nm2?wf?oA{C#5Id&b(#R=A21&&nz(C~n^9a2nIpjX1YJo=(^VKB=oWfz)|cvJ6?4p6O- zl6A4_4DecCf2pA|Bd;tKHZWO})hqUuIS*XYnn z7Wp?_NMT$v?qXDowf$3g>X`9a-5!`+$akoC*!hywd`|^ZgNT=iYvf<|u>&Qe$WI;| zw#nV^PB4PhpjPfd;sR->U9H5JkwN5FJC_1OFVm+gS#+}fVTfy>d&+gKmK|6h^@Wd& z41;WEpU@B^bEjgef?E4BQ^DkYUy-cw9F4U3|K} zhRU(LS(B`1pZS)sN3Cu2Zt)%#Pg~!|nkzHwsQ#8BTM19Q7*TPj99-Wy>G8uV2yla( zJbIX4hjG*#S2}Q-czl?9{r;i>mD5cEQ>tj6D&M{iiT>iveVAoODlpe%mSexsk$kGe z?al;EPvrKch{Sm*_cc}t0wW<`jmg8wq;JebrPgteF67{s3M)P;o*1hJw}9OnsC-=& z4Mbn{&(4lWf|uuAMOC{le~cr-dZQw!n)XGFC@|9UkFT`UFS!J5nT*8XYmGQh zk241#DrXvh3A$`d^RP}7_=8bCqz?6p4q7b=RPQ4)r;^1I;=p4=@bmYe^Pgw}pc#n| z&OJ5H&!P3N6$KXck+%y;V5N)#$ZOcPUm-Lp2!6%0$u`~~v3cYS6}F(kr1=t4OXfXp zSe?#%TV4UgY45HG;{FM=MiQv*Y8E<}Bu?c$Mrh)kGo_)KiHpsZs=3}0QYU(Qj)q?9 zSI$Z)x4Md@zDWu$92>eeT2+V^=(6VoOi{GaZ*s0vO?zFTR+=!XB~e@&3a9^pD27(wkTvRVc%Wb>Dux(a=6*@55$d~&cW z;2DEHKlXZZNyd^{ncDk*gO^l#j6-1!pR`P7;{Hf*;RnfG>VW%Z_Jvx4b&|lY{)bh9 z=KmgM%#do=&kZJu{%0=Hcc^|VTu-VRNc+!}|Hq4_e|sj|MV&-HH?l3R$1$lgB~vHd)c&^C-~mTPX%fU~_xLT&M4EO1!4A-Nk4h4Epo_jsZN9o9Lpan* z5CV@4nT5GR7uaZnsNeaYzk(*Zgi7&SkS%-!8v097*H}Wfy3(#07&7k_oU%PA+{>kj zEu?aYTp@@qIhw1%g`-2~Jp8MI{EU8fdQ&UHrJrQhIl|u52fwlfF-`RuJe|a%4pw5f3kg|6Ly^{jB6sa5$f(CiB2?+cJq) z;;X=htdf}78ZLiJxU(++TpC_E{ zp=pWfdcVsLi|a4QRwpAN(S8%Jb`*v9em0fcF#%d?D8USIwVc~QR5cJjAt{ij*)Y?~ zGO39gA2sG~odUN%*fRkXnd9&Mm5kT|s{Q6X!;Y1As0}HO$ty1=RsImiHuCeqGBm}o zf#nx1Re!d1ZK%Sumzz7^vh;4?1Uw>&8}Z1U?dI7W$VE`KjNjfyWXedxp>S9<^`qL% zWW((GIf7@NO7dyxU>D!|x-lO!l4#FB3P~kh)A=iw{bf6U#qwV=%0>~Od`3Zdqf^Bp zH6uq~W{%;Zm5Wpvj@xqBKYY5_Gb2aU;l5dYU-lOxsVb{7iHWP%ZsZ!WPmP&I+S37| zSYiu?TLu0l@ciAmw08d%e(D}GF6kzRvZ7^P3nLywAe~}vR{(!^iv<{P66hAdLB*D& zVRhg$akl{f>>uYbljf&chMWSoh4-o`T|?aE+QSBKQXe+{Yh#KYxI5g=q+4q7-+R`n zVzRGz3}*xGXe0Ig;Gte}t$K=ZQ{B-<`e<5^??I#atBnY7n|zW_ zNj5s}R!+DFtl};6e{w4&%>Ukjh#Ck?uW)0GqQ#@pY@#` z(qdT0#yVz9dczZ;o4)a~@`$jN;#vlA3Y+P-&<$Qbv6GY!10+AvOVVqsJ`y%-KPQY% zXfpQjosX$Ym(cVGMCZLU^O&=liFs?JV)i>PF;bi*4pw{Q(mr$R@oDa`WA@-h9Oa?; zEmPrr&!LOsXu%4~V8ZE%6Mgb$8U;U@{U{g%rHupzgvcoP`JdhImQ6}Q>sT$SfQvix z>4I3r47phCLnRcxJTBby$Sc9w!M;7t7!`vb99s+>6yxLf+24ItyL}L@^-k?QFUox| zEt)wOnX)co=r$+(xnh@7D5objSLOG+n6a(hb(CKm+t=Piqi`LHmz&wd+ioOfi6vv6 z18(GKk$X@MUnAx~xvBNEjz-ab1IuA{6!c%|?1llV)-y_;yjusA-QZ9=zXck)6@ z@`{@@wk15r*rXhx4&8k&@g~rYDwC8)agL zt8TEbAH^A4>=}=>3PL5Y7!7e59I6T{u2~*X$Jt1+x0t(&O47~rVTsFyHy)>nU-iIo zetKkadBqiF^aKlJcu~U{a8B?m0G1z5;}@#~mqHxA$2_Y>(djbPxqY>Qg~s2Y7yRyr zdiR@8up<>-B9B2XS;FjWe249<@Fvc5eyN1|Gcs($UcG+!u?u(4PBv9>dUN0B)ijVA zO9Z(o;gaDgIWm}WE|j${#?ByyvQToO^BC)Q|Ou`%`dw20zq z5KaU4Hm4G_#kGIJ%OQ*JfXilW(06|DS@aT7 zOHQn85SSGt+6zOE<%R)Axg;Lp)7)rZl~?leS3B))@rb%46_xSO5(mAfY(9X1z3GpvctK#D>N$q za|IQ*x3R}9?8vkd4tW~D0i~={FRtR?rAwNvHve2>XM%-LGP9uqjp6@Xbde`bsOG?z z>75@t5WS6PRylQwD)y0$ZTe!ZDxw$f#U!UOm_tyDc<=ROjUQIL@WCkIzBj7*LJ@$Y z{fB+8=PjNT2og^0nG<7|KpGKS3qX?2Z!C!w(^&ReQ8=;0pLVQt$$*%bc9k94eVf4H z(93?+haL-nnHk}S(_mM9l|Hy^*kB8P5ZQyxgd8|`eGPi{mht5M&Y%p9ijs!%+YCc& z*Fq|H(~DNOxRxF4r5edvKPOs?>p2gyn}(iLv~$J zx-o$;IeX)B^7qIs5(6tjJ!GjrJ7PGs9r zn=A*)yIbX;E5|f%Jv<{Zl63Bjn~$>u;8CdanSkif>_o7vFzX_5MFog>(}~z#8%U>p zyzPts{87j=VXvGaB7df;n~y!6C#6Dx`xGe!*d#T1;UAiQjw#l=L?No;H&}YQ)AW8! zUz)2V^Yk8FBriN1zlsPgIjOcG+iZ97A-?u~d;^V>BEMKfWcKU|upHhYn>Q!HQ4?|4 z!&JgAl?t-N|75j7B#K08J=2SG6X0_l7v;hA=y_GBG_!k5!aG4HEigg(#<(=}S>`(( zs2}d-e?rA-(+(XZqa@tn591SRB3+OgK#3FuqIjh!kbt2CkYMOViu5K3C`Chn2!v`V(z~HZ7rca~ z@TejJ0g?GhCE#2TmS%o2W5oB002N% z2?TI}Sm0M@Ysmsoh#tz4gM(vX1-;CooIyr*Apih)_umAbZ;b!OYKn#G+lE>MdWMF% z273U)!opO1?)!zfxdwTt1O|H*Y-@?LwhEb>TIw^IOqTxt@xK=^Tie32xI-w$%mM%e zoo3_U^sJtoh7&3XRI;TA~`lD)zcl}Z?3JIeCYhRw~8#eH+jo# zWWnZQvcKE&hSr=Rp~LI_Ib$PJ0^fEk+E%Fv7(?`wFxv3tdDOr@Y+E}+R%=_CK2oVu zs8O4VpBfi*OAWOsCaeCP$%e5NXqR5mvBzVRVEf*K{0{svcZKP6gh< z9Tmt$#I`F^3$AoI$d6a=@Fur@zIh;nKGfj%N!N&+gpT_Zr*K`s^*6l}QF22*O@w`v zS@L#&QX;gLw0e0!)A<_>e3({tB-gDTkO9bDcf$QfYZGO(Ob=z?k*22}NbcD){o|gN ztI`6xWc|`zZ0-1DojVj_x87yP#U>OPMW3=5>zG*!@bhI87J#tR{{xbB@xw zI{#cd$C>v0K?r?8o^v)VKxc|O<5XXTcJOLEbVZJ8Od%sC&4bSSOTsdcLJ+<@vuxC* z_nBiGB@OBBQ>+79E8fNC0qJ?_J16f+vA(j>rLq|tX}VeY0IX&LK{q|=QM1LMcP zNF0Y_<=-?h@s5Uv(k2j49dED!9!BDa1+4M(kx4@2t%ihVlIWZoO4_g-}qiFlv1GrDDA*|$ts8Rxy1jl(>Athqz>B0!O>OdabDL zfckl+rLXwUnQn1;CorX?Xs)$QtZC0syZ@Rxs2JCnYs9Y(`mx_!b}b5A$Y8zH4LE3{ zo33~l8U?15kBo~fMmxioWeq`6IGrehhByL#Q>`joocQOT1KDD*BqRLzEE-fC1??wP z0e|B%{1a<|CQNNcVJ&cNNm}Ys$o0}=25<6JsxKfF2*^GZZ{h~be)sE911u*s0&M`% zg3cgg0LAN{>pdHwE4oj3N;yR`$}wWZ(@kS@(GLMtg*+N;K|^!VH)Y&|)q1#f<;TFp zG_ghShqkl)&xfLgtivUmVy`cP>)K}&5WJw~*_D7EUfu|aAi|kNu;X$*894K9S54;G z<3;f2PGNzpfoQ>+E}m*?5gE7>1Ke~5((y7!K17Bdfq}_ocoGZaW>4k;wCUbpUxePOvlpbv(9azj_t!Ilb@F7@6ttJ>^6Zd z-A^F)cRU#STP0u@GY`;r7f{@gZeLo|kII@qx4&cqjcR=n^TkoWb*f)7%VsKpxtNd9 zinHHs^%+C-Jz4uZXxzp5cwny8eGB)|a^>b4F=7B3-b?mf@8%)<0Km?clT1}YD@x%n zZd=Y!gc$@BT=c^b?Ig@NEKcK&KBLB)ms0IXuw>d5yG#pQfFSQxWslybt2{%t zL`uxyDM@$x4sxj)k6~VxKU4|b>q^Cp#t&#n%F9h8RrBM|AW3$@OUPxeeCSQtTX>VF z6|<0?gcT{V6*r_cxIq;bUikB&l*)@UkjrDVprYc!rpB1(v=|ZS>CyZ*&}9L=-PChjv8Y7xgd=RwQ6rsxXfFK?IWP|=yhWC}|;@;2A z2yB=rRbb(_x1G!VK^ZxaMBw%aZb3A<&3o9yW~lGG2L`{~Z+&;9RLI(m82gX%TIcS> z?zG10`9h75P)v2n#a?2{$$)l3$DR($MQ)kJv)tuNBJPGUkM4b{$r@z(T-JCF)jXCm z`-^V%GF3U#s$Wr@PAGr)#RT$G&IX|q5Hk_zaZDLe{s;~11