From a5b3d71e5963876cc88bd4692b15cb2e56437cb1 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 16 Sep 2024 15:37:40 +0300 Subject: [PATCH 1/5] feat: cli icon on the desktop --- applications/services/cli/cli_vcp.c | 35 ++++++++++++++++++ assets/icons/StatusBar/Console_active_8x8.png | Bin 0 -> 4347 bytes 2 files changed, 35 insertions(+) create mode 100644 assets/icons/StatusBar/Console_active_8x8.png diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index cdabaaa05..9eabd4d7d 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -2,6 +2,9 @@ #include #include #include +#include +#include +#include #define TAG "CliVcp" @@ -43,6 +46,10 @@ typedef struct { FuriHalUsbInterface* usb_if_prev; uint8_t data_buffer[USB_CDC_PKT_LEN]; + + // CLI icon + Gui* gui; + ViewPort* view_port; } CliVcp; static int32_t vcp_worker(void* context); @@ -64,6 +71,13 @@ static CliVcp* vcp = NULL; static const uint8_t ascii_soh = 0x01; static const uint8_t ascii_eot = 0x04; +static void cli_vcp_icon_draw_callback(Canvas* canvas, void* context) { + furi_assert(canvas); + furi_assert(context); + const Icon* icon = context; + canvas_draw_icon(canvas, 0, 0, icon); +} + static void cli_vcp_init(void) { if(vcp == NULL) { vcp = malloc(sizeof(CliVcp)); @@ -115,6 +129,18 @@ static int32_t vcp_worker(void* context) { if(vcp->connected == false) { vcp->connected = true; furi_stream_buffer_send(vcp->rx_stream, &ascii_soh, 1, FuriWaitForever); + + // GUI icon + furi_assert(!vcp->gui); + furi_assert(!vcp->view_port); + const Icon* icon = &I_Console_active_8x8; + vcp->gui = furi_record_open(RECORD_GUI); + vcp->view_port = view_port_alloc(); + view_port_set_width(vcp->view_port, icon_get_width(icon)); + // casting const away. we know that we cast it right back in the callback + view_port_draw_callback_set( + vcp->view_port, cli_vcp_icon_draw_callback, (void*)icon); + gui_add_view_port(vcp->gui, vcp->view_port, GuiLayerStatusBarLeft); } } @@ -126,6 +152,15 @@ static int32_t vcp_worker(void* context) { vcp->connected = false; furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0); furi_stream_buffer_send(vcp->rx_stream, &ascii_eot, 1, FuriWaitForever); + + // remove GUI icon + furi_assert(vcp->gui); + furi_assert(vcp->view_port); + gui_remove_view_port(vcp->gui, vcp->view_port); + view_port_free(vcp->view_port); + furi_record_close(RECORD_GUI); + vcp->gui = NULL; + vcp->view_port = NULL; } } diff --git a/assets/icons/StatusBar/Console_active_8x8.png b/assets/icons/StatusBar/Console_active_8x8.png new file mode 100644 index 0000000000000000000000000000000000000000..0423c12563b7602ed5ca6b513b2ceb9eab3751f8 GIT binary patch literal 4347 zcmeHKeQZ-z6o2jb7-Qg^AYVgYIzU8T-$!4&_HBhhJ6Hpq1FEYZfUob}*FISLO8d5U z2pS0B_)x;YL39D4l1xQ|Mo>U;D09Tfha!-OAOS-Zi9{rTM49KkcHIahW{Lc_*WBLo z&OP^c?)lwwPkUEZR!kj`Gdu^wumPTO_e|)swU*rnz7O2DZ#9M)1|h#k@_5GSBmF_q z3p1qc3|(ZSAq&d_eFL+PPkM%{+H{O7^+F7VMYFE?1?;<-(&eMUtx# zh9#R8uXC1vSz6FK_nB?u1`jT-=fviU+8Ot^J-yuI%icNp-GO5^zA$P1iczhtGopn3 z#s~hI=zYhr2B$fiNF-#XAfdijI&Lj~8*pZ=`vbaBgE(sKM#z2O~c*xT-B z4~kXG&px*2+qHR9D4d?P<$=~wC2c32ixz#h`pWpr@fTm2Gdjq$Jd)S8DdImc;(+D# z#=~`6?BA?vn7pW>gg@GQ<-@zpd;k7w_t_m=f-7ED=WVu6A3x{^e?)ka^_`Q?)xR-s z!{sBNY>%Efx4rS*?RAge`l7PpnWrvrhh~i0Q9rsq4}Wu2{?|M5wmnhU>Rxs2{6n|+ zHnL&bn5uqj3i>yfF8}$){`RwpkEIvpedo<-U2=HHsar#{mhLTmQ#$oi+pg1ggYihq zt0%5hU2M7Z$gevaA9NUR?pm_?x5hD-4I7JFj;=QkYbfj;y7_0_*?-pTd$fP<8KZN4 zL^-s$X#yr+99NfBP+&aY`|CN^lZmS5mG;f@wd+}-DX}+BE^J;3d7PAee%0@t&IwA` z%!^6@nUmqDzKf^x)Zy1e&u6E2HRQy%-Evp5P z^n&g>BkrqX3zmDQq%TG$mrJ7*aLEu^3vj_N@HdAB(eg^XRaCRVN6hysbK zQ5Znuus-@WdsUV?$+~TA+K3)b*M)$)4E}A_9kJ`iVCD64ZbhikoO;|&leS(?R0LV% zbdeG)HY>%81aD&l1RD@V!r_qY1mj==HX*>U5+inj@j~GaCjl$um!A$ zpe;6 zizqu`W06IqDQh6WA{|QG2X0cO$7y2BRHvr0npY(y7IvDZ%aNL7=YUTRqgg7ikwzCW zHrDPavN#w^k&Q0uG$22f+Zbyq2ec ztm`2bPA{WM%%>>TPE)2fXNJ9SoJ3yb-B8uRTc%q3I;!GacOW~MUW1WLFPe-D03y#y#2fztNS`d7VNL z_!pFb%hJeCuX1qJ>JykY)t$OJ|K-~IB1rwBIguzx2z? zEhx5bpY|!JhN)$K)un{Bf92R6ckZes1BZYS>2Xi;4L$bWS~>@#a+iH{wSDfsirGeQ xBe!~MX~>_uD8J$A(wiHu5zXJfw(8gg!yjV@=5G4E?>d-@dCDr>?@xYi>A&8Jxyb+k literal 0 HcmV?d00001 From 15b271bd922ed0e315206492e67cc1469652aa4c Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 16 Sep 2024 16:27:24 +0300 Subject: [PATCH 2/5] fix: cli autolock inhibit --- applications/services/cli/cli_vcp.c | 43 +++++++++++-------- applications/services/desktop/desktop.c | 43 ++++++++++++++++++- applications/services/desktop/desktop.h | 14 ++++++ applications/services/desktop/desktop_i.h | 1 + .../services/desktop/views/desktop_events.h | 2 + scripts/serial_cli.py | 2 +- 6 files changed, 83 insertions(+), 22 deletions(-) diff --git a/applications/services/cli/cli_vcp.c b/applications/services/cli/cli_vcp.c index 9eabd4d7d..a74710284 100644 --- a/applications/services/cli/cli_vcp.c +++ b/applications/services/cli/cli_vcp.c @@ -5,6 +5,7 @@ #include #include #include +#include #define TAG "CliVcp" @@ -50,6 +51,9 @@ typedef struct { // CLI icon Gui* gui; ViewPort* view_port; + + // Autolocking inhibition + Desktop* desktop; } CliVcp; static int32_t vcp_worker(void* context); @@ -117,6 +121,15 @@ static int32_t vcp_worker(void* context) { FURI_LOG_D(TAG, "Start"); vcp->running = true; + // GUI icon + vcp->desktop = furi_record_open(RECORD_DESKTOP); + const Icon* icon = &I_Console_active_8x8; + vcp->gui = furi_record_open(RECORD_GUI); + vcp->view_port = view_port_alloc(); + view_port_set_width(vcp->view_port, icon_get_width(icon)); + // casting const away. we know that we cast it right back in the callback + view_port_draw_callback_set(vcp->view_port, cli_vcp_icon_draw_callback, (void*)icon); + while(1) { uint32_t flags = furi_thread_flags_wait(VCP_THREAD_FLAG_ALL, FuriFlagWaitAny, FuriWaitForever); @@ -129,18 +142,8 @@ static int32_t vcp_worker(void* context) { if(vcp->connected == false) { vcp->connected = true; furi_stream_buffer_send(vcp->rx_stream, &ascii_soh, 1, FuriWaitForever); - - // GUI icon - furi_assert(!vcp->gui); - furi_assert(!vcp->view_port); - const Icon* icon = &I_Console_active_8x8; - vcp->gui = furi_record_open(RECORD_GUI); - vcp->view_port = view_port_alloc(); - view_port_set_width(vcp->view_port, icon_get_width(icon)); - // casting const away. we know that we cast it right back in the callback - view_port_draw_callback_set( - vcp->view_port, cli_vcp_icon_draw_callback, (void*)icon); gui_add_view_port(vcp->gui, vcp->view_port, GuiLayerStatusBarLeft); + desktop_api_add_external_inhibitor(vcp->desktop); } } @@ -152,15 +155,8 @@ static int32_t vcp_worker(void* context) { vcp->connected = false; furi_stream_buffer_receive(vcp->tx_stream, vcp->data_buffer, USB_CDC_PKT_LEN, 0); furi_stream_buffer_send(vcp->rx_stream, &ascii_eot, 1, FuriWaitForever); - - // remove GUI icon - furi_assert(vcp->gui); - furi_assert(vcp->view_port); gui_remove_view_port(vcp->gui, vcp->view_port); - view_port_free(vcp->view_port); - furi_record_close(RECORD_GUI); - vcp->gui = NULL; - vcp->view_port = NULL; + desktop_api_remove_external_inhibitor(vcp->desktop); } } @@ -225,6 +221,10 @@ static int32_t vcp_worker(void* context) { } if(flags & VcpEvtStop) { + if(vcp->connected) { + gui_remove_view_port(vcp->gui, vcp->view_port); + desktop_api_remove_external_inhibitor(vcp->desktop); + } vcp->connected = false; vcp->running = false; furi_hal_cdc_set_callbacks(VCP_IF_NUM, NULL, NULL); @@ -238,6 +238,11 @@ static int32_t vcp_worker(void* context) { break; } } + + view_port_free(vcp->view_port); + furi_record_close(RECORD_DESKTOP); + furi_record_close(RECORD_GUI); + FURI_LOG_D(TAG, "End"); return 0; } diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index e57e1eb00..93d00c78d 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -19,6 +19,8 @@ static void desktop_auto_lock_arm(Desktop*); static void desktop_auto_lock_inhibit(Desktop*); static void desktop_start_auto_lock_timer(Desktop*); static void desktop_apply_settings(Desktop*); +static void desktop_auto_lock_add_inhibitor(Desktop* desktop); +static void desktop_auto_lock_remove_inhibitor(Desktop* desktop); static void desktop_loader_callback(const void* message, void* context) { furi_assert(context); @@ -130,16 +132,22 @@ static bool desktop_custom_event_callback(void* context, uint32_t event) { animation_manager_unload_and_stall_animation(desktop->animation_manager); } - desktop_auto_lock_inhibit(desktop); + desktop_auto_lock_add_inhibitor(desktop); desktop->app_running = true; furi_semaphore_release(desktop->animation_semaphore); } else if(event == DesktopGlobalAfterAppFinished) { animation_manager_load_and_continue_animation(desktop->animation_manager); - desktop_auto_lock_arm(desktop); + desktop_auto_lock_remove_inhibitor(desktop); desktop->app_running = false; + } else if(event == DesktopGlobalAddExternalInhibitor) { + desktop_auto_lock_add_inhibitor(desktop); + + } else if(event == DesktopGlobalRemoveExternalInhibitor) { + desktop_auto_lock_remove_inhibitor(desktop); + } else if(event == DesktopGlobalAutoLock) { if(!desktop->app_running && !desktop->locked) { desktop_lock(desktop); @@ -205,6 +213,24 @@ static void desktop_auto_lock_arm(Desktop* desktop) { } } +static void desktop_auto_lock_add_inhibitor(Desktop* desktop) { + furi_check(furi_semaphore_release(desktop->auto_lock_inhibitors) == FuriStatusOk); + FURI_LOG_D( + TAG, + "%lu autolock inhibitors (+1)", + furi_semaphore_get_count(desktop->auto_lock_inhibitors)); + desktop_auto_lock_inhibit(desktop); +} + +static void desktop_auto_lock_remove_inhibitor(Desktop* desktop) { + furi_check(furi_semaphore_acquire(desktop->auto_lock_inhibitors, 0) == FuriStatusOk); + uint32_t inhibitors = furi_semaphore_get_count(desktop->auto_lock_inhibitors); + FURI_LOG_D(TAG, "%lu autolock inhibitors (-1)", inhibitors); + if(inhibitors == 0) { + desktop_auto_lock_arm(desktop); + } +} + static void desktop_auto_lock_inhibit(Desktop* desktop) { desktop_stop_auto_lock_timer(desktop); if(desktop->input_events_subscription) { @@ -371,6 +397,7 @@ static Desktop* desktop_alloc(void) { desktop->notification = furi_record_open(RECORD_NOTIFICATION); desktop->input_events_pubsub = furi_record_open(RECORD_INPUT_EVENTS); + desktop->auto_lock_inhibitors = furi_semaphore_alloc(UINT32_MAX, 0); desktop->auto_lock_timer = furi_timer_alloc(desktop_auto_lock_timer_callback, FuriTimerTypeOnce, desktop); @@ -503,6 +530,18 @@ void desktop_api_set_settings(Desktop* instance, const DesktopSettings* settings view_dispatcher_send_custom_event(instance->view_dispatcher, DesktopGlobalSaveSettings); } +void desktop_api_add_external_inhibitor(Desktop* instance) { + furi_assert(instance); + view_dispatcher_send_custom_event( + instance->view_dispatcher, DesktopGlobalAddExternalInhibitor); +} + +void desktop_api_remove_external_inhibitor(Desktop* instance) { + furi_assert(instance); + view_dispatcher_send_custom_event( + instance->view_dispatcher, DesktopGlobalRemoveExternalInhibitor); +} + /* * Application thread */ diff --git a/applications/services/desktop/desktop.h b/applications/services/desktop/desktop.h index e83bc3ee4..4f1556f7c 100644 --- a/applications/services/desktop/desktop.h +++ b/applications/services/desktop/desktop.h @@ -21,3 +21,17 @@ FuriPubSub* desktop_api_get_status_pubsub(Desktop* instance); void desktop_api_get_settings(Desktop* instance, DesktopSettings* settings); void desktop_api_set_settings(Desktop* instance, const DesktopSettings* settings); + +/** + * @brief Adds 1 to the count of active external autolock inhibitors + * + * Autolocking will not get triggered while there's at least 1 inhibitor + */ +void desktop_api_add_external_inhibitor(Desktop* instance); + +/** + * @brief Removes 1 from the count of active external autolock inhibitors + * + * Autolocking will not get triggered while there's at least 1 inhibitor + */ +void desktop_api_remove_external_inhibitor(Desktop* instance); diff --git a/applications/services/desktop/desktop_i.h b/applications/services/desktop/desktop_i.h index 1dc7c7d21..b62dac63c 100644 --- a/applications/services/desktop/desktop_i.h +++ b/applications/services/desktop/desktop_i.h @@ -73,6 +73,7 @@ struct Desktop { FuriPubSub* input_events_pubsub; FuriPubSubSubscription* input_events_subscription; + FuriSemaphore* auto_lock_inhibitors; FuriTimer* auto_lock_timer; FuriTimer* update_clock_timer; diff --git a/applications/services/desktop/views/desktop_events.h b/applications/services/desktop/views/desktop_events.h index 07631dfac..94348184d 100644 --- a/applications/services/desktop/views/desktop_events.h +++ b/applications/services/desktop/views/desktop_events.h @@ -57,4 +57,6 @@ typedef enum { DesktopGlobalApiUnlock, DesktopGlobalSaveSettings, DesktopGlobalReloadSettings, + DesktopGlobalAddExternalInhibitor, + DesktopGlobalRemoveExternalInhibitor, } DesktopEvent; diff --git a/scripts/serial_cli.py b/scripts/serial_cli.py index 8e35d57fa..095b5dd35 100644 --- a/scripts/serial_cli.py +++ b/scripts/serial_cli.py @@ -13,7 +13,7 @@ def main(): parser.add_argument("-p", "--port", help="CDC Port", default="auto") args = parser.parse_args() if not (port := resolve_port(logger, args.port)): - logger.error("Is Flipper connected via USB and not in DFU mode?") + logger.error("Is Flipper connected via USB, currently unlocked and not in DFU mode?") return 1 subprocess.call( [ From 1a9aca2d8c6e1c8625ea5200423844973621a696 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 16 Sep 2024 17:11:31 +0300 Subject: [PATCH 3/5] fix: always autolock if pin set --- applications/services/desktop/desktop.c | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/applications/services/desktop/desktop.c b/applications/services/desktop/desktop.c index 93d00c78d..ceea867ba 100644 --- a/applications/services/desktop/desktop.c +++ b/applications/services/desktop/desktop.c @@ -562,7 +562,7 @@ int32_t desktop_srv(void* p) { scene_manager_next_scene(desktop->scene_manager, DesktopSceneMain); - if(furi_hal_rtc_is_flag_set(FuriHalRtcFlagLock)) { + if(desktop_pin_code_is_set()) { desktop_lock(desktop); } From 4a8202514d7b6b6695667c083cd32b611d40f7c5 Mon Sep 17 00:00:00 2001 From: Anna Antonenko Date: Mon, 16 Sep 2024 19:29:55 +0300 Subject: [PATCH 4/5] style: fix linter errors --- assets/icons/StatusBar/Console_active_8x8.png | Bin 4347 -> 4559 bytes scripts/serial_cli.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/assets/icons/StatusBar/Console_active_8x8.png b/assets/icons/StatusBar/Console_active_8x8.png index 0423c12563b7602ed5ca6b513b2ceb9eab3751f8..794f4e59726186ddc3c0998ba81fe8814c86ad72 100644 GIT binary patch delta 703 zcmeyZcwTve1Scai0|UdqgE}WBD&{dVPn_V)`$v<3AsQ$MrYAm-Ww*34va~X`oXo;# zSsz^zBE`VK*qZ6=9N_8ftWZ#tpO%@E%D_-Dr*@*P$6*JFw)@+>LbbTW1WQi@%zd*+ zsc$Qn>k5Ia)-YKgy}!+Byq;a%iyuT4d`RE;>cOKmtD9FVaQthqu(r_L<}s6DjY_wM22 z-?3BDE3f~xteQIKB*T0MndDuGg82gHbE2%C*FG}P_Tu_z`LV@OT&3>Gt~Z~gpEn(@ znl6k_iIMw`yX6FQ%Jq|Zt%e{Y>_5SvL z)3g8nGSuxZTD>ky{4N6nU-agyOkGSWCTSKaNtVV2x|U|C2D&C@h8DU>$p#j>rYUL3 ziOI=^=H_XMlbc!lxlIjB3@r@I%q$GeCd;z*!J~N-n>viW`2m|C<77Ve7?tFdG?UcC zRAXHO!xUp(6AKF?-9$@+L|vn#R3l3>OCv)QW24D^?0yiPCI*`yv*&Rr^L>-v2TVz9 z0X`wFK>Gjx|4VPq2fCGkG0EHAg`tC0)?;$IfFiqqtj1o`MDxl00vhTXJIEZ3k6x-JDr((fm9oOGEKu-x zRI5@5$5yA+d}`C{94EJ_96cIXplk7CV?=2|;kBy=Lj_pu9p=c+5s6)LYEP6RcVqdU1Cn8gttDZ*WPupU%<^k!7zBf@KF_;DRW#?$^`Q-ZM_OK5${O zp5!ED){I}DDaIzd*A3T;fv#G<_n;)_XGH$-gk-#A)yXT7)Fio%} zdAqv+X(0INyt{sKgrFijkBqhH8t*-m8w536^gUf1LnJP*?Kk9NFyLv~^5uX0KEGHd x-)UCe3mh^ccxr?ueQEkN<(uw`E3>*TKV$r;!N)V>9s5L(fu62@F6*2UngGI64HW Date: Tue, 17 Sep 2024 13:26:28 +0300 Subject: [PATCH 5/5] style: fix linter error(?) --- assets/icons/StatusBar/Console_active_8x8.png | Bin 4559 -> 75 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/assets/icons/StatusBar/Console_active_8x8.png b/assets/icons/StatusBar/Console_active_8x8.png index 794f4e59726186ddc3c0998ba81fe8814c86ad72..f2230a374fc93be0309b026550135d2c21c155fb 100644 GIT binary patch delta 56 zcmX@F>^(u!hye)Rlr*aWDIrf6$B>FS$q5Zi>};%Rwrp$+%uEaub)uI30;%$J^>bP0 Hl+XkKg!m2J literal 4559 zcmeHLYitx%6rL^8LP@b84*_e&K}?Ix&SPh%Gh>%hb_-o(aV?9KhlbgiJKKTXon>~~ zZZQ%BK`Wq$ijp804WeKmAcBH|icyR)K7ObK6a=EE2)=?31Uxgl-L}ERG|Iou=Jwt> z=iKj}^WAgL_HJ8v%EXf5;l&t+l?3Yo_0adRo2wW6z4+#sgBa!-gc^ccFgV)L^aH_F zm@8-J>NGBZIrgm8(E7Uyuz}D&32h+AZF{>7clCDq%`hI38}EQ|dCtb3&%oRg==VTl zVLS+Wa0Aot&#rp}#`e?4`S?vFTS&!QbgL1c7z~B6zbY_n7UNCpy^=vsngF854~Jm`+Vu#rFWD~c3)U>Q%Uf_<Fkj9;a^UWx=gr^Bj%|Bl?cC65 zei)xAeRI!o`sPW+KZ!rxS2Og+g(vr%{&~bViw`VZKaBt6h1TQr_Hd)}>dF}(hub!_ zjeKzP?fa@v?0Bnn`Aajm?)cjGM?b%qKx@C>IpL8vh&A~5MJq;6DnBs(vF4S7uBnh7 zzWew4%l6l~*UlbAn`53X7`*yK(WNCvU-$kp_0eQ~d4Fr)=jYzNw)9Bv;oid+$L{*! zoxY`+!mT^k?5{7Otg6nWWMv+*@F&*^??aF2zc`mjh2;?D5|BU#ogj@8)l4RL{X$@mSR~FEXZ_g z(vmY|GCk6U$YKOgS~2yMr5j1y#+0K*ljS1_(Btj?B~qbK9zL1Qu>gEf897BUZkkFY zsLnIeR&6tY+YZ|PbGSzUayHBLC49f9TY_+6~qV|R?fDio3ehSFp&8VvXd z+h0jF6kU~^A+30LFDfLqikd*OA{*tED9dTA+6gL{Oj~kNK{gbCyLG_f zG(nAu9-0(+M3WrP2xL^D1=6c(imWJ1Wu+!}g1F7pAuHv0XRmB16`)v6)-*4}le9pK zB*$(B5d2Lk^U^4^mAy=p$G+-609bWlJ;6gpZi4Cz~=Iiin;-Q!Uvh zjj3cIVvZ4Lo);Ltvcu>OWTqkEY)q$;IvQ1xCIF%giK8dv7@|_in4_?_MS@^}SlP}~ zKz8I1i{#W%!;Bb4+(+b#b6y>SDUEV6hM;=n z(zRW#|3fMujb?b3lSzSB?QHUTNkQWT5^*f6@nWSwv)QQf=xIZmtxG|z6{G- zM(l&zWMO!6q!#*1mo9BQv;i)jm@9hg!~pg{@3K00>6@yXk;X9hNV~Z*8>^c^SY!o5 zwMD1;mX?;4zwVVQK{U*&ZLs`C!oG!Im;J~v{oejV!DwJGP!s9%nKEZlK~Z7v!s^08 Ytk+CzTk_