From b501ad4d5103887fd1bc9c8a02bcfc6d1bbf80ce Mon Sep 17 00:00:00 2001 From: Comma Device Date: Sun, 24 Aug 2025 00:48:54 +0000 Subject: [PATCH 001/188] nice encoder debugging script --- system/loggerd/tests/vidc_debug.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100755 system/loggerd/tests/vidc_debug.sh diff --git a/system/loggerd/tests/vidc_debug.sh b/system/loggerd/tests/vidc_debug.sh new file mode 100755 index 0000000000..7471f2ab08 --- /dev/null +++ b/system/loggerd/tests/vidc_debug.sh @@ -0,0 +1,13 @@ +#!/usr/bin/env bash +set -e + +cd /sys/kernel/debug/tracing +echo "" > trace +echo 1 > tracing_on +echo 1 > /sys/kernel/debug/tracing/events/msm_vidc/enable + +echo 0xff > /sys/module/videobuf2_core/parameters/debug +echo 0x7fffffff > /sys/kernel/debug/msm_vidc/debug_level +echo 0xff > /sys/devices/platform/soc/aa00000.qcom,vidc/video4linux/video33/dev_debug + +cat /sys/kernel/debug/tracing/trace_pipe From ca1a626d7aa968b3a975383bc2019610cbfd987c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 25 Aug 2025 08:16:08 -0400 Subject: [PATCH 002/188] fix --- sunnypilot/selfdrive/selfdrived/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 5651e01084..f39fafefdb 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -1,6 +1,6 @@ import cereal.messaging as messaging from cereal import log, car, custom -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, AlertCallbackType, wrong_car_mode_alert From 1f017c411cf72e777513fef4e8c48b33f01b20b5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 25 Aug 2025 13:31:28 -0400 Subject: [PATCH 003/188] drop new state machine for now --- .../lib/speed_limit_controller/state.py | 58 ------------------- 1 file changed, 58 deletions(-) delete mode 100644 sunnypilot/selfdrive/controls/lib/speed_limit_controller/state.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/state.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/state.py deleted file mode 100644 index 8cf5829010..0000000000 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/state.py +++ /dev/null @@ -1,58 +0,0 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" -from cereal import custom -from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP - -EventNameSP = custom.OnroadEventSP.EventName -State = custom.LongitudinalPlanSP.SpeedLimitControlState - -ACTIVE_STATES = (State.active, State.adapting) -ENABLED_STATES = (State.preActive, State.tempInactive, *ACTIVE_STATES) - - -class StateMachine: - def __init__(self): - self.state = State.inactive - - def update(self, events_sp: EventsSP) -> tuple[bool, bool]: - # INACTIVE - if self.state == State.inactive: - if events_sp.has(EventNameSP.speedLimitAdapting): - self.state = State.adapting - elif events_sp.has(EventNameSP.speedLimitActive): - self.state = State.activ - - # ACTIVE - elif self.state == State.active: - if events_sp.has(EventNameSP.speedLimitDisable): - self.state = State.inactive - elif events_sp.has(EventNameSP.speedLimitUserCancel): - self.state = State.tempInactive - elif events_sp.has(EventNameSP.speedLimitAdapting): - self.state = State.adapting - - # ADAPTING - elif self.state == State.adapting: - if events_sp.has(EventNameSP.speedLimitDisable): - self.state = State.inactive - elif events_sp.has(EventNameSP.speedLimitUserCancel): - self.state = State.tempInactive - elif events_sp.has(EventNameSP.speedLimitReached): - self.state = State.active - - # TEMP INACTIVE - elif self.state == State.tempInactive: - if events_sp.has(EventNameSP.speedLimitDisable): - self.state = State.inactive - elif events_sp.has(EventNameSP.speedLimitValueChange): - # When speed limit changes, reactivate - self.state = State.inactive - - enabled = self.state in ENABLED_STATES - active = self.state in ACTIVE_STATES - - return enabled, active From 9579d331fcc86f2588cffd426d8005a9fff1f5a4 Mon Sep 17 00:00:00 2001 From: Nayan Date: Mon, 25 Aug 2025 13:49:49 -0400 Subject: [PATCH 004/188] ui: sunnylink panel title & message (#1181) add title & message to clarify sponsorship isn't required for basic functions --- .../qt/offroad/settings/sunnylink_panel.cc | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 3d4e070963..96d945ab53 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc @@ -34,6 +34,27 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { vlayout->setContentsMargins(50, 20, 50, 20); auto *list = new ListWidget(this, false); + + QVBoxLayout *titleLayout = new QVBoxLayout; + QLabel *title = new QLabel(tr("🚀 sunnylink 🚀")); + title->setStyleSheet("font-size: 90px; font-weight: 500; font-family: 'Noto Color Emoji';"); + titleLayout->addWidget(title, 0, Qt::AlignCenter); + + QLabel *sunnylinkDesc = new QLabel("
"+ + tr("For secure backup, restore, and remote configuration")+ "
"); + + QLabel *sponsorMsg = new QLabel("
"+ + tr("Sponsorship isn't required for basic backup/restore") + "
" + + tr("Click the sponsor button for more details")+ "
"); + + sunnylinkDesc->setStyleSheet("font-size: 40px; font-weight: 100; font-family: 'Noto';"); + sponsorMsg->setStyleSheet("font-size: 35px; font-weight: 100; font-family: 'Noto';"); + + titleLayout->addWidget(sunnylinkDesc, 0, Qt::AlignCenter); + titleLayout->addWidget(sponsorMsg, 0, Qt::AlignCenter); + + list->addItem(titleLayout); + QString sunnylinkEnabledBtnDesc = tr("This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that."); sunnylinkEnabledBtn = new ParamControl( "SunnylinkEnabled", From aea467ff02ca68988d0e82824e9ec18fd5c459f5 Mon Sep 17 00:00:00 2001 From: commaci-public <60409688+commaci-public@users.noreply.github.com> Date: Mon, 25 Aug 2025 11:43:49 -0700 Subject: [PATCH 005/188] [bot] Update Python packages (#36053) * Update Python packages * bump --------- Co-authored-by: Vehicle Researcher Co-authored-by: Shane Smiskol --- opendbc_repo | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- tinygrad_repo | 2 +- uv.lock | 184 ++++++++++++----------- 4 files changed, 99 insertions(+), 91 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 43006b9a41..0fe56bc289 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 43006b9a41e233325cb7cbcb6ff40de0234217a0 +Subproject commit 0fe56bc289d72d2f278e9c085f2ada6ecb13e688 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index a4297096c0..8e4ff1e2a8 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -6d3219bca9f66a229b38a5382d301a92b0147edb \ No newline at end of file +209b47bea61e145cf2d27eb3ab650c97bcd1d33f \ No newline at end of file diff --git a/tinygrad_repo b/tinygrad_repo index c30a113b2a..e146418f65 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit c30a113b2a876cabaea1049601fea3a0b758c5b1 +Subproject commit e146418f6566301ffe8ebea093c0081409241c8d diff --git a/uv.lock b/uv.lock index 7cfaceed97..7010cdfb83 100644 --- a/uv.lock +++ b/uv.lock @@ -763,40 +763,48 @@ wheels = [ [[package]] name = "lxml" -version = "6.0.0" +version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c5/ed/60eb6fa2923602fba988d9ca7c5cdbd7cf25faa795162ed538b527a35411/lxml-6.0.0.tar.gz", hash = "sha256:032e65120339d44cdc3efc326c9f660f5f7205f3a535c1fdbf898b29ea01fb72", size = 4096938, upload-time = "2025-06-26T16:28:19.373Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8f/bd/f9d01fd4132d81c6f43ab01983caea69ec9614b913c290a26738431a015d/lxml-6.0.1.tar.gz", hash = "sha256:2b3a882ebf27dd026df3801a87cf49ff791336e0f94b0fad195db77e01240690", size = 4070214, upload-time = "2025-08-22T10:37:53.525Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/23/828d4cc7da96c611ec0ce6147bbcea2fdbde023dc995a165afa512399bbf/lxml-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4ee56288d0df919e4aac43b539dd0e34bb55d6a12a6562038e8d6f3ed07f9e36", size = 8438217, upload-time = "2025-06-26T16:25:34.349Z" }, - { url = "https://files.pythonhosted.org/packages/f1/33/5ac521212c5bcb097d573145d54b2b4a3c9766cda88af5a0e91f66037c6e/lxml-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b8dd6dd0e9c1992613ccda2bcb74fc9d49159dbe0f0ca4753f37527749885c25", size = 4590317, upload-time = "2025-06-26T16:25:38.103Z" }, - { url = "https://files.pythonhosted.org/packages/2b/2e/45b7ca8bee304c07f54933c37afe7dd4d39ff61ba2757f519dcc71bc5d44/lxml-6.0.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d7ae472f74afcc47320238b5dbfd363aba111a525943c8a34a1b657c6be934c3", size = 5221628, upload-time = "2025-06-26T16:25:40.878Z" }, - { url = "https://files.pythonhosted.org/packages/32/23/526d19f7eb2b85da1f62cffb2556f647b049ebe2a5aa8d4d41b1fb2c7d36/lxml-6.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5592401cdf3dc682194727c1ddaa8aa0f3ddc57ca64fd03226a430b955eab6f6", size = 4949429, upload-time = "2025-06-28T18:47:20.046Z" }, - { url = "https://files.pythonhosted.org/packages/ac/cc/f6be27a5c656a43a5344e064d9ae004d4dcb1d3c9d4f323c8189ddfe4d13/lxml-6.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:58ffd35bd5425c3c3b9692d078bf7ab851441434531a7e517c4984d5634cd65b", size = 5087909, upload-time = "2025-06-28T18:47:22.834Z" }, - { url = "https://files.pythonhosted.org/packages/3b/e6/8ec91b5bfbe6972458bc105aeb42088e50e4b23777170404aab5dfb0c62d/lxml-6.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f720a14aa102a38907c6d5030e3d66b3b680c3e6f6bc95473931ea3c00c59967", size = 5031713, upload-time = "2025-06-26T16:25:43.226Z" }, - { url = "https://files.pythonhosted.org/packages/33/cf/05e78e613840a40e5be3e40d892c48ad3e475804db23d4bad751b8cadb9b/lxml-6.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c2a5e8d207311a0170aca0eb6b160af91adc29ec121832e4ac151a57743a1e1e", size = 5232417, upload-time = "2025-06-26T16:25:46.111Z" }, - { url = "https://files.pythonhosted.org/packages/ac/8c/6b306b3e35c59d5f0b32e3b9b6b3b0739b32c0dc42a295415ba111e76495/lxml-6.0.0-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:2dd1cc3ea7e60bfb31ff32cafe07e24839df573a5e7c2d33304082a5019bcd58", size = 4681443, upload-time = "2025-06-26T16:25:48.837Z" }, - { url = "https://files.pythonhosted.org/packages/59/43/0bd96bece5f7eea14b7220476835a60d2b27f8e9ca99c175f37c085cb154/lxml-6.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cfcf84f1defed7e5798ef4f88aa25fcc52d279be731ce904789aa7ccfb7e8d2", size = 5074542, upload-time = "2025-06-26T16:25:51.65Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3d/32103036287a8ca012d8518071f8852c68f2b3bfe048cef2a0202eb05910/lxml-6.0.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:a52a4704811e2623b0324a18d41ad4b9fabf43ce5ff99b14e40a520e2190c851", size = 4729471, upload-time = "2025-06-26T16:25:54.571Z" }, - { url = "https://files.pythonhosted.org/packages/ca/a8/7be5d17df12d637d81854bd8648cd329f29640a61e9a72a3f77add4a311b/lxml-6.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c16304bba98f48a28ae10e32a8e75c349dd742c45156f297e16eeb1ba9287a1f", size = 5256285, upload-time = "2025-06-26T16:25:56.997Z" }, - { url = "https://files.pythonhosted.org/packages/cd/d0/6cb96174c25e0d749932557c8d51d60c6e292c877b46fae616afa23ed31a/lxml-6.0.0-cp311-cp311-win32.whl", hash = "sha256:f8d19565ae3eb956d84da3ef367aa7def14a2735d05bd275cd54c0301f0d0d6c", size = 3612004, upload-time = "2025-06-26T16:25:59.11Z" }, - { url = "https://files.pythonhosted.org/packages/ca/77/6ad43b165dfc6dead001410adeb45e88597b25185f4479b7ca3b16a5808f/lxml-6.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:b2d71cdefda9424adff9a3607ba5bbfc60ee972d73c21c7e3c19e71037574816", size = 4003470, upload-time = "2025-06-26T16:26:01.655Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bc/4c50ec0eb14f932a18efc34fc86ee936a66c0eb5f2fe065744a2da8a68b2/lxml-6.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:8a2e76efbf8772add72d002d67a4c3d0958638696f541734304c7f28217a9cab", size = 3682477, upload-time = "2025-06-26T16:26:03.808Z" }, - { url = "https://files.pythonhosted.org/packages/89/c3/d01d735c298d7e0ddcedf6f028bf556577e5ab4f4da45175ecd909c79378/lxml-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78718d8454a6e928470d511bf8ac93f469283a45c354995f7d19e77292f26108", size = 8429515, upload-time = "2025-06-26T16:26:06.776Z" }, - { url = "https://files.pythonhosted.org/packages/06/37/0e3eae3043d366b73da55a86274a590bae76dc45aa004b7042e6f97803b1/lxml-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:84ef591495ffd3f9dcabffd6391db7bb70d7230b5c35ef5148354a134f56f2be", size = 4601387, upload-time = "2025-06-26T16:26:09.511Z" }, - { url = "https://files.pythonhosted.org/packages/a3/28/e1a9a881e6d6e29dda13d633885d13acb0058f65e95da67841c8dd02b4a8/lxml-6.0.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:2930aa001a3776c3e2601cb8e0a15d21b8270528d89cc308be4843ade546b9ab", size = 5228928, upload-time = "2025-06-26T16:26:12.337Z" }, - { url = "https://files.pythonhosted.org/packages/9a/55/2cb24ea48aa30c99f805921c1c7860c1f45c0e811e44ee4e6a155668de06/lxml-6.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:219e0431ea8006e15005767f0351e3f7f9143e793e58519dc97fe9e07fae5563", size = 4952289, upload-time = "2025-06-28T18:47:25.602Z" }, - { url = "https://files.pythonhosted.org/packages/31/c0/b25d9528df296b9a3306ba21ff982fc5b698c45ab78b94d18c2d6ae71fd9/lxml-6.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bd5913b4972681ffc9718bc2d4c53cde39ef81415e1671ff93e9aa30b46595e7", size = 5111310, upload-time = "2025-06-28T18:47:28.136Z" }, - { url = "https://files.pythonhosted.org/packages/e9/af/681a8b3e4f668bea6e6514cbcb297beb6de2b641e70f09d3d78655f4f44c/lxml-6.0.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:390240baeb9f415a82eefc2e13285016f9c8b5ad71ec80574ae8fa9605093cd7", size = 5025457, upload-time = "2025-06-26T16:26:15.068Z" }, - { url = "https://files.pythonhosted.org/packages/99/b6/3a7971aa05b7be7dfebc7ab57262ec527775c2c3c5b2f43675cac0458cad/lxml-6.0.0-cp312-cp312-manylinux_2_27_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:d6e200909a119626744dd81bae409fc44134389e03fbf1d68ed2a55a2fb10991", size = 5657016, upload-time = "2025-07-03T19:19:06.008Z" }, - { url = "https://files.pythonhosted.org/packages/69/f8/693b1a10a891197143c0673fcce5b75fc69132afa81a36e4568c12c8faba/lxml-6.0.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ca50bd612438258a91b5b3788c6621c1f05c8c478e7951899f492be42defc0da", size = 5257565, upload-time = "2025-06-26T16:26:17.906Z" }, - { url = "https://files.pythonhosted.org/packages/a8/96/e08ff98f2c6426c98c8964513c5dab8d6eb81dadcd0af6f0c538ada78d33/lxml-6.0.0-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:c24b8efd9c0f62bad0439283c2c795ef916c5a6b75f03c17799775c7ae3c0c9e", size = 4713390, upload-time = "2025-06-26T16:26:20.292Z" }, - { url = "https://files.pythonhosted.org/packages/a8/83/6184aba6cc94d7413959f6f8f54807dc318fdcd4985c347fe3ea6937f772/lxml-6.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:afd27d8629ae94c5d863e32ab0e1d5590371d296b87dae0a751fb22bf3685741", size = 5066103, upload-time = "2025-06-26T16:26:22.765Z" }, - { url = "https://files.pythonhosted.org/packages/ee/01/8bf1f4035852d0ff2e36a4d9aacdbcc57e93a6cd35a54e05fa984cdf73ab/lxml-6.0.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:54c4855eabd9fc29707d30141be99e5cd1102e7d2258d2892314cf4c110726c3", size = 4791428, upload-time = "2025-06-26T16:26:26.461Z" }, - { url = "https://files.pythonhosted.org/packages/29/31/c0267d03b16954a85ed6b065116b621d37f559553d9339c7dcc4943a76f1/lxml-6.0.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c907516d49f77f6cd8ead1322198bdfd902003c3c330c77a1c5f3cc32a0e4d16", size = 5678523, upload-time = "2025-07-03T19:19:09.837Z" }, - { url = "https://files.pythonhosted.org/packages/5c/f7/5495829a864bc5f8b0798d2b52a807c89966523140f3d6fa3a58ab6720ea/lxml-6.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:36531f81c8214e293097cd2b7873f178997dae33d3667caaae8bdfb9666b76c0", size = 5281290, upload-time = "2025-06-26T16:26:29.406Z" }, - { url = "https://files.pythonhosted.org/packages/79/56/6b8edb79d9ed294ccc4e881f4db1023af56ba451909b9ce79f2a2cd7c532/lxml-6.0.0-cp312-cp312-win32.whl", hash = "sha256:690b20e3388a7ec98e899fd54c924e50ba6693874aa65ef9cb53de7f7de9d64a", size = 3613495, upload-time = "2025-06-26T16:26:31.588Z" }, - { url = "https://files.pythonhosted.org/packages/0b/1e/cc32034b40ad6af80b6fd9b66301fc0f180f300002e5c3eb5a6110a93317/lxml-6.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:310b719b695b3dd442cdfbbe64936b2f2e231bb91d998e99e6f0daf991a3eba3", size = 4014711, upload-time = "2025-06-26T16:26:33.723Z" }, - { url = "https://files.pythonhosted.org/packages/55/10/dc8e5290ae4c94bdc1a4c55865be7e1f31dfd857a88b21cbba68b5fea61b/lxml-6.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:8cb26f51c82d77483cdcd2b4a53cda55bbee29b3c2f3ddeb47182a2a9064e4eb", size = 3674431, upload-time = "2025-06-26T16:26:35.959Z" }, + { url = "https://files.pythonhosted.org/packages/29/c8/262c1d19339ef644cdc9eb5aad2e85bd2d1fa2d7c71cdef3ede1a3eed84d/lxml-6.0.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c6acde83f7a3d6399e6d83c1892a06ac9b14ea48332a5fbd55d60b9897b9570a", size = 8422719, upload-time = "2025-08-22T10:32:24.848Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d4/1b0afbeb801468a310642c3a6f6704e53c38a4a6eb1ca6faea013333e02f/lxml-6.0.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0d21c9cacb6a889cbb8eeb46c77ef2c1dd529cde10443fdeb1de847b3193c541", size = 4575763, upload-time = "2025-08-22T10:32:27.057Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c1/8db9b5402bf52ceb758618313f7423cd54aea85679fcf607013707d854a8/lxml-6.0.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:847458b7cd0d04004895f1fb2cca8e7c0f8ec923c49c06b7a72ec2d48ea6aca2", size = 4943244, upload-time = "2025-08-22T10:32:28.847Z" }, + { url = "https://files.pythonhosted.org/packages/e7/78/838e115358dd2369c1c5186080dd874a50a691fb5cd80db6afe5e816e2c6/lxml-6.0.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1dc13405bf315d008fe02b1472d2a9d65ee1c73c0a06de5f5a45e6e404d9a1c0", size = 5081725, upload-time = "2025-08-22T10:32:30.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/b6/bdcb3a3ddd2438c5b1a1915161f34e8c85c96dc574b0ef3be3924f36315c/lxml-6.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f540c229a8c0a770dcaf6d5af56a5295e0fc314fc7ef4399d543328054bcea", size = 5021238, upload-time = "2025-08-22T10:32:32.49Z" }, + { url = "https://files.pythonhosted.org/packages/73/e5/1bfb96185dc1a64c7c6fbb7369192bda4461952daa2025207715f9968205/lxml-6.0.1-cp311-cp311-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:d2f73aef768c70e8deb8c4742fca4fd729b132fda68458518851c7735b55297e", size = 5343744, upload-time = "2025-08-22T10:32:34.385Z" }, + { url = "https://files.pythonhosted.org/packages/a2/ae/df3ea9ebc3c493b9c6bdc6bd8c554ac4e147f8d7839993388aab57ec606d/lxml-6.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e7f4066b85a4fa25ad31b75444bd578c3ebe6b8ed47237896341308e2ce923c3", size = 5223477, upload-time = "2025-08-22T10:32:36.256Z" }, + { url = "https://files.pythonhosted.org/packages/37/b3/65e1e33600542c08bc03a4c5c9c306c34696b0966a424a3be6ffec8038ed/lxml-6.0.1-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:0cce65db0cd8c750a378639900d56f89f7d6af11cd5eda72fde054d27c54b8ce", size = 4676626, upload-time = "2025-08-22T10:32:38.793Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/ee3ed8f3a60e9457d7aea46542d419917d81dbfd5700fe64b2a36fb5ef61/lxml-6.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c372d42f3eee5844b69dcab7b8d18b2f449efd54b46ac76970d6e06b8e8d9a66", size = 5066042, upload-time = "2025-08-22T10:32:41.134Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b9/8394538e7cdbeb3bfa36bc74924be1a4383e0bb5af75f32713c2c4aa0479/lxml-6.0.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2e2b0e042e1408bbb1c5f3cfcb0f571ff4ac98d8e73f4bf37c5dd179276beedd", size = 4724714, upload-time = "2025-08-22T10:32:43.94Z" }, + { url = "https://files.pythonhosted.org/packages/b3/21/3ef7da1ea2a73976c1a5a311d7cde5d379234eec0968ee609517714940b4/lxml-6.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cc73bb8640eadd66d25c5a03175de6801f63c535f0f3cf50cac2f06a8211f420", size = 5247376, upload-time = "2025-08-22T10:32:46.263Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/0980016f124f00c572cba6f4243e13a8e80650843c66271ee692cddf25f3/lxml-6.0.1-cp311-cp311-win32.whl", hash = "sha256:7c23fd8c839708d368e406282d7953cee5134f4592ef4900026d84566d2b4c88", size = 3609499, upload-time = "2025-08-22T10:32:48.156Z" }, + { url = "https://files.pythonhosted.org/packages/b1/08/28440437521f265eff4413eb2a65efac269c4c7db5fd8449b586e75d8de2/lxml-6.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:2516acc6947ecd3c41a4a4564242a87c6786376989307284ddb115f6a99d927f", size = 4036003, upload-time = "2025-08-22T10:32:50.662Z" }, + { url = "https://files.pythonhosted.org/packages/7b/dc/617e67296d98099213a505d781f04804e7b12923ecd15a781a4ab9181992/lxml-6.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:cb46f8cfa1b0334b074f40c0ff94ce4d9a6755d492e6c116adb5f4a57fb6ad96", size = 3679662, upload-time = "2025-08-22T10:32:52.739Z" }, + { url = "https://files.pythonhosted.org/packages/b0/a9/82b244c8198fcdf709532e39a1751943a36b3e800b420adc739d751e0299/lxml-6.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c03ac546adaabbe0b8e4a15d9ad815a281afc8d36249c246aecf1aaad7d6f200", size = 8422788, upload-time = "2025-08-22T10:32:56.612Z" }, + { url = "https://files.pythonhosted.org/packages/c9/8d/1ed2bc20281b0e7ed3e6c12b0a16e64ae2065d99be075be119ba88486e6d/lxml-6.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:33b862c7e3bbeb4ba2c96f3a039f925c640eeba9087a4dc7a572ec0f19d89392", size = 4593547, upload-time = "2025-08-22T10:32:59.016Z" }, + { url = "https://files.pythonhosted.org/packages/76/53/d7fd3af95b72a3493bf7fbe842a01e339d8f41567805cecfecd5c71aa5ee/lxml-6.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a3ec1373f7d3f519de595032d4dcafae396c29407cfd5073f42d267ba32440d", size = 4948101, upload-time = "2025-08-22T10:33:00.765Z" }, + { url = "https://files.pythonhosted.org/packages/9d/51/4e57cba4d55273c400fb63aefa2f0d08d15eac021432571a7eeefee67bed/lxml-6.0.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03b12214fb1608f4cffa181ec3d046c72f7e77c345d06222144744c122ded870", size = 5108090, upload-time = "2025-08-22T10:33:03.108Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6e/5f290bc26fcc642bc32942e903e833472271614e24d64ad28aaec09d5dae/lxml-6.0.1-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:207ae0d5f0f03b30f95e649a6fa22aa73f5825667fee9c7ec6854d30e19f2ed8", size = 5021791, upload-time = "2025-08-22T10:33:06.972Z" }, + { url = "https://files.pythonhosted.org/packages/13/d4/2e7551a86992ece4f9a0f6eebd4fb7e312d30f1e372760e2109e721d4ce6/lxml-6.0.1-cp312-cp312-manylinux_2_26_i686.manylinux_2_28_i686.whl", hash = "sha256:32297b09ed4b17f7b3f448de87a92fb31bb8747496623483788e9f27c98c0f00", size = 5358861, upload-time = "2025-08-22T10:33:08.967Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5f/cb49d727fc388bf5fd37247209bab0da11697ddc5e976ccac4826599939e/lxml-6.0.1-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7e18224ea241b657a157c85e9cac82c2b113ec90876e01e1f127312006233756", size = 5652569, upload-time = "2025-08-22T10:33:10.815Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b8/66c1ef8c87ad0f958b0a23998851e610607c74849e75e83955d5641272e6/lxml-6.0.1-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a07a994d3c46cd4020c1ea566345cf6815af205b1e948213a4f0f1d392182072", size = 5252262, upload-time = "2025-08-22T10:33:12.673Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ef/131d3d6b9590e64fdbb932fbc576b81fcc686289da19c7cb796257310e82/lxml-6.0.1-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:2287fadaa12418a813b05095485c286c47ea58155930cfbd98c590d25770e225", size = 4710309, upload-time = "2025-08-22T10:33:14.952Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3f/07f48ae422dce44902309aa7ed386c35310929dc592439c403ec16ef9137/lxml-6.0.1-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b4e597efca032ed99f418bd21314745522ab9fa95af33370dcee5533f7f70136", size = 5265786, upload-time = "2025-08-22T10:33:16.721Z" }, + { url = "https://files.pythonhosted.org/packages/11/c7/125315d7b14ab20d9155e8316f7d287a4956098f787c22d47560b74886c4/lxml-6.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9696d491f156226decdd95d9651c6786d43701e49f32bf23715c975539aa2b3b", size = 5062272, upload-time = "2025-08-22T10:33:18.478Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c3/51143c3a5fc5168a7c3ee626418468ff20d30f5a59597e7b156c1e61fba8/lxml-6.0.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e4e3cd3585f3c6f87cdea44cda68e692cc42a012f0131d25957ba4ce755241a7", size = 4786955, upload-time = "2025-08-22T10:33:20.34Z" }, + { url = "https://files.pythonhosted.org/packages/11/86/73102370a420ec4529647b31c4a8ce8c740c77af3a5fae7a7643212d6f6e/lxml-6.0.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:45cbc92f9d22c28cd3b97f8d07fcefa42e569fbd587dfdac76852b16a4924277", size = 5673557, upload-time = "2025-08-22T10:33:22.282Z" }, + { url = "https://files.pythonhosted.org/packages/d7/2d/aad90afaec51029aef26ef773b8fd74a9e8706e5e2f46a57acd11a421c02/lxml-6.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:f8c9bcfd2e12299a442fba94459adf0b0d001dbc68f1594439bfa10ad1ecb74b", size = 5254211, upload-time = "2025-08-22T10:33:24.15Z" }, + { url = "https://files.pythonhosted.org/packages/63/01/c9e42c8c2d8b41f4bdefa42ab05448852e439045f112903dd901b8fbea4d/lxml-6.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1e9dc2b9f1586e7cd77753eae81f8d76220eed9b768f337dc83a3f675f2f0cf9", size = 5275817, upload-time = "2025-08-22T10:33:26.007Z" }, + { url = "https://files.pythonhosted.org/packages/bc/1f/962ea2696759abe331c3b0e838bb17e92224f39c638c2068bf0d8345e913/lxml-6.0.1-cp312-cp312-win32.whl", hash = "sha256:987ad5c3941c64031f59c226167f55a04d1272e76b241bfafc968bdb778e07fb", size = 3610889, upload-time = "2025-08-22T10:33:28.169Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/22c86a990b51b44442b75c43ecb2f77b8daba8c4ba63696921966eac7022/lxml-6.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:abb05a45394fd76bf4a60c1b7bec0e6d4e8dfc569fc0e0b1f634cd983a006ddc", size = 4010925, upload-time = "2025-08-22T10:33:29.874Z" }, + { url = "https://files.pythonhosted.org/packages/b2/21/dc0c73325e5eb94ef9c9d60dbb5dcdcb2e7114901ea9509735614a74e75a/lxml-6.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:c4be29bce35020d8579d60aa0a4e95effd66fcfce31c46ffddf7e5422f73a299", size = 3671922, upload-time = "2025-08-22T10:33:31.535Z" }, + { url = "https://files.pythonhosted.org/packages/41/37/41961f53f83ded57b37e65e4f47d1c6c6ef5fd02cb1d6ffe028ba0efa7d4/lxml-6.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:b556aaa6ef393e989dac694b9c95761e32e058d5c4c11ddeef33f790518f7a5e", size = 3903412, upload-time = "2025-08-22T10:37:40.758Z" }, + { url = "https://files.pythonhosted.org/packages/3d/47/8631ea73f3dc776fb6517ccde4d5bd5072f35f9eacbba8c657caa4037a69/lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:64fac7a05ebb3737b79fd89fe5a5b6c5546aac35cfcfd9208eb6e5d13215771c", size = 4224810, upload-time = "2025-08-22T10:37:42.839Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b8/39ae30ca3b1516729faeef941ed84bf8f12321625f2644492ed8320cb254/lxml-6.0.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:038d3c08babcfce9dc89aaf498e6da205efad5b7106c3b11830a488d4eadf56b", size = 4329221, upload-time = "2025-08-22T10:37:45.223Z" }, + { url = "https://files.pythonhosted.org/packages/9c/ea/048dea6cdfc7a72d40ae8ed7e7d23cf4a6b6a6547b51b492a3be50af0e80/lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:445f2cee71c404ab4259bc21e20339a859f75383ba2d7fb97dfe7c163994287b", size = 4270228, upload-time = "2025-08-22T10:37:47.276Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d4/c2b46e432377c45d611ae2f669aa47971df1586c1a5240675801d0f02bac/lxml-6.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e352d8578e83822d70bea88f3d08b9912528e4c338f04ab707207ab12f4b7aac", size = 4416077, upload-time = "2025-08-22T10:37:49.822Z" }, + { url = "https://files.pythonhosted.org/packages/b6/db/8f620f1ac62cf32554821b00b768dd5957ac8e3fd051593532be5b40b438/lxml-6.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:51bd5d1a9796ca253db6045ab45ca882c09c071deafffc22e06975b7ace36300", size = 3518127, upload-time = "2025-08-22T10:37:51.66Z" }, ] [[package]] @@ -4452,38 +4460,38 @@ wheels = [ [[package]] name = "pyzmq" -version = "27.0.1" +version = "27.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/30/5f/557d2032a2f471edbcc227da724c24a1c05887b5cda1e3ae53af98b9e0a5/pyzmq-27.0.1.tar.gz", hash = "sha256:45c549204bc20e7484ffd2555f6cf02e572440ecf2f3bdd60d4404b20fddf64b", size = 281158, upload-time = "2025-08-03T05:05:40.352Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/66/159f38d184f08b5f971b467f87b1ab142ab1320d5200825c824b32b84b66/pyzmq-27.0.2.tar.gz", hash = "sha256:b398dd713b18de89730447347e96a0240225e154db56e35b6bb8447ffdb07798", size = 281440, upload-time = "2025-08-21T04:23:26.334Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ae/18/a8e0da6ababbe9326116fb1c890bf1920eea880e8da621afb6bc0f39a262/pyzmq-27.0.1-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:9729190bd770314f5fbba42476abf6abe79a746eeda11d1d68fd56dd70e5c296", size = 1332721, upload-time = "2025-08-03T05:03:15.237Z" }, - { url = "https://files.pythonhosted.org/packages/75/a4/9431ba598651d60ebd50dc25755402b770322cf8432adcc07d2906e53a54/pyzmq-27.0.1-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:696900ef6bc20bef6a242973943574f96c3f97d2183c1bd3da5eea4f559631b1", size = 908249, upload-time = "2025-08-03T05:03:16.933Z" }, - { url = "https://files.pythonhosted.org/packages/f0/7a/e624e1793689e4e685d2ee21c40277dd4024d9d730af20446d88f69be838/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f96a63aecec22d3f7fdea3c6c98df9e42973f5856bb6812c3d8d78c262fee808", size = 668649, upload-time = "2025-08-03T05:03:18.49Z" }, - { url = "https://files.pythonhosted.org/packages/6c/29/0652a39d4e876e0d61379047ecf7752685414ad2e253434348246f7a2a39/pyzmq-27.0.1-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c512824360ea7490390566ce00bee880e19b526b312b25cc0bc30a0fe95cb67f", size = 856601, upload-time = "2025-08-03T05:03:20.194Z" }, - { url = "https://files.pythonhosted.org/packages/36/2d/8d5355d7fc55bb6e9c581dd74f58b64fa78c994079e3a0ea09b1b5627cde/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dfb2bb5e0f7198eaacfb6796fb0330afd28f36d985a770745fba554a5903595a", size = 1657750, upload-time = "2025-08-03T05:03:22.055Z" }, - { url = "https://files.pythonhosted.org/packages/ab/f4/cd032352d5d252dc6f5ee272a34b59718ba3af1639a8a4ef4654f9535cf5/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:4f6886c59ba93ffde09b957d3e857e7950c8fe818bd5494d9b4287bc6d5bc7f1", size = 2034312, upload-time = "2025-08-03T05:03:23.578Z" }, - { url = "https://files.pythonhosted.org/packages/e4/1a/c050d8b6597200e97a4bd29b93c769d002fa0b03083858227e0376ad59bc/pyzmq-27.0.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b99ea9d330e86ce1ff7f2456b33f1bf81c43862a5590faf4ef4ed3a63504bdab", size = 1893632, upload-time = "2025-08-03T05:03:25.167Z" }, - { url = "https://files.pythonhosted.org/packages/6a/29/173ce21d5097e7fcf284a090e8beb64fc683c6582b1f00fa52b1b7e867ce/pyzmq-27.0.1-cp311-cp311-win32.whl", hash = "sha256:571f762aed89025ba8cdcbe355fea56889715ec06d0264fd8b6a3f3fa38154ed", size = 566587, upload-time = "2025-08-03T05:03:26.769Z" }, - { url = "https://files.pythonhosted.org/packages/53/ab/22bd33e7086f0a2cc03a5adabff4bde414288bb62a21a7820951ef86ec20/pyzmq-27.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:ee16906c8025fa464bea1e48128c048d02359fb40bebe5333103228528506530", size = 632873, upload-time = "2025-08-03T05:03:28.685Z" }, - { url = "https://files.pythonhosted.org/packages/90/14/3e59b4a28194285ceeff725eba9aa5ba8568d1cb78aed381dec1537c705a/pyzmq-27.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:ba068f28028849da725ff9185c24f832ccf9207a40f9b28ac46ab7c04994bd41", size = 558918, upload-time = "2025-08-03T05:03:30.085Z" }, - { url = "https://files.pythonhosted.org/packages/0e/9b/c0957041067c7724b310f22c398be46399297c12ed834c3bc42200a2756f/pyzmq-27.0.1-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:af7ebce2a1e7caf30c0bb64a845f63a69e76a2fadbc1cac47178f7bb6e657bdd", size = 1305432, upload-time = "2025-08-03T05:03:32.177Z" }, - { url = "https://files.pythonhosted.org/packages/8e/55/bd3a312790858f16b7def3897a0c3eb1804e974711bf7b9dcb5f47e7f82c/pyzmq-27.0.1-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:8f617f60a8b609a13099b313e7e525e67f84ef4524b6acad396d9ff153f6e4cd", size = 895095, upload-time = "2025-08-03T05:03:33.918Z" }, - { url = "https://files.pythonhosted.org/packages/20/50/fc384631d8282809fb1029a4460d2fe90fa0370a0e866a8318ed75c8d3bb/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d59dad4173dc2a111f03e59315c7bd6e73da1a9d20a84a25cf08325b0582b1a", size = 651826, upload-time = "2025-08-03T05:03:35.818Z" }, - { url = "https://files.pythonhosted.org/packages/7e/0a/2356305c423a975000867de56888b79e44ec2192c690ff93c3109fd78081/pyzmq-27.0.1-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f5b6133c8d313bde8bd0d123c169d22525300ff164c2189f849de495e1344577", size = 839751, upload-time = "2025-08-03T05:03:37.265Z" }, - { url = "https://files.pythonhosted.org/packages/d7/1b/81e95ad256ca7e7ccd47f5294c1c6da6e2b64fbace65b84fe8a41470342e/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:58cca552567423f04d06a075f4b473e78ab5bdb906febe56bf4797633f54aa4e", size = 1641359, upload-time = "2025-08-03T05:03:38.799Z" }, - { url = "https://files.pythonhosted.org/packages/50/63/9f50ec965285f4e92c265c8f18344e46b12803666d8b73b65d254d441435/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:4b9d8e26fb600d0d69cc9933e20af08552e97cc868a183d38a5c0d661e40dfbb", size = 2020281, upload-time = "2025-08-03T05:03:40.338Z" }, - { url = "https://files.pythonhosted.org/packages/02/4a/19e3398d0dc66ad2b463e4afa1fc541d697d7bc090305f9dfb948d3dfa29/pyzmq-27.0.1-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:2329f0c87f0466dce45bba32b63f47018dda5ca40a0085cc5c8558fea7d9fc55", size = 1877112, upload-time = "2025-08-03T05:03:42.012Z" }, - { url = "https://files.pythonhosted.org/packages/bf/42/c562e9151aa90ed1d70aac381ea22a929d6b3a2ce4e1d6e2e135d34fd9c6/pyzmq-27.0.1-cp312-abi3-win32.whl", hash = "sha256:57bb92abdb48467b89c2d21da1ab01a07d0745e536d62afd2e30d5acbd0092eb", size = 558177, upload-time = "2025-08-03T05:03:43.979Z" }, - { url = "https://files.pythonhosted.org/packages/40/96/5c50a7d2d2b05b19994bf7336b97db254299353dd9b49b565bb71b485f03/pyzmq-27.0.1-cp312-abi3-win_amd64.whl", hash = "sha256:ff3f8757570e45da7a5bedaa140489846510014f7a9d5ee9301c61f3f1b8a686", size = 618923, upload-time = "2025-08-03T05:03:45.438Z" }, - { url = "https://files.pythonhosted.org/packages/13/33/1ec89c8f21c89d21a2eaff7def3676e21d8248d2675705e72554fb5a6f3f/pyzmq-27.0.1-cp312-abi3-win_arm64.whl", hash = "sha256:df2c55c958d3766bdb3e9d858b911288acec09a9aab15883f384fc7180df5bed", size = 552358, upload-time = "2025-08-03T05:03:46.887Z" }, - { url = "https://files.pythonhosted.org/packages/b4/1a/49f66fe0bc2b2568dd4280f1f520ac8fafd73f8d762140e278d48aeaf7b9/pyzmq-27.0.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7fb0ee35845bef1e8c4a152d766242164e138c239e3182f558ae15cb4a891f94", size = 835949, upload-time = "2025-08-03T05:05:13.798Z" }, - { url = "https://files.pythonhosted.org/packages/49/94/443c1984b397eab59b14dd7ae8bc2ac7e8f32dbc646474453afcaa6508c4/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f379f11e138dfd56c3f24a04164f871a08281194dd9ddf656a278d7d080c8ad0", size = 799875, upload-time = "2025-08-03T05:05:15.632Z" }, - { url = "https://files.pythonhosted.org/packages/30/f1/fd96138a0f152786a2ba517e9c6a8b1b3516719e412a90bb5d8eea6b660c/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b978c0678cffbe8860ec9edc91200e895c29ae1ac8a7085f947f8e8864c489fb", size = 567403, upload-time = "2025-08-03T05:05:17.326Z" }, - { url = "https://files.pythonhosted.org/packages/16/57/34e53ef2b55b1428dac5aabe3a974a16c8bda3bf20549ba500e3ff6cb426/pyzmq-27.0.1-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ebccf0d760bc92a4a7c751aeb2fef6626144aace76ee8f5a63abeb100cae87f", size = 747032, upload-time = "2025-08-03T05:05:19.074Z" }, - { url = "https://files.pythonhosted.org/packages/81/b7/769598c5ae336fdb657946950465569cf18803140fe89ce466d7f0a57c11/pyzmq-27.0.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:77fed80e30fa65708546c4119840a46691290efc231f6bfb2ac2a39b52e15811", size = 544566, upload-time = "2025-08-03T05:05:20.798Z" }, + { url = "https://files.pythonhosted.org/packages/42/73/034429ab0f4316bf433eb6c20c3f49d1dc13b2ed4e4d951b283d300a0f35/pyzmq-27.0.2-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:063845960df76599ad4fad69fa4d884b3ba38304272104fdcd7e3af33faeeb1d", size = 1333169, upload-time = "2025-08-21T04:21:12.483Z" }, + { url = "https://files.pythonhosted.org/packages/35/02/c42b3b526eb03a570c889eea85a5602797f800a50ba8b09ddbf7db568b78/pyzmq-27.0.2-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:845a35fb21b88786aeb38af8b271d41ab0967985410f35411a27eebdc578a076", size = 909176, upload-time = "2025-08-21T04:21:13.835Z" }, + { url = "https://files.pythonhosted.org/packages/1b/35/a1c0b988fabbdf2dc5fe94b7c2bcfd61e3533e5109297b8e0daf1d7a8d2d/pyzmq-27.0.2-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:515d20b5c3c86db95503faa989853a8ab692aab1e5336db011cd6d35626c4cb1", size = 668972, upload-time = "2025-08-21T04:21:15.315Z" }, + { url = "https://files.pythonhosted.org/packages/a0/63/908ac865da32ceaeecea72adceadad28ca25b23a2ca5ff018e5bff30116f/pyzmq-27.0.2-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:862aedec0b0684a5050cdb5ec13c2da96d2f8dffda48657ed35e312a4e31553b", size = 856962, upload-time = "2025-08-21T04:21:16.652Z" }, + { url = "https://files.pythonhosted.org/packages/2f/5a/90b3cc20b65cdf9391896fcfc15d8db21182eab810b7ea05a2986912fbe2/pyzmq-27.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2cb5bcfc51c7a4fce335d3bc974fd1d6a916abbcdd2b25f6e89d37b8def25f57", size = 1657712, upload-time = "2025-08-21T04:21:18.666Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3c/32a5a80f9be4759325b8d7b22ce674bb87e586b4c80c6a9d77598b60d6f0/pyzmq-27.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38ff75b2a36e3a032e9fef29a5871e3e1301a37464e09ba364e3c3193f62982a", size = 2035054, upload-time = "2025-08-21T04:21:20.073Z" }, + { url = "https://files.pythonhosted.org/packages/13/61/71084fe2ff2d7dc5713f8740d735336e87544845dae1207a8e2e16d9af90/pyzmq-27.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:7a5709abe8d23ca158a9d0a18c037f4193f5b6afeb53be37173a41e9fb885792", size = 1894010, upload-time = "2025-08-21T04:21:21.96Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/77169cfb13b696e50112ca496b2ed23c4b7d8860a1ec0ff3e4b9f9926221/pyzmq-27.0.2-cp311-cp311-win32.whl", hash = "sha256:47c5dda2018c35d87be9b83de0890cb92ac0791fd59498847fc4eca6ff56671d", size = 566819, upload-time = "2025-08-21T04:21:23.31Z" }, + { url = "https://files.pythonhosted.org/packages/37/cd/86c4083e0f811f48f11bc0ddf1e7d13ef37adfd2fd4f78f2445f1cc5dec0/pyzmq-27.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:f54ca3e98f8f4d23e989c7d0edcf9da7a514ff261edaf64d1d8653dd5feb0a8b", size = 633264, upload-time = "2025-08-21T04:21:24.761Z" }, + { url = "https://files.pythonhosted.org/packages/a0/69/5b8bb6a19a36a569fac02153a9e083738785892636270f5f68a915956aea/pyzmq-27.0.2-cp311-cp311-win_arm64.whl", hash = "sha256:2ef3067cb5b51b090fb853f423ad7ed63836ec154374282780a62eb866bf5768", size = 559316, upload-time = "2025-08-21T04:21:26.1Z" }, + { url = "https://files.pythonhosted.org/packages/68/69/b3a729e7b03e412bee2b1823ab8d22e20a92593634f664afd04c6c9d9ac0/pyzmq-27.0.2-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:5da05e3c22c95e23bfc4afeee6ff7d4be9ff2233ad6cb171a0e8257cd46b169a", size = 1305910, upload-time = "2025-08-21T04:21:27.609Z" }, + { url = "https://files.pythonhosted.org/packages/15/b7/f6a6a285193d489b223c340b38ee03a673467cb54914da21c3d7849f1b10/pyzmq-27.0.2-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:4e4520577971d01d47e2559bb3175fce1be9103b18621bf0b241abe0a933d040", size = 895507, upload-time = "2025-08-21T04:21:29.005Z" }, + { url = "https://files.pythonhosted.org/packages/17/e6/c4ed2da5ef9182cde1b1f5d0051a986e76339d71720ec1a00be0b49275ad/pyzmq-27.0.2-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:56d7de7bf73165b90bd25a8668659ccb134dd28449116bf3c7e9bab5cf8a8ec9", size = 652670, upload-time = "2025-08-21T04:21:30.71Z" }, + { url = "https://files.pythonhosted.org/packages/0e/66/d781ab0636570d32c745c4e389b1c6b713115905cca69ab6233508622edd/pyzmq-27.0.2-cp312-abi3-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:340e7cddc32f147c6c00d116a3f284ab07ee63dbd26c52be13b590520434533c", size = 840581, upload-time = "2025-08-21T04:21:32.008Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/f24790caf565d72544f5c8d8500960b9562c1dc848d6f22f3c7e122e73d4/pyzmq-27.0.2-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ba95693f9df8bb4a9826464fb0fe89033936f35fd4a8ff1edff09a473570afa0", size = 1641931, upload-time = "2025-08-21T04:21:33.371Z" }, + { url = "https://files.pythonhosted.org/packages/65/65/77d27b19fc5e845367f9100db90b9fce924f611b14770db480615944c9c9/pyzmq-27.0.2-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:ca42a6ce2d697537da34f77a1960d21476c6a4af3e539eddb2b114c3cf65a78c", size = 2021226, upload-time = "2025-08-21T04:21:35.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/65/1ed14421ba27a4207fa694772003a311d1142b7f543179e4d1099b7eb746/pyzmq-27.0.2-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3e44e665d78a07214b2772ccbd4b9bcc6d848d7895f1b2d7653f047b6318a4f6", size = 1878047, upload-time = "2025-08-21T04:21:36.749Z" }, + { url = "https://files.pythonhosted.org/packages/dd/dc/e578549b89b40dc78a387ec471c2a360766690c0a045cd8d1877d401012d/pyzmq-27.0.2-cp312-abi3-win32.whl", hash = "sha256:272d772d116615397d2be2b1417b3b8c8bc8671f93728c2f2c25002a4530e8f6", size = 558757, upload-time = "2025-08-21T04:21:38.2Z" }, + { url = "https://files.pythonhosted.org/packages/b5/89/06600980aefcc535c758414da969f37a5194ea4cdb73b745223f6af3acfb/pyzmq-27.0.2-cp312-abi3-win_amd64.whl", hash = "sha256:734be4f44efba0aa69bf5f015ed13eb69ff29bf0d17ea1e21588b095a3147b8e", size = 619281, upload-time = "2025-08-21T04:21:39.909Z" }, + { url = "https://files.pythonhosted.org/packages/30/84/df8a5c089552d17c9941d1aea4314b606edf1b1622361dae89aacedc6467/pyzmq-27.0.2-cp312-abi3-win_arm64.whl", hash = "sha256:41f0bd56d9279392810950feb2785a419c2920bbf007fdaaa7f4a07332ae492d", size = 552680, upload-time = "2025-08-21T04:21:41.571Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/027d0032a1e3b1aabcef0e309b9ff8a4099bdd5a60ab38b36a676ff2bd7b/pyzmq-27.0.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e297784aea724294fe95e442e39a4376c2f08aa4fae4161c669f047051e31b02", size = 836007, upload-time = "2025-08-21T04:23:00.447Z" }, + { url = "https://files.pythonhosted.org/packages/25/20/2ed1e6168aaea323df9bb2c451309291f53ba3af372ffc16edd4ce15b9e5/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:e3659a79ded9745bc9c2aef5b444ac8805606e7bc50d2d2eb16dc3ab5483d91f", size = 799932, upload-time = "2025-08-21T04:23:02.052Z" }, + { url = "https://files.pythonhosted.org/packages/fd/25/5c147307de546b502c9373688ce5b25dc22288d23a1ebebe5d587bf77610/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3dba49ff037d02373a9306b58d6c1e0be031438f822044e8767afccfdac4c6b", size = 567459, upload-time = "2025-08-21T04:23:03.593Z" }, + { url = "https://files.pythonhosted.org/packages/71/06/0dc56ffc615c8095cd089c9b98ce5c733e990f09ce4e8eea4aaf1041a532/pyzmq-27.0.2-pp311-pypy311_pp73-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:de84e1694f9507b29e7b263453a2255a73e3d099d258db0f14539bad258abe41", size = 747088, upload-time = "2025-08-21T04:23:05.334Z" }, + { url = "https://files.pythonhosted.org/packages/06/f6/4a50187e023b8848edd3f0a8e197b1a7fb08d261d8c60aae7cb6c3d71612/pyzmq-27.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:f0944d65ba2b872b9fcece08411d6347f15a874c775b4c3baae7f278550da0fb", size = 544639, upload-time = "2025-08-21T04:23:07.279Z" }, ] [[package]] @@ -4524,7 +4532,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -4532,21 +4540,21 @@ dependencies = [ { name = "idna" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c9/74/b3ff8e6c8446842c3f5c837e9c3dfcfe2018ea6ecef224c710c85ef728f4/requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf", size = 134517, upload-time = "2025-08-18T20:46:02.573Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, + { url = "https://files.pythonhosted.org/packages/1e/db/4254e3eabe8020b458f1a747140d32277ec7a271daf1d235b70dc0b4e6e3/requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6", size = 64738, upload-time = "2025-08-18T20:46:00.542Z" }, ] [[package]] name = "ruamel-yaml" -version = "0.18.14" +version = "0.18.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml-clib", marker = "platform_python_implementation == 'CPython'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/39/87/6da0df742a4684263261c253f00edd5829e6aca970fff69e75028cccc547/ruamel.yaml-0.18.14.tar.gz", hash = "sha256:7227b76aaec364df15936730efbf7d72b30c0b79b1d578bbb8e3dcb2d81f52b7", size = 145511, upload-time = "2025-06-09T08:51:09.828Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/db/f3950f5e5031b618aae9f423a39bf81a55c148aecd15a34527898e752cf4/ruamel.yaml-0.18.15.tar.gz", hash = "sha256:dbfca74b018c4c3fba0b9cc9ee33e53c371194a9000e694995e620490fd40700", size = 146865, upload-time = "2025-08-19T11:15:10.694Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/af/6d/6fe4805235e193aad4aaf979160dd1f3c487c57d48b810c816e6e842171b/ruamel.yaml-0.18.14-py3-none-any.whl", hash = "sha256:710ff198bb53da66718c7db27eec4fbcc9aa6ca7204e4c1df2f282b6fe5eb6b2", size = 118570, upload-time = "2025-06-09T08:51:06.348Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e5/f2a0621f1781b76a38194acae72f01e37b1941470407345b6e8653ad7640/ruamel.yaml-0.18.15-py3-none-any.whl", hash = "sha256:148f6488d698b7a5eded5ea793a025308b25eca97208181b6a026037f391f701", size = 119702, upload-time = "2025-08-19T11:15:07.696Z" }, ] [[package]] @@ -4586,28 +4594,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.9" +version = "0.12.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/4a/45/2e403fa7007816b5fbb324cb4f8ed3c7402a927a0a0cb2b6279879a8bfdc/ruff-0.12.9.tar.gz", hash = "sha256:fbd94b2e3c623f659962934e52c2bea6fc6da11f667a427a368adaf3af2c866a", size = 5254702, upload-time = "2025-08-14T16:08:55.2Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/20/53bf098537adb7b6a97d98fcdebf6e916fcd11b2e21d15f8c171507909cc/ruff-0.12.9-py3-none-linux_armv6l.whl", hash = "sha256:fcebc6c79fcae3f220d05585229463621f5dbf24d79fdc4936d9302e177cfa3e", size = 11759705, upload-time = "2025-08-14T16:08:12.968Z" }, - { url = "https://files.pythonhosted.org/packages/20/4d/c764ee423002aac1ec66b9d541285dd29d2c0640a8086c87de59ebbe80d5/ruff-0.12.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:aed9d15f8c5755c0e74467731a007fcad41f19bcce41cd75f768bbd687f8535f", size = 12527042, upload-time = "2025-08-14T16:08:16.54Z" }, - { url = "https://files.pythonhosted.org/packages/8b/45/cfcdf6d3eb5fc78a5b419e7e616d6ccba0013dc5b180522920af2897e1be/ruff-0.12.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5b15ea354c6ff0d7423814ba6d44be2807644d0c05e9ed60caca87e963e93f70", size = 11724457, upload-time = "2025-08-14T16:08:18.686Z" }, - { url = "https://files.pythonhosted.org/packages/72/e6/44615c754b55662200c48bebb02196dbb14111b6e266ab071b7e7297b4ec/ruff-0.12.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d596c2d0393c2502eaabfef723bd74ca35348a8dac4267d18a94910087807c53", size = 11949446, upload-time = "2025-08-14T16:08:21.059Z" }, - { url = "https://files.pythonhosted.org/packages/fd/d1/9b7d46625d617c7df520d40d5ac6cdcdf20cbccb88fad4b5ecd476a6bb8d/ruff-0.12.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1b15599931a1a7a03c388b9c5df1bfa62be7ede6eb7ef753b272381f39c3d0ff", size = 11566350, upload-time = "2025-08-14T16:08:23.433Z" }, - { url = "https://files.pythonhosted.org/packages/59/20/b73132f66f2856bc29d2d263c6ca457f8476b0bbbe064dac3ac3337a270f/ruff-0.12.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3d02faa2977fb6f3f32ddb7828e212b7dd499c59eb896ae6c03ea5c303575756", size = 13270430, upload-time = "2025-08-14T16:08:25.837Z" }, - { url = "https://files.pythonhosted.org/packages/a2/21/eaf3806f0a3d4c6be0a69d435646fba775b65f3f2097d54898b0fd4bb12e/ruff-0.12.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:17d5b6b0b3a25259b69ebcba87908496e6830e03acfb929ef9fd4c58675fa2ea", size = 14264717, upload-time = "2025-08-14T16:08:27.907Z" }, - { url = "https://files.pythonhosted.org/packages/d2/82/1d0c53bd37dcb582b2c521d352fbf4876b1e28bc0d8894344198f6c9950d/ruff-0.12.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:72db7521860e246adbb43f6ef464dd2a532ef2ef1f5dd0d470455b8d9f1773e0", size = 13684331, upload-time = "2025-08-14T16:08:30.352Z" }, - { url = "https://files.pythonhosted.org/packages/3b/2f/1c5cf6d8f656306d42a686f1e207f71d7cebdcbe7b2aa18e4e8a0cb74da3/ruff-0.12.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a03242c1522b4e0885af63320ad754d53983c9599157ee33e77d748363c561ce", size = 12739151, upload-time = "2025-08-14T16:08:32.55Z" }, - { url = "https://files.pythonhosted.org/packages/47/09/25033198bff89b24d734e6479e39b1968e4c992e82262d61cdccaf11afb9/ruff-0.12.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fc83e4e9751e6c13b5046d7162f205d0a7bac5840183c5beebf824b08a27340", size = 12954992, upload-time = "2025-08-14T16:08:34.816Z" }, - { url = "https://files.pythonhosted.org/packages/52/8e/d0dbf2f9dca66c2d7131feefc386523404014968cd6d22f057763935ab32/ruff-0.12.9-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:881465ed56ba4dd26a691954650de6ad389a2d1fdb130fe51ff18a25639fe4bb", size = 12899569, upload-time = "2025-08-14T16:08:36.852Z" }, - { url = "https://files.pythonhosted.org/packages/a0/bd/b614d7c08515b1428ed4d3f1d4e3d687deffb2479703b90237682586fa66/ruff-0.12.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:43f07a3ccfc62cdb4d3a3348bf0588358a66da756aa113e071b8ca8c3b9826af", size = 11751983, upload-time = "2025-08-14T16:08:39.314Z" }, - { url = "https://files.pythonhosted.org/packages/58/d6/383e9f818a2441b1a0ed898d7875f11273f10882f997388b2b51cb2ae8b5/ruff-0.12.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:07adb221c54b6bba24387911e5734357f042e5669fa5718920ee728aba3cbadc", size = 11538635, upload-time = "2025-08-14T16:08:41.297Z" }, - { url = "https://files.pythonhosted.org/packages/20/9c/56f869d314edaa9fc1f491706d1d8a47747b9d714130368fbd69ce9024e9/ruff-0.12.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:f5cd34fabfdea3933ab85d72359f118035882a01bff15bd1d2b15261d85d5f66", size = 12534346, upload-time = "2025-08-14T16:08:43.39Z" }, - { url = "https://files.pythonhosted.org/packages/bd/4b/d8b95c6795a6c93b439bc913ee7a94fda42bb30a79285d47b80074003ee7/ruff-0.12.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:f6be1d2ca0686c54564da8e7ee9e25f93bdd6868263805f8c0b8fc6a449db6d7", size = 13017021, upload-time = "2025-08-14T16:08:45.889Z" }, - { url = "https://files.pythonhosted.org/packages/c7/c1/5f9a839a697ce1acd7af44836f7c2181cdae5accd17a5cb85fcbd694075e/ruff-0.12.9-py3-none-win32.whl", hash = "sha256:cc7a37bd2509974379d0115cc5608a1a4a6c4bff1b452ea69db83c8855d53f93", size = 11734785, upload-time = "2025-08-14T16:08:48.062Z" }, - { url = "https://files.pythonhosted.org/packages/fa/66/cdddc2d1d9a9f677520b7cfc490d234336f523d4b429c1298de359a3be08/ruff-0.12.9-py3-none-win_amd64.whl", hash = "sha256:6fb15b1977309741d7d098c8a3cb7a30bc112760a00fb6efb7abc85f00ba5908", size = 12840654, upload-time = "2025-08-14T16:08:50.158Z" }, - { url = "https://files.pythonhosted.org/packages/ac/fd/669816bc6b5b93b9586f3c1d87cd6bc05028470b3ecfebb5938252c47a35/ruff-0.12.9-py3-none-win_arm64.whl", hash = "sha256:63c8c819739d86b96d500cce885956a1a48ab056bbcbc61b747ad494b2485089", size = 11949623, upload-time = "2025-08-14T16:08:52.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, ] [[package]] @@ -4824,11 +4832,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] From 1d74a97ba65b9b9ea074ef870ab70907be33918c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Mon, 25 Aug 2025 13:50:10 -0700 Subject: [PATCH 006/188] torqued: apply offset (#36005) * torqued: apply latAccelOffset to torque control feed forward * test learned latAccelOffset captures roll compensation bias on straight road driving, when the device is not flush in roll relative to the car * test correct torqued latAccelOffset parameter convergence --------- Co-authored-by: felsager --- selfdrive/controls/lib/latcontrol_torque.py | 6 +- .../tests/test_torqued_lat_accel_offset.py | 71 +++++++++++++++++++ selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 75 insertions(+), 4 deletions(-) create mode 100644 selfdrive/controls/tests/test_torqued_lat_accel_offset.py diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index dffe85c473..5a2814e089 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -52,10 +52,8 @@ class LatControlTorque(LatControl): actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - desired_lateral_accel = desired_curvature * CS.vEgo ** 2 - # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature - # desired_lateral_jerk = desired_curvature_rate * CS.vEgo ** 2 + desired_lateral_accel = desired_curvature * CS.vEgo ** 2 actual_lateral_accel = actual_curvature * CS.vEgo ** 2 lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 @@ -67,6 +65,8 @@ class LatControlTorque(LatControl): # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly pid_log.error = float(setpoint - measurement) ff = gravity_adjusted_lateral_accel + # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll + ff -= self.torque_params.latAccelOffset ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 diff --git a/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/selfdrive/controls/tests/test_torqued_lat_accel_offset.py new file mode 100644 index 0000000000..5ba9980020 --- /dev/null +++ b/selfdrive/controls/tests/test_torqued_lat_accel_offset.py @@ -0,0 +1,71 @@ +import numpy as np +from cereal import car, messaging +from opendbc.car import ACCELERATION_DUE_TO_GRAVITY +from opendbc.car import structs +from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD +from openpilot.common.realtime import DT_MDL +from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS + +np.random.seed(0) + +LA_ERR_STD = 1.0 +INPUT_NOISE_STD = 0.1 +V_EGO = 30.0 + +WARMUP_BUCKET_POINTS = (1.5*MIN_BUCKET_POINTS).astype(int) +STRAIGHT_ROAD_LA_BOUNDS = (0.02, 0.03) + +ROLL_BIAS_DEG = 1.0 +ROLL_COMPENSATION_BIAS = ACCELERATION_DUE_TO_GRAVITY*float(np.sin(np.deg2rad(ROLL_BIAS_DEG))) +TORQUE_TUNE = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=0.0, friction=0.2) +TORQUE_TUNE_BIASED = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=-ROLL_COMPENSATION_BIAS, friction=0.2) + + +def generate_inputs(torque_tune, la_err_std, input_noise_std=None): + rng = np.random.default_rng(0) + steer_torques = np.concat([rng.uniform(bnd[0], bnd[1], pts) for bnd, pts in zip(STEER_BUCKET_BOUNDS, WARMUP_BUCKET_POINTS, strict=True)]) + la_errs = rng.normal(scale=la_err_std, size=steer_torques.size) + frictions = np.array([get_friction(la_err, 0.0, FRICTION_THRESHOLD, torque_tune) for la_err in la_errs]) + lat_accels = torque_tune.latAccelFactor*steer_torques + torque_tune.latAccelOffset + frictions + if input_noise_std is not None: + steer_torques += rng.normal(scale=input_noise_std, size=steer_torques.size) + lat_accels += rng.normal(scale=input_noise_std, size=steer_torques.size) + return steer_torques, lat_accels + +def get_warmed_up_estimator(steer_torques, lat_accels): + est = TorqueEstimator(car.CarParams()) + for steer_torque, lat_accel in zip(steer_torques, lat_accels, strict=True): + est.filtered_points.add_point(steer_torque, lat_accel) + return est + +def simulate_straight_road_msgs(est): + carControl = messaging.new_message('carControl').carControl + carOutput = messaging.new_message('carOutput').carOutput + carState = messaging.new_message('carState').carState + livePose = messaging.new_message('livePose').livePose + carControl.latActive = True + carState.vEgo = V_EGO + carState.steeringPressed = False + ts = DT_MDL*np.arange(2*POINTS_PER_BUCKET) + steer_torques = np.concat((np.linspace(-0.03, -0.02, POINTS_PER_BUCKET), np.linspace(0.02, 0.03, POINTS_PER_BUCKET))) + lat_accels = TORQUE_TUNE.latAccelFactor * steer_torques + for t, steer_torque, lat_accel in zip(ts, steer_torques, lat_accels, strict=True): + carOutput.actuatorsOutput.torque = float(-steer_torque) + livePose.orientationNED.x = float(np.deg2rad(ROLL_BIAS_DEG)) + livePose.angularVelocityDevice.z = float(lat_accel / V_EGO) + for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)): + est.handle_log(t, which, msg) + +def test_estimated_offset(): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + msg = est.get_msg() + # TODO add lataccelfactor and friction check when we have more accurate estimates + assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.03 + +def test_straight_road_roll_bias(): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + simulate_straight_road_msgs(est) + msg = est.get_msg() + assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 8e4ff1e2a8..7d21526144 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -209b47bea61e145cf2d27eb3ab650c97bcd1d33f \ No newline at end of file +866f2cb1f0e49b2cb7115aa8131164b3a75fb2c5 \ No newline at end of file From c4a7f25b626886f9143d0e9bae1eaa37a9083538 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Aug 2025 15:17:37 -0700 Subject: [PATCH 007/188] raylib: refactor NetworkManager constants (#36056) * new file * import * and this --- system/ui/lib/networkmanager.py | 44 +++++++++++++++++++++++++++++++++ system/ui/lib/wifi_manager.py | 27 ++++---------------- 2 files changed, 49 insertions(+), 22 deletions(-) create mode 100644 system/ui/lib/networkmanager.py diff --git a/system/ui/lib/networkmanager.py b/system/ui/lib/networkmanager.py new file mode 100644 index 0000000000..07b5d42f4b --- /dev/null +++ b/system/ui/lib/networkmanager.py @@ -0,0 +1,44 @@ +from enum import IntEnum + + +# NetworkManager device states +class NMDeviceState(IntEnum): + UNKNOWN = 0 + DISCONNECTED = 30 + PREPARE = 40 + STATE_CONFIG = 50 + NEED_AUTH = 60 + IP_CONFIG = 70 + ACTIVATED = 100 + DEACTIVATING = 110 + + +# NetworkManager constants +NM = "org.freedesktop.NetworkManager" +NM_PATH = '/org/freedesktop/NetworkManager' +NM_IFACE = 'org.freedesktop.NetworkManager' +NM_ACCESS_POINT_IFACE = 'org.freedesktop.NetworkManager.AccessPoint' +NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings' +NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings' +NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection' +NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' +NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties' +NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" + +NM_DEVICE_TYPE_WIFI = 2 +NM_DEVICE_TYPE_MODEM = 8 +NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 +NM_DEVICE_STATE_REASON_NEW_ACTIVATION = 60 + +# https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApFlags +NM_802_11_AP_FLAGS_NONE = 0x0 +NM_802_11_AP_FLAGS_PRIVACY = 0x1 +NM_802_11_AP_FLAGS_WPS = 0x2 + +# https://developer.gnome.org/NetworkManager/1.26/nm-dbus-types.html#NM80211ApSecurityFlags +NM_802_11_AP_SEC_PAIR_WEP40 = 0x00000001 +NM_802_11_AP_SEC_PAIR_WEP104 = 0x00000002 +NM_802_11_AP_SEC_GROUP_WEP40 = 0x00000010 +NM_802_11_AP_SEC_GROUP_WEP104 = 0x00000020 +NM_802_11_AP_SEC_KEY_MGMT_PSK = 0x00000100 +NM_802_11_AP_SEC_KEY_MGMT_802_1X = 0x00000200 diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 4cb741bc95..7bd292fa43 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -14,6 +14,11 @@ from dbus_next import BusType, Variant, Message from dbus_next.errors import DBusError from dbus_next.constants import MessageType +from openpilot.system.ui.lib.networkmanager import (NM, NM_PATH, NM_IFACE, NM_SETTINGS_PATH, NM_SETTINGS_IFACE, + NM_CONNECTION_IFACE, NM_WIRELESS_IFACE, NM_PROPERTIES_IFACE, + NM_DEVICE_IFACE, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, + NMDeviceState) + try: from openpilot.common.params import Params except ImportError: @@ -23,32 +28,10 @@ from openpilot.common.swaglog import cloudlog T = TypeVar("T") -# NetworkManager constants -NM = "org.freedesktop.NetworkManager" -NM_PATH = '/org/freedesktop/NetworkManager' -NM_IFACE = 'org.freedesktop.NetworkManager' -NM_SETTINGS_PATH = '/org/freedesktop/NetworkManager/Settings' -NM_SETTINGS_IFACE = 'org.freedesktop.NetworkManager.Settings' -NM_CONNECTION_IFACE = 'org.freedesktop.NetworkManager.Settings.Connection' -NM_WIRELESS_IFACE = 'org.freedesktop.NetworkManager.Device.Wireless' -NM_PROPERTIES_IFACE = 'org.freedesktop.DBus.Properties' -NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" - -NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 - TETHERING_IP_ADDRESS = "192.168.43.1" DEFAULT_TETHERING_PASSWORD = "swagswagcomma" -# NetworkManager device states -class NMDeviceState(IntEnum): - DISCONNECTED = 30 - PREPARE = 40 - NEED_AUTH = 60 - IP_CONFIG = 70 - ACTIVATED = 100 - - class SecurityType(IntEnum): OPEN = 0 WPA = 1 From e89c6b3b88569916a47067e56cd51d0fbc96e549 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Aug 2025 15:23:14 -0700 Subject: [PATCH 008/188] raylib: remove redundant networking class (#36057) remove a class --- selfdrive/ui/layouts/network.py | 17 ----------------- selfdrive/ui/layouts/settings/settings.py | 8 ++++++-- 2 files changed, 6 insertions(+), 19 deletions(-) delete mode 100644 selfdrive/ui/layouts/network.py diff --git a/selfdrive/ui/layouts/network.py b/selfdrive/ui/layouts/network.py deleted file mode 100644 index 856be26e97..0000000000 --- a/selfdrive/ui/layouts/network.py +++ /dev/null @@ -1,17 +0,0 @@ -import pyray as rl -from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper -from openpilot.system.ui.widgets import Widget -from openpilot.system.ui.widgets.network import WifiManagerUI - - -class NetworkLayout(Widget): - def __init__(self): - super().__init__() - self.wifi_manager = WifiManagerWrapper() - self.wifi_ui = WifiManagerUI(self.wifi_manager) - - def _render(self, rect: rl.Rectangle): - self.wifi_ui.render(rect) - - def shutdown(self): - self.wifi_manager.shutdown() diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index 674e5005f4..7d2a23576e 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -2,7 +2,6 @@ import pyray as rl from dataclasses import dataclass from enum import IntEnum from collections.abc import Callable -from openpilot.selfdrive.ui.layouts.network import NetworkLayout from openpilot.selfdrive.ui.layouts.settings.developer import DeveloperLayout from openpilot.selfdrive.ui.layouts.settings.device import DeviceLayout from openpilot.selfdrive.ui.layouts.settings.firehose import FirehoseLayout @@ -10,7 +9,9 @@ from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.network import WifiManagerUI # Settings close button SETTINGS_CLOSE_TEXT = "×" @@ -53,9 +54,12 @@ class SettingsLayout(Widget): self._current_panel = PanelType.DEVICE # Panel configuration + self.wifi_manager = WifiManagerWrapper() + self.wifi_ui = WifiManagerUI(self.wifi_manager) + self._panels = { PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), - PanelType.NETWORK: PanelInfo("Network", NetworkLayout()), + PanelType.NETWORK: PanelInfo("Network", self.wifi_ui), PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), From 15fcbf24f1b2f1ecc920eb48e39dbeb2c0747e59 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Aug 2025 15:52:13 -0700 Subject: [PATCH 009/188] raylib home ui: show/hide events (#36058) * it's a widget * proper events * bottom --- selfdrive/ui/layouts/main.py | 12 +++++++++--- selfdrive/ui/layouts/settings/settings.py | 14 ++++++++++---- system/ui/widgets/__init__.py | 7 +++++++ 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index 3ab8525d33..777d2f4c3f 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -63,14 +63,20 @@ class MainLayout(Widget): # Don't hide sidebar from interactive timeout if self._current_mode != MainState.ONROAD: self._sidebar.set_visible(False) - self._current_mode = MainState.ONROAD + self._set_current_layout(MainState.ONROAD) else: - self._current_mode = MainState.HOME + self._set_current_layout(MainState.HOME) self._sidebar.set_visible(True) + def _set_current_layout(self, layout: MainState): + if layout != self._current_mode: + self._layouts[self._current_mode].hide_event() + self._current_mode = layout + self._layouts[self._current_mode].show_event() + def open_settings(self, panel_type: PanelType): self._layouts[MainState.SETTINGS].set_current_panel(panel_type) - self._current_mode = MainState.SETTINGS + self._set_current_layout(MainState.SETTINGS) self._sidebar.set_visible(False) def _on_settings_clicked(self): diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index 7d2a23576e..fe35ea094a 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -44,7 +44,7 @@ class PanelType(IntEnum): @dataclass class PanelInfo: name: str - instance: object + instance: Widget button_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) @@ -153,8 +153,14 @@ class SettingsLayout(Widget): def set_current_panel(self, panel_type: PanelType): if panel_type != self._current_panel: + self._panels[self._current_panel].instance.hide_event() self._current_panel = panel_type + self._panels[self._current_panel].instance.show_event() - def close_settings(self): - if self._close_callback: - self._close_callback() + def show_event(self): + super().show_event() + self._panels[self._current_panel].instance.show_event() + + def hide_event(self): + super().hide_event() + self._panels[self._current_panel].instance.hide_event() diff --git a/system/ui/widgets/__init__.py b/system/ui/widgets/__init__.py index d45f48ac38..cca2cec7ad 100644 --- a/system/ui/widgets/__init__.py +++ b/system/ui/widgets/__init__.py @@ -129,3 +129,10 @@ class Widget(abc.ABC): def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: """Optionally handle mouse release events.""" return False + + def show_event(self): + """Optionally handle show event. Parent must manually call this""" + + def hide_event(self): + """Optionally handle hide event. Parent must manually call this""" + From 73e66c4a0b9f5afc2ba23d9c64468a841f3c03f9 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 21:22:20 -0400 Subject: [PATCH 010/188] [bot] Update Python packages (#1178) Update Python packages Co-authored-by: github-actions[bot] --- docs/CARS.md | 16 ++++++++++++---- opendbc_repo | 2 +- uv.lock | 6 +++--- 3 files changed, 16 insertions(+), 8 deletions(-) diff --git a/docs/CARS.md b/docs/CARS.md index e10dc8ae77..dbc1dbcd74 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,12 +4,13 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 326 Supported Cars +# 334 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| |Acura|ILX 2016-18|Technology Plus Package or AcuraWatch Plus|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Acura|ILX 2019|All|openpilot|26 mph|25 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Acura|MDX 2025|All except Type S|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Acura|RDX 2016-18|AcuraWatch Plus or Advance Package|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Acura|RDX 2019-21|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Audi|A3 2014-19|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -72,12 +73,15 @@ A supported vehicle is one that just works when you install a comma device. All |Genesis|GV80 2023[6](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai M connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |GMC|Sierra 1500 2020-21|Driver Alert Package II|openpilot available[1](#footnotes)|0 mph|6 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 GM connector
- 1 comma 3X
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Accord 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Accord 2023|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Accord 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Accord Hybrid 2018-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Accord Hybrid 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|City (Brazil only) 2023|All|openpilot available[1](#footnotes)|0 mph|14 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic 2016-18|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic 2019-21|All|openpilot available[1](#footnotes)|0 mph|2 mph[5](#footnotes)|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback 2017-21|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hatchback Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -85,7 +89,9 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Clarity 2018-21|Honda Sensing|openpilot|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Nidec connector + Honda Clarity Proxy Board
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|CR-V 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|15 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|CR-V 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|CR-V Hybrid 2017-22|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|CR-V Hybrid 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|e 2020|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Fit 2018-20|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Freed 2020|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -96,6 +102,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Ridgeline 2017-25|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Azera 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai K connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Azera Hybrid 2019|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -120,7 +127,7 @@ A supported vehicle is one that just works when you install a comma device. All |Hyundai|Ioniq Plug-in Hybrid 2019|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai C connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Ioniq Plug-in Hybrid 2020-22|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai H connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Kona 2020|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|6 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Hyundai|Kona 2022|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Hyundai|Kona 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Kona Electric 2018-21|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai G connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Kona Electric 2022-23|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai O connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Hyundai|Kona Electric (with HDA II, Korea only) 2023[6](#footnotes)|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai R connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -298,6 +305,7 @@ A supported vehicle is one that just works when you install a comma device. All |Toyota|RAV4 Hybrid 2022|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Toyota|RAV4 Hybrid 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Toyota|Sienna 2018-20|All|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Toyota|Wildlander PHEV 2021|All|openpilot|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Arteon 2018-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Arteon eHybrid 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Volkswagen|Arteon R 2020-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| diff --git a/opendbc_repo b/opendbc_repo index aa0aa1b7aa..ee25c18829 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit aa0aa1b7aacc15e5e9228a91d7f7043d8b39f9e2 +Subproject commit ee25c18829fcb229fcf6576194ddd8638b33ba55 diff --git a/uv.lock b/uv.lock index 49ae160993..7010cdfb83 100644 --- a/uv.lock +++ b/uv.lock @@ -4832,11 +4832,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.1" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/98/5a/da40306b885cc8c09109dc2e1abd358d5684b1425678151cdaed4731c822/typing_extensions-4.14.1.tar.gz", hash = "sha256:38b39f4aeeab64884ce9f74c94263ef78f3c22467c8724005483154c26648d36", size = 107673, upload-time = "2025-07-04T13:28:34.16Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/00/d631e67a838026495268c2f6884f3711a15a9a2a96cd244fdaea53b823fb/typing_extensions-4.14.1-py3-none-any.whl", hash = "sha256:d1e1e3b58374dc93031d6eda2420a48ea44a36c2b4766a4fdeb3710755731d76", size = 43906, upload-time = "2025-07-04T13:28:32.743Z" }, + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] [[package]] From 6bbf42c16aa104a4352c71666165ef0be91fe04b Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 25 Aug 2025 23:50:58 -0400 Subject: [PATCH 011/188] [bot] Update translations (#1183) Update translations Co-authored-by: github-actions[bot] Co-authored-by: Jason Wen --- selfdrive/ui/translations/main_ar.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_de.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_es.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_fr.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_ja.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_ko.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_pt-BR.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_th.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_tr.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_zh-CHS.ts | 16 ++++++++++++++++ selfdrive/ui/translations/main_zh-CHT.ts | 16 ++++++++++++++++ 11 files changed, 176 insertions(+) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index f72ea8ee83..ef7110e72e 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -2103,6 +2103,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 193e2fd895..dde9241460 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -2085,6 +2085,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 1d1723cde7..3b53e7f944 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -2087,6 +2087,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 959321bb4b..45705defd9 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -2083,6 +2083,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 28b5b00e51..3faa183954 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -2082,6 +2082,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index d69fda88b0..756cad6045 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -2096,6 +2096,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index c5171b19f7..0a886d435c 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -2087,6 +2087,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 3bd2d84a92..7b89f2a694 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -2078,6 +2078,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index db9c15580a..0598d08364 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -2077,6 +2077,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 2316360891..1f4db2d6d0 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -2082,6 +2082,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index 16bb393dea..1a8784846c 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -2082,6 +2082,22 @@ Warning: You are on a metered connection! [Don't use] Enable sunnylink uploader + + 🚀 sunnylink 🚀 + + + + For secure backup, restore, and remote configuration + + + + Sponsorship isn't required for basic backup/restore + + + + Click the sponsor button for more details + + SunnylinkSponsorPopup From 2dc0f97c933cf4c684aeb1ac95faa752f9790e3b Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Aug 2025 22:29:14 -0700 Subject: [PATCH 012/188] raylib: fix slow Toggles panel first load (#36061) fix slow load on toggles page --- system/ui/widgets/list_view.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/ui/widgets/list_view.py b/system/ui/widgets/list_view.py index e871eef0a1..a8d81a8ba2 100644 --- a/system/ui/widgets/list_view.py +++ b/system/ui/widgets/list_view.py @@ -213,6 +213,7 @@ class ListItem(Widget): self.set_rect(rl.Rectangle(0, 0, ITEM_BASE_WIDTH, ITEM_BASE_HEIGHT)) self._font = gui_app.font(FontWeight.NORMAL) + self._icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) if self.icon else None # Cached properties for performance self._prev_max_width: int = 0 @@ -261,8 +262,7 @@ class ListItem(Widget): if self.title: # Draw icon if present if self.icon: - icon_texture = gui_app.texture(os.path.join("icons", self.icon), ICON_SIZE, ICON_SIZE) - rl.draw_texture(icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - icon_texture.width) // 2), rl.WHITE) + rl.draw_texture(self._icon_texture, int(content_x), int(self._rect.y + (ITEM_BASE_HEIGHT - self._icon_texture.width) // 2), rl.WHITE) text_x += ICON_SIZE + ITEM_PADDING # Draw main text From 7a2f2ddf32236fd497a6539ce7898a6cd915ca19 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Aug 2025 22:30:09 -0700 Subject: [PATCH 013/188] raylib: speed up network panel first load (#36062) * debug * debug * clean up --- system/ui/widgets/network.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 0bb759a919..2ecfd36be7 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -44,6 +44,7 @@ class WifiManagerUI(Widget): self.btn_width: int = 200 self.scroll_panel = GuiScrollPanel() self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) + self._load_icons() self._networks: list[NetworkInfo] = [] self._networks_buttons: dict[str, Button] = {} @@ -64,6 +65,10 @@ class WifiManagerUI(Widget): self.wifi_manager.start() self.wifi_manager.connect() + def _load_icons(self): + for icon in STRENGTH_ICONS + ["icons/checkmark.png", "icons/circled_slash.png", "icons/lock_closed.png"]: + gui_app.texture(icon, ICON_SIZE, ICON_SIZE) + def _render(self, rect: rl.Rectangle): with self._lock: if not self._networks: From a70e4c3074accd67146ffa62905cc9722a9dfe60 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Mon, 25 Aug 2025 22:30:27 -0700 Subject: [PATCH 014/188] raylib: rm debug print --- selfdrive/ui/lib/prime_state.py | 1 - 1 file changed, 1 deletion(-) diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index da2ff899dd..a1b2472f30 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -25,7 +25,6 @@ class PrimeType(IntEnum): @lru_cache(maxsize=1) def get_token(dongle_id: str, t: int): - print('getting token') return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS) From 5359f6d35464448491e1d036d6970ee67addc821 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 26 Aug 2025 01:23:59 -0700 Subject: [PATCH 015/188] raylib: clean up networking (#36039) * stasj * remove one of many classes * clean up and fix * clean up * stash/draft: oh this is sick * so epic * some clean up * what the fuck, it doesn't even use these * more epic initializers + make it kind of work * so simple, wonder if we should further 2x reduce line count * i've never ever seen this pattern b4, rm * remove bs add niceness * minor organization * set security type and support listing and rming conns * forget and connect * jeepney is actually pretty good, it's 2x faster to get wifi device (0.005s to 0.002s) * temp * do blocking add in worker thread * add jeepney * lets finish with python-dbus first then evaluate - revert jeepney This reverts commit 7de04b11c2285c298bb1ec907782026c795ab207. and * safe wrap * missing * saved connections * set rest of callbacks * skip hidden APs, simplify _running * add state management * either wrong password or disconnected for now * i can't believe we didn't check this... * disable button if unsupported!!! * hide/show event no lag hopefully yayay * fix hide event * remove old wifi manager * cache wifi device path + some clean up * more clean up * more clean up * temp disable blocking prime thread * hackily get device path once * ok * debug * fix open networks * debug * clean up * all threads wait for device path, and user functions dont ever attempt to get, just skip * same place * helper * Revert "helper" This reverts commit e237d9a720915fb6bd67c539123d3e2d9d582ce1. * organize? * Revert "organize?" This reverts commit 3aca3e5d629c947711ade88799febeb3f61eda87. * c word is a bad word * rk monitor debug for now * nothing crazy * improve checkmark responsiveness * when forgetting: this is correct, but feels unresponsive * this feels good * need these two to keep forgetting and activating responsive * sort by connected, strength, then name * handle non-critical race condition * log more * unused * oh jubilee is sick you can block on signals!! * proof of concept to see if works on device whoiops * so sucking fick * ah this is not generic, it's a filter on the return vals * flip around to not drop * oh thank god * fix * stash * atomic replace * clean up * add old to keep track of what's moved over * these are already moved * so much junk * so much junk * more * tethering wasn't used so we can ignore that for now * no params now * rm duplicate imports * not used anymore * move get wifi device over to jeepney! ~no additional lines * request scan w/ keepney * get_conns * _connection_by_ssid_jeepney is 2x faster (0.01 vs 0.02s) * do forget and activate * _update_networks matches! * rm old update_networks * replace connect_to_network, about same time (yes i removed thread call) * no more python-dbus!k * doesn't hurt * AP.from_dbus: actually handle incorrect paths w/ jeep + more efficient single call * properly handle errors * it's jeepney now * less state * using the thread safe router passes a race condition test that conn failed! * bad to copy from old wifimanager * fix conn usage * clean up * curious if locks are lagging * not for now * Revert "curious if locks are lagging" This reverts commit 085dd185b083f5905a4e71ba3e8c0565175e04aa. * clean up _monitor_state * remove tests * clean up dataclasses * sort * lint: okay fine it can be non by virtue of exiting right at the perfect time * some network clean up * some wifi manager clean up * this is handled * stop can be called manually, from deleting wifimanager, or exiting python. some protection * its not mutable anymore * scan on enter * clean up * back * lint * catch dbus fail to connect catch dbus fail to connect --- pyproject.toml | 2 +- selfdrive/ui/layouts/settings/settings.py | 8 +- system/ui/lib/wifi_manager.py | 970 ++++++++-------------- system/ui/setup.py | 5 +- system/ui/updater.py | 5 +- system/ui/widgets/network.py | 58 +- uv.lock | 22 +- 7 files changed, 385 insertions(+), 685 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7103163458..556b78009d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,8 +101,8 @@ dev = [ "av", "azure-identity", "azure-storage-blob", - "dbus-next", "dictdiffer", + "jeepney", "matplotlib", "opencv-python-headless", "parameterized >=0.8, <0.9", diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index fe35ea094a..a731a9158c 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -9,7 +9,7 @@ from openpilot.selfdrive.ui.layouts.settings.software import SoftwareLayout from openpilot.selfdrive.ui.layouts.settings.toggles import TogglesLayout from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper +from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.network import WifiManagerUI @@ -54,12 +54,12 @@ class SettingsLayout(Widget): self._current_panel = PanelType.DEVICE # Panel configuration - self.wifi_manager = WifiManagerWrapper() - self.wifi_ui = WifiManagerUI(self.wifi_manager) + wifi_manager = WifiManager() + wifi_manager.set_active(False) self._panels = { PanelType.DEVICE: PanelInfo("Device", DeviceLayout()), - PanelType.NETWORK: PanelInfo("Network", self.wifi_ui), + PanelType.NETWORK: PanelInfo("Network", WifiManagerUI(wifi_manager)), PanelType.TOGGLES: PanelInfo("Toggles", TogglesLayout()), PanelType.SOFTWARE: PanelInfo("Software", SoftwareLayout()), PanelType.FIREHOSE: PanelInfo("Firehose", FirehoseLayout()), diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 7bd292fa43..96940c2b3b 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -1,35 +1,35 @@ -import asyncio -import concurrent.futures -import copy +import atexit import threading import time import uuid from collections.abc import Callable from dataclasses import dataclass from enum import IntEnum -from typing import TypeVar +from typing import Any -from dbus_next.aio import MessageBus -from dbus_next import BusType, Variant, Message -from dbus_next.errors import DBusError -from dbus_next.constants import MessageType +from jeepney import DBusAddress, new_method_call +from jeepney.bus_messages import MatchRule, message_bus +from jeepney.io.blocking import open_dbus_connection as open_dbus_connection_blocking +from jeepney.io.threading import DBusRouter, open_dbus_connection as open_dbus_connection_threading +from jeepney.low_level import MessageType +from jeepney.wrappers import Properties -from openpilot.system.ui.lib.networkmanager import (NM, NM_PATH, NM_IFACE, NM_SETTINGS_PATH, NM_SETTINGS_IFACE, - NM_CONNECTION_IFACE, NM_WIRELESS_IFACE, NM_PROPERTIES_IFACE, - NM_DEVICE_IFACE, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, - NMDeviceState) - -try: - from openpilot.common.params import Params -except ImportError: - # Params/Cythonized modules are not available in zipapp - Params = None from openpilot.common.swaglog import cloudlog - -T = TypeVar("T") +from openpilot.system.ui.lib.networkmanager import (NM, NM_WIRELESS_IFACE, NM_802_11_AP_SEC_PAIR_WEP40, + NM_802_11_AP_SEC_PAIR_WEP104, NM_802_11_AP_SEC_GROUP_WEP40, + NM_802_11_AP_SEC_GROUP_WEP104, NM_802_11_AP_SEC_KEY_MGMT_PSK, + NM_802_11_AP_SEC_KEY_MGMT_802_1X, NM_802_11_AP_FLAGS_NONE, + NM_802_11_AP_FLAGS_PRIVACY, NM_802_11_AP_FLAGS_WPS, + NM_PATH, NM_IFACE, NM_ACCESS_POINT_IFACE, NM_SETTINGS_PATH, + NM_SETTINGS_IFACE, NM_CONNECTION_IFACE, NM_DEVICE_IFACE, + NM_DEVICE_TYPE_WIFI, NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT, + NM_DEVICE_STATE_REASON_NEW_ACTIVATION, + NMDeviceState) TETHERING_IP_ADDRESS = "192.168.43.1" DEFAULT_TETHERING_PASSWORD = "swagswagcomma" +SIGNAL_QUEUE_SIZE = 10 +SCAN_PERIOD_SECONDS = 10 class SecurityType(IntEnum): @@ -40,673 +40,377 @@ class SecurityType(IntEnum): UNSUPPORTED = 4 -@dataclass -class NetworkInfo: +def get_security_type(flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType: + wpa_props = wpa_flags | rsn_flags + + # obtained by looking at flags of networks in the office as reported by an Android phone + supports_wpa = (NM_802_11_AP_SEC_PAIR_WEP40 | NM_802_11_AP_SEC_PAIR_WEP104 | NM_802_11_AP_SEC_GROUP_WEP40 | + NM_802_11_AP_SEC_GROUP_WEP104 | NM_802_11_AP_SEC_KEY_MGMT_PSK) + + if (flags == NM_802_11_AP_FLAGS_NONE) or ((flags & NM_802_11_AP_FLAGS_WPS) and not (wpa_props & supports_wpa)): + return SecurityType.OPEN + elif (flags & NM_802_11_AP_FLAGS_PRIVACY) and (wpa_props & supports_wpa) and not (wpa_props & NM_802_11_AP_SEC_KEY_MGMT_802_1X): + return SecurityType.WPA + else: + cloudlog.warning(f"Unsupported network! flags: {flags}, wpa_flags: {wpa_flags}, rsn_flags: {rsn_flags}") + return SecurityType.UNSUPPORTED + + +@dataclass(frozen=True) +class Network: ssid: str strength: int is_connected: bool security_type: SecurityType - path: str + is_saved: bool + + @classmethod + def from_dbus(cls, ssid: str, aps: list["AccessPoint"], is_saved: bool) -> "Network": + # we only want to show the strongest AP for each Network/SSID + strongest_ap = max(aps, key=lambda ap: ap.strength) + is_connected = any(ap.is_connected for ap in aps) + security_type = get_security_type(strongest_ap.flags, strongest_ap.wpa_flags, strongest_ap.rsn_flags) + + return cls( + ssid=ssid, + strength=strongest_ap.strength, + is_connected=is_connected and is_saved, + security_type=security_type, + is_saved=is_saved, + ) + + +@dataclass(frozen=True) +class AccessPoint: + ssid: str bssid: str - is_saved: bool = False - # saved_path: str + strength: int + is_connected: bool + flags: int + wpa_flags: int + rsn_flags: int + ap_path: str + @classmethod + def from_dbus(cls, ap_props: dict[str, tuple[str, Any]], ap_path: str, active_ap_path: str) -> "AccessPoint": + ssid = bytes(ap_props['Ssid'][1]).decode("utf-8", "replace") + bssid = str(ap_props['HwAddress'][1]) + strength = int(ap_props['Strength'][1]) + flags = int(ap_props['Flags'][1]) + wpa_flags = int(ap_props['WpaFlags'][1]) + rsn_flags = int(ap_props['RsnFlags'][1]) -@dataclass -class WifiManagerCallbacks: - need_auth: Callable[[str], None] | None = None - activated: Callable[[], None] | None = None - forgotten: Callable[[str], None] | None = None - networks_updated: Callable[[list[NetworkInfo]], None] | None = None - connection_failed: Callable[[str, str], None] | None = None # Added for error feedback + return cls( + ssid=ssid, + bssid=bssid, + strength=strength, + is_connected=ap_path == active_ap_path, + flags=flags, + wpa_flags=wpa_flags, + rsn_flags=rsn_flags, + ap_path=ap_path, + ) class WifiManager: - def __init__(self, callbacks): - self.callbacks: WifiManagerCallbacks = callbacks - self.networks: list[NetworkInfo] = [] - self.bus: MessageBus = None - self.device_path: str = "" - self.device_proxy = None - self.saved_connections: dict[str, str] = {} - self.active_ap_path: str = "" - self.scan_task: asyncio.Task | None = None - # Set tethering ssid as "weedle" + first 4 characters of a dongle id - self._tethering_ssid = "weedle" - if Params is not None: - dongle_id = Params().get("DongleId") - if dongle_id: - self._tethering_ssid += "-" + dongle_id[:4] - self.running: bool = True - self._current_connection_ssid: str | None = None + def __init__(self): + self._networks = [] # a network can be comprised of multiple APs + self._active = True # used to not run when not in settings + self._exit = False - async def connect(self) -> None: - """Connect to the DBus system bus.""" + # DBus connections try: - self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() - while not await self._find_wifi_device(): - await asyncio.sleep(1) + self._router_main = DBusRouter(open_dbus_connection_threading(bus="SYSTEM")) # used by scanner / general method calls + self._conn_monitor = open_dbus_connection_blocking(bus="SYSTEM") # used by state monitor thread + self._nm = DBusAddress(NM_PATH, bus_name=NM, interface=NM_IFACE) + except FileNotFoundError: + cloudlog.exception("Failed to connect to system D-Bus") + self._exit = True - await self._setup_signals(self.device_path) - self.active_ap_path = await self.get_active_access_point() - await self.add_tethering_connection(self._tethering_ssid, DEFAULT_TETHERING_PASSWORD) - self.saved_connections = await self._get_saved_connections() - self.scan_task = asyncio.create_task(self._periodic_scan()) - except DBusError as e: - cloudlog.error(f"Failed to connect to DBus: {e}") - raise - except Exception as e: - cloudlog.error(f"Unexpected error during connect: {e}") - raise + # Store wifi device path + self._wifi_device: str | None = None - async def shutdown(self) -> None: - self.running = False - if self.scan_task: - self.scan_task.cancel() - try: - await self.scan_task - except asyncio.CancelledError: - pass - if self.bus: - self.bus.disconnect() + # State + self._connecting_to_ssid: str = "" + self._last_network_update: float = 0.0 - async def _request_scan(self) -> None: - try: - interface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) - await interface.call_request_scan({}) - except DBusError as e: - cloudlog.warning(f"Scan request failed: {str(e)}") + # Callbacks + # TODO: implement a callback queue to avoid blocking UI thread + self._need_auth: Callable[[str], None] | None = None + self._activated: Callable[[], None] | None = None + self._forgotten: Callable[[str], None] | None = None + self._networks_updated: Callable[[list[Network]], None] | None = None + self._disconnected: Callable[[], None] | None = None - async def get_active_access_point(self): - try: - props_iface = self.device_proxy.get_interface(NM_PROPERTIES_IFACE) - ap_path = await props_iface.call_get(NM_WIRELESS_IFACE, 'ActiveAccessPoint') - return ap_path.value - except DBusError as e: - cloudlog.error(f"Error fetching active access point: {str(e)}") - return '' + self._lock = threading.Lock() - async def forget_connection(self, ssid: str) -> bool: - path = self.saved_connections.get(ssid) - if not path: - return False + self._scan_thread = threading.Thread(target=self._network_scanner, daemon=True) + self._scan_thread.start() - try: - nm_iface = await self._get_interface(NM, path, NM_CONNECTION_IFACE) - await nm_iface.call_delete() + self._state_thread = threading.Thread(target=self._monitor_state, daemon=True) + self._state_thread.start() - if self._current_connection_ssid == ssid: - self._current_connection_ssid = None + atexit.register(self.stop) - if ssid in self.saved_connections: - del self.saved_connections[ssid] + def set_callbacks(self, need_auth: Callable[[str], None], + activated: Callable[[], None] | None, + forgotten: Callable[[str], None], + networks_updated: Callable[[list[Network]], None], + disconnected: Callable[[], None]): + self._need_auth = need_auth + self._activated = activated + self._forgotten = forgotten + self._networks_updated = networks_updated + self._disconnected = disconnected - for network in self.networks: - if network.ssid == ssid: - network.is_saved = False - network.is_connected = False + def set_active(self, active: bool): + self._active = active + + # Scan immediately if we haven't scanned in a while + if active and time.monotonic() - self._last_network_update > SCAN_PERIOD_SECONDS / 2: + self._last_network_update = 0.0 + + def _monitor_state(self): + device_path = self._wait_for_wifi_device() + if device_path is None: + return + + rule = MatchRule( + type="signal", + interface=NM_DEVICE_IFACE, + member="StateChanged", + path=device_path, + ) + + # Filter for StateChanged signal + self._conn_monitor.send_and_get_reply(message_bus.AddMatch(rule)) + + with self._conn_monitor.filter(rule, bufsize=SIGNAL_QUEUE_SIZE) as q: + while not self._exit: + # TODO: always run, and ensure callbacks don't block UI thread + if not self._active: + time.sleep(1) + continue + + # Block until a matching signal arrives + try: + msg = self._conn_monitor.recv_until_filtered(q, timeout=1) + except TimeoutError: + continue + + new_state, previous_state, change_reason = msg.body + + # BAD PASSWORD + if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): + self.forget_connection(self._connecting_to_ssid, block=True) + if self._need_auth is not None: + self._need_auth(self._connecting_to_ssid) + self._connecting_to_ssid = "" + + elif new_state == NMDeviceState.ACTIVATED: + if self._activated is not None: + self._update_networks() + self._activated() + self._connecting_to_ssid = "" + + elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: + self._connecting_to_ssid = "" + if self._disconnected is not None: + self._disconnected() + + def _network_scanner(self): + self._wait_for_wifi_device() + + while not self._exit: + if self._active: + if time.monotonic() - self._last_network_update > SCAN_PERIOD_SECONDS: + # Scan for networks every 10 seconds + # TODO: should update when scan is complete (PropertiesChanged), but this is more than good enough for now + self._update_networks() + self._request_scan() + self._last_network_update = time.monotonic() + time.sleep(1 / 2.) + + def _wait_for_wifi_device(self) -> str | None: + with self._lock: + device_path: str | None = None + while not self._exit: + device_path = self._get_wifi_device() + if device_path is not None: break + time.sleep(1) + return device_path - # Notify UI of forgotten connection - if self.callbacks.networks_updated: - self.callbacks.networks_updated(copy.deepcopy(self.networks)) + def _get_wifi_device(self) -> str | None: + if self._wifi_device is not None: + return self._wifi_device - return True - except DBusError as e: - cloudlog.error(f"Failed to delete connection for SSID: {ssid}. Error: {e}") - return False + device_paths = self._router_main.send_and_get_reply(new_method_call(self._nm, 'GetDevices')).body[0] + for device_path in device_paths: + dev_addr = DBusAddress(device_path, bus_name=NM, interface=NM_DEVICE_IFACE) + dev_type = self._router_main.send_and_get_reply(Properties(dev_addr).get('DeviceType')).body[0][1] - async def activate_connection(self, ssid: str) -> bool: - connection_path = self.saved_connections.get(ssid) - if not connection_path: - return False - try: - nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) - await nm_iface.call_activate_connection(connection_path, self.device_path, "/") - return True - except DBusError as e: - cloudlog.error(f"Failed to activate connection {ssid}: {str(e)}") - return False + if dev_type == NM_DEVICE_TYPE_WIFI: + self._wifi_device = device_path + break - async def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False) -> None: - """Connect to a selected Wi-Fi network.""" - try: - self._current_connection_ssid = ssid + return self._wifi_device - if ssid in self.saved_connections: - # Forget old connection if new password provided - if password: - await self.forget_connection(ssid) - await asyncio.sleep(0.2) # NetworkManager delay - else: - # Just activate existing connection - await self.activate_connection(ssid) - return + def _get_connections(self) -> list[str]: + settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) + return list(self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0]) + + def _connection_by_ssid(self, ssid: str, known_connections: list[str] | None = None) -> str | None: + for conn_path in known_connections or self._get_connections(): + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, "GetSettings")) + + # ignore connections removed during iteration (need auth, etc.) + if reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to get connection properties for {conn_path}") + continue + + settings = reply.body[0] + if "802-11-wireless" in settings and settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") == ssid: + return conn_path + return None + + def connect_to_network(self, ssid: str, password: str): + def worker(): + # Clear all connections that may already exist to the network we are connecting to + self._connecting_to_ssid = ssid + self.forget_connection(ssid, block=True) + + is_hidden = False connection = { 'connection': { - 'type': Variant('s', '802-11-wireless'), - 'uuid': Variant('s', str(uuid.uuid4())), - 'id': Variant('s', f'openpilot connection {ssid}'), - 'autoconnect-retries': Variant('i', 0), + 'type': ('s', '802-11-wireless'), + 'uuid': ('s', str(uuid.uuid4())), + 'id': ('s', f'openpilot connection {ssid}'), + 'autoconnect-retries': ('i', 0), }, '802-11-wireless': { - 'ssid': Variant('ay', ssid.encode('utf-8')), - 'hidden': Variant('b', is_hidden), - 'mode': Variant('s', 'infrastructure'), + 'ssid': ('ay', ssid.encode("utf-8")), + 'hidden': ('b', is_hidden), + 'mode': ('s', 'infrastructure'), }, 'ipv4': { - 'method': Variant('s', 'auto'), - 'dns-priority': Variant('i', 600), + 'method': ('s', 'auto'), + 'dns-priority': ('i', 600), }, - 'ipv6': {'method': Variant('s', 'ignore')}, + 'ipv6': {'method': ('s', 'ignore')}, } - if bssid: - connection['802-11-wireless']['bssid'] = Variant('ay', bssid.encode('utf-8')) - if password: connection['802-11-wireless-security'] = { - 'key-mgmt': Variant('s', 'wpa-psk'), - 'auth-alg': Variant('s', 'open'), - 'psk': Variant('s', password), + 'key-mgmt': ('s', 'wpa-psk'), + 'auth-alg': ('s', 'open'), + 'psk': ('s', password), } - nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) - await nm_iface.call_add_and_activate_connection(connection, self.device_path, "/") - except Exception as e: - self._current_connection_ssid = None - cloudlog.error(f"Error connecting to network: {e}") - # Notify UI of failure - if self.callbacks.connection_failed: - self.callbacks.connection_failed(ssid, str(e)) + settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) + self._router_main.send_and_get_reply(new_method_call(settings_addr, 'AddConnection', 'a{sa{sv}}', (connection,))) + self.activate_connection(ssid, block=True) - def is_saved(self, ssid: str) -> bool: - return ssid in self.saved_connections + threading.Thread(target=worker, daemon=True).start() - async def _find_wifi_device(self) -> bool: - nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) - devices = await nm_iface.get_devices() + def forget_connection(self, ssid: str, block: bool = False): + def worker(): + conn_path = self._connection_by_ssid(ssid) + if conn_path is not None: + conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) + self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) - for device_path in devices: - device = await self.bus.introspect(NM, device_path) - device_proxy = self.bus.get_proxy_object(NM, device_path, device) - device_interface = device_proxy.get_interface(NM_DEVICE_IFACE) - device_type = await device_interface.get_device_type() # type: ignore[attr-defined] - if device_type == 2: # Wi-Fi device - self.device_path = device_path - self.device_proxy = device_proxy - return True + if self._forgotten is not None: + self._update_networks() + self._forgotten(ssid) - return False + if block: + worker() + else: + threading.Thread(target=worker, daemon=True).start() - async def add_tethering_connection(self, ssid: str, password: str = "12345678") -> bool: - """Create a WiFi tethering connection.""" - if len(password) < 8: - print("Tethering password must be at least 8 characters") - return False + def activate_connection(self, ssid: str, block: bool = False): + def worker(): + conn_path = self._connection_by_ssid(ssid) + if conn_path is not None: + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return - try: - # First, check if a hotspot connection already exists - settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) - connection_paths = await settings_iface.call_list_connections() + self._connecting_to_ssid = ssid + self._router_main.send(new_method_call(self._nm, 'ActivateConnection', 'ooo', + (conn_path, self._wifi_device, "/"))) - # Look for an existing hotspot connection - for path in connection_paths: - try: - settings = await self._get_connection_settings(path) - conn_type = settings.get('connection', {}).get('type', Variant('s', '')).value - wifi_mode = settings.get('802-11-wireless', {}).get('mode', Variant('s', '')).value + if block: + worker() + else: + threading.Thread(target=worker, daemon=True).start() - if conn_type == '802-11-wireless' and wifi_mode == 'ap': - # Extract the SSID to check - connection_ssid = self._extract_ssid(settings) - if connection_ssid == ssid: - return True - except DBusError: + def _request_scan(self): + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return + + wifi_addr = DBusAddress(self._wifi_device, bus_name=NM, interface=NM_WIRELESS_IFACE) + reply = self._router_main.send_and_get_reply(new_method_call(wifi_addr, 'RequestScan', 'a{sv}', ({},))) + + if reply.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to request scan: {reply}") + + def _update_networks(self): + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return + + # returns '/' if no active AP + wifi_addr = DBusAddress(self._wifi_device, NM, interface=NM_WIRELESS_IFACE) + active_ap_path = self._router_main.send_and_get_reply(Properties(wifi_addr).get('ActiveAccessPoint')).body[0][1] + ap_paths = self._router_main.send_and_get_reply(new_method_call(wifi_addr, 'GetAllAccessPoints')).body[0] + + aps: dict[str, list[AccessPoint]] = {} + + for ap_path in ap_paths: + ap_addr = DBusAddress(ap_path, NM, interface=NM_ACCESS_POINT_IFACE) + ap_props = self._router_main.send_and_get_reply(Properties(ap_addr).get_all()) + + # some APs have been seen dropping off during iteration + if ap_props.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to get AP properties for {ap_path}") + continue + + try: + ap = AccessPoint.from_dbus(ap_props.body[0], ap_path, active_ap_path) + if ap.ssid == "": continue - connection = { - 'connection': { - 'id': Variant('s', 'Hotspot'), - 'uuid': Variant('s', str(uuid.uuid4())), - 'type': Variant('s', '802-11-wireless'), - 'interface-name': Variant('s', 'wlan0'), - 'autoconnect': Variant('b', False), - }, - '802-11-wireless': { - 'band': Variant('s', 'bg'), - 'mode': Variant('s', 'ap'), - 'ssid': Variant('ay', ssid.encode('utf-8')), - }, - '802-11-wireless-security': { - 'group': Variant('as', ['ccmp']), - 'key-mgmt': Variant('s', 'wpa-psk'), - 'pairwise': Variant('as', ['ccmp']), - 'proto': Variant('as', ['rsn']), - 'psk': Variant('s', password), - }, - 'ipv4': { - 'method': Variant('s', 'shared'), - 'address-data': Variant('aa{sv}', [{'address': Variant('s', TETHERING_IP_ADDRESS), 'prefix': Variant('u', 24)}]), - 'gateway': Variant('s', TETHERING_IP_ADDRESS), - 'never-default': Variant('b', True), - }, - 'ipv6': { - 'method': Variant('s', 'ignore'), - }, - } + if ap.ssid not in aps: + aps[ap.ssid] = [] - settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) - new_connection = await settings_iface.call_add_connection(connection) - print(f"Added tethering connection with path: {new_connection}") - return True - except DBusError as e: - print(f"Failed to add tethering connection: {e}") - return False - except Exception as e: - print(f"Unexpected error adding tethering connection: {e}") - return False + aps[ap.ssid].append(ap) + except Exception: + # catch all for parsing errors + cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - async def get_tethering_password(self) -> str: - """Get the current tethering password.""" - try: - hotspot_path = self.saved_connections.get(self._tethering_ssid) - if hotspot_path: - conn_iface = await self._get_interface(NM, hotspot_path, NM_CONNECTION_IFACE) - secrets = await conn_iface.call_get_secrets('802-11-wireless-security') - if secrets and '802-11-wireless-security' in secrets: - psk = secrets.get('802-11-wireless-security', {}).get('psk', Variant('s', '')).value - return str(psk) if psk is not None else "" - return "" - except DBusError as e: - print(f"Failed to get tethering password: {e}") - return "" - except Exception as e: - print(f"Unexpected error getting tethering password: {e}") - return "" + known_connections = self._get_connections() + networks = [Network.from_dbus(ssid, ap_list, self._connection_by_ssid(ssid, known_connections) is not None) + for ssid, ap_list in aps.items()] + networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower())) + self._networks = networks - async def set_tethering_password(self, password: str) -> bool: - """Set the tethering password.""" - if len(password) < 8: - cloudlog.error("Tethering password must be at least 8 characters") - return False + if self._networks_updated is not None: + self._networks_updated(self._networks) - try: - hotspot_path = self.saved_connections.get(self._tethering_ssid) - if not hotspot_path: - print("No hotspot connection found") - return False + def __del__(self): + self.stop() - # Update the connection settings with new password - settings = await self._get_connection_settings(hotspot_path) - if '802-11-wireless-security' not in settings: - settings['802-11-wireless-security'] = {} - settings['802-11-wireless-security']['psk'] = Variant('s', password) + def stop(self): + if not self._exit: + self._exit = True + self._scan_thread.join() + self._state_thread.join() - # Apply changes - conn_iface = await self._get_interface(NM, hotspot_path, NM_CONNECTION_IFACE) - await conn_iface.call_update(settings) - - # Check if connection is active and restart if needed - is_active = False - nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) - active_connections = await nm_iface.get_active_connections() - - for conn_path in active_connections: - props_iface = await self._get_interface(NM, conn_path, NM_PROPERTIES_IFACE) - conn_id_path = await props_iface.call_get('org.freedesktop.NetworkManager.Connection.Active', 'Connection') - if conn_id_path.value == hotspot_path: - is_active = True - await nm_iface.call_deactivate_connection(conn_path) - break - - if is_active: - await nm_iface.call_activate_connection(hotspot_path, self.device_path, "/") - - print("Tethering password updated successfully") - return True - except DBusError as e: - print(f"Failed to set tethering password: {e}") - return False - except Exception as e: - print(f"Unexpected error setting tethering password: {e}") - return False - - async def is_tethering_active(self) -> bool: - """Check if tethering is active for the specified SSID.""" - try: - hotspot_path = self.saved_connections.get(self._tethering_ssid) - if not hotspot_path: - return False - - nm_iface = await self._get_interface(NM, NM_PATH, NM_IFACE) - active_connections = await nm_iface.get_active_connections() - - for conn_path in active_connections: - props_iface = await self._get_interface(NM, conn_path, NM_PROPERTIES_IFACE) - conn_id_path = await props_iface.call_get('org.freedesktop.NetworkManager.Connection.Active', 'Connection') - - if conn_id_path.value == hotspot_path: - return True - - return False - except Exception: - return False - - async def _periodic_scan(self): - while self.running: - try: - await self._request_scan() - await asyncio.sleep(30) - except asyncio.CancelledError: - break - except DBusError as e: - cloudlog.error(f"Scan failed: {e}") - await asyncio.sleep(5) - - async def _setup_signals(self, device_path: str) -> None: - rules = [ - f"type='signal',interface='{NM_PROPERTIES_IFACE}',member='PropertiesChanged',path='{device_path}'", - f"type='signal',interface='{NM_DEVICE_IFACE}',member='StateChanged',path='{device_path}'", - f"type='signal',interface='{NM_SETTINGS_IFACE}',member='NewConnection',path='{NM_SETTINGS_PATH}'", - f"type='signal',interface='{NM_SETTINGS_IFACE}',member='ConnectionRemoved',path='{NM_SETTINGS_PATH}'", - ] - for rule in rules: - await self._add_match_rule(rule) - - # Set up signal handlers - self.device_proxy.get_interface(NM_PROPERTIES_IFACE).on_properties_changed(self._on_properties_changed) - self.device_proxy.get_interface(NM_DEVICE_IFACE).on_state_changed(self._on_state_changed) - - settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) - settings_iface.on_new_connection(self._on_new_connection) - settings_iface.on_connection_removed(self._on_connection_removed) - - def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): - if interface == NM_WIRELESS_IFACE and 'LastScan' in changed: - asyncio.create_task(self._refresh_networks()) - elif interface == NM_WIRELESS_IFACE and "ActiveAccessPoint" in changed: - new_ap_path = changed["ActiveAccessPoint"].value - if self.active_ap_path != new_ap_path: - self.active_ap_path = new_ap_path - - def _on_state_changed(self, new_state: int, old_state: int, reason: int): - if new_state == NMDeviceState.ACTIVATED: - if self.callbacks.activated: - self.callbacks.activated() - self._current_connection_ssid = None - asyncio.create_task(self._refresh_networks()) - elif new_state in (NMDeviceState.DISCONNECTED, NMDeviceState.NEED_AUTH): - for network in self.networks: - network.is_connected = False - - # BAD PASSWORD - if new_state == NMDeviceState.NEED_AUTH and reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and self.callbacks.need_auth: - if self._current_connection_ssid: - asyncio.create_task(self.forget_connection(self._current_connection_ssid)) - self.callbacks.need_auth(self._current_connection_ssid) - else: - # Try to find the network from active_ap_path - for network in self.networks: - if network.path == self.active_ap_path: - asyncio.create_task(self.forget_connection(network.ssid)) - self.callbacks.need_auth(network.ssid) - break - else: - # Couldn't identify the network that needs auth - cloudlog.error("Network needs authentication but couldn't identify which one") - - def _on_new_connection(self, path: str) -> None: - """Callback for NewConnection signal.""" - asyncio.create_task(self._add_saved_connection(path)) - - def _on_connection_removed(self, path: str) -> None: - """Callback for ConnectionRemoved signal.""" - for ssid, p in list(self.saved_connections.items()): - if path == p: - del self.saved_connections[ssid] - - if self.callbacks.forgotten: - self.callbacks.forgotten(ssid) - break - - async def _add_saved_connection(self, path: str) -> None: - """Add a new saved connection to the dictionary.""" - try: - settings = await self._get_connection_settings(path) - if ssid := self._extract_ssid(settings): - self.saved_connections[ssid] = path - except DBusError as e: - cloudlog.error(f"Failed to add connection {path}: {e}") - - def _extract_ssid(self, settings: dict) -> str | None: - """Extract SSID from connection settings.""" - ssid_variant = settings.get('802-11-wireless', {}).get('ssid', Variant('ay', b'')).value - return bytes(ssid_variant).decode('utf-8') if ssid_variant else None - - async def _add_match_rule(self, rule): - """Add a match rule on the bus.""" - reply = await self.bus.call( - Message( - message_type=MessageType.METHOD_CALL, - destination='org.freedesktop.DBus', - interface="org.freedesktop.DBus", - path='/org/freedesktop/DBus', - member='AddMatch', - signature='s', - body=[rule], - ) - ) - - assert reply.message_type == MessageType.METHOD_RETURN - return reply - - async def _refresh_networks(self): - """Get a list of available networks via NetworkManager.""" - wifi_iface = self.device_proxy.get_interface(NM_WIRELESS_IFACE) - access_points = await wifi_iface.get_access_points() - self.active_ap_path = await self.get_active_access_point() - network_dict = {} - for ap_path in access_points: - try: - props_iface = await self._get_interface(NM, ap_path, NM_PROPERTIES_IFACE) - properties = await props_iface.call_get_all('org.freedesktop.NetworkManager.AccessPoint') - ssid_variant = properties['Ssid'].value - ssid = bytes(ssid_variant).decode('utf-8') - if not ssid: - continue - - bssid = properties.get('HwAddress', Variant('s', '')).value - strength = properties['Strength'].value - flags = properties['Flags'].value - wpa_flags = properties['WpaFlags'].value - rsn_flags = properties['RsnFlags'].value - - # May be multiple access points for each SSID. Use first for ssid - # and security type, then update the rest using all APs - if ssid not in network_dict: - network_dict[ssid] = NetworkInfo( - ssid=ssid, - strength=0, - security_type=self._get_security_type(flags, wpa_flags, rsn_flags), - path="", - bssid="", - is_connected=False, - is_saved=ssid in self.saved_connections - ) - - existing_network = network_dict.get(ssid) - if existing_network.strength < strength: - existing_network.strength = strength - existing_network.path = ap_path - existing_network.bssid = bssid - if self.active_ap_path == ap_path: - existing_network.is_connected = self._current_connection_ssid != ssid - - except DBusError as e: - cloudlog.error(f"Error fetching networks: {e}") - except Exception as e: - cloudlog.error({e}) - - self.networks = sorted( - network_dict.values(), - key=lambda network: ( - not network.is_connected, - -network.strength, # Higher signal strength first - network.ssid.lower(), - ), - ) - - if self.callbacks.networks_updated: - self.callbacks.networks_updated(copy.deepcopy(self.networks)) - - async def _get_connection_settings(self, path): - """Fetch connection settings for a specific connection path.""" - try: - settings = await self._get_interface(NM, path, NM_CONNECTION_IFACE) - return await settings.call_get_settings() - except DBusError as e: - cloudlog.error(f"Failed to get settings for {path}: {str(e)}") - return {} - - async def _process_chunk(self, paths_chunk): - """Process a chunk of connection paths.""" - tasks = [self._get_connection_settings(path) for path in paths_chunk] - return await asyncio.gather(*tasks, return_exceptions=True) - - async def _get_saved_connections(self) -> dict[str, str]: - try: - settings_iface = await self._get_interface(NM, NM_SETTINGS_PATH, NM_SETTINGS_IFACE) - connection_paths = await settings_iface.call_list_connections() - saved_ssids: dict[str, str] = {} - batch_size = 20 - for i in range(0, len(connection_paths), batch_size): - chunk = connection_paths[i : i + batch_size] - results = await self._process_chunk(chunk) - for path, config in zip(chunk, results, strict=True): - if isinstance(config, dict) and '802-11-wireless' in config: - if ssid := self._extract_ssid(config): - saved_ssids[ssid] = path - return saved_ssids - except DBusError as e: - cloudlog.error(f"Error fetching saved connections: {str(e)}") - return {} - - async def _get_interface(self, bus_name: str, path: str, name: str): - introspection = await self.bus.introspect(bus_name, path) - proxy = self.bus.get_proxy_object(bus_name, path, introspection) - return proxy.get_interface(name) - - def _get_security_type(self, flags: int, wpa_flags: int, rsn_flags: int) -> SecurityType: - """Determine the security type based on flags.""" - if flags == 0 and not (wpa_flags or rsn_flags): - return SecurityType.OPEN - if rsn_flags & 0x200: # SAE (WPA3 Personal) - # TODO: support WPA3 - return SecurityType.UNSUPPORTED - if rsn_flags: # RSN indicates WPA2 or higher - return SecurityType.WPA2 - if wpa_flags: # WPA flags indicate WPA - return SecurityType.WPA - return SecurityType.UNSUPPORTED - - -class WifiManagerWrapper: - def __init__(self): - self._manager: WifiManager | None = None - self._callbacks: WifiManagerCallbacks = WifiManagerCallbacks() - - self._thread = threading.Thread(target=self._run, daemon=True) - self._loop: asyncio.EventLoop | None = None - self._running = False - - def set_callbacks(self, callbacks: WifiManagerCallbacks): - self._callbacks = callbacks - - def start(self) -> None: - if not self._running: - self._thread.start() - while self._thread is not None and not self._running: - time.sleep(0.1) - - def _run(self): - self._loop = asyncio.new_event_loop() - asyncio.set_event_loop(self._loop) - - try: - self._manager = WifiManager(self._callbacks) - self._running = True - self._loop.run_forever() - except Exception as e: - cloudlog.error(f"Error in WifiManagerWrapper thread: {e}") - finally: - if self._loop.is_running(): - self._loop.stop() - self._running = False - - def shutdown(self) -> None: - if self._running: - if self._manager is not None and self._loop: - shutdown_future = asyncio.run_coroutine_threadsafe(self._manager.shutdown(), self._loop) - shutdown_future.result(timeout=3.0) - - if self._loop and self._loop.is_running(): - self._loop.call_soon_threadsafe(self._loop.stop) - if self._thread and self._thread.is_alive(): - self._thread.join(timeout=2.0) - self._running = False - - def is_saved(self, ssid: str) -> bool: - """Check if a network is saved.""" - return self._run_coroutine_sync(lambda manager: manager.is_saved(ssid), default=False) - - def connect(self): - """Connect to DBus and start Wi-Fi scanning.""" - if not self._manager: - return - self._run_coroutine(self._manager.connect()) - - def forget_connection(self, ssid: str): - """Forget a saved Wi-Fi connection.""" - if not self._manager: - return - self._run_coroutine(self._manager.forget_connection(ssid)) - - def activate_connection(self, ssid: str): - """Activate an existing Wi-Fi connection.""" - if not self._manager: - return - self._run_coroutine(self._manager.activate_connection(ssid)) - - def connect_to_network(self, ssid: str, password: str = None, bssid: str = None, is_hidden: bool = False): - """Connect to a Wi-Fi network.""" - if not self._manager: - return - self._run_coroutine(self._manager.connect_to_network(ssid, password, bssid, is_hidden)) - - def _run_coroutine(self, coro): - """Run a coroutine in the async thread.""" - if not self._running or not self._loop: - cloudlog.error("WifiManager thread is not running") - return - asyncio.run_coroutine_threadsafe(coro, self._loop) - - def _run_coroutine_sync(self, func: Callable[[WifiManager], T], default: T) -> T: - """Run a function synchronously in the async thread.""" - if not self._running or not self._loop or not self._manager: - return default - future = concurrent.futures.Future[T]() - - def wrapper(manager: WifiManager) -> None: - try: - future.set_result(func(manager)) - except Exception as e: - future.set_exception(e) - - try: - self._loop.call_soon_threadsafe(wrapper, self._manager) - return future.result(timeout=1.0) - except Exception as e: - cloudlog.error(f"WifiManagerWrapper property access failed: {e}") - return default + self._router_main.close() + self._router_main.conn.close() + self._conn_monitor.close() diff --git a/system/ui/setup.py b/system/ui/setup.py index 800ca7662c..a985e783be 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -19,7 +19,7 @@ from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle, ButtonRadio from openpilot.system.ui.widgets.keyboard import Keyboard from openpilot.system.ui.widgets.label import Label, TextAlignment -from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper +from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManager NetworkType = log.DeviceState.NetworkType @@ -72,8 +72,7 @@ class Setup(Widget): self.download_url = "" self.download_progress = 0 self.download_thread = None - self.wifi_manager = WifiManagerWrapper() - self.wifi_ui = WifiManagerUI(self.wifi_manager) + self.wifi_ui = WifiManagerUI(WifiManager()) self.keyboard = Keyboard() self.selected_radio = None self.warning = gui_app.texture("icons/warning.png", 150, 150) diff --git a/system/ui/updater.py b/system/ui/updater.py index 31799d3628..b3cdc82cf5 100755 --- a/system/ui/updater.py +++ b/system/ui/updater.py @@ -7,7 +7,7 @@ from enum import IntEnum from openpilot.system.hardware import HARDWARE from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper +from openpilot.system.ui.lib.wifi_manager import WifiManager from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import gui_button, ButtonStyle from openpilot.system.ui.widgets.label import gui_text_box, gui_label @@ -43,8 +43,7 @@ class Updater(Widget): self.show_reboot_button = False self.process = None self.update_thread = None - self.wifi_manager = WifiManagerWrapper() - self.wifi_manager_ui = WifiManagerUI(self.wifi_manager) + self.wifi_manager_ui = WifiManagerUI(WifiManager()) def install_update(self): self.current_screen = Screen.PROGRESS diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 2ecfd36be7..6f5a00015b 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -6,7 +6,7 @@ from typing import cast import pyray as rl from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.wifi_manager import NetworkInfo, WifiManagerCallbacks, WifiManagerWrapper, SecurityType +from openpilot.system.ui.lib.wifi_manager import WifiManager, SecurityType, Network from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import ButtonStyle, Button from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog @@ -36,34 +36,35 @@ class UIState(IntEnum): class WifiManagerUI(Widget): - def __init__(self, wifi_manager: WifiManagerWrapper): + def __init__(self, wifi_manager: WifiManager): super().__init__() + self.wifi_manager = wifi_manager self.state: UIState = UIState.IDLE - self._state_network: NetworkInfo | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING + self._state_network: Network | None = None # for CONNECTING / NEEDS_AUTH / SHOW_FORGET_CONFIRM / FORGETTING self._password_retry: bool = False # for NEEDS_AUTH self.btn_width: int = 200 self.scroll_panel = GuiScrollPanel() self.keyboard = Keyboard(max_text_size=MAX_PASSWORD_LENGTH, min_text_size=MIN_PASSWORD_LENGTH, show_password_toggle=True) self._load_icons() - self._networks: list[NetworkInfo] = [] + self._networks: list[Network] = [] self._networks_buttons: dict[str, Button] = {} self._forget_networks_buttons: dict[str, Button] = {} self._lock = Lock() - self.wifi_manager = wifi_manager self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") - self.wifi_manager.set_callbacks( - WifiManagerCallbacks( - need_auth=self._on_need_auth, - activated=self._on_activated, - forgotten=self._on_forgotten, - networks_updated=self._on_network_updated, - connection_failed=self._on_connection_failed - ) - ) - self.wifi_manager.start() - self.wifi_manager.connect() + self.wifi_manager.set_callbacks(need_auth=self._on_need_auth, + activated=self._on_activated, + forgotten=self._on_forgotten, + networks_updated=self._on_network_updated, + disconnected=self._on_disconnected) + + def show_event(self): + # start/stop scanning when widget is visible + self.wifi_manager.set_active(True) + + def hide_event(self): + self.wifi_manager.set_active(False) def _load_icons(self): for icon in STRENGTH_ICONS + ["icons/checkmark.png", "icons/circled_slash.png", "icons/lock_closed.png"]: @@ -78,7 +79,7 @@ class WifiManagerUI(Widget): if self.state == UIState.NEEDS_AUTH and self._state_network: self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") self.keyboard.reset() - gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(NetworkInfo, self._state_network), result)) + gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') self._confirm_dialog.reset() @@ -86,7 +87,7 @@ class WifiManagerUI(Widget): else: self._draw_network_list(rect) - def _on_password_entered(self, network: NetworkInfo, result: int): + def _on_password_entered(self, network: Network, result: int): if result == 1: password = self.keyboard.text self.keyboard.clear() @@ -121,7 +122,7 @@ class WifiManagerUI(Widget): rl.end_scissor_mode() - def _draw_network_item(self, rect, network: NetworkInfo, clicked: bool): + def _draw_network_item(self, rect, network: Network, clicked: bool): spacing = 50 ssid_rect = rl.Rectangle(rect.x, rect.y, rect.width - self.btn_width * 2, ITEM_HEIGHT) signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) @@ -174,10 +175,10 @@ class WifiManagerUI(Widget): self.state = UIState.SHOW_FORGET_CONFIRM self._state_network = network - def _draw_status_icon(self, rect, network: NetworkInfo): + def _draw_status_icon(self, rect, network: Network): """Draw the status icon based on network's connection state""" icon_file = None - if network.is_connected: + if network.is_connected and self.state != UIState.CONNECTING: icon_file = "icons/checkmark.png" elif network.security_type == SecurityType.UNSUPPORTED: icon_file = "icons/circled_slash.png" @@ -191,12 +192,12 @@ class WifiManagerUI(Widget): icon_rect = rl.Vector2(rect.x, rect.y + (ICON_SIZE - texture.height) / 2) rl.draw_texture_v(texture, icon_rect, rl.WHITE) - def _draw_signal_strength_icon(self, rect: rl.Rectangle, network: NetworkInfo): + def _draw_signal_strength_icon(self, rect: rl.Rectangle, network: Network): """Draw the Wi-Fi signal strength icon based on network's signal strength""" strength_level = max(0, min(3, round(network.strength / 33.0))) rl.draw_texture_v(gui_app.texture(STRENGTH_ICONS[strength_level], ICON_SIZE, ICON_SIZE), rl.Vector2(rect.x, rect.y), rl.WHITE) - def connect_to_network(self, network: NetworkInfo, password=''): + def connect_to_network(self, network: Network, password=''): self.state = UIState.CONNECTING self._state_network = network if network.is_saved and not password: @@ -204,13 +205,12 @@ class WifiManagerUI(Widget): else: self.wifi_manager.connect_to_network(network.ssid, password) - def forget_network(self, network: NetworkInfo): + def forget_network(self, network: Network): self.state = UIState.FORGETTING self._state_network = network - network.is_saved = False self.wifi_manager.forget_connection(network.ssid) - def _on_network_updated(self, networks: list[NetworkInfo]): + def _on_network_updated(self, networks: list[Network]): with self._lock: self._networks = networks for n in self._networks: @@ -237,7 +237,7 @@ class WifiManagerUI(Widget): if self.state == UIState.FORGETTING: self.state = UIState.IDLE - def _on_connection_failed(self, ssid: str, error: str): + def _on_disconnected(self): with self._lock: if self.state == UIState.CONNECTING: self.state = UIState.IDLE @@ -245,13 +245,11 @@ class WifiManagerUI(Widget): def main(): gui_app.init_window("Wi-Fi Manager") - wifi_manager = WifiManagerWrapper() - wifi_ui = WifiManagerUI(wifi_manager) + wifi_ui = WifiManagerUI(WifiManager()) for _ in gui_app.render(): wifi_ui.render(rl.Rectangle(50, 50, gui_app.width - 100, gui_app.height - 100)) - wifi_manager.shutdown() gui_app.close() diff --git a/uv.lock b/uv.lock index 7010cdfb83..72c3d2f8e2 100644 --- a/uv.lock +++ b/uv.lock @@ -442,15 +442,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/c8/46ac27096684f33e27dab749ef43c6b0119c6a0d852971eaefb73256dc4c/cython-3.1.3-py3-none-any.whl", hash = "sha256:d13025b34f72f77bf7f65c1cd628914763e6c285f4deb934314c922b91e6be5a", size = 1225725, upload-time = "2025-08-13T06:19:09.593Z" }, ] -[[package]] -name = "dbus-next" -version = "0.2.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ce/45/6a40fbe886d60a8c26f480e7d12535502b5ba123814b3b9a0b002ebca198/dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5", size = 71112, upload-time = "2021-07-25T22:11:28.398Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" }, -] - [[package]] name = "dictdiffer" version = "0.9.0" @@ -690,6 +681,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/15/aa/0aca39a37d3c7eb941ba736ede56d689e7be91cab5d9ca846bde3999eba6/isodate-0.7.2-py3-none-any.whl", hash = "sha256:28009937d8031054830160fce6d409ed342816b543597cece116d966c6d99e15", size = 22320, upload-time = "2024-10-08T23:04:09.501Z" }, ] +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + [[package]] name = "jinja2" version = "3.1.6" @@ -1241,8 +1241,8 @@ dev = [ { name = "av" }, { name = "azure-identity" }, { name = "azure-storage-blob" }, - { name = "dbus-next" }, { name = "dictdiffer" }, + { name = "jeepney" }, { name = "matplotlib" }, { name = "opencv-python-headless" }, { name = "parameterized" }, @@ -1294,11 +1294,11 @@ requires-dist = [ { name = "codespell", marker = "extra == 'testing'" }, { name = "crcmod" }, { name = "cython" }, - { name = "dbus-next", marker = "extra == 'dev'" }, { name = "dictdiffer", marker = "extra == 'dev'" }, { name = "future-fstrings" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, { name = "inputs" }, + { name = "jeepney", marker = "extra == 'dev'" }, { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, { name = "libusb1" }, From 23b4aaf2a5e3008bf230cfaa86027b1ccbaedbb7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 26 Aug 2025 03:25:01 -0700 Subject: [PATCH 016/188] raylib networking: remove locking on UI thread (#36063) * use callback queue to make this thread safe and remove locks (which lag ui thread?) * woah this works * no more lock! * always run signal handler and store callbacks, like qt * debug * more * okay not for now * combine _get_connections and _connection_by_ssid, closer to qt and not an explosion of GetSettings dbus calls * debug * try this * skip * len * skip hidden networks * actually slower * stash * back to 8929f37d495a524d4a996d66b82d4a947fbf4f1c * clean up --- system/ui/lib/wifi_manager.py | 26 ++++++++----- system/ui/widgets/network.py | 72 ++++++++++++++++------------------- 2 files changed, 50 insertions(+), 48 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 96940c2b3b..58181157c0 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -133,12 +133,12 @@ class WifiManager: # State self._connecting_to_ssid: str = "" self._last_network_update: float = 0.0 + self._callback_queue: list[Callable] = [] # Callbacks - # TODO: implement a callback queue to avoid blocking UI thread self._need_auth: Callable[[str], None] | None = None self._activated: Callable[[], None] | None = None - self._forgotten: Callable[[str], None] | None = None + self._forgotten: Callable[[], None] | None = None self._networks_updated: Callable[[list[Network]], None] | None = None self._disconnected: Callable[[], None] | None = None @@ -154,7 +154,7 @@ class WifiManager: def set_callbacks(self, need_auth: Callable[[str], None], activated: Callable[[], None] | None, - forgotten: Callable[[str], None], + forgotten: Callable[[], None], networks_updated: Callable[[list[Network]], None], disconnected: Callable[[], None]): self._need_auth = need_auth @@ -163,6 +163,15 @@ class WifiManager: self._networks_updated = networks_updated self._disconnected = disconnected + def _enqueue_callback(self, cb: Callable, *args): + self._callback_queue.append(lambda: cb(*args)) + + def process_callbacks(self): + # Call from UI thread to run any pending callbacks + to_run, self._callback_queue = self._callback_queue, [] + for cb in to_run: + cb() + def set_active(self, active: bool): self._active = active @@ -187,7 +196,6 @@ class WifiManager: with self._conn_monitor.filter(rule, bufsize=SIGNAL_QUEUE_SIZE) as q: while not self._exit: - # TODO: always run, and ensure callbacks don't block UI thread if not self._active: time.sleep(1) continue @@ -204,19 +212,19 @@ class WifiManager: if new_state == NMDeviceState.NEED_AUTH and change_reason == NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT and len(self._connecting_to_ssid): self.forget_connection(self._connecting_to_ssid, block=True) if self._need_auth is not None: - self._need_auth(self._connecting_to_ssid) + self._enqueue_callback(self._need_auth, self._connecting_to_ssid) self._connecting_to_ssid = "" elif new_state == NMDeviceState.ACTIVATED: if self._activated is not None: self._update_networks() - self._activated() + self._enqueue_callback(self._activated) self._connecting_to_ssid = "" elif new_state == NMDeviceState.DISCONNECTED and change_reason != NM_DEVICE_STATE_REASON_NEW_ACTIVATION: self._connecting_to_ssid = "" if self._disconnected is not None: - self._disconnected() + self._enqueue_callback(self._disconnected) def _network_scanner(self): self._wait_for_wifi_device() @@ -324,7 +332,7 @@ class WifiManager: if self._forgotten is not None: self._update_networks() - self._forgotten(ssid) + self._enqueue_callback(self._forgotten) if block: worker() @@ -400,7 +408,7 @@ class WifiManager: self._networks = networks if self._networks_updated is not None: - self._networks_updated(self._networks) + self._enqueue_callback(self._networks_updated, self._networks) def __del__(self): self.stop() diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 6f5a00015b..3e6317a49c 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,6 +1,5 @@ from enum import IntEnum from functools import partial -from threading import Lock from typing import cast import pyray as rl @@ -50,7 +49,6 @@ class WifiManagerUI(Widget): self._networks: list[Network] = [] self._networks_buttons: dict[str, Button] = {} self._forget_networks_buttons: dict[str, Button] = {} - self._lock = Lock() self._confirm_dialog = ConfirmDialog("", "Forget", "Cancel") self.wifi_manager.set_callbacks(need_auth=self._on_need_auth, @@ -71,21 +69,22 @@ class WifiManagerUI(Widget): gui_app.texture(icon, ICON_SIZE, ICON_SIZE) def _render(self, rect: rl.Rectangle): - with self._lock: - if not self._networks: - gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) - return + self.wifi_manager.process_callbacks() - if self.state == UIState.NEEDS_AUTH and self._state_network: - self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") - self.keyboard.reset() - gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) - elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: - self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') - self._confirm_dialog.reset() - gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) - else: - self._draw_network_list(rect) + if not self._networks: + gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + return + + if self.state == UIState.NEEDS_AUTH and self._state_network: + self.keyboard.set_title("Wrong password" if self._password_retry else "Enter password", f"for {self._state_network.ssid}") + self.keyboard.reset() + gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(cast(Network, self._state_network), result)) + elif self.state == UIState.SHOW_FORGET_CONFIRM and self._state_network: + self._confirm_dialog.set_text(f'Forget Wi-Fi Network "{self._state_network.ssid}"?') + self._confirm_dialog.reset() + gui_app.set_modal_overlay(self._confirm_dialog, callback=lambda result: self.on_forgot_confirm_finished(self._state_network, result)) + else: + self._draw_network_list(rect) def _on_password_entered(self, network: Network, result: int): if result == 1: @@ -211,36 +210,31 @@ class WifiManagerUI(Widget): self.wifi_manager.forget_connection(network.ssid) def _on_network_updated(self, networks: list[Network]): - with self._lock: - self._networks = networks - for n in self._networks: - self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, - button_style=ButtonStyle.NO_EFFECT) - self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, - font_size=45) + self._networks = networks + for n in self._networks: + self._networks_buttons[n.ssid] = Button(n.ssid, partial(self._networks_buttons_callback, n), font_size=55, text_alignment=TextAlignment.LEFT, + button_style=ButtonStyle.NO_EFFECT) + self._forget_networks_buttons[n.ssid] = Button("Forget", partial(self._forget_networks_buttons_callback, n), button_style=ButtonStyle.FORGET_WIFI, + font_size=45) def _on_need_auth(self, ssid): - with self._lock: - network = next((n for n in self._networks if n.ssid == ssid), None) - if network: - self.state = UIState.NEEDS_AUTH - self._state_network = network - self._password_retry = True + network = next((n for n in self._networks if n.ssid == ssid), None) + if network: + self.state = UIState.NEEDS_AUTH + self._state_network = network + self._password_retry = True def _on_activated(self): - with self._lock: - if self.state == UIState.CONNECTING: - self.state = UIState.IDLE + if self.state == UIState.CONNECTING: + self.state = UIState.IDLE - def _on_forgotten(self, ssid): - with self._lock: - if self.state == UIState.FORGETTING: - self.state = UIState.IDLE + def _on_forgotten(self): + if self.state == UIState.FORGETTING: + self.state = UIState.IDLE def _on_disconnected(self): - with self._lock: - if self.state == UIState.CONNECTING: - self.state = UIState.IDLE + if self.state == UIState.CONNECTING: + self.state = UIState.IDLE def main(): From 8059106caee2500bdc8186fd9ce3ea9d36a352f9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 26 Aug 2025 03:33:08 -0700 Subject: [PATCH 017/188] raylib networking: reduce DBus calls (#36065) * this reduces getsettings calls from n*n to n * these are combined now * same check --- system/ui/lib/wifi_manager.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 58181157c0..418c418d01 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -264,12 +264,12 @@ class WifiManager: return self._wifi_device - def _get_connections(self) -> list[str]: + def _get_connections(self) -> dict[str, str]: settings_addr = DBusAddress(NM_SETTINGS_PATH, bus_name=NM, interface=NM_SETTINGS_IFACE) - return list(self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0]) + known_connections = self._router_main.send_and_get_reply(new_method_call(settings_addr, 'ListConnections')).body[0] - def _connection_by_ssid(self, ssid: str, known_connections: list[str] | None = None) -> str | None: - for conn_path in known_connections or self._get_connections(): + conns: dict[str, str] = {} + for conn_path in known_connections: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) reply = self._router_main.send_and_get_reply(new_method_call(conn_addr, "GetSettings")) @@ -279,9 +279,11 @@ class WifiManager: continue settings = reply.body[0] - if "802-11-wireless" in settings and settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") == ssid: - return conn_path - return None + if "802-11-wireless" in settings: + ssid = settings['802-11-wireless']['ssid'][1].decode("utf-8", "replace") + if ssid != "": + conns[ssid] = conn_path + return conns def connect_to_network(self, ssid: str, password: str): def worker(): @@ -325,7 +327,7 @@ class WifiManager: def forget_connection(self, ssid: str, block: bool = False): def worker(): - conn_path = self._connection_by_ssid(ssid) + conn_path = self._get_connections().get(ssid, None) if conn_path is not None: conn_addr = DBusAddress(conn_path, bus_name=NM, interface=NM_CONNECTION_IFACE) self._router_main.send_and_get_reply(new_method_call(conn_addr, 'Delete')) @@ -341,7 +343,7 @@ class WifiManager: def activate_connection(self, ssid: str, block: bool = False): def worker(): - conn_path = self._connection_by_ssid(ssid) + conn_path = self._get_connections().get(ssid, None) if conn_path is not None: if self._wifi_device is None: cloudlog.warning("No WiFi device found") @@ -402,8 +404,7 @@ class WifiManager: cloudlog.exception(f"Failed to parse AP properties for {ap_path}") known_connections = self._get_connections() - networks = [Network.from_dbus(ssid, ap_list, self._connection_by_ssid(ssid, known_connections) is not None) - for ssid, ap_list in aps.items()] + networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()] networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower())) self._networks = networks From ec254074d12b2d8c1446d82e9762b739709952aa Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 26 Aug 2025 03:51:45 -0700 Subject: [PATCH 018/188] raylib: prevent Firehose from blocking UI (#36067) * stop blocking ui thread for 1s!! * rm * whoopsiedaisy whoopsiedaisy * meh --- selfdrive/ui/layouts/settings/firehose.py | 6 +++--- selfdrive/ui/lib/api_helpers.py | 14 ++++++++++++++ selfdrive/ui/lib/prime_state.py | 15 +++------------ 3 files changed, 20 insertions(+), 15 deletions(-) create mode 100644 selfdrive/ui/lib/api_helpers.py diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index 74a8f317d5..b3db1fa5f0 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -2,7 +2,7 @@ import pyray as rl import time import threading -from openpilot.common.api import Api, api_get +from openpilot.common.api import api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.ui_state import ui_state @@ -11,7 +11,7 @@ from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.widgets import Widget - +from openpilot.selfdrive.ui.lib.api_helpers import get_token TITLE = "Firehose Mode" DESCRIPTION = ( @@ -163,7 +163,7 @@ class FirehoseLayout(Widget): dongle_id = self.params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return - identity_token = Api(dongle_id).get_token() + identity_token = get_token(dongle_id) response = api_get(f"v1/devices/{dongle_id}/firehose_stats", access_token=identity_token) if response.status_code == 200: data = response.json() diff --git a/selfdrive/ui/lib/api_helpers.py b/selfdrive/ui/lib/api_helpers.py new file mode 100644 index 0000000000..b83efedb60 --- /dev/null +++ b/selfdrive/ui/lib/api_helpers.py @@ -0,0 +1,14 @@ +import time +from functools import lru_cache +from openpilot.common.api import Api + +TOKEN_EXPIRY_HOURS = 2 + + +@lru_cache(maxsize=1) +def _get_token(dongle_id: str, t: int): + return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS) + + +def get_token(dongle_id: str): + return _get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60))) diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index a1b2472f30..be2132c1b7 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -2,14 +2,12 @@ from enum import IntEnum import os import threading import time -from functools import lru_cache -from openpilot.common.api import Api, api_get +from openpilot.common.api import api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID - -TOKEN_EXPIRY_HOURS = 2 +from openpilot.selfdrive.ui.lib.api_helpers import get_token class PrimeType(IntEnum): @@ -23,11 +21,6 @@ class PrimeType(IntEnum): PURPLE = 5, -@lru_cache(maxsize=1) -def get_token(dongle_id: str, t: int): - return Api(dongle_id).get_token(expiry_hours=TOKEN_EXPIRY_HOURS) - - class PrimeState: FETCH_INTERVAL = 5.0 # seconds between API calls API_TIMEOUT = 10.0 # seconds for API requests @@ -57,15 +50,13 @@ class PrimeState: return try: - identity_token = get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60))) + identity_token = get_token(dongle_id) response = api_get(f"v1.1/devices/{dongle_id}", timeout=self.API_TIMEOUT, access_token=identity_token) if response.status_code == 200: data = response.json() is_paired = data.get("is_paired", False) prime_type = data.get("prime_type", 0) self.set_type(PrimeType(prime_type) if is_paired else PrimeType.UNPAIRED) - elif response.status_code == 401: - get_token.cache_clear() except Exception as e: cloudlog.error(f"Failed to fetch prime status: {e}") From 4cd76f4966c091a0fdb9df7dce9cfb966ed132d7 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Tue, 26 Aug 2025 03:55:05 -0700 Subject: [PATCH 019/188] raylib networking: prevent concurrently updating networks (#36066) * dont run by multiple threads at the same time! * this doesn't work since we rely on is_connected * Revert "this doesn't work since we rely on is_connected" This reverts commit 7455b2fe831bf5c9524e8ee71a9966de32a9755a. --- system/ui/lib/wifi_manager.py | 63 ++++++++++++++++++----------------- 1 file changed, 32 insertions(+), 31 deletions(-) diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index 418c418d01..af9ae943ea 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -370,46 +370,47 @@ class WifiManager: cloudlog.warning(f"Failed to request scan: {reply}") def _update_networks(self): - if self._wifi_device is None: - cloudlog.warning("No WiFi device found") - return + with self._lock: + if self._wifi_device is None: + cloudlog.warning("No WiFi device found") + return - # returns '/' if no active AP - wifi_addr = DBusAddress(self._wifi_device, NM, interface=NM_WIRELESS_IFACE) - active_ap_path = self._router_main.send_and_get_reply(Properties(wifi_addr).get('ActiveAccessPoint')).body[0][1] - ap_paths = self._router_main.send_and_get_reply(new_method_call(wifi_addr, 'GetAllAccessPoints')).body[0] + # returns '/' if no active AP + wifi_addr = DBusAddress(self._wifi_device, NM, interface=NM_WIRELESS_IFACE) + active_ap_path = self._router_main.send_and_get_reply(Properties(wifi_addr).get('ActiveAccessPoint')).body[0][1] + ap_paths = self._router_main.send_and_get_reply(new_method_call(wifi_addr, 'GetAllAccessPoints')).body[0] - aps: dict[str, list[AccessPoint]] = {} + aps: dict[str, list[AccessPoint]] = {} - for ap_path in ap_paths: - ap_addr = DBusAddress(ap_path, NM, interface=NM_ACCESS_POINT_IFACE) - ap_props = self._router_main.send_and_get_reply(Properties(ap_addr).get_all()) + for ap_path in ap_paths: + ap_addr = DBusAddress(ap_path, NM, interface=NM_ACCESS_POINT_IFACE) + ap_props = self._router_main.send_and_get_reply(Properties(ap_addr).get_all()) - # some APs have been seen dropping off during iteration - if ap_props.header.message_type == MessageType.error: - cloudlog.warning(f"Failed to get AP properties for {ap_path}") - continue - - try: - ap = AccessPoint.from_dbus(ap_props.body[0], ap_path, active_ap_path) - if ap.ssid == "": + # some APs have been seen dropping off during iteration + if ap_props.header.message_type == MessageType.error: + cloudlog.warning(f"Failed to get AP properties for {ap_path}") continue - if ap.ssid not in aps: - aps[ap.ssid] = [] + try: + ap = AccessPoint.from_dbus(ap_props.body[0], ap_path, active_ap_path) + if ap.ssid == "": + continue - aps[ap.ssid].append(ap) - except Exception: - # catch all for parsing errors - cloudlog.exception(f"Failed to parse AP properties for {ap_path}") + if ap.ssid not in aps: + aps[ap.ssid] = [] - known_connections = self._get_connections() - networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()] - networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower())) - self._networks = networks + aps[ap.ssid].append(ap) + except Exception: + # catch all for parsing errors + cloudlog.exception(f"Failed to parse AP properties for {ap_path}") - if self._networks_updated is not None: - self._enqueue_callback(self._networks_updated, self._networks) + known_connections = self._get_connections() + networks = [Network.from_dbus(ssid, ap_list, ssid in known_connections) for ssid, ap_list in aps.items()] + networks.sort(key=lambda n: (-n.is_connected, -n.strength, n.ssid.lower())) + self._networks = networks + + if self._networks_updated is not None: + self._enqueue_callback(self._networks_updated, self._networks) def __del__(self): self.stop() From 342ff24510542d5ba2d4bb9c193308db0c9ad462 Mon Sep 17 00:00:00 2001 From: royjr Date: Tue, 26 Aug 2025 11:49:55 -0400 Subject: [PATCH 020/188] feature: external storage (#979) * external storage * fix mountStorage * fix perms * works for now * better * lagless * move to sp qt * orderish * fix ui crash * cleanup * fix format * offroad only * debug external storage * dont care about delete * just use cloudlog * show logs if using external storage * better text * wipe entire drive * allow partitionless drive to be formatted * label while formatting * this works * better * cleaner * cleaner logs * keep upstream happy --------- Co-authored-by: DevTekVE --- selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/developer_panel.cc | 5 + .../sunnypilot/qt/widgets/external_storage.cc | 170 ++++++++++++++++++ .../sunnypilot/qt/widgets/external_storage.h | 34 ++++ system/athena/athenad.py | 24 ++- system/hardware/hw.py | 4 + system/loggerd/config.py | 17 +- system/loggerd/deleter.py | 37 ++++ 8 files changed, 280 insertions(+), 12 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc create mode 100644 selfdrive/ui/sunnypilot/qt/widgets/external_storage.h diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 810338aae8..2f3c8ddd8d 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -4,6 +4,7 @@ widgets_src = [ "sunnypilot/qt/widgets/controls.cc", "sunnypilot/qt/widgets/drive_stats.cc", "sunnypilot/qt/widgets/expandable_row.cc", + "sunnypilot/qt/widgets/external_storage.cc", "sunnypilot/qt/widgets/prime.cc", "sunnypilot/qt/widgets/scrollview.cc", "sunnypilot/qt/network/networking.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc index 58193f9fe8..a4a6bf481c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc @@ -5,9 +5,14 @@ * See the LICENSE.md file in the root directory for more details. */ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h" DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(parent) { + #ifndef __APPLE__ + addItem(new ExternalStorageControl()); + #endif + // Advanced Controls Toggle showAdvancedControls = new ParamControlSP("ShowAdvancedControls", tr("Show Advanced Controls"), tr("Toggle visibility of advanced sunnypilot controls.\nThis only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state."), ""); addItem(showAdvancedControls); diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc new file mode 100644 index 0000000000..f951187511 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.cc @@ -0,0 +1,170 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#include "selfdrive/ui/sunnypilot/qt/widgets/external_storage.h" + +#include +#include +#include +#include +#include + +#include "common/params.h" +#include "selfdrive/ui/qt/api.h" +#include "selfdrive/ui/qt/widgets/input.h" +#include "selfdrive/ui/sunnypilot/ui.h" + +ExternalStorageControl::ExternalStorageControl() : + ButtonControl(tr("External Storage"), "", tr("Extend your comma device's storage by inserting a USB drive into the aux port.")) { + + QObject::connect(this, &ButtonControl::clicked, [=]() { + if (text() == tr("CHECK") || text() == tr("MOUNT")) { + mountStorage(); + } else if (text() == tr("UNMOUNT")) { + unmountStorage(); + } else if (text() == tr("FORMAT")) { + if (ConfirmationDialog::confirm(tr("Are you sure you want to format this drive? This will erase all data."), tr("Format"), this)) { + formatStorage(); + } + } + }); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &ExternalStorageControl::updateState); + updateState(!uiState()->scene.started); + + refresh(); +} + +void ExternalStorageControl::updateState(bool offroad) { + setEnabled(offroad); +} + +void ExternalStorageControl::debouncedRefresh() { + if (refreshPending) return; + refreshPending = true; + + QTimer::singleShot(250, this, [=]() { + refreshPending = false; + refresh(); + }); +} + +void ExternalStorageControl::refresh() { + QtConcurrent::run([=]() { + auto run = [](const QString &cmd) { + QProcess p; + p.start("sh", QStringList() << "-c" << cmd); + p.waitForFinished(); + return p.exitCode() == 0; + }; + + bool isMounted = run("findmnt -n /mnt/external_realdata"); + bool hasDrive = run("lsblk -f /dev/sdg"); + bool hasFs = run("lsblk -f /dev/sdg1 | grep -q ext4"); + bool hasLabel = run("sudo blkid /dev/sdg1 | grep -q 'LABEL=\"openpilot\"'"); + + QString info; + if (isMounted && hasLabel) { + QProcess df; + df.start("sh", QStringList() << "-c" << "df -h /mnt/external_realdata | awk 'NR==2 {print $3 \"/\" $2}'"); + df.waitForFinished(); + info = df.readAllStandardOutput().trimmed(); + } + + QMetaObject::invokeMethod(this, [=]() { + if (formatting) { + setValue(tr("formatting")); + setText(tr("FORMAT")); + setEnabled(false); + } else { + if (!hasDrive) { + setValue(tr("insert drive")); + setText(tr("CHECK")); + } else if (!hasFs || !hasLabel) { + setValue(tr("needs format")); + setText(tr("FORMAT")); + } else if (isMounted) { + setValue(info); + setText(tr("UNMOUNT")); + } else { + setValue("drive detected"); + setText(tr("MOUNT")); + } + updateState(!uiState()->scene.started); + } + }, Qt::QueuedConnection); + }); +} + +void ExternalStorageControl::mountStorage() { + setValue(tr("mounting")); + setEnabled(false); + + QtConcurrent::run([=]() { + QProcess process; + process.start("sh", QStringList() << "-c" << + "sudo mount -o remount,rw / && " + "sudo mkdir -p /mnt/external_realdata && " + "grep -q '/dev/sdg1 /mnt/external_realdata' /etc/fstab || " + "echo '/dev/sdg1 /mnt/external_realdata ext4 defaults,nofail 0 2' | sudo tee -a /etc/fstab && " + "sudo systemctl daemon-reexec && " + "sudo mount /mnt/external_realdata && " + "sudo chown -R comma:comma /mnt/external_realdata && " + "sudo chmod -R 775 /mnt/external_realdata && " + "sudo mount -o remount,ro /"); + process.waitForFinished(); + + QMetaObject::invokeMethod(this, [=]() { + debouncedRefresh(); + }, Qt::QueuedConnection); + }); +} + +void ExternalStorageControl::unmountStorage() { + setValue(tr("unmounting")); + setEnabled(false); + + QtConcurrent::run([=]() { + QProcess process; + process.start("sh", QStringList() << "-c" << "sudo umount /mnt/external_realdata"); + process.waitForFinished(); + + QMetaObject::invokeMethod(this, [=]() { + debouncedRefresh(); + }, Qt::QueuedConnection); + }); +} + +void ExternalStorageControl::formatStorage() { + unmountStorage(); + formatting = true; + setValue(tr("formatting")); + setEnabled(false); + + QProcess *process = new QProcess(this); + connect(process, static_cast(&QProcess::finished), + this, [=](int exitCode, QProcess::ExitStatus status) { + process->deleteLater(); + formatting = false; + if (exitCode == 0 && status == QProcess::NormalExit) { + mountStorage(); + } else { + setValue(tr("needs format")); + updateState(!uiState()->scene.started); + } + }); + process->start("sh", QStringList() << "-c" << + "sudo wipefs -a /dev/sdg && " + "sudo parted -s /dev/sdg mklabel gpt mkpart primary ext4 0% 100% && " + "sudo mkfs.ext4 -F -L openpilot /dev/sdg1" + ); +} + +void ExternalStorageControl::showEvent(QShowEvent *event) { + ButtonControl::showEvent(event); + QTimer::singleShot(100, this, &ExternalStorageControl::debouncedRefresh); +} diff --git a/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h new file mode 100644 index 0000000000..d26eefd18c --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/widgets/external_storage.h @@ -0,0 +1,34 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +#include "system/hardware/hw.h" +#include "selfdrive/ui/sunnypilot/qt/widgets/controls.h" +#define ButtonControl ButtonControlSP + +class ExternalStorageControl : public ButtonControl { + Q_OBJECT + +public: + ExternalStorageControl(); + +protected: + void showEvent(QShowEvent *event) override; + +private: + Params params; + + bool refreshPending = false; + bool formatting = false; + void updateState(bool offroad); + void refresh(); + void debouncedRefresh(); + void mountStorage(); + void unmountStorage(); + void formatStorage(); +}; diff --git a/system/athena/athenad.py b/system/athena/athenad.py index f97b8e55bb..42c9cf8a1c 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -381,20 +381,22 @@ def setNavDestination(latitude: int = 0, longitude: int = 0, place_name: str = N return {"success": 1} -def scan_dir(path: str, prefix: str) -> list[str]: +def scan_dir(path: str, prefix: str, base: str | None = None) -> list[str]: + if base is None: + base = path files = [] # only walk directories that match the prefix # (glob and friends traverse entire dir tree) with os.scandir(path) as i: for e in i: - rel_path = os.path.relpath(e.path, Paths.log_root()) + rel_path = os.path.relpath(e.path, base) if e.is_dir(follow_symlinks=False): # add trailing slash rel_path = os.path.join(rel_path, '') # if prefix is a partial dir name, current dir will start with prefix # if prefix is a partial file name, prefix with start with dir name if rel_path.startswith(prefix) or prefix.startswith(rel_path): - files.extend(scan_dir(e.path, prefix)) + files.extend(scan_dir(e.path, prefix, base)) else: if rel_path.startswith(prefix): files.append(rel_path) @@ -402,7 +404,12 @@ def scan_dir(path: str, prefix: str) -> list[str]: @dispatcher.add_method def listDataDirectory(prefix='') -> list[str]: - return scan_dir(Paths.log_root(), prefix) + internal_files = scan_dir(Paths.log_root(), prefix, Paths.log_root()) + try: + external_files = scan_dir(Paths.log_root_external(), prefix, Paths.log_root_external()) + except FileNotFoundError: + external_files = [] + return sorted(set(internal_files + external_files)) @dispatcher.add_method @@ -427,8 +434,13 @@ def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlRespo failed.append(file.fn) continue - path = os.path.join(Paths.log_root(), file.fn) - if not os.path.exists(path) and not os.path.exists(strip_zst_extension(path)): + path_internal = os.path.join(Paths.log_root(), file.fn) + path_external = os.path.join(Paths.log_root_external(), file.fn) + if os.path.exists(path_internal) or os.path.exists(strip_zst_extension(path_internal)): + path = path_internal + elif os.path.exists(path_external) or os.path.exists(strip_zst_extension(path_external)): + path = path_external + else: failed.append(file.fn) continue diff --git a/system/hardware/hw.py b/system/hardware/hw.py index 5e40fff136..d24857e8bd 100644 --- a/system/hardware/hw.py +++ b/system/hardware/hw.py @@ -20,6 +20,10 @@ class Paths: else: return '/data/media/0/realdata/' + @staticmethod + def log_root_external() -> str: + return '/mnt/external_realdata/' + @staticmethod def swaglog_root() -> str: if PC: diff --git a/system/loggerd/config.py b/system/loggerd/config.py index e1c47c768d..d9befb5613 100644 --- a/system/loggerd/config.py +++ b/system/loggerd/config.py @@ -9,21 +9,26 @@ STATS_DIR_FILE_LIMIT = 10000 STATS_SOCKET = "ipc:///tmp/stats" STATS_FLUSH_TIME_S = 60 -def get_available_percent(default: float) -> float: +PATH_DICT = { + "internal": Paths.log_root(), + "external": Paths.log_root_external() +} + +def get_available_percent(default: float, path_type="internal") -> float: try: - statvfs = os.statvfs(Paths.log_root()) + statvfs = os.statvfs(PATH_DICT[path_type]) available_percent = 100.0 * statvfs.f_bavail / statvfs.f_blocks - except OSError: + except (OSError, KeyError): available_percent = default return available_percent -def get_available_bytes(default: int) -> int: +def get_available_bytes(default: int, path_type="internal") -> int: try: - statvfs = os.statvfs(Paths.log_root()) + statvfs = os.statvfs(PATH_DICT[path_type]) available_bytes = statvfs.f_bavail * statvfs.f_frsize - except OSError: + except (OSError, KeyError): available_bytes = default return available_bytes diff --git a/system/loggerd/deleter.py b/system/loggerd/deleter.py index eb8fd35f21..058f5c301d 100755 --- a/system/loggerd/deleter.py +++ b/system/loggerd/deleter.py @@ -1,7 +1,9 @@ #!/usr/bin/env python3 import os +import time import shutil import threading +from pathlib import Path from openpilot.system.hardware.hw import Paths from openpilot.common.swaglog import cloudlog from openpilot.system.loggerd.config import get_available_bytes, get_available_percent @@ -61,6 +63,41 @@ def deleter_thread(exit_event: threading.Event): if any(name.endswith(".lock") for name in os.listdir(delete_path)): continue + if Path(Paths.log_root_external()).is_mount(): + out_of_bytes_external = get_available_bytes(default=MIN_BYTES + 1, path_type="external") < MIN_BYTES + out_of_percent_external = get_available_percent(default=MIN_PERCENT + 1, path_type="external") < MIN_PERCENT + + if out_of_percent_external or out_of_bytes_external: + dirs_external = listdir_by_creation(Paths.log_root_external()) + + # remove the earliest external directory we can + for delete_dir_external in sorted(dirs_external): + delete_path_external = os.path.join(Paths.log_root_external(), delete_dir_external) + try: + cloudlog.warning(f"deleting {delete_path_external}") + shutil.rmtree(delete_path_external) + break + except OSError: + cloudlog.exception(f"issue deleting {delete_path_external}") + + # move directory from internal to external + path_external = os.path.join(Paths.log_root_external(), delete_dir) + try: + cloudlog.warning(f"moving {delete_path} to {path_external}") + start = time.monotonic() + shutil.move(delete_path, path_external) + cloudlog.warning(f"moved {delete_path} to {path_external} in {time.monotonic() - start:.2f}s") + break + except Exception: + cloudlog.error(f"issue moving {delete_path} to {path_external}") + try: + cloudlog.warning(f"deleting {delete_path}") + shutil.rmtree(delete_path) + break + except OSError: + cloudlog.exception(f"issue deleting {delete_path}") + continue + try: cloudlog.info(f"deleting {delete_path}") shutil.rmtree(delete_path) From 8450f9f3330221caa89f38807c916f35fcbae615 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Tue, 26 Aug 2025 09:57:08 -0700 Subject: [PATCH 021/188] update: more migration --- system/updated/updated.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/system/updated/updated.py b/system/updated/updated.py index 11928bc24c..d1e745ec1a 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -243,7 +243,13 @@ class Updater: if b is None: b = self.get_branch(BASEDIR) b = { - ("tici", "release3"): "release-tici" + ("tici", "release3"): "release-tici", + ("tici", "release3-staging"): "release-tici", + ("tici", "master"): "master-tici", + ("tici", "nightly"): "release-tici", + ("tici", "nightly-dev"): "release-tici", + + ("tizi", "release3"): "release-tizi", }.get((HARDWARE.get_device_type(), b), b) return b From 8ee3c7b485a07250ae6caf341cb9daa02a31e26d Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Tue, 26 Aug 2025 11:52:58 -0700 Subject: [PATCH 022/188] add back dbus-next --- pyproject.toml | 1 + uv.lock | 23 +++++++++++++++++------ 2 files changed, 18 insertions(+), 6 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 556b78009d..7d6516c0fb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -101,6 +101,7 @@ dev = [ "av", "azure-identity", "azure-storage-blob", + "dbus-next", # TODO: remove once we moved everything to jeepney "dictdiffer", "jeepney", "matplotlib", diff --git a/uv.lock b/uv.lock index 72c3d2f8e2..95a60715b5 100644 --- a/uv.lock +++ b/uv.lock @@ -442,6 +442,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/56/c8/46ac27096684f33e27dab749ef43c6b0119c6a0d852971eaefb73256dc4c/cython-3.1.3-py3-none-any.whl", hash = "sha256:d13025b34f72f77bf7f65c1cd628914763e6c285f4deb934314c922b91e6be5a", size = 1225725, upload-time = "2025-08-13T06:19:09.593Z" }, ] +[[package]] +name = "dbus-next" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/45/6a40fbe886d60a8c26f480e7d12535502b5ba123814b3b9a0b002ebca198/dbus_next-0.2.3.tar.gz", hash = "sha256:f4eae26909332ada528c0a3549dda8d4f088f9b365153952a408e28023a626a5", size = 71112, upload-time = "2021-07-25T22:11:28.398Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" }, +] + [[package]] name = "dictdiffer" version = "0.9.0" @@ -1241,6 +1250,7 @@ dev = [ { name = "av" }, { name = "azure-identity" }, { name = "azure-storage-blob" }, + { name = "dbus-next" }, { name = "dictdiffer" }, { name = "jeepney" }, { name = "matplotlib" }, @@ -1294,6 +1304,7 @@ requires-dist = [ { name = "codespell", marker = "extra == 'testing'" }, { name = "crcmod" }, { name = "cython" }, + { name = "dbus-next", marker = "extra == 'dev'" }, { name = "dictdiffer", marker = "extra == 'dev'" }, { name = "future-fstrings" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, @@ -1467,11 +1478,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] @@ -4629,15 +4640,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.35.0" +version = "2.35.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/83/055dc157b719651ef13db569bb8cf2103df11174478649735c1b2bf3f6bc/sentry_sdk-2.35.0.tar.gz", hash = "sha256:5ea58d352779ce45d17bc2fa71ec7185205295b83a9dbb5707273deb64720092", size = 343014, upload-time = "2025-08-14T17:11:20.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/72/75/6223b9ffa0bf5a79ece08055469be73c18034e46ed082742a0899cc58351/sentry_sdk-2.35.1.tar.gz", hash = "sha256:241b41e059632fe1f7c54ae6e1b93af9456aebdfc297be9cf7ecfd6da5167e8e", size = 343145, upload-time = "2025-08-26T08:23:32.429Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3d/742617a7c644deb0c1628dcf6bb2d2165ab7c6aab56fe5222758994007f8/sentry_sdk-2.35.0-py2.py3-none-any.whl", hash = "sha256:6e0c29b9a5d34de8575ffb04d289a987ff3053cf2c98ede445bea995e3830263", size = 363806, upload-time = "2025-08-14T17:11:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/62/1f/5feb6c42cc30126e9574eabc28139f8c626b483a47c537f648d133628df0/sentry_sdk-2.35.1-py2.py3-none-any.whl", hash = "sha256:13b6d6cfdae65d61fe1396a061cf9113b20f0ec1bcb257f3826b88f01bb55720", size = 363887, upload-time = "2025-08-26T08:23:30.335Z" }, ] [[package]] From f5d67b5eee14b3f7a0f31cee875338f4cc88b46a Mon Sep 17 00:00:00 2001 From: Jaume Balust Date: Tue, 26 Aug 2025 16:23:24 -0600 Subject: [PATCH 023/188] cereal: fix frequency precision by changing from int to float (#36060) --- cereal/messaging/socketmaster.cc | 2 +- cereal/services.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/cereal/messaging/socketmaster.cc b/cereal/messaging/socketmaster.cc index 1a7a48980e..7f7e2795c4 100644 --- a/cereal/messaging/socketmaster.cc +++ b/cereal/messaging/socketmaster.cc @@ -33,7 +33,7 @@ MessageContext message_context; struct SubMaster::SubMessage { std::string name; SubSocket *socket = nullptr; - int freq = 0; + float freq = 0.0f; bool updated = false, alive = false, valid = false, ignore_alive; uint64_t rcv_time = 0, rcv_frame = 0; void *allocated_msg_reader = nullptr; diff --git a/cereal/services.py b/cereal/services.py index a4c7463f20..edeca412ce 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -109,12 +109,12 @@ def build_header(): h += "#include \n" h += "#include \n" - h += "struct service { std::string name; bool should_log; int frequency; int decimation; };\n" + h += "struct service { std::string name; bool should_log; float frequency; int decimation; };\n" h += "static std::map services = {\n" for k, v in SERVICE_LIST.items(): should_log = "true" if v.should_log else "false" decimation = -1 if v.decimation is None else v.decimation - h += ' { "%s", {"%s", %s, %d, %d}},\n' % \ + h += ' { "%s", {"%s", %s, %f, %d}},\n' % \ (k, k, should_log, v.frequency, decimation) h += "};\n" From 82582576587c9c6d89ccf85f7bd306f23be396a2 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Tue, 26 Aug 2025 15:34:34 -0700 Subject: [PATCH 024/188] ci: modernize test onroad (#36059) * start * fix * better * more * test * Revert "test" This reverts commit 17066ac123668cb7280cf85e3f21a3043b4785b0. * remove --- Jenkinsfile | 9 ++++----- selfdrive/test/test_onroad.py | 17 +++-------------- 2 files changed, 7 insertions(+), 19 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 0905abd6da..f0c0cf1370 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -178,7 +178,7 @@ node { try { if (env.BRANCH_NAME == 'devel-staging') { - deviceStage("build release3-staging", "tici-needs-can", [], [ + deviceStage("build release3-staging", "tizi-needs-can", [], [ step("build release3-staging", "RELEASE_BRANCH=release3-staging $SOURCE_DIR/release/build_release.sh"), ]) } @@ -186,12 +186,12 @@ node { if (env.BRANCH_NAME == '__nightly') { parallel ( 'nightly': { - deviceStage("build nightly", "tici-needs-can", [], [ + deviceStage("build nightly", "tizi-needs-can", [], [ step("build nightly", "RELEASE_BRANCH=nightly $SOURCE_DIR/release/build_release.sh"), ]) }, 'nightly-dev': { - deviceStage("build nightly-dev", "tici-needs-can", [], [ + deviceStage("build nightly-dev", "tizi-needs-can", [], [ step("build nightly-dev", "PANDA_DEBUG_BUILD=1 RELEASE_BRANCH=nightly-dev $SOURCE_DIR/release/build_release.sh"), ]) }, @@ -200,9 +200,8 @@ node { if (!env.BRANCH_NAME.matches(excludeRegex)) { parallel ( - // tici tests 'onroad tests': { - deviceStage("onroad", "tici-needs-can", ["UNSAFE=1"], [ + deviceStage("onroad", "tizi-needs-can", ["UNSAFE=1"], [ step("build openpilot", "cd system/manager && ./build.py"), step("check dirty", "release/check-dirty.sh"), step("onroad tests", "pytest selfdrive/test/test_onroad.py -s", [timeout: 60]), diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 0149653c84..3cb1af10df 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -18,7 +18,6 @@ from openpilot.common.timeout import Timeout from openpilot.common.params import Params from openpilot.selfdrive.selfdrived.events import EVENTS, ET from openpilot.selfdrive.test.helpers import set_params_enabled, release_only -from openpilot.system.hardware import HARDWARE from openpilot.system.hardware.hw import Paths from openpilot.tools.lib.logreader import LogReader from openpilot.tools.lib.log_time_series import msgs_to_time_series @@ -33,7 +32,7 @@ CPU usage budget TEST_DURATION = 25 LOG_OFFSET = 8 -MAX_TOTAL_CPU = 280. # total for all 8 cores +MAX_TOTAL_CPU = 287. # total for all 8 cores PROCS = { # Baseline CPU usage by process "selfdrive.controls.controlsd": 16.0, @@ -67,20 +66,10 @@ PROCS = { "system.statsd": 1.0, "system.loggerd.uploader": 15.0, "system.loggerd.deleter": 1.0, + "./pandad": 19.0, + "system.qcomgpsd.qcomgpsd": 1.0, } -PROCS.update({ - "tici": { - "./pandad": 5.0, - "./ubloxd": 1.0, - "system.ubloxd.pigeond": 6.0, - }, - "tizi": { - "./pandad": 19.0, - "system.qcomgpsd.qcomgpsd": 1.0, - } -}.get(HARDWARE.get_device_type(), {})) - TIMINGS = { # rtols: max/min, rsd "can": [2.5, 0.35], From 30d5f4ed52179f05c10a7e9768464a5764c6c2c1 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 26 Aug 2025 19:08:12 -0400 Subject: [PATCH 025/188] more fixes --- .../lib/speed_limit_controller/speed_limit_resolver.py | 4 ++-- .../speed_limit_controller/tests/test_speed_limit_resolver.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index 98452699b3..43f78087b2 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -63,7 +63,7 @@ class SpeedLimitResolver: gps_data = sm[self._gps_location_service] map_data = sm['liveMapDataSP'] - gps_fix_age = time.time() - gps_data.unixTimestampMillis * 1e-3 + gps_fix_age = time.monotonic() - gps_data.unixTimestampMillis * 1e-3 if gps_fix_age > LIMIT_MAX_MAP_DATA_AGE: debug(f'SL: Ignoring map data as is too old. Age: {gps_fix_age}') return @@ -77,7 +77,7 @@ class SpeedLimitResolver: gps_data = sm[self._gps_location_service] map_data = sm['liveMapDataSP'] - distance_since_fix = self._v_ego * (time.time() - gps_data.unixTimestampMillis * 1e-3) + distance_since_fix = self._v_ego * (time.monotonic() - gps_data.unixTimestampMillis * 1e-3) distance_to_speed_limit_ahead = max(0., map_data.speedLimitAheadDistance - distance_since_fix) self._limit_solutions[Source.map_data] = speed_limit diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py index 0dc5dd585c..62a9329958 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py @@ -38,7 +38,7 @@ def setup_sm_mock(mocker: MockerFixture): 'speedLimitAheadDistance': 0., }, mocker) gps_data = create_mock({ - 'unixTimestampMillis': time.time() * 1e3, + 'unixTimestampMillis': time.monotonic() * 1e3, }, mocker) sm_mock = mocker.MagicMock() sm_mock.__getitem__.side_effect = lambda key: { @@ -124,7 +124,7 @@ class TestSpeedLimitResolverValidation: def test_old_map_data_ignored(self, resolver_class, policy, mocker: MockerFixture): resolver = resolver_class(policy) sm_mock = mocker.MagicMock() - sm_mock['gpsLocation'].unixTimestampMillis = (time.time() - 2 * LIMIT_MAX_MAP_DATA_AGE) * 1e3 + sm_mock['gpsLocation'].unixTimestampMillis = (time.monotonic() - 2 * LIMIT_MAX_MAP_DATA_AGE) * 1e3 resolver._get_from_map_data(sm_mock) assert resolver._limit_solutions[Source.map_data] == 0. assert resolver._distance_solutions[Source.map_data] == 0. From ea6677c464f74b429ccb2dd4e367b1e6ef4a3cdd Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Tue, 26 Aug 2025 17:16:57 -0700 Subject: [PATCH 026/188] AGNOS 13 (#36069) * staging * prod --- launch_env.sh | 2 +- system/hardware/tici/agnos.json | 28 ++++++------- system/hardware/tici/all-partitions.json | 52 ++++++++++++------------ 3 files changed, 41 insertions(+), 41 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 4c011c6ac0..7124b360f8 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="12.8" + export AGNOS_VERSION="13" fi export STAGING_ROOT="/data/safe_staging" diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index 941a4956bf..b252a8110e 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -23,14 +23,14 @@ }, { "name": "abl", - "url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz", - "hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", - "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", + "url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz", + "hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee", + "hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee", "size": 274432, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6" + "ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee" }, { "name": "aop", @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", - "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", - "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "url": "https://commadist.azureedge.net/agnosupdate/boot-3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a.img.xz", + "hash": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", + "hash_raw": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", "size": 18515968, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" + "ondevice_hash": "41d693d7e752c04210b4f8d68015d2367ee83e1cd54cc7b0aca3b79b4855e6b1" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz", - "hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126", - "hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", + "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img.xz", + "hash": "3596cd5d8a51dabcdd75c29f9317ca3dad9036b1083630ad719eaf584fdb1ce9", + "hash_raw": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7", + "ondevice_hash": "32cdbc0ce176e0ea92944e53be875c12374512fa09b6041e42e683519d36591e", "alt": { - "hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", - "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img", + "hash": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", + "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img", "size": 5368709120 } } diff --git a/system/hardware/tici/all-partitions.json b/system/hardware/tici/all-partitions.json index 5891e2748a..8bc6680b98 100644 --- a/system/hardware/tici/all-partitions.json +++ b/system/hardware/tici/all-partitions.json @@ -152,14 +152,14 @@ }, { "name": "abl", - "url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz", - "hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", - "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", + "url": "https://commadist.azureedge.net/agnosupdate/abl-556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee.img.xz", + "hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee", + "hash_raw": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee", "size": 274432, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6" + "ondevice_hash": "556bbb4ed1c671402b217bd2f3c07edce4f88b0bbd64e92241b82e396aa9ebee" }, { "name": "aop", @@ -339,62 +339,62 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", - "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", - "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "url": "https://commadist.azureedge.net/agnosupdate/boot-3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a.img.xz", + "hash": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", + "hash_raw": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", "size": 18515968, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" + "ondevice_hash": "41d693d7e752c04210b4f8d68015d2367ee83e1cd54cc7b0aca3b79b4855e6b1" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz", - "hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126", - "hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", + "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img.xz", + "hash": "3596cd5d8a51dabcdd75c29f9317ca3dad9036b1083630ad719eaf584fdb1ce9", + "hash_raw": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", "size": 5368709120, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7", + "ondevice_hash": "32cdbc0ce176e0ea92944e53be875c12374512fa09b6041e42e683519d36591e", "alt": { - "hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", - "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img", + "hash": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", + "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img", "size": 5368709120 } }, { "name": "userdata_90", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a.img.xz", - "hash": "6a11d448bac50467791809339051eed2894aae971c37bf6284b3b972a99ba3ac", - "hash_raw": "602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-a3695e6b4bade3dd9c2711cd92e93e9ac7744207c2af03b78f0b9a17e89d357f.img.xz", + "hash": "eeb50afb13973d7e54013fdb3ce0f4f396b8608c8325442966cad6b67e39a8d9", + "hash_raw": "a3695e6b4bade3dd9c2711cd92e93e9ac7744207c2af03b78f0b9a17e89d357f", "size": 96636764160, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "e014d92940a696bf8582807259820ab73948b950656ed83a45da738f26083705" + "ondevice_hash": "537088b516805b32b1b4ad176e7f3fc6bc828e296398ce65cbf5f6150fb1a26f" }, { "name": "userdata_89", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48.img.xz", - "hash": "748e31a5fc01fc256c012e359c3382d1f98cce98feafe8ecc0fca3e47caef116", - "hash_raw": "4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-cbe9979b42b265c9e25a50e876faf5b592fe175aeb5936f2a97b345a6d4e53f5.img.xz", + "hash": "9456a8b117736e6f8eb35cc97fc62ddc8255f38a1be5959a6911498d6aaee08d", + "hash_raw": "cbe9979b42b265c9e25a50e876faf5b592fe175aeb5936f2a97b345a6d4e53f5", "size": 95563022336, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "c181b93050787adcfef730c086bcb780f28508d84e6376d9b80d37e5dc02b55e" + "ondevice_hash": "9e7293cf9a377cb2f3477698e7143e6085a42f7355d7eace5bf9e590992941a8" }, { "name": "userdata_30", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668.img.xz", - "hash": "09ff390e639e4373d772e1688d05a5ac77a573463ed1deeff86390686fa686f9", - "hash_raw": "80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-0b25bb660f1c0c4475fc22a32a51cd32bb980f55b95069e6ab56dd8e47f00c31.img.xz", + "hash": "6c5c98c0fec64355ead5dfc9c1902653b4ea9a071e7b968d1ccd36565082f6b7", + "hash_raw": "0b25bb660f1c0c4475fc22a32a51cd32bb980f55b95069e6ab56dd8e47f00c31", "size": 32212254720, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "2c01ab470c02121c721ff6afc25582437e821686207f3afef659387afb69c507" + "ondevice_hash": "42b5c09a36866d9a52e78b038901669d5bebb02176c498ce11618f200bdfe6b5" } ] \ No newline at end of file From 561210d2d2ff4dde84ce6a671b7ed3ccbf3a57f9 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 26 Aug 2025 20:34:17 -0400 Subject: [PATCH 027/188] internalize output --- sunnypilot/selfdrive/controls/lib/longitudinal_planner.py | 4 +--- .../lib/speed_limit_controller/speed_limit_controller.py | 4 +++- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index e3dee73912..6bbd21ae86 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -38,9 +38,7 @@ class LongitudinalPlannerSP: def update_v_cruise(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> float: self.events_sp.clear() - self.slc.update(sm, v_ego, a_ego, v_cruise, self.events_sp) - - v_cruise_slc = self.slc.speed_limit_offseted if self.slc.is_active else V_CRUISE_UNSET + v_cruise_slc = self.slc.update(sm, v_ego, a_ego, v_cruise, self.events_sp) v_cruise_final = min(v_cruise, v_cruise_slc) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 66d6461eb3..b8d654af28 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -268,7 +268,7 @@ class SpeedLimitController: elif self._speed_limit_changed != 0: events_sp.add(EventNameSP.speedLimitValueChange) - def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, events_sp: EventsSP) -> None: + def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, events_sp: EventsSP) -> float: _car_state = sm['carState'] self._op_engaged = sm['carControl'].longActive self._v_ego = v_ego @@ -283,3 +283,5 @@ class SpeedLimitController: self._update_calculations() self._state_transition() self._update_events(events_sp) + + return self.speed_limit_offseted From 727a4ae8cb9e9568a7a5580d01ad6999c953921c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 26 Aug 2025 20:44:14 -0400 Subject: [PATCH 028/188] unused --- .../lib/speed_limit_controller/speed_limit_controller.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index b8d654af28..c64ddfd6c2 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -51,7 +51,6 @@ class SpeedLimitController: self._source = Source.none self._state = SpeedLimitControlState.inactive self._state_prev = SpeedLimitControlState.inactive - self._gas_pressed = False self._pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise self._offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) @@ -274,7 +273,6 @@ class SpeedLimitController: self._v_ego = v_ego self._a_ego = a_ego self._v_cruise_setpoint = v_cruise_setpoint if not np.isnan(v_cruise_setpoint) else 0.0 - self._gas_pressed = _car_state.gasPressed self._current_time = time.monotonic() self._speed_limit, self._distance, self._source = self._resolver.resolve(v_ego, self.speed_limit, sm) From 1081dc4d6c2d162625f799fbc46c119139d9a621 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 26 Aug 2025 20:46:05 -0400 Subject: [PATCH 029/188] rearrange --- .../speed_limit_controller/speed_limit_controller.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index c64ddfd6c2..af38641393 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -170,7 +170,11 @@ class SpeedLimitController: return Engage.auto - def _update_calculations(self) -> None: + def _update_calculations(self, v_ego: float, a_ego: float, v_cruise_setpoint: float) -> None: + self._v_cruise_setpoint = v_cruise_setpoint if not np.isnan(v_cruise_setpoint) else 0.0 + self._v_ego = v_ego + self._a_ego = a_ego + # Update current velocity offset (error) self._v_offset = self.speed_limit_offseted - self._v_ego @@ -268,17 +272,13 @@ class SpeedLimitController: events_sp.add(EventNameSP.speedLimitValueChange) def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, events_sp: EventsSP) -> float: - _car_state = sm['carState'] self._op_engaged = sm['carControl'].longActive - self._v_ego = v_ego - self._a_ego = a_ego - self._v_cruise_setpoint = v_cruise_setpoint if not np.isnan(v_cruise_setpoint) else 0.0 self._current_time = time.monotonic() self._speed_limit, self._distance, self._source = self._resolver.resolve(v_ego, self.speed_limit, sm) self._update_params() - self._update_calculations() + self._update_calculations(v_ego, a_ego, v_cruise_setpoint) self._state_transition() self._update_events(events_sp) From 13122c6c1d407b2fc8149f3bafbb86b4e06ef8f4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 26 Aug 2025 23:26:15 -0400 Subject: [PATCH 030/188] auto draft --- cereal/custom.capnp | 2 + .../lib/speed_limit_controller/__init__.py | 11 +- .../lib/speed_limit_controller/helpers.py | 8 +- .../speed_limit_controller.py | 133 ++++++++++-------- sunnypilot/selfdrive/selfdrived/events.py | 7 + 5 files changed, 96 insertions(+), 65 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 52c648f388..432f961885 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -152,6 +152,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { preActive @2; adapting @3; # Reducing speed to match new speed limit. active @4; # Cruising at speed limit. + pending @5; # Awaiting new speed limit. } } @@ -196,6 +197,7 @@ struct OnroadEventSP @0xda96579883444c35 { speedLimitActive @18; speedLimitConfirmed @19; speedLimitValueChange @20; + speedLimitPreActive @21; } } diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py index b0a6675491..ecde484372 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py @@ -1,8 +1,10 @@ from cereal import custom +SpeedLimitControlState = custom.LongitudinalPlanSP.SpeedLimitControlState + DEBUG = True -PARAMS_UPDATE_PERIOD = 2. # secs. Time between parameter updates. -TEMP_INACTIVE_GUARD_PERIOD = 1. # secs. Time to wait after activation before considering temp deactivation signal. +PARAMS_UPDATE_PERIOD = 3. # secs. Time between parameter updates. +PRE_ACTIVE_GUARD_PERIOD = 5. # secs. Time to wait after activation before considering temp deactivation signal. # Lookup table for speed limit percent offset depending on speed. LIMIT_PERC_OFFSET_V = [0.1, 0.05, 0.038] # 55, 105, 135 km/h @@ -16,4 +18,7 @@ LIMIT_MIN_SPEED = 8.33 # m/s, Minimum speed limit to provide as solution on lim LIMIT_SPEED_OFFSET_TH = -1. # m/s Maximum offset between speed limit and current speed for adapting state. LIMIT_MAX_MAP_DATA_AGE = 10. # s Maximum time to hold to map data, then consider it invalid inside limits controllers. -SpeedLimitControlState = custom.LongitudinalPlanSP.SpeedLimitControlState +# Speed Limit Control Auto mode constants +REQUIRED_INITIAL_CRUISE_SPEED = 35.7632 # m/s 80 MPH # TODO-SP: customizable with params +CRUISE_SPEED_TOLERANCE = 0.44704 # m/s ±1 MPH tolerance # TODO-SP: metric vs imperial +FALLBACK_CRUISE_SPEED = 255.0 # m/s fallback when no speed limit available diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py index b15c63bf05..53c9bfb5a5 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py @@ -11,9 +11,13 @@ def debug(msg): def description_for_state(speed_limit_control_state): if speed_limit_control_state == SpeedLimitControlState.inactive: return 'INACTIVE' - if speed_limit_control_state == SpeedLimitControlState.tempInactive: - return 'TEMP_INACTIVE' + if speed_limit_control_state == SpeedLimitControlState.preActive: + return 'PRE_ACTIVE' + if speed_limit_control_state == SpeedLimitControlState.pending: + return 'PENDING' if speed_limit_control_state == SpeedLimitControlState.adapting: return 'ADAPTING' if speed_limit_control_state == SpeedLimitControlState.active: return 'ACTIVE' + + return '' diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index af38641393..6152158287 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -5,7 +5,8 @@ from cereal import messaging, custom from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V, \ - PARAMS_UPDATE_PERIOD, TEMP_INACTIVE_GUARD_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState + PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_CRUISE_SPEED, \ + CRUISE_SPEED_TOLERANCE, FALLBACK_CRUISE_SPEED from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Policy, Engage, OffsetType from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.helpers import description_for_state, debug @@ -16,7 +17,7 @@ from openpilot.selfdrive.modeld.constants import ModelConstants EventNameSP = custom.OnroadEventSP.EventName ACTIVE_STATES = (SpeedLimitControlState.active, SpeedLimitControlState.adapting) -ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.tempInactive, *ACTIVE_STATES) +ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pending, *ACTIVE_STATES) class SpeedLimitController: @@ -44,9 +45,11 @@ class SpeedLimitController: self._v_cruise_setpoint = 0. self._v_cruise_setpoint_prev = 0. self._v_cruise_setpoint_changed = False + self._initial_max_set = False self._speed_limit = 0. self._speed_limit_prev = 0. self._speed_limit_changed = False + self._last_valid_speed_limit_offsetted = 0. self._distance = 0. self._source = Source.none self._state = SpeedLimitControlState.inactive @@ -68,23 +71,20 @@ class SpeedLimitController: # Mapping functions to state transitions self.state_transition_strategy = { - # Transition functions for each state SpeedLimitControlState.inactive: self.transition_state_from_inactive, - SpeedLimitControlState.tempInactive: self.transition_state_from_temp_inactive, + SpeedLimitControlState.preActive: self.transition_state_from_preactive, + SpeedLimitControlState.pending: self.transition_state_from_pending, SpeedLimitControlState.adapting: self.transition_state_from_adapting, SpeedLimitControlState.active: self.transition_state_from_active, - SpeedLimitControlState.preActive: self.transition_state_from_pre_active, } - # FIXME-SP: unused? - # Solution functions mapped to respective states + # Solution functions mapped to respective states self.acceleration_solutions = { - # Solution functions for each state - SpeedLimitControlState.tempInactive: self.get_current_acceleration_as_target, SpeedLimitControlState.inactive: self.get_current_acceleration_as_target, + SpeedLimitControlState.preActive: self.get_current_acceleration_as_target, + SpeedLimitControlState.pending: self.get_current_acceleration_as_target, SpeedLimitControlState.adapting: self.get_adapting_state_target_acceleration, SpeedLimitControlState.active: self.get_active_state_target_acceleration, - SpeedLimitControlState.preActive: self.get_current_acceleration_as_target, } @property @@ -95,12 +95,6 @@ class SpeedLimitController: def state(self, value) -> None: if value != self._state: debug(f'Speed Limit Controller state: {description_for_state(value)}') - - if value == SpeedLimitControlState.tempInactive: - # Reset previous speed limit to current value as to prevent going out of tempInactive in - # a single cycle when the speed limit changes at the same time the user has temporarily deactivated it. - self._speed_limit_prev = self._speed_limit - self._state = value @property @@ -113,7 +107,18 @@ class SpeedLimitController: @property def speed_limit_offseted(self) -> float: - return self._speed_limit + self.speed_limit_offset + # If we have a current valid speed limit, use it + if self._speed_limit > 0: + current_offsetted = self._speed_limit + self.speed_limit_offset + self._last_valid_speed_limit_offsetted = current_offsetted + return current_offsetted + + # If no current speed limit but we have a last valid one, use that + if self._last_valid_speed_limit_offsetted > 0: + return self._last_valid_speed_limit_offsetted + + # Fallback + return FALLBACK_CRUISE_SPEED @property def speed_limit_offset(self) -> float: @@ -164,12 +169,24 @@ class SpeedLimitController: self._last_params_update = self._current_time - def _read_engage_type_param(self) -> Engage: - if self._pcm_cruise_op_long: - return Engage.auto - + @staticmethod + def _read_engage_type_param() -> Engage: return Engage.auto + def _initial_max_set_confirmed(self) -> bool: + return abs(self._v_cruise_setpoint - REQUIRED_INITIAL_CRUISE_SPEED) <= CRUISE_SPEED_TOLERANCE + + def _detect_manual_cruise_change(self) -> bool: + if not self.is_active: + return False + + # If cruise speed changed and it's not what SLC would set + if self._v_cruise_setpoint_changed: + expected_cruise = self.speed_limit_offseted + return abs(self._v_cruise_setpoint - expected_cruise) > CRUISE_SPEED_TOLERANCE + + return False + def _update_calculations(self, v_ego: float, a_ego: float, v_cruise_setpoint: float) -> None: self._v_cruise_setpoint = v_cruise_setpoint if not np.isnan(v_cruise_setpoint) else 0.0 self._v_ego = v_ego @@ -199,77 +216,73 @@ class SpeedLimitController: int(round((self._speed_limit + self.speed_limit_warning_offset) * self._speed_factor)) def transition_state_from_inactive(self) -> None: - """ Make state transition from inactive state """ - if self._engage_type == Engage.auto: + self.state = SpeedLimitControlState.preActive + self._initial_max_set = False + + def transition_state_from_preactive(self) -> None: + if self._initial_max_set_confirmed(): + self._initial_max_set = True + if self._speed_limit > 0: + if self._v_offset < LIMIT_SPEED_OFFSET_TH: + self.state = SpeedLimitControlState.adapting + else: + self.state = SpeedLimitControlState.active + else: + self.state = SpeedLimitControlState.pending + elif self._v_cruise_setpoint_changed and self._current_time > (self._last_op_engaged_time + PRE_ACTIVE_GUARD_PERIOD): + # User set cruise to something other than 80 MPH, permanently disable for this session + self.state = SpeedLimitControlState.inactive + + def transition_state_from_pending(self) -> None: + if self._speed_limit > 0: if self._v_offset < LIMIT_SPEED_OFFSET_TH: self.state = SpeedLimitControlState.adapting else: self.state = SpeedLimitControlState.active - def transition_state_from_temp_inactive(self) -> None: - """ Make state transition from temporary inactive state """ - if self._speed_limit_changed: - if self._engage_type == Engage.auto: - self.state = SpeedLimitControlState.inactive - - def transition_state_from_pre_active(self) -> None: - """ Make state transition from preActive state """ - pass - def transition_state_from_adapting(self) -> None: - """ Make state transition from adapting state """ - if self._v_offset >= LIMIT_SPEED_OFFSET_TH: + if self._detect_manual_cruise_change(): + self.state = SpeedLimitControlState.inactive + elif self._v_offset >= LIMIT_SPEED_OFFSET_TH: self.state = SpeedLimitControlState.active def transition_state_from_active(self) -> None: - """ Make state transition from active state """ - if self._engage_type == Engage.auto: - if self._v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting + if self._detect_manual_cruise_change(): + self.state = SpeedLimitControlState.inactive + elif self._speed_limit > 0 and self._v_offset < LIMIT_SPEED_OFFSET_TH: + self.state = SpeedLimitControlState.adapting def _state_transition(self) -> None: self._state_prev = self._state - # In any case, if op is disabled, or speed limit control is disabled or no valid speed limit - # or gas is pressed, deactivate. - if not self._op_engaged or not self._enabled or self._speed_limit == 0: + # If op is disabled or SLC is disabled, go inactive + if not self._op_engaged or not self._enabled: self.state = SpeedLimitControlState.inactive - return - - # In any case, we deactivate the speed limit controller temporarily if the user changes the cruise speed. - # Ignore if a minimum amount of time has not passed since activation. This is to prevent temp inactivations - # due to controlsd logic changing cruise setpoint when going active. - if self._engage_type == Engage.auto and self._v_cruise_setpoint_changed and \ - self._current_time > (self._last_op_engaged_time + TEMP_INACTIVE_GUARD_PERIOD): - self.state = SpeedLimitControlState.tempInactive + self._initial_max_set = False return self.state_transition_strategy[self.state]() - self._update_v_cruise_setpoint_prev() # always for Engage.auto - def get_current_acceleration_as_target(self) -> float: - """ When state is inactive or tempInactive, preserve current acceleration """ return self._a_ego def get_adapting_state_target_acceleration(self) -> float: - """ In adapting state, calculate target acceleration based on speed limit and current velocity """ if self.distance > 0: return (self.speed_limit_offseted ** 2 - self._v_ego ** 2) / (2. * self.distance) return self._v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) def get_active_state_target_acceleration(self) -> float: - """ In active state, aim to keep speed constant around control time horizon """ return self._v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) def _update_events(self, events_sp: EventsSP) -> None: if self.is_active: - if self._engage_type == Engage.auto: - if self._state_prev not in ACTIVE_STATES: - events_sp.add(EventNameSP.speedLimitActive) - elif self._speed_limit_changed != 0: - events_sp.add(EventNameSP.speedLimitValueChange) + if self.state == SpeedLimitControlState.preActive: + events_sp.add(EventNameSP.speedLimitPreActive) + elif self._state_prev not in ACTIVE_STATES: + events_sp.add(EventNameSP.speedLimitActive) + elif self._speed_limit_changed: + events_sp.add(EventNameSP.speedLimitValueChange) def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, events_sp: EventsSP) -> float: self._op_engaged = sm['carControl'].longActive diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index f39fafefdb..a73a87cc64 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -159,4 +159,11 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.WARNING: speed_limit_adjust_alert, }, + EventNameSP.speedLimitPreActive: { + ET.WARNING: Alert( + "Auto Speed Limit Control: Activation Required", + "Manually change set speed to 80 MPH to activate", + AlertStatus.normal, AlertSize.mid, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 3.), + }, } From f40f7f9ecee7a51dcc9f07b9337247e9bc2592c0 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Tue, 26 Aug 2025 21:45:49 -0700 Subject: [PATCH 031/188] Revert "torqued: apply offset (#36005)" This reverts commit 1d74a97ba65b9b9ea074ef870ab70907be33918c. --- selfdrive/controls/lib/latcontrol_torque.py | 6 +- .../tests/test_torqued_lat_accel_offset.py | 71 ------------------- selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 4 insertions(+), 75 deletions(-) delete mode 100644 selfdrive/controls/tests/test_torqued_lat_accel_offset.py diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 5a2814e089..dffe85c473 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -52,8 +52,10 @@ class LatControlTorque(LatControl): actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - desired_lateral_accel = desired_curvature * CS.vEgo ** 2 + + # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature + # desired_lateral_jerk = desired_curvature_rate * CS.vEgo ** 2 actual_lateral_accel = actual_curvature * CS.vEgo ** 2 lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 @@ -65,8 +67,6 @@ class LatControlTorque(LatControl): # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly pid_log.error = float(setpoint - measurement) ff = gravity_adjusted_lateral_accel - # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll - ff -= self.torque_params.latAccelOffset ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 diff --git a/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/selfdrive/controls/tests/test_torqued_lat_accel_offset.py deleted file mode 100644 index 5ba9980020..0000000000 --- a/selfdrive/controls/tests/test_torqued_lat_accel_offset.py +++ /dev/null @@ -1,71 +0,0 @@ -import numpy as np -from cereal import car, messaging -from opendbc.car import ACCELERATION_DUE_TO_GRAVITY -from opendbc.car import structs -from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD -from openpilot.common.realtime import DT_MDL -from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS - -np.random.seed(0) - -LA_ERR_STD = 1.0 -INPUT_NOISE_STD = 0.1 -V_EGO = 30.0 - -WARMUP_BUCKET_POINTS = (1.5*MIN_BUCKET_POINTS).astype(int) -STRAIGHT_ROAD_LA_BOUNDS = (0.02, 0.03) - -ROLL_BIAS_DEG = 1.0 -ROLL_COMPENSATION_BIAS = ACCELERATION_DUE_TO_GRAVITY*float(np.sin(np.deg2rad(ROLL_BIAS_DEG))) -TORQUE_TUNE = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=0.0, friction=0.2) -TORQUE_TUNE_BIASED = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=-ROLL_COMPENSATION_BIAS, friction=0.2) - - -def generate_inputs(torque_tune, la_err_std, input_noise_std=None): - rng = np.random.default_rng(0) - steer_torques = np.concat([rng.uniform(bnd[0], bnd[1], pts) for bnd, pts in zip(STEER_BUCKET_BOUNDS, WARMUP_BUCKET_POINTS, strict=True)]) - la_errs = rng.normal(scale=la_err_std, size=steer_torques.size) - frictions = np.array([get_friction(la_err, 0.0, FRICTION_THRESHOLD, torque_tune) for la_err in la_errs]) - lat_accels = torque_tune.latAccelFactor*steer_torques + torque_tune.latAccelOffset + frictions - if input_noise_std is not None: - steer_torques += rng.normal(scale=input_noise_std, size=steer_torques.size) - lat_accels += rng.normal(scale=input_noise_std, size=steer_torques.size) - return steer_torques, lat_accels - -def get_warmed_up_estimator(steer_torques, lat_accels): - est = TorqueEstimator(car.CarParams()) - for steer_torque, lat_accel in zip(steer_torques, lat_accels, strict=True): - est.filtered_points.add_point(steer_torque, lat_accel) - return est - -def simulate_straight_road_msgs(est): - carControl = messaging.new_message('carControl').carControl - carOutput = messaging.new_message('carOutput').carOutput - carState = messaging.new_message('carState').carState - livePose = messaging.new_message('livePose').livePose - carControl.latActive = True - carState.vEgo = V_EGO - carState.steeringPressed = False - ts = DT_MDL*np.arange(2*POINTS_PER_BUCKET) - steer_torques = np.concat((np.linspace(-0.03, -0.02, POINTS_PER_BUCKET), np.linspace(0.02, 0.03, POINTS_PER_BUCKET))) - lat_accels = TORQUE_TUNE.latAccelFactor * steer_torques - for t, steer_torque, lat_accel in zip(ts, steer_torques, lat_accels, strict=True): - carOutput.actuatorsOutput.torque = float(-steer_torque) - livePose.orientationNED.x = float(np.deg2rad(ROLL_BIAS_DEG)) - livePose.angularVelocityDevice.z = float(lat_accel / V_EGO) - for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)): - est.handle_log(t, which, msg) - -def test_estimated_offset(): - steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) - est = get_warmed_up_estimator(steer_torques, lat_accels) - msg = est.get_msg() - # TODO add lataccelfactor and friction check when we have more accurate estimates - assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.03 - -def test_straight_road_roll_bias(): - steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) - est = get_warmed_up_estimator(steer_torques, lat_accels) - simulate_straight_road_msgs(est) - msg = est.get_msg() - assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 7d21526144..8e4ff1e2a8 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -866f2cb1f0e49b2cb7115aa8131164b3a75fb2c5 \ No newline at end of file +209b47bea61e145cf2d27eb3ab650c97bcd1d33f \ No newline at end of file From b976135d2f3c2ea7c28bf6b92084068f38c7d108 Mon Sep 17 00:00:00 2001 From: felsager <76905857+felsager@users.noreply.github.com> Date: Wed, 27 Aug 2025 13:06:01 -0700 Subject: [PATCH 032/188] torqued: apply offset (with more robust unit test) (#36075) * torqued: apply latAccelOffset to torque control feed forward * test learned latAccelOffset captures roll compensation bias on straight road driving, when the device is not flush in roll relative to the car * test correct torqued latAccelOffset parameter convergence --- selfdrive/controls/lib/latcontrol_torque.py | 6 +- .../tests/test_torqued_lat_accel_offset.py | 70 +++++++++++++++++++ selfdrive/test/process_replay/ref_commit | 2 +- 3 files changed, 74 insertions(+), 4 deletions(-) create mode 100644 selfdrive/controls/tests/test_torqued_lat_accel_offset.py diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index dffe85c473..5a2814e089 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -52,10 +52,8 @@ class LatControlTorque(LatControl): actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - desired_lateral_accel = desired_curvature * CS.vEgo ** 2 - # desired rate is the desired rate of change in the setpoint, not the absolute desired curvature - # desired_lateral_jerk = desired_curvature_rate * CS.vEgo ** 2 + desired_lateral_accel = desired_curvature * CS.vEgo ** 2 actual_lateral_accel = actual_curvature * CS.vEgo ** 2 lateral_accel_deadzone = curvature_deadzone * CS.vEgo ** 2 @@ -67,6 +65,8 @@ class LatControlTorque(LatControl): # do error correction in lateral acceleration space, convert at end to handle non-linear torque responses correctly pid_log.error = float(setpoint - measurement) ff = gravity_adjusted_lateral_accel + # latAccelOffset corrects roll compensation bias from device roll misalignment relative to car roll + ff -= self.torque_params.latAccelOffset ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 diff --git a/selfdrive/controls/tests/test_torqued_lat_accel_offset.py b/selfdrive/controls/tests/test_torqued_lat_accel_offset.py new file mode 100644 index 0000000000..84389856b6 --- /dev/null +++ b/selfdrive/controls/tests/test_torqued_lat_accel_offset.py @@ -0,0 +1,70 @@ +import numpy as np +from cereal import car, messaging +from opendbc.car import ACCELERATION_DUE_TO_GRAVITY +from opendbc.car import structs +from opendbc.car.lateral import get_friction, FRICTION_THRESHOLD +from openpilot.common.realtime import DT_MDL +from openpilot.selfdrive.locationd.torqued import TorqueEstimator, MIN_BUCKET_POINTS, POINTS_PER_BUCKET, STEER_BUCKET_BOUNDS + +np.random.seed(0) + +LA_ERR_STD = 1.0 +INPUT_NOISE_STD = 0.08 +V_EGO = 30.0 + +WARMUP_BUCKET_POINTS = (1.5*MIN_BUCKET_POINTS).astype(int) +STRAIGHT_ROAD_LA_BOUNDS = (0.02, 0.03) + +ROLL_BIAS_DEG = 2.0 +ROLL_COMPENSATION_BIAS = ACCELERATION_DUE_TO_GRAVITY*float(np.sin(np.deg2rad(ROLL_BIAS_DEG))) +TORQUE_TUNE = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=0.0, friction=0.2) +TORQUE_TUNE_BIASED = structs.CarParams.LateralTorqueTuning(latAccelFactor=2.0, latAccelOffset=-ROLL_COMPENSATION_BIAS, friction=0.2) + +def generate_inputs(torque_tune, la_err_std, input_noise_std=None): + rng = np.random.default_rng(0) + steer_torques = np.concat([rng.uniform(bnd[0], bnd[1], pts) for bnd, pts in zip(STEER_BUCKET_BOUNDS, WARMUP_BUCKET_POINTS, strict=True)]) + la_errs = rng.normal(scale=la_err_std, size=steer_torques.size) + frictions = np.array([get_friction(la_err, 0.0, FRICTION_THRESHOLD, torque_tune) for la_err in la_errs]) + lat_accels = torque_tune.latAccelFactor*steer_torques + torque_tune.latAccelOffset + frictions + if input_noise_std is not None: + steer_torques += rng.normal(scale=input_noise_std, size=steer_torques.size) + lat_accels += rng.normal(scale=input_noise_std, size=steer_torques.size) + return steer_torques, lat_accels + +def get_warmed_up_estimator(steer_torques, lat_accels): + est = TorqueEstimator(car.CarParams()) + for steer_torque, lat_accel in zip(steer_torques, lat_accels, strict=True): + est.filtered_points.add_point(steer_torque, lat_accel) + return est + +def simulate_straight_road_msgs(est): + carControl = messaging.new_message('carControl').carControl + carOutput = messaging.new_message('carOutput').carOutput + carState = messaging.new_message('carState').carState + livePose = messaging.new_message('livePose').livePose + carControl.latActive = True + carState.vEgo = V_EGO + carState.steeringPressed = False + ts = DT_MDL*np.arange(2*POINTS_PER_BUCKET) + steer_torques = np.concat((np.linspace(-0.03, -0.02, POINTS_PER_BUCKET), np.linspace(0.02, 0.03, POINTS_PER_BUCKET))) + lat_accels = TORQUE_TUNE.latAccelFactor * steer_torques + for t, steer_torque, lat_accel in zip(ts, steer_torques, lat_accels, strict=True): + carOutput.actuatorsOutput.torque = float(-steer_torque) + livePose.orientationNED.x = float(np.deg2rad(ROLL_BIAS_DEG)) + livePose.angularVelocityDevice.z = float(lat_accel / V_EGO) + for which, msg in (('carControl', carControl), ('carOutput', carOutput), ('carState', carState), ('livePose', livePose)): + est.handle_log(t, which, msg) + +def test_estimated_offset(): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE_BIASED, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + msg = est.get_msg() + # TODO add lataccelfactor and friction check when we have more accurate estimates + assert abs(msg.liveTorqueParameters.latAccelOffsetRaw - TORQUE_TUNE_BIASED.latAccelOffset) < 0.1 + +def test_straight_road_roll_bias(): + steer_torques, lat_accels = generate_inputs(TORQUE_TUNE, la_err_std=LA_ERR_STD, input_noise_std=INPUT_NOISE_STD) + est = get_warmed_up_estimator(steer_torques, lat_accels) + simulate_straight_road_msgs(est) + msg = est.get_msg() + assert (msg.liveTorqueParameters.latAccelOffsetRaw < -0.05) and np.isfinite(msg.liveTorqueParameters.latAccelOffsetRaw) diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index 8e4ff1e2a8..e1133061f5 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -209b47bea61e145cf2d27eb3ab650c97bcd1d33f \ No newline at end of file +4c677a3ebcbd3d4faa3de98e3fb9c0bb83b47926 \ No newline at end of file From 375dfe16a80c60c52934bf998de9be2c3b21ec59 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 13:39:21 -0700 Subject: [PATCH 033/188] jenkins: remove bmx device --- Jenkinsfile | 4 ---- 1 file changed, 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index f0c0cf1370..76f7f2cea4 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -249,10 +249,6 @@ node { step("build", "cd system/manager && ./build.py"), step("test sensord", "pytest system/sensord/tests/test_sensord.py"), ]) - deviceStage("BMX + LSM", "tici-bmx-lsm", ["UNSAFE=1"], [ - step("build", "cd system/manager && ./build.py"), - step("test sensord", "pytest system/sensord/tests/test_sensord.py"), - ]) }, 'replay': { deviceStage("model-replay", "tici-replay", ["UNSAFE=1"], [ From f8ff156869f4abc64c595dc2a442071df4c8543d Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Wed, 27 Aug 2025 13:48:23 -0700 Subject: [PATCH 034/188] modeld: desire->desire_pulse (#36076) consistent naming --- selfdrive/modeld/modeld.py | 12 ++++++------ selfdrive/modeld/models/driving_policy.onnx | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 8bc8bf01ab..6e09f40e22 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -106,7 +106,7 @@ class ModelState: # policy inputs self.numpy_inputs = { - 'desire': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32), + 'desire_pulse': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32), 'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32), 'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), } @@ -131,13 +131,13 @@ class ModelState: def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: # Model decides when action is completed, so desire input is just a pulse triggered on rising edge - inputs['desire'][0] = 0 - new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) - self.prev_desire[:] = inputs['desire'] + inputs['desire_pulse'][0] = 0 + new_desire = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0) + self.prev_desire[:] = inputs['desire_pulse'] self.full_desire[0,:-1] = self.full_desire[0,1:] self.full_desire[0,-1] = new_desire - self.numpy_inputs['desire'][:] = self.full_desire.reshape((1,ModelConstants.INPUT_HISTORY_BUFFER_LEN,ModelConstants.TEMPORAL_SKIP,-1)).max(axis=2) + self.numpy_inputs['desire_pulse'][:] = self.full_desire.reshape((1,ModelConstants.INPUT_HISTORY_BUFFER_LEN,ModelConstants.TEMPORAL_SKIP,-1)).max(axis=2) self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names} @@ -313,7 +313,7 @@ def main(demo=False): bufs = {name: buf_extra if 'big' in name else buf_main for name in model.vision_input_names} transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.vision_input_names} inputs:dict[str, np.ndarray] = { - 'desire': vec_desire, + 'desire_pulse': vec_desire, 'traffic_convention': traffic_convention, } diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 867a0d3b9b..eb6bd7b8a4 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:04b763fb71efe57a8a4c4168a8043ecd58939015026ded0dc755ded6905ac251 -size 12343523 +oid sha256:72e98a95541f200bd2faeae8d718997483696fd4801fc7d718c167b05854707d +size 12343535 From a3fcde2ae8d0d6442a739e6a271b3c1fb8f70146 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 14:19:23 -0700 Subject: [PATCH 035/188] jenkins: tizi-replay --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 76f7f2cea4..dd420fdeb1 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -251,7 +251,7 @@ node { ]) }, 'replay': { - deviceStage("model-replay", "tici-replay", ["UNSAFE=1"], [ + deviceStage("model-replay", "tizi-replay", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py", [diffPaths: ["selfdrive/modeld/", "tinygrad_repo", "selfdrive/test/process_replay/model_replay.py"]]), step("model replay", "selfdrive/test/process_replay/model_replay.py", [diffPaths: ["selfdrive/modeld/", "tinygrad_repo", "selfdrive/test/process_replay/model_replay.py"]]), ]) From b309bf417355ee891dd9d0b2e828f1cefdb89da0 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 14:26:56 -0700 Subject: [PATCH 036/188] jenkins: sensord device --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index dd420fdeb1..8eb113e87d 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -245,7 +245,7 @@ node { ]) }, 'sensord': { - deviceStage("LSM + MMC", "tici-lsmc", ["UNSAFE=1"], [ + deviceStage("LSM + MMC", "tizi-lsmc", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), step("test sensord", "pytest system/sensord/tests/test_sensord.py"), ]) From 2aa7648bb87d45e47b3db7ae58262a722b4b39e2 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 14:29:39 -0700 Subject: [PATCH 037/188] jenkins: remove ar device --- Jenkinsfile | 7 ------- 1 file changed, 7 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 8eb113e87d..54dff318d9 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -223,13 +223,6 @@ node { step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"), ]) }, - 'camerad AR0231': { - deviceStage("AR0231", "tici-ar0231", ["UNSAFE=1"], [ - step("build", "cd system/manager && ./build.py"), - step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]), - step("test exposure", "pytest system/camerad/test/test_exposure.py"), - ]) - }, 'camerad OX03C10': { deviceStage("OX03C10", "tici-ox03c10", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), From a254a05df057e5ad8ea3650de553eef4d6804852 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 14:39:02 -0700 Subject: [PATCH 038/188] jenkins: replace tici-common (#36073) * common * remove * test * Revert "test" This reverts commit 2c76a8f818e42e0af1d4540dede3595fe0d59ed9. * Reapply "test" This reverts commit d9974dd8564d0699dcfa3aac0ffb2dca33f3b47d. * Revert "Reapply "test"" This reverts commit 2377c6ab20df5dd06886f3dd9a0be07abfce9df6. * tizi bounds --- Jenkinsfile | 3 +-- system/hardware/tici/tests/test_power_draw.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/Jenkinsfile b/Jenkinsfile index 54dff318d9..6c4e01854f 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -208,12 +208,11 @@ node { ]) }, 'HW + Unit Tests': { - deviceStage("tici-hardware", "tici-common", ["UNSAFE=1"], [ + deviceStage("tizi-hardware", "tizi-common", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]), step("test power draw", "pytest -s system/hardware/tici/tests/test_power_draw.py"), step("test encoder", "LD_LIBRARY_PATH=/usr/local/lib pytest system/loggerd/tests/test_encoder.py", [diffPaths: ["system/loggerd/"]]), - step("test pigeond", "pytest system/ubloxd/tests/test_pigeond.py", [diffPaths: ["system/ubloxd/"]]), step("test manager", "pytest system/manager/test/test_manager.py"), ]) }, diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py index db0fab884c..46b45460b5 100644 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/system/hardware/tici/tests/test_power_draw.py @@ -32,8 +32,8 @@ class Proc: PROCS = [ Proc(['camerad'], 1.75, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), - Proc(['modeld'], 1.12, atol=0.2, msgs=['modelV2']), - Proc(['dmonitoringmodeld'], 0.6, msgs=['driverStateV2']), + Proc(['modeld'], 1.24, atol=0.2, msgs=['modelV2']), + Proc(['dmonitoringmodeld'], 0.7, msgs=['driverStateV2']), Proc(['encoderd'], 0.23, msgs=[]), ] From 1d8dc8a69a188285e65595d83224e54d497178dc Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 27 Aug 2025 15:11:58 -0700 Subject: [PATCH 039/188] camerad: remove AR0231 (#36070) --- system/camerad/SConscript | 2 +- system/camerad/cameras/spectra.cc | 3 +- system/camerad/sensors/ar0231.cc | 136 ---------------------- system/camerad/sensors/ar0231_registers.h | 121 ------------------- system/camerad/sensors/sensor.h | 12 -- 5 files changed, 2 insertions(+), 272 deletions(-) delete mode 100644 system/camerad/sensors/ar0231.cc delete mode 100644 system/camerad/sensors/ar0231_registers.h diff --git a/system/camerad/SConscript b/system/camerad/SConscript index fe5cf87b78..734f748a2a 100644 --- a/system/camerad/SConscript +++ b/system/camerad/SConscript @@ -4,7 +4,7 @@ libs = [common, 'OpenCL', messaging, visionipc, gpucommon] if arch != "Darwin": camera_obj = env.Object(['cameras/camera_qcom2.cc', 'cameras/camera_common.cc', 'cameras/spectra.cc', - 'cameras/cdm.cc', 'sensors/ar0231.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc']) + 'cameras/cdm.cc', 'sensors/ox03c10.cc', 'sensors/os04c10.cc']) env.Program('camerad', ['main.cc', camera_obj], LIBS=libs) if GetOption("extras") and arch == "x86_64": diff --git a/system/camerad/cameras/spectra.cc b/system/camerad/cameras/spectra.cc index 47ae9061f4..caf7871573 100644 --- a/system/camerad/cameras/spectra.cc +++ b/system/camerad/cameras/spectra.cc @@ -1004,8 +1004,7 @@ bool SpectraCamera::openSensor() { }; // Figure out which sensor we have - if (!init_sensor_lambda(new AR0231) && - !init_sensor_lambda(new OX03C10) && + if (!init_sensor_lambda(new OX03C10) && !init_sensor_lambda(new OS04C10)) { LOGE("** sensor %d FAILED bringup, disabling", cc.camera_num); enabled = false; diff --git a/system/camerad/sensors/ar0231.cc b/system/camerad/sensors/ar0231.cc deleted file mode 100644 index e4ae29f079..0000000000 --- a/system/camerad/sensors/ar0231.cc +++ /dev/null @@ -1,136 +0,0 @@ -#include -#include - -#include "system/camerad/sensors/sensor.h" - -namespace { - -const size_t AR0231_REGISTERS_HEIGHT = 2; -// TODO: this extra height is universal and doesn't apply per camera -const size_t AR0231_STATS_HEIGHT = 2 + 8; - -const float sensor_analog_gains_AR0231[] = { - 1.0 / 8.0, 2.0 / 8.0, 2.0 / 7.0, 3.0 / 7.0, // 0, 1, 2, 3 - 3.0 / 6.0, 4.0 / 6.0, 4.0 / 5.0, 5.0 / 5.0, // 4, 5, 6, 7 - 5.0 / 4.0, 6.0 / 4.0, 6.0 / 3.0, 7.0 / 3.0, // 8, 9, 10, 11 - 7.0 / 2.0, 8.0 / 2.0, 8.0 / 1.0}; // 12, 13, 14, 15 = bypass - -} // namespace - -AR0231::AR0231() { - image_sensor = cereal::FrameData::ImageSensor::AR0231; - bayer_pattern = CAM_ISP_PATTERN_BAYER_GRGRGR; - pixel_size_mm = 0.003; - data_word = true; - frame_width = 1928; - frame_height = 1208; - frame_stride = (frame_width * 12 / 8) + 4; - extra_height = AR0231_REGISTERS_HEIGHT + AR0231_STATS_HEIGHT; - - registers_offset = 0; - frame_offset = AR0231_REGISTERS_HEIGHT; - stats_offset = AR0231_REGISTERS_HEIGHT + frame_height; - - start_reg_array.assign(std::begin(start_reg_array_ar0231), std::end(start_reg_array_ar0231)); - init_reg_array.assign(std::begin(init_array_ar0231), std::end(init_array_ar0231)); - probe_reg_addr = 0x3000; - probe_expected_data = 0x354; - bits_per_pixel = 12; - mipi_format = CAM_FORMAT_MIPI_RAW_12; - frame_data_type = 0x12; // Changing stats to 0x2C doesn't work, so change pixels to 0x12 instead - mclk_frequency = 19200000; //Hz - - readout_time_ns = 22850000; - - dc_gain_factor = 2.5; - dc_gain_min_weight = 0; - dc_gain_max_weight = 1; - dc_gain_on_grey = 0.2; - dc_gain_off_grey = 0.3; - exposure_time_min = 2; // with HDR, fastest ss - exposure_time_max = 0x0855; // with HDR, slowest ss, 40ms - analog_gain_min_idx = 0x1; // 0.25x - analog_gain_rec_idx = 0x6; // 0.8x - analog_gain_max_idx = 0xD; // 4.0x - analog_gain_cost_delta = 0; - analog_gain_cost_low = 0.1; - analog_gain_cost_high = 5.0; - for (int i = 0; i <= analog_gain_max_idx; i++) { - sensor_analog_gains[i] = sensor_analog_gains_AR0231[i]; - } - min_ev = exposure_time_min * sensor_analog_gains[analog_gain_min_idx]; - max_ev = exposure_time_max * dc_gain_factor * sensor_analog_gains[analog_gain_max_idx]; - target_grey_factor = 1.0; - - black_level = 168; - color_correct_matrix = { - 0x000000af, 0x00000ff9, 0x00000fd8, - 0x00000fbc, 0x000000bb, 0x00000009, - 0x00000fb6, 0x00000fe0, 0x000000ea, - }; - for (int i = 0; i < 65; i++) { - float fx = i / 64.0; - const float gamma_k = 0.75; - const float gamma_b = 0.125; - const float mp = 0.01; // ideally midpoint should be adaptive - const float rk = 9 - 100*mp; - // poly approximation for s curve - fx = (fx > mp) ? - ((rk * (fx-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(fx-mp))) + gamma_k*mp + gamma_b) : - ((rk * (fx-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(fx-mp))) + gamma_k*mp + gamma_b); - gamma_lut_rgb.push_back((uint32_t)(fx*1023.0 + 0.5)); - } - prepare_gamma_lut(); - linearization_lut = { - 0x02000000, 0x02000000, 0x02000000, 0x02000000, - 0x020007ff, 0x020007ff, 0x020007ff, 0x020007ff, - 0x02000bff, 0x02000bff, 0x02000bff, 0x02000bff, - 0x020017ff, 0x020017ff, 0x020017ff, 0x020017ff, - 0x02001bff, 0x02001bff, 0x02001bff, 0x02001bff, - 0x020023ff, 0x020023ff, 0x020023ff, 0x020023ff, - 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, - 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, - 0x00003fff, 0x00003fff, 0x00003fff, 0x00003fff, - }; - linearization_pts = {0x07ff0bff, 0x17ff1bff, 0x23ff3fff, 0x3fff3fff}; - vignetting_lut = { - 0x00eaa755, 0x00cf2679, 0x00bc05e0, 0x00acc566, 0x00a1450a, 0x009984cc, 0x0095a4ad, 0x009584ac, 0x009944ca, 0x00a0c506, 0x00ac0560, 0x00bb25d9, 0x00ce2671, 0x00e90748, 0x01112889, 0x014a2a51, 0x01984cc2, - 0x00db06d8, 0x00c30618, 0x00afe57f, 0x00a0a505, 0x009524a9, 0x008d646b, 0x0089844c, 0x0089644b, 0x008d2469, 0x0094a4a5, 0x009fe4ff, 0x00af0578, 0x00c20610, 0x00d986cc, 0x00fda7ed, 0x01320990, 0x017aebd7, - 0x00d1868c, 0x00baa5d5, 0x00a7853c, 0x009844c2, 0x008cc466, 0x0085a42d, 0x0083641b, 0x0083641b, 0x0085842c, 0x008c4462, 0x0097a4bd, 0x00a6c536, 0x00b9a5cd, 0x00d06683, 0x00f1678b, 0x01226913, 0x0167ab3d, - 0x00cd0668, 0x00b625b1, 0x00a30518, 0x0093c49e, 0x00884442, 0x00830418, 0x0080e407, 0x0080c406, 0x0082e417, 0x0087c43e, 0x00932499, 0x00a22511, 0x00b525a9, 0x00cbe65f, 0x00eb0758, 0x011a68d3, 0x015daaed, - 0x00cc4662, 0x00b565ab, 0x00a24512, 0x00930498, 0x0087843c, 0x0082a415, 0x00806403, 0x00806403, 0x00828414, 0x00870438, 0x00926493, 0x00a1850c, 0x00b465a3, 0x00cb2659, 0x00ea2751, 0x011928c9, 0x015c2ae1, - 0x00cf667b, 0x00b885c4, 0x00a5652b, 0x009624b1, 0x008aa455, 0x00846423, 0x00822411, 0x00822411, 0x00844422, 0x008a2451, 0x009564ab, 0x00a48524, 0x00b785bc, 0x00ce4672, 0x00ee6773, 0x011e88f4, 0x0162eb17, - 0x00d6c6b6, 0x00bf65fb, 0x00ac4562, 0x009d04e8, 0x0091848c, 0x0089c44e, 0x00862431, 0x00860430, 0x0089844c, 0x00910488, 0x009c64e3, 0x00ab655b, 0x00be65f3, 0x00d566ab, 0x00f847c2, 0x012b2959, 0x01726b93, - 0x00e3e71f, 0x00ca0650, 0x00b705b8, 0x00a7a53d, 0x009c24e1, 0x009484a4, 0x00908484, 0x00908484, 0x009424a1, 0x009bc4de, 0x00a70538, 0x00b625b1, 0x00c90648, 0x00e26713, 0x0108e847, 0x013fe9ff, 0x018bcc5e, - 0x00f807c0, 0x00d966cb, 0x00c5862c, 0x00b625b1, 0x00aaa555, 0x00a30518, 0x009f04f8, 0x009f04f8, 0x00a2a515, 0x00aa2551, 0x00b585ac, 0x00c4a625, 0x00d846c2, 0x00f647b2, 0x0121a90d, 0x015e4af2, 0x01b8cdc6, - 0x011548aa, 0x00f1678b, 0x00d886c4, 0x00c86643, 0x00bce5e7, 0x00b545aa, 0x00b1658b, 0x00b1458a, 0x00b505a8, 0x00bc85e4, 0x00c7c63e, 0x00d786bc, 0x00efe77f, 0x0113489a, 0x0144ea27, 0x01888c44, 0x01fdcfee, - 0x013e49f2, 0x0113e89f, 0x00f5a7ad, 0x00e0c706, 0x00d30698, 0x00cb665b, 0x00c7663b, 0x00c7663b, 0x00cb0658, 0x00d2a695, 0x00dfe6ff, 0x00f467a3, 0x01122891, 0x013be9df, 0x01750ba8, 0x01cfae7d, 0x025912c8, - 0x01766bb3, 0x01446a23, 0x011fc8fe, 0x0105e82f, 0x00f467a3, 0x00e9874c, 0x00e46723, 0x00e44722, 0x00e92749, 0x00f3a79d, 0x0104c826, 0x011e48f2, 0x01424a12, 0x01738b9c, 0x01bf6dfb, 0x023611b0, 0x02ced676, - 0x01cf8e7c, 0x01866c33, 0x015aaad5, 0x013ae9d7, 0x01250928, 0x011768bb, 0x0110a885, 0x01108884, 0x0116e8b7, 0x01242921, 0x0139a9cd, 0x0158eac7, 0x01840c20, 0x01cb0e58, 0x0233719b, 0x02b9d5ce, 0x03645b22, - }; -} - -std::vector AR0231::getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const { - uint16_t analog_gain_reg = 0xFF00 | (new_exp_g << 4) | new_exp_g; - return { - {0x3366, analog_gain_reg}, - {0x3362, (uint16_t)(dc_gain_enabled ? 0x1 : 0x0)}, - {0x3012, (uint16_t)exposure_time}, - }; -} - -int AR0231::getSlaveAddress(int port) const { - assert(port >= 0 && port <= 2); - return (int[]){0x20, 0x30, 0x20}[port]; -} - -float AR0231::getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const { - // Cost of ev diff - float score = std::abs(desired_ev - (exp_t * exp_gain)) * 10; - // Cost of absolute gain - float m = exp_g_idx > analog_gain_rec_idx ? analog_gain_cost_high : analog_gain_cost_low; - score += std::abs(exp_g_idx - (int)analog_gain_rec_idx) * m; - // Cost of changing gain - score += std::abs(exp_g_idx - gain_idx) * (score + 1.0) / 10.0; - return score; -} diff --git a/system/camerad/sensors/ar0231_registers.h b/system/camerad/sensors/ar0231_registers.h deleted file mode 100644 index e0872a673a..0000000000 --- a/system/camerad/sensors/ar0231_registers.h +++ /dev/null @@ -1,121 +0,0 @@ -#pragma once - -const struct i2c_random_wr_payload start_reg_array_ar0231[] = {{0x301A, 0x91C}}; -const struct i2c_random_wr_payload stop_reg_array_ar0231[] = {{0x301A, 0x918}}; - -const struct i2c_random_wr_payload init_array_ar0231[] = { - {0x301A, 0x0018}, // RESET_REGISTER - - // **NOTE**: if this is changed, readout_time_ns must be updated in the Sensor config - - // CLOCK Settings - // input clock is 19.2 / 2 * 0x37 = 528 MHz - // pixclk is 528 / 6 = 88 MHz - // full roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*FRAME_LENGTH_LINES)) = 39.99 ms - // img roll time is 1000/(PIXCLK/(LINE_LENGTH_PCK*Y_OUTPUT_CONTROL)) = 22.85 ms - {0x302A, 0x0006}, // VT_PIX_CLK_DIV - {0x302C, 0x0001}, // VT_SYS_CLK_DIV - {0x302E, 0x0002}, // PRE_PLL_CLK_DIV - {0x3030, 0x0037}, // PLL_MULTIPLIER - {0x3036, 0x000C}, // OP_PIX_CLK_DIV - {0x3038, 0x0001}, // OP_SYS_CLK_DIV - - // FORMAT - {0x3040, 0xC000}, // READ_MODE - {0x3004, 0x0000}, // X_ADDR_START_ - {0x3008, 0x0787}, // X_ADDR_END_ - {0x3002, 0x0000}, // Y_ADDR_START_ - {0x3006, 0x04B7}, // Y_ADDR_END_ - {0x3032, 0x0000}, // SCALING_MODE - {0x30A2, 0x0001}, // X_ODD_INC_ - {0x30A6, 0x0001}, // Y_ODD_INC_ - {0x3402, 0x0788}, // X_OUTPUT_CONTROL - {0x3404, 0x04B8}, // Y_OUTPUT_CONTROL - {0x3064, 0x1982}, // SMIA_TEST - {0x30BA, 0x11F2}, // DIGITAL_CTRL - - // Enable external trigger and disable GPIO outputs - {0x30CE, 0x0120}, // SLAVE_SH_SYNC_MODE | FRAME_START_MODE - {0x340A, 0xE0}, // GPIO3_INPUT_DISABLE | GPIO2_INPUT_DISABLE | GPIO1_INPUT_DISABLE - {0x340C, 0x802}, // GPIO_HIDRV_EN | GPIO0_ISEL=2 - - // Readout timing - {0x300C, 0x0672}, // LINE_LENGTH_PCK (valid for 3-exposure HDR) - {0x300A, 0x0855}, // FRAME_LENGTH_LINES - {0x3042, 0x0000}, // EXTRA_DELAY - - // Readout Settings - {0x31AE, 0x0204}, // SERIAL_FORMAT, 4-lane MIPI - {0x31AC, 0x0C0C}, // DATA_FORMAT_BITS, 12 -> 12 - {0x3342, 0x1212}, // MIPI_F1_PDT_EDT - {0x3346, 0x1212}, // MIPI_F2_PDT_EDT - {0x334A, 0x1212}, // MIPI_F3_PDT_EDT - {0x334E, 0x1212}, // MIPI_F4_PDT_EDT - {0x3344, 0x0011}, // MIPI_F1_VDT_VC - {0x3348, 0x0111}, // MIPI_F2_VDT_VC - {0x334C, 0x0211}, // MIPI_F3_VDT_VC - {0x3350, 0x0311}, // MIPI_F4_VDT_VC - {0x31B0, 0x0053}, // FRAME_PREAMBLE - {0x31B2, 0x003B}, // LINE_PREAMBLE - {0x301A, 0x001C}, // RESET_REGISTER - - // Noise Corrections - {0x3092, 0x0C24}, // ROW_NOISE_CONTROL - {0x337A, 0x0C80}, // DBLC_SCALE0 - {0x3370, 0x03B1}, // DBLC - {0x3044, 0x0400}, // DARK_CONTROL - - // Enable temperature sensor - {0x30B4, 0x0007}, // TEMPSENS0_CTRL_REG - {0x30B8, 0x0007}, // TEMPSENS1_CTRL_REG - - // Enable dead pixel correction using - // the 1D line correction scheme - {0x31E0, 0x0003}, - - // HDR Settings - {0x3082, 0x0004}, // OPERATION_MODE_CTRL - {0x3238, 0x0444}, // EXPOSURE_RATIO - - {0x1008, 0x0361}, // FINE_INTEGRATION_TIME_MIN - {0x100C, 0x0589}, // FINE_INTEGRATION_TIME2_MIN - {0x100E, 0x07B1}, // FINE_INTEGRATION_TIME3_MIN - {0x1010, 0x0139}, // FINE_INTEGRATION_TIME4_MIN - - // TODO: do these have to be lower than LINE_LENGTH_PCK? - {0x3014, 0x08CB}, // FINE_INTEGRATION_TIME_ - {0x321E, 0x0894}, // FINE_INTEGRATION_TIME2 - - {0x31D0, 0x0000}, // COMPANDING, no good in 10 bit? - {0x33DA, 0x0000}, // COMPANDING - {0x318E, 0x0200}, // PRE_HDR_GAIN_EN - - // DLO Settings - {0x3100, 0x4000}, // DLO_CONTROL0 - {0x3280, 0x0CCC}, // T1 G1 - {0x3282, 0x0CCC}, // T1 R - {0x3284, 0x0CCC}, // T1 B - {0x3286, 0x0CCC}, // T1 G2 - {0x3288, 0x0FA0}, // T2 G1 - {0x328A, 0x0FA0}, // T2 R - {0x328C, 0x0FA0}, // T2 B - {0x328E, 0x0FA0}, // T2 G2 - - // Initial Gains - {0x3022, 0x0001}, // GROUPED_PARAMETER_HOLD_ - {0x3366, 0xFF77}, // ANALOG_GAIN (1x) - - {0x3060, 0x3333}, // ANALOG_COLOR_GAIN - - {0x3362, 0x0000}, // DC GAIN - - {0x305A, 0x00F8}, // red gain - {0x3058, 0x0122}, // blue gain - {0x3056, 0x009A}, // g1 gain - {0x305C, 0x009A}, // g2 gain - - {0x3022, 0x0000}, // GROUPED_PARAMETER_HOLD_ - - // Initial Integration Time - {0x3012, 0x0005}, -}; diff --git a/system/camerad/sensors/sensor.h b/system/camerad/sensors/sensor.h index d4be3cf036..96aa8b604f 100644 --- a/system/camerad/sensors/sensor.h +++ b/system/camerad/sensors/sensor.h @@ -10,7 +10,6 @@ #include "media/cam_sensor.h" #include "cereal/gen/cpp/log.capnp.h" -#include "system/camerad/sensors/ar0231_registers.h" #include "system/camerad/sensors/ox03c10_registers.h" #include "system/camerad/sensors/os04c10_registers.h" @@ -88,17 +87,6 @@ public: }; }; -class AR0231 : public SensorInfo { -public: - AR0231(); - std::vector getExposureRegisters(int exposure_time, int new_exp_g, bool dc_gain_enabled) const override; - float getExposureScore(float desired_ev, int exp_t, int exp_g_idx, float exp_gain, int gain_idx) const override; - int getSlaveAddress(int port) const override; - -private: - mutable std::map> ar0231_register_lut; -}; - class OX03C10 : public SensorInfo { public: OX03C10(); From bb06468eadc230fc59315539b81626046acfc09c Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Wed, 27 Aug 2025 16:41:57 -0700 Subject: [PATCH 040/188] safety standards for forks (#36077) standards for forks --- docs/SAFETY.md | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/docs/SAFETY.md b/docs/SAFETY.md index 18a450a395..25815e3372 100644 --- a/docs/SAFETY.md +++ b/docs/SAFETY.md @@ -16,7 +16,7 @@ industry standards of safety for Level 2 Driver Assistance Systems. In particula ISO26262 guidelines, including those from [pertinent documents](https://www.nhtsa.gov/sites/nhtsa.dot.gov/files/documents/13498a_812_573_alcsystemreport.pdf) released by NHTSA. In addition, we impose strict coding guidelines (like [MISRA C : 2012](https://www.misra.org.uk/what-is-misra/)) on parts of openpilot that are safety relevant. We also perform software-in-the-loop, -hardware-in-the-loop and in-vehicle tests before each software release. +hardware-in-the-loop, and in-vehicle tests before each software release. Following Hazard and Risk Analysis and FMEA, at a very high level, we have designed openpilot ensuring two main safety requirements. @@ -29,8 +29,18 @@ ensuring two main safety requirements. For additional safety implementation details, refer to [panda safety model](https://github.com/commaai/panda#safety-model). For vehicle specific implementation of the safety concept, refer to [opendbc/safety/safety](https://github.com/commaai/opendbc/tree/master/opendbc/safety/safety). -**Extra note**: comma.ai strongly discourages the use of openpilot forks with safety code either missing or - not fully meeting the above requirements. +[^1]: For these actuator limits we observe ISO11270 and ISO15622. Lateral limits described there translate to 0.9 seconds of maximum actuation to achieve a 1m lateral deviation. -[^1]: For these actuator limits we observe ISO11270 and ISO15622. Lateral limits described there translate to 0.9 seconds of maximum actuation to achieve a 1m lateral deviation. +--- +### Forks of openpilot + +* Do not disable or nerf [driver monitoring](https://github.com/commaai/openpilot/tree/master/selfdrive/monitoring) +* Do not disable or nerf [excessive actuation checks](https://github.com/commaai/openpilot/tree/master/selfdrive/selfdrived/helpers.py) +* If your fork modifies any of the code in `opendbc/safety/`: + * your fork cannot use the openpilot trademark + * your fork must preserve the full [safety test suite](https://github.com/commaai/opendbc/tree/master/opendbc/safety/tests) and all tests must pass, including any new coverage required by the fork's changes + +Failure to comply with these standards will get you and your users banned from comma.ai servers. + +**comma.ai strongly discourages the use of openpilot forks with safety code either missing or not fully meeting the above requirements.** From a2c5fca787de19a84b47eb8123ac1c46b980dd0e Mon Sep 17 00:00:00 2001 From: ZwX1616 Date: Wed, 27 Aug 2025 17:54:53 -0700 Subject: [PATCH 041/188] modeld input queues class (#36072) * move from xx * no get_single * stupid name * thats fine * desire_pulse * 1less * desire->desire_pulse * simplify * reduce copies * more less --- selfdrive/modeld/constants.py | 9 +-- selfdrive/modeld/fill_model_msg.py | 2 +- selfdrive/modeld/modeld.py | 88 +++++++++++++++++++++++------- 3 files changed, 73 insertions(+), 26 deletions(-) diff --git a/selfdrive/modeld/constants.py b/selfdrive/modeld/constants.py index 5ca0a86bc8..ff7e1d8600 100644 --- a/selfdrive/modeld/constants.py +++ b/selfdrive/modeld/constants.py @@ -13,12 +13,9 @@ class ModelConstants: META_T_IDXS = [2., 4., 6., 8., 10.] # model inputs constants - MODEL_FREQ = 20 - HISTORY_FREQ = 5 - HISTORY_LEN_SECONDS = 5 - TEMPORAL_SKIP = MODEL_FREQ // HISTORY_FREQ - FULL_HISTORY_BUFFER_LEN = MODEL_FREQ * HISTORY_LEN_SECONDS - INPUT_HISTORY_BUFFER_LEN = HISTORY_FREQ * HISTORY_LEN_SECONDS + N_FRAMES = 2 + MODEL_RUN_FREQ = 20 + MODEL_CONTEXT_FREQ = 5 # "model_trained_fps" FEATURE_LEN = 512 diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index a2b54b420e..82c4c92b1d 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -149,7 +149,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D meta.hardBrakePredicted = hard_brake_predicted.item() # confidence - if vipc_frame_id % (2*ModelConstants.MODEL_FREQ) == 0: + if vipc_frame_id % (2*ModelConstants.MODEL_RUN_FREQ) == 0: # any disengage prob brake_disengage_probs = net_output_data['meta'][0,Meta.BRAKE_DISENGAGE] gas_disengage_probs = net_output_data['meta'][0,Meta.GAS_DISENGAGE] diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index 6e09f40e22..e08fc30c2e 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -77,6 +77,64 @@ class FrameMeta: if vipc is not None: self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof +class InputQueues: + def __init__ (self, model_fps, env_fps, n_frames_input): + assert env_fps % model_fps == 0 + assert env_fps >= model_fps + self.model_fps = model_fps + self.env_fps = env_fps + self.n_frames_input = n_frames_input + + self.dtypes = {} + self.shapes = {} + self.q = {} + + def update_dtypes_and_shapes(self, input_dtypes, input_shapes) -> None: + self.dtypes.update(input_dtypes) + if self.env_fps == self.model_fps: + self.shapes.update(input_shapes) + else: + for k in input_shapes: + shape = list(input_shapes[k]) + if 'img' in k: + n_channels = shape[1] // self.n_frames_input + shape[1] = (self.env_fps // self.model_fps + (self.n_frames_input - 1)) * n_channels + else: + shape[1] = (self.env_fps // self.model_fps) * shape[1] + self.shapes[k] = tuple(shape) + + def reset(self) -> None: + self.q = {k: np.zeros(self.shapes[k], dtype=self.dtypes[k]) for k in self.dtypes.keys()} + + def enqueue(self, inputs:dict[str, np.ndarray]) -> None: + for k in inputs.keys(): + if inputs[k].dtype != self.dtypes[k]: + raise ValueError(f'supplied input <{k}({inputs[k].dtype})> has wrong dtype, expected {self.dtypes[k]}') + input_shape = list(self.shapes[k]) + input_shape[1] = -1 + single_input = inputs[k].reshape(tuple(input_shape)) + sz = single_input.shape[1] + self.q[k][:,:-sz] = self.q[k][:,sz:] + self.q[k][:,-sz:] = single_input + + def get(self, *names) -> dict[str, np.ndarray]: + if self.env_fps == self.model_fps: + return {k: self.q[k] for k in names} + else: + out = {} + for k in names: + shape = self.shapes[k] + if 'img' in k: + n_channels = shape[1] // (self.env_fps // self.model_fps + (self.n_frames_input - 1)) + out[k] = np.concatenate([self.q[k][:, s:s+n_channels] for s in np.linspace(0, shape[1] - n_channels, self.n_frames_input, dtype=int)], axis=1) + elif 'pulse' in k: + # any pulse within interval counts + out[k] = self.q[k].reshape((shape[0], shape[1] * self.model_fps // self.env_fps, self.env_fps // self.model_fps, -1)).max(axis=2) + else: + idxs = np.arange(-1, -shape[1], -self.env_fps // self.model_fps)[::-1] + out[k] = self.q[k][:, idxs] + return out + class ModelState: frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] @@ -97,19 +155,15 @@ class ModelState: self.policy_output_slices = policy_metadata['output_slices'] policy_output_size = policy_metadata['output_shapes']['outputs'][1] - self.frames = {name: DrivingModelFrame(context, ModelConstants.TEMPORAL_SKIP) for name in self.vision_input_names} + self.frames = {name: DrivingModelFrame(context, ModelConstants.MODEL_RUN_FREQ//ModelConstants.MODEL_CONTEXT_FREQ) for name in self.vision_input_names} self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) - self.full_features_buffer = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32) - self.full_desire = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32) - self.temporal_idxs = slice(-1-(ModelConstants.TEMPORAL_SKIP*(ModelConstants.INPUT_HISTORY_BUFFER_LEN-1)), None, ModelConstants.TEMPORAL_SKIP) - # policy inputs - self.numpy_inputs = { - 'desire_pulse': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.DESIRE_LEN), dtype=np.float32), - 'traffic_convention': np.zeros((1, ModelConstants.TRAFFIC_CONVENTION_LEN), dtype=np.float32), - 'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), - } + self.numpy_inputs = {k: np.zeros(self.policy_input_shapes[k], dtype=np.float32) for k in self.policy_input_shapes} + self.full_input_queues = InputQueues(ModelConstants.MODEL_CONTEXT_FREQ, ModelConstants.MODEL_RUN_FREQ, ModelConstants.N_FRAMES) + for k in ['desire_pulse', 'features_buffer']: + self.full_input_queues.update_dtypes_and_shapes({k: self.numpy_inputs[k].dtype}, {k: self.numpy_inputs[k].shape}) + self.full_input_queues.reset() # img buffers are managed in openCL transform code self.vision_inputs: dict[str, Tensor] = {} @@ -135,11 +189,6 @@ class ModelState: new_desire = np.where(inputs['desire_pulse'] - self.prev_desire > .99, inputs['desire_pulse'], 0) self.prev_desire[:] = inputs['desire_pulse'] - self.full_desire[0,:-1] = self.full_desire[0,1:] - self.full_desire[0,-1] = new_desire - self.numpy_inputs['desire_pulse'][:] = self.full_desire.reshape((1,ModelConstants.INPUT_HISTORY_BUFFER_LEN,ModelConstants.TEMPORAL_SKIP,-1)).max(axis=2) - - self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names} if TICI and not USBGPU: @@ -158,9 +207,10 @@ class ModelState: self.vision_output = self.vision_run(**self.vision_inputs).contiguous().realize().uop.base.buffer.numpy() vision_outputs_dict = self.parser.parse_vision_outputs(self.slice_outputs(self.vision_output, self.vision_output_slices)) - self.full_features_buffer[0,:-1] = self.full_features_buffer[0,1:] - self.full_features_buffer[0,-1] = vision_outputs_dict['hidden_state'][0, :] - self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[0, self.temporal_idxs] + self.full_input_queues.enqueue({'features_buffer': vision_outputs_dict['hidden_state'], 'desire_pulse': new_desire}) + for k in ['desire_pulse', 'features_buffer']: + self.numpy_inputs[k][:] = self.full_input_queues.get(k)[k] + self.numpy_inputs['traffic_convention'][:] = inputs['traffic_convention'] self.policy_output = self.policy_run(**self.policy_inputs).contiguous().realize().uop.base.buffer.numpy() policy_outputs_dict = self.parser.parse_policy_outputs(self.slice_outputs(self.policy_output, self.policy_output_slices)) @@ -218,7 +268,7 @@ def main(demo=False): params = Params() # setup filter to track dropped frames - frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_FREQ) + frame_dropped_filter = FirstOrderFilter(0., 10., 1. / ModelConstants.MODEL_RUN_FREQ) frame_id = 0 last_vipc_frame_id = 0 run_count = 0 From 93f7925c4d4a535e18da5cf6a7d49f7382eca1a4 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 18:36:10 -0700 Subject: [PATCH 042/188] jenkins: tizi ox --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 6c4e01854f..cba09e19f3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -223,7 +223,7 @@ node { ]) }, 'camerad OX03C10': { - deviceStage("OX03C10", "tici-ox03c10", ["UNSAFE=1"], [ + deviceStage("OX03C10", "tizi-ox03c10", ["UNSAFE=1"], [ step("build", "cd system/manager && ./build.py"), step("test camerad", "pytest system/camerad/test/test_camerad.py", [timeout: 60]), step("test exposure", "pytest system/camerad/test/test_exposure.py"), From 7a19a110011900b0c0bc6dd6d9d8176ffef856f8 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 18:46:50 -0700 Subject: [PATCH 043/188] jenkins: tizi loopback device --- Jenkinsfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index cba09e19f3..3e9da05632 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -217,7 +217,7 @@ node { ]) }, 'loopback': { - deviceStage("loopback", "tici-loopback", ["UNSAFE=1"], [ + deviceStage("loopback", "tizi-loopback", ["UNSAFE=1"], [ step("build openpilot", "cd system/manager && ./build.py"), step("test pandad loopback", "pytest selfdrive/pandad/tests/test_pandad_loopback.py"), ]) From 63961dec45c634f478fec88e180c928e6f9709d4 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 27 Aug 2025 19:04:16 -0700 Subject: [PATCH 044/188] jenkins: run pandad test once --- Jenkinsfile | 1 - 1 file changed, 1 deletion(-) diff --git a/Jenkinsfile b/Jenkinsfile index 3e9da05632..f3a63d3dec 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -253,7 +253,6 @@ node { step("build openpilot", "cd system/manager && ./build.py"), step("test pandad loopback", "SINGLE_PANDA=1 pytest selfdrive/pandad/tests/test_pandad_loopback.py"), step("test pandad spi", "pytest selfdrive/pandad/tests/test_pandad_spi.py"), - step("test pandad", "pytest selfdrive/pandad/tests/test_pandad.py", [diffPaths: ["panda", "selfdrive/pandad/"]]), step("test amp", "pytest system/hardware/tici/tests/test_amplifier.py"), // TODO: enable once new AGNOS is available // step("test esim", "pytest system/hardware/tici/tests/test_esim.py"), From 2aee69d267904da03fddcb8c54ce573c343e6c9b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 28 Aug 2025 00:15:08 -0400 Subject: [PATCH 045/188] rename --- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 6152158287..26105e7fa5 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -252,7 +252,7 @@ class SpeedLimitController: elif self._speed_limit > 0 and self._v_offset < LIMIT_SPEED_OFFSET_TH: self.state = SpeedLimitControlState.adapting - def _state_transition(self) -> None: + def _state_control(self) -> None: self._state_prev = self._state # If op is disabled or SLC is disabled, go inactive @@ -292,7 +292,7 @@ class SpeedLimitController: self._update_params() self._update_calculations(v_ego, a_ego, v_cruise_setpoint) - self._state_transition() + self._state_control() self._update_events(events_sp) return self.speed_limit_offseted From 20feab0eaebf6d929ab5015483c5b5328932e0e8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 28 Aug 2025 00:20:46 -0400 Subject: [PATCH 046/188] this --- .../speed_limit_controller.py | 228 +++++++++--------- 1 file changed, 114 insertions(+), 114 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 26105e7fa5..7ce750449d 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -30,44 +30,44 @@ class SpeedLimitController: def __init__(self, CP): self.params = Params() - self._CP = CP - self._policy = self.params.get("SpeedLimitControlPolicy", return_default=True) - self._resolver = SpeedLimitResolver(self._policy) - self._last_params_update = 0.0 - self._last_op_engaged_time = 0.0 - self._is_metric = self.params.get_bool("IsMetric") - self._enabled = self.params.get_bool("SpeedLimitControl") - self._op_engaged = False - self._op_engaged_prev = False - self._v_ego = 0. - self._a_ego = 0. - self._v_offset = 0. - self._v_cruise_setpoint = 0. - self._v_cruise_setpoint_prev = 0. - self._v_cruise_setpoint_changed = False - self._initial_max_set = False + self.CP = CP + self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) + self.resolver = SpeedLimitResolver(self.policy) + self.last_params_update = 0.0 + self.last_op_engaged_time = 0.0 + self.is_metric = self.params.get_bool("IsMetric") + self.enabled = self.params.get_bool("SpeedLimitControl") + self.op_engaged = False + self.op_engaged_prev = False + self.v_ego = 0. + self.a_ego = 0. + self.v_offset = 0. + self.v_cruise_setpoint = 0. + self.v_cruise_setpoint_prev = 0. + self.v_cruise_setpoint_changed = False + self.initial_max_set = False self._speed_limit = 0. - self._speed_limit_prev = 0. - self._speed_limit_changed = False - self._last_valid_speed_limit_offsetted = 0. + self.speed_limit_prev = 0. + self.speed_limit_changed = False + self.last_valid_speed_limit_offsetted = 0. self._distance = 0. self._source = Source.none - self._state = SpeedLimitControlState.inactive - self._state_prev = SpeedLimitControlState.inactive - self._pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise + self.state = SpeedLimitControlState.inactive + self.state_prev = SpeedLimitControlState.inactive + self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise - self._offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) - self._offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) - self._warning_type = self.params.get("SpeedLimitWarningType", return_default=True) - self._warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) - self._warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) - self._engage_type = self._read_engage_type_param() - self._current_time = 0. - self._v_cruise_rounded = 0. - self._v_cruise_prev_rounded = 0. - self._speed_limit_offsetted_rounded = 0. - self._speed_limit_warning_offsetted_rounded = 0. - self._speed_factor = CV.MS_TO_KPH if self._is_metric else CV.MS_TO_MPH + self.offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) + self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) + self.warning_type = self.params.get("SpeedLimitWarningType", return_default=True) + self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) + self.warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) + self.engage_type = self.read_engage_type_param() + self.current_time = 0. + self.v_cruise_rounded = 0. + self.v_cruise_prev_rounded = 0. + self.speed_limit_offsetted_rounded = 0. + self.speed_limit_warning_offsetted_rounded = 0. + self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH # Mapping functions to state transitions self.state_transition_strategy = { @@ -89,44 +89,44 @@ class SpeedLimitController: @property def state(self) -> SpeedLimitControlState: - return self._state + return self.state @state.setter def state(self, value) -> None: - if value != self._state: + if value != self.state: debug(f'Speed Limit Controller state: {description_for_state(value)}') - self._state = value + self.state = value @property def is_enabled(self) -> bool: - return self.state in ENABLED_STATES and self._enabled + return self.state in ENABLED_STATES and self.enabled @property def is_active(self) -> bool: - return self.state in ACTIVE_STATES and self._enabled + return self.state in ACTIVE_STATES and self.enabled @property def speed_limit_offseted(self) -> float: # If we have a current valid speed limit, use it if self._speed_limit > 0: current_offsetted = self._speed_limit + self.speed_limit_offset - self._last_valid_speed_limit_offsetted = current_offsetted + self.last_valid_speed_limit_offsetted = current_offsetted return current_offsetted # If no current speed limit but we have a last valid one, use that - if self._last_valid_speed_limit_offsetted > 0: - return self._last_valid_speed_limit_offsetted + if self.last_valid_speed_limit_offsetted > 0: + return self.last_valid_speed_limit_offsetted # Fallback return FALLBACK_CRUISE_SPEED @property def speed_limit_offset(self) -> float: - return self._get_offset(self._offset_type, self._offset_value) + return self.get_offset(self.offset_type, self.offset_value) @property def speed_limit_warning_offset(self) -> float: - return self._get_offset(self._warning_offset_type, self._warning_offset_value) + return self.get_offset(self.warning_offset_type, self.warning_offset_value) @property def speed_limit(self) -> float: @@ -140,159 +140,159 @@ class SpeedLimitController: def source(self) -> Source: return self._source - def _get_offset(self, offset_type: OffsetType, offset_value: int) -> float: + def get_offset(self, offset_type: OffsetType, offset_value: int) -> float: if offset_type == OffsetType.default: return float(np.interp(self._speed_limit, LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V) * self._speed_limit) elif offset_type == OffsetType.fixed: - return offset_value * (CV.KPH_TO_MS if self._is_metric else CV.MPH_TO_MS) + return offset_value * (CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS) elif offset_type == OffsetType.percentage: return offset_value * 0.01 * self._speed_limit else: raise NotImplementedError("Offset not supported") - def _update_v_cruise_setpoint_prev(self) -> None: - self._v_cruise_setpoint_prev = self._v_cruise_setpoint + def update_v_cruise_setpoint_prev(self) -> None: + self.v_cruise_setpoint_prev = self.v_cruise_setpoint - def _update_params(self) -> None: - if self._current_time > self._last_params_update + PARAMS_UPDATE_PERIOD: - self._enabled = self.params.get_bool("SpeedLimitControl") - self._offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) - self._offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) - self._warning_type = self.params.get("SpeedLimitWarningType", return_default=True) - self._warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) - self._warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) - self._policy = Policy(self.params.get("SpeedLimitControlPolicy", return_default=True)) - self._is_metric = self.params.get_bool("IsMetric") - self._speed_factor = CV.MS_TO_KPH if self._is_metric else CV.MS_TO_MPH - self._resolver.change_policy(self._policy) - self._engage_type = self._read_engage_type_param() + def update_params(self) -> None: + if self.current_time > self.last_params_update + PARAMS_UPDATE_PERIOD: + self.enabled = self.params.get_bool("SpeedLimitControl") + self.offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) + self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) + self.warning_type = self.params.get("SpeedLimitWarningType", return_default=True) + self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) + self.warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) + self.policy = Policy(self.params.get("SpeedLimitControlPolicy", return_default=True)) + self.is_metric = self.params.get_bool("IsMetric") + self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH + self.resolver.change_policy(self.policy) + self.engage_type = self.read_engage_type_param() - self._last_params_update = self._current_time + self.last_params_update = self.current_time @staticmethod - def _read_engage_type_param() -> Engage: + def read_engage_type_param() -> Engage: return Engage.auto - def _initial_max_set_confirmed(self) -> bool: - return abs(self._v_cruise_setpoint - REQUIRED_INITIAL_CRUISE_SPEED) <= CRUISE_SPEED_TOLERANCE + def initial_max_set_confirmed(self) -> bool: + return abs(self.v_cruise_setpoint - REQUIRED_INITIAL_CRUISE_SPEED) <= CRUISE_SPEED_TOLERANCE - def _detect_manual_cruise_change(self) -> bool: + def detect_manual_cruise_change(self) -> bool: if not self.is_active: return False # If cruise speed changed and it's not what SLC would set - if self._v_cruise_setpoint_changed: + if self.v_cruise_setpoint_changed: expected_cruise = self.speed_limit_offseted - return abs(self._v_cruise_setpoint - expected_cruise) > CRUISE_SPEED_TOLERANCE + return abs(self.v_cruise_setpoint - expected_cruise) > CRUISE_SPEED_TOLERANCE return False - def _update_calculations(self, v_ego: float, a_ego: float, v_cruise_setpoint: float) -> None: - self._v_cruise_setpoint = v_cruise_setpoint if not np.isnan(v_cruise_setpoint) else 0.0 - self._v_ego = v_ego - self._a_ego = a_ego + def update_calculations(self, v_ego: float, a_ego: float, v_cruise_setpoint: float) -> None: + self.v_cruise_setpoint = v_cruise_setpoint if not np.isnan(v_cruise_setpoint) else 0.0 + self.v_ego = v_ego + self.a_ego = a_ego # Update current velocity offset (error) - self._v_offset = self.speed_limit_offseted - self._v_ego + self.v_offset = self.speed_limit_offseted - self.v_ego # Track the time op becomes active to prevent going to tempInactive right away after # op enabling since controlsd will change the cruise speed every time on enabling and this will # cause a temp inactive transition if the controller is updated before controlsd sets actual cruise # speed. - if not self._op_engaged_prev and self._op_engaged: - self._last_op_engaged_time = self._current_time + if not self.op_engaged_prev and self.op_engaged: + self.last_op_engaged_time = self.current_time # Update change tracking variables - self._speed_limit_changed = self._speed_limit != self._speed_limit_prev - self._v_cruise_setpoint_changed = self._v_cruise_setpoint != self._v_cruise_setpoint_prev - self._speed_limit_prev = self._speed_limit - self._update_v_cruise_setpoint_prev() # always for Engage.auto - self._op_engaged_prev = self._op_engaged + self.speed_limit_changed = self._speed_limit != self.speed_limit_prev + self.v_cruise_setpoint_changed = self.v_cruise_setpoint != self.v_cruise_setpoint_prev + self.speed_limit_prev = self._speed_limit + self.update_v_cruise_setpoint_prev() # always for Engage.auto + self.op_engaged_prev = self.op_engaged - self._v_cruise_rounded = int(round(self._v_cruise_setpoint * self._speed_factor)) - self._v_cruise_prev_rounded = int(round(self._v_cruise_setpoint_prev * self._speed_factor)) - self._speed_limit_offsetted_rounded = 0 if self._speed_limit == 0 else int(round((self._speed_limit + self.speed_limit_offset) * self._speed_factor)) - self._speed_limit_warning_offsetted_rounded = 0 if self._speed_limit == 0 else \ - int(round((self._speed_limit + self.speed_limit_warning_offset) * self._speed_factor)) + self.v_cruise_rounded = int(round(self.v_cruise_setpoint * self.speed_factor)) + self.v_cruise_prev_rounded = int(round(self.v_cruise_setpoint_prev * self.speed_factor)) + self.speed_limit_offsetted_rounded = 0 if self._speed_limit == 0 else int(round((self._speed_limit + self.speed_limit_offset) * self.speed_factor)) + self.speed_limit_warning_offsetted_rounded = 0 if self._speed_limit == 0 else \ + int(round((self._speed_limit + self.speed_limit_warning_offset) * self.speed_factor)) def transition_state_from_inactive(self) -> None: self.state = SpeedLimitControlState.preActive - self._initial_max_set = False + self.initial_max_set = False def transition_state_from_preactive(self) -> None: - if self._initial_max_set_confirmed(): - self._initial_max_set = True + if self.initial_max_set_confirmed(): + self.initial_max_set = True if self._speed_limit > 0: - if self._v_offset < LIMIT_SPEED_OFFSET_TH: + if self.v_offset < LIMIT_SPEED_OFFSET_TH: self.state = SpeedLimitControlState.adapting else: self.state = SpeedLimitControlState.active else: self.state = SpeedLimitControlState.pending - elif self._v_cruise_setpoint_changed and self._current_time > (self._last_op_engaged_time + PRE_ACTIVE_GUARD_PERIOD): + elif self.v_cruise_setpoint_changed and self.current_time > (self.last_op_engaged_time + PRE_ACTIVE_GUARD_PERIOD): # User set cruise to something other than 80 MPH, permanently disable for this session self.state = SpeedLimitControlState.inactive def transition_state_from_pending(self) -> None: if self._speed_limit > 0: - if self._v_offset < LIMIT_SPEED_OFFSET_TH: + if self.v_offset < LIMIT_SPEED_OFFSET_TH: self.state = SpeedLimitControlState.adapting else: self.state = SpeedLimitControlState.active def transition_state_from_adapting(self) -> None: - if self._detect_manual_cruise_change(): + if self.detect_manual_cruise_change(): self.state = SpeedLimitControlState.inactive - elif self._v_offset >= LIMIT_SPEED_OFFSET_TH: + elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: self.state = SpeedLimitControlState.active def transition_state_from_active(self) -> None: - if self._detect_manual_cruise_change(): + if self.detect_manual_cruise_change(): self.state = SpeedLimitControlState.inactive - elif self._speed_limit > 0 and self._v_offset < LIMIT_SPEED_OFFSET_TH: + elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: self.state = SpeedLimitControlState.adapting - def _state_control(self) -> None: - self._state_prev = self._state + def state_control(self) -> None: + self.state_prev = self.state # If op is disabled or SLC is disabled, go inactive - if not self._op_engaged or not self._enabled: + if not self.op_engaged or not self.enabled: self.state = SpeedLimitControlState.inactive - self._initial_max_set = False + self.initial_max_set = False return self.state_transition_strategy[self.state]() def get_current_acceleration_as_target(self) -> float: - return self._a_ego + return self.a_ego def get_adapting_state_target_acceleration(self) -> float: - if self.distance > 0: - return (self.speed_limit_offseted ** 2 - self._v_ego ** 2) / (2. * self.distance) + if self._distance > 0: + return (self.speed_limit_offseted ** 2 - self.v_ego ** 2) / (2. * self._distance) - return self._v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) + return self.v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) def get_active_state_target_acceleration(self) -> float: - return self._v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) + return self.v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) - def _update_events(self, events_sp: EventsSP) -> None: + def update_events(self, events_sp: EventsSP) -> None: if self.is_active: if self.state == SpeedLimitControlState.preActive: events_sp.add(EventNameSP.speedLimitPreActive) - elif self._state_prev not in ACTIVE_STATES: + elif self.state_prev not in ACTIVE_STATES: events_sp.add(EventNameSP.speedLimitActive) - elif self._speed_limit_changed: + elif self.speed_limit_changed: events_sp.add(EventNameSP.speedLimitValueChange) def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, events_sp: EventsSP) -> float: - self._op_engaged = sm['carControl'].longActive - self._current_time = time.monotonic() + self.op_engaged = sm['carControl'].longActive + self.current_time = time.monotonic() - self._speed_limit, self._distance, self._source = self._resolver.resolve(v_ego, self.speed_limit, sm) + self._speed_limit, self._distance, self._source = self.resolver.resolve(v_ego, self._speed_limit, sm) - self._update_params() - self._update_calculations(v_ego, a_ego, v_cruise_setpoint) - self._state_control() - self._update_events(events_sp) + self.update_params() + self.update_calculations(v_ego, a_ego, v_cruise_setpoint) + self.state_control() + self.update_events(events_sp) return self.speed_limit_offseted From 3e2549f2b8675ce482200e06855857122f3583e3 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 28 Aug 2025 08:19:39 -0700 Subject: [PATCH 047/188] remove tici-specific code (#36078) * remove tici-specific code * lil more * update those --- README.md | 6 +++--- common/params_keys.h | 1 - docs/CONTRIBUTING.md | 2 +- docs/how-to/connect-to-comma.md | 8 ++++---- docs/how-to/replay-a-drive.md | 2 +- docs/how-to/turn-the-speed-blue.md | 2 +- mkdocs.yml | 2 +- selfdrive/pandad/pandad.py | 5 ----- selfdrive/pandad/tests/bootstub.panda.bin | Bin 15732 -> 0 bytes selfdrive/pandad/tests/test_pandad.py | 6 +----- selfdrive/pandad/tests/test_pandad_spi.py | 3 --- selfdrive/selfdrived/alerts_offroad.json | 4 ---- selfdrive/selfdrived/selfdrived.py | 10 +--------- selfdrive/ui/tests/test_ui/run.py | 2 +- selfdrive/ui/ui.cc | 3 +-- selfdrive/ui/ui_state.py | 5 +---- system/camerad/cameras/hw.h | 2 +- system/hardware/hardwared.py | 7 ------- system/hardware/tici/amplifier.py | 12 ------------ system/hardware/tici/hardware.py | 15 +++++---------- system/hardware/tici/pins.py | 5 ----- system/ui/reset.py | 5 ----- system/updated/updated.py | 6 ------ 23 files changed, 22 insertions(+), 91 deletions(-) delete mode 100755 selfdrive/pandad/tests/bootstub.panda.bin diff --git a/README.md b/README.md index f3af58f85b..9f1819afbf 100644 --- a/README.md +++ b/README.md @@ -42,10 +42,10 @@ Using openpilot in a car ------ To use openpilot in a car, you need four things: -1. **Supported Device:** a comma 3/3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x). -2. **Software:** The setup procedure for the comma 3/3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version. +1. **Supported Device:** a comma 3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x). +2. **Software:** The setup procedure for the comma 3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version. 3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md). -4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3/3X to your car. +4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3X to your car. We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play. diff --git a/common/params_keys.h b/common/params_keys.h index c54600462c..211b4d550b 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -94,7 +94,6 @@ inline static std::unordered_map keys = { {"Offroad_NeosUpdate", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_NoFirmware", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, {"Offroad_Recalibration", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, - {"Offroad_StorageMissing", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_TemperatureTooHigh", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_UnregisteredHardware", {CLEAR_ON_MANAGER_START, JSON}}, {"Offroad_UpdateFailed", {CLEAR_ON_MANAGER_START, JSON}}, diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 154734b7fc..7583095eaf 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -39,7 +39,7 @@ All of these are examples of good PRs: ### First contribution [Projects / openpilot bounties](https://github.com/orgs/commaai/projects/26/views/1?pane=info) is the best place to get started and goes in-depth on what's expected when working on a bounty. -There's lot of bounties that don't require a comma 3/3X or a car. +There's lot of bounties that don't require a comma 3X or a car. ## Pull Requests diff --git a/docs/how-to/connect-to-comma.md b/docs/how-to/connect-to-comma.md index cbaccaae6a..5f02e11599 100644 --- a/docs/how-to/connect-to-comma.md +++ b/docs/how-to/connect-to-comma.md @@ -1,11 +1,11 @@ -# connect to a comma 3/3X +# connect to a comma 3X -A comma 3/3X is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console). +A comma 3X is a normal [Linux](https://github.com/commaai/agnos-builder) computer that exposes [SSH](https://wiki.archlinux.org/title/Secure_Shell) and a [serial console](https://wiki.archlinux.org/title/Working_with_the_serial_console). ## Serial Console On both the comma three and 3X, the serial console is accessible from the main OBD-C port. -Connect the comma 3/3X to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power. +Connect the comma 3X to your computer with a normal USB C cable, or use a [comma serial](https://comma.ai/shop/comma-serial) for steady 12V power. On the comma three, the serial console is exposed through a UART-to-USB chip, and `tools/scripts/serial.sh` can be used to connect. @@ -45,7 +45,7 @@ In order to use ADB on your device, you'll need to perform the following steps u * Here's an example command for connecting to your device using its tethered connection: `adb connect 192.168.43.1:5555` > [!NOTE] -> The default port for ADB is 5555 on the comma 3/3X. +> The default port for ADB is 5555 on the comma 3X. For more info on ADB, see the [Android Debug Bridge (ADB) documentation](https://developer.android.com/tools/adb). diff --git a/docs/how-to/replay-a-drive.md b/docs/how-to/replay-a-drive.md index 084b6bf825..b0db36a46f 100644 --- a/docs/how-to/replay-a-drive.md +++ b/docs/how-to/replay-a-drive.md @@ -8,7 +8,7 @@ Replaying is a critical tool for openpilot development and debugging. Just run `tools/replay/replay --demo`. ## Replaying CAN data -*Hardware required: jungle and comma 3/3X* +*Hardware required: jungle and comma 3X* 1. Connect your PC to a jungle. 2. diff --git a/docs/how-to/turn-the-speed-blue.md b/docs/how-to/turn-the-speed-blue.md index 13b3b03e80..eb6e75afa2 100644 --- a/docs/how-to/turn-the-speed-blue.md +++ b/docs/how-to/turn-the-speed-blue.md @@ -3,7 +3,7 @@ In 30 minutes, we'll get an openpilot development environment set up on your computer and make some changes to openpilot's UI. -And if you have a comma 3/3X, we'll deploy the change to your device for testing. +And if you have a comma 3X, we'll deploy the change to your device for testing. ## 1. Set up your development environment diff --git a/mkdocs.yml b/mkdocs.yml index a66d1c76d4..550f807aca 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,7 +21,7 @@ nav: - What is openpilot?: getting-started/what-is-openpilot.md - How-to: - Turn the speed blue: how-to/turn-the-speed-blue.md - - Connect to a comma 3/3X: how-to/connect-to-comma.md + - Connect to a comma 3X: how-to/connect-to-comma.md # - Make your first pull request: how-to/make-first-pr.md #- Replay a drive: how-to/replay-a-drive.md - Concepts: diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index 4e49813f5d..d75af283f2 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -85,11 +85,6 @@ def main() -> None: cloudlog.event("pandad.flash_and_connect", count=count) params.remove("PandaSignatures") - # TODO: remove this in the next AGNOS - # wait until USB is up before counting - if time.monotonic() < 60.: - no_internal_panda_count = 0 - # Handle missing internal panda if no_internal_panda_count > 0: if no_internal_panda_count == 3: diff --git a/selfdrive/pandad/tests/bootstub.panda.bin b/selfdrive/pandad/tests/bootstub.panda.bin deleted file mode 100755 index 43db5370613b9a3966790c2a14f5e121907a5ade..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 15732 zcmch834Bx4w)Z~gBu(0;Z8`v*fD8<61K0tiOcIiEnl^<36@}uEwp3D3C=`%Ug5b3{ zp`!Ofc`Ax}RU9Z9wBgo*^vMC z0#F5523Q4H57-QN5b!8q2VfWAIlxPReSmtvyMRVOGvE`zmw;n{Q-Cu7AK)UO9nb}k z0LlSG9SMj93<3-W7y;>k8vx?~lK|5I`G6UK62N@Goq$Tf62Nl6y?~8?Er4wRFW?)% zcEHns-GJu-e+9e&cpK0FXaf8l@EPDcz*)d~z-2)H(sm-=17HRcwF(dghzHQ}B!oi& z!$WDBXSyz)h4^^D6o3^_04N5`1!yM7G;~e{ks*^Ox>=&y49htLwWUX?%6LyvcG1!)|=8A zM)F%#tfozxJdnTTf{L3ajv{P}j+ca|Bxc;ZKHA%N=gGcg{)DK=Om5{O6ua$hB=Se} zT_P$>iXA_qH)i+1)(AGgmE#JK$3DsJ_?XUc+F+h@>P&BRl8p-9?0?JU$}sz1cl~L1 zg30WE#nmFJ87_@$+26(tHT(Ctmh$!?+5D!gc#+w}ip=_lEnVKIHkT%cC;CPF4soMy zhq&Q%oWgP;x_pPY=`=!ihj`y0X=e?9-u~*Sn%T46XuhQA%OT~V* z+KhQPO~K}zOxdSwOJI`M3=o+}i}_ItCo(3%n_5VW1CbLaGAXr|8@x=8 z%XHPt=%TP5*xk)sTEg$f*ctq7e3ye}jK(34<~-u>`iS1`!$=5LMx%&kMMaT9!-`yy zB7zm!B4*NiKBDvbPLG4k0B?EsFXl;`mLqMmlnFd`Dka>r&yt5w^qTbvydrz};)*3p z%r7~IIhU9#%*7YlYnTZ~E84s*=M!FOIY01M)zXFhA$}Fl^IraTQLl?nXIr}}cIO$t zec*zE%Rw8i&28m6hvaZqcEvMUv(gvj6<;{W?&W9lQQnd~u5v}5x;31u%$uBNt-QbT z@0E(XX3JbXir>W#;J@~bOpo~XPZyLNrSu&rz0Gl*-&plI$9)y^k$M3!RaK)C& zy~t~>RNA?_h=CXnpJ$EL`HmYQAB@u%`OkdSEV^58Qo{UGxr2%Mtv}w28jCdiqlqtB zh*xYpl9(VeOQKg!HF0eW%X%eYhC~eU{AF;tP>hz96AFG1&+TUn3cBhlN%%&(NTy%7 zWfjh8#8tA_>1iV|oPVlX-Sl2tZp_VI9dC+e3{h<==220VDG5!INppSrUC9)2efllw zT56UgydvfNuJs-%=Xb4lN;$u4{rGRK9g?t3QnV#-X=6_=0iTYFjpjr}YOID6xzyMQ zPGnORIeRUBZ!XWtyTush^OWb8O1pie>C;@IUWs+fbU(9S$vgQ3lvpJvAHk<1b{k@n zuu4iWFla;kTSXl|cz&W(R*-IGxILw6 zK|L|53zB&z=PuJj4hE;H%d50EpC<;^P7KLq)%mEeC1x=sf05Qz=l5Z?y|n87{j6WX zGEXvfb5zrslm!vBOm>7zVJIzQ_seZ3Gv-&&ma`{Tx%@0S+-@dvK4Hx)@(#DV`qFBe zPFcs*mmj3%RNm8m{^S4s`>9-Ic5ksTM`<6-ZYJldkIm!w9pIhne5`RwmygTvyOk%V zT_DOgRFS-b&u$6hnF-l>f3}SE`Ws6`?cI5R5>bOtgHVl7jZlG5akq9#%xFcMR>9#! zpF18OzRVw^Q1oyH(#j2FTDe$*pW`$`pH>Tr?^{&fWquB&N<&iK7rp`f8Q;yK;!9ps zeCGGXipggSckTQ(~-3-NtYgD_Q6L7Z`4mxx~zYSI@K%Cb ziOec>&Nfdl&ol{D$=-fy&(M5n~-))NEKe0-}+fBLd*n+qsGCb1xcKwQj z6&nhjOisnhNN0+(#Qcym-omvMcepUy4N}DhX40tx6)WQ#`ub z#YSgknJI5~iqXMjzp%1wWo)RQ0W%VeTo0MM4SmNB7Z@e$3{upFhorOJ-RwW5z zq!kCtHRUx6Er$^L`(trqXlhdACeC>>6FQJIqE5(dhFrsqviJj6MxYq#kU- z{-IyBTU*aUk5M<$QXj_1$hGhNUG3^npISGxKJ**Y@(Sj!HGk=2`Dncw{oW}Vc$%ZM zQR?%W7z1acy_Vpvx#H6{(!Q8nE^WZ^AI=d!tB8nE^LP2w*H&bzJZ`1DB6K#KEkfUZ z-P#ZZYp!-{W-57RSk4;O6y@bcB{V^L8B6LJJzX!lTK|-kqb_@^8<|mlUpn6o`7A~e z-Sul#dfi$n|FBj}n##@cEan&0va=!5#yqLP46X#3IyGj+?Y|vbu_Ro0A2+3qmi!zf zhBlv^-}9f%@9HnV&F{8$?$&+o!7zwyG;a-YBP0?R984%+rU_8#lW@_pR8nWn*i zl5n-@k9+jgb#u7qe`yZ2*Ucg44|C|_XBMkJB1k!w1HaM}H8eCQIu9+C1NPfCFk^39 z$qakJqQE&OZ3>dgheP|hE6^|d=o!|Ut9}Ca20Fl_vK#@2M?nt6&8*q*v6{(IXV$D_ z{3_;VQJ1-m#EjeyiK4nrq9|Hj1nbtz8q#G=qcKn;ORXA6R>*Yo#Ggc&ZXa)r0dLt_ zNZLJK=5dXFJkHHa0j6%8jm}^XVmUSrvE7Je+7yWWIS`K1IvVxfmck8TdJ|%AN*T6D zgBqo;N@miTz+#P3Nbpu;y@(&qqOdp)QC}9i=;3c9it?aOB`Zh`gi+Jgv?`f zKh*9>P@r|j1k{Mpais@nJ4&AqNg6s5NtjCI5_nVXR@ZZBMsJ)wWli0X63hN>m%sSJ zEN#iOot6PD(V?-0VMKjnV*=r!aSV~^Ddn+9!k{L~C5=8c^f)RD{`<6#_x961+S^at zFY{bK?ep(b+S~sJv|qbM`=x8NKlmN(Lo)5M-_fS`TmSu3KxT=WssGsxx4);Kzf-`0 zxj5Bbp1wPdtzwMyPRqK$a+)SaN43HlncoNU#DG)Kn+;8v>dT)>eR|>Bovu4$P8rZ{ zpLvH>M=f_Yok!adWCZjMDU*lR*hsZTpHJ&n6-5{#A&a3)2P3ezGA zmN9B~luapfPGL}Kh~>kRTPg@``dv@b^f<8&VptGK8akWRS)p z5DN>cF%CLbCaB}p2D)anMt39S722xvY6Ctp`b;TtP!N6e>cYx-XRJ^tYp-%aFWK4&&s|PgeAWuJTa2Kb+H-27Owe z(w9TfmqXB(Lv1KDhe8+Hm7>0u{cYSd&CoRMe?=&y^P*`>D9kPm&5N;Vu_hmT%$OsA z2FV>_fXN}MI|n=*aJNSREO?Z_9iA}YHjfJUl1B~P>Inxv=ZOIR*%Jx;qelb$gC`33 zTTe9b2~Q00zdTytqn=pc7Ec`T7oK?FPdy31A9)4Ugf$KcOfdAqd4*Zm71n?7{ zk-(36(tscKqyyJ_GJqfOn1Jv1i~`=|84bL_GX{9A=LX=_o=o5xPZn^sCmZ-4PY&>6 z&sg9^o^iknJvRcEdBy|Z;mHMF;F$nC&odEtu4fYP9M5FnV$W3IB99rkz%vb)_gH|f z9xJfflLtJ-GaY!MhX)?-$p;?mu>og!3V_FWrU08fHvy-43W0}v?7#+35%5sY4B%AH zEZ`*1Y~X>OIl%Fr5@4<8X5c8#T;K@Lt-vbJJYa=qJ}~3C4LI0vCvZ=L16XV*1MX}n z2for!0erb(A@HvamB8m4wC>`o(h3Ev$29m27IaO&dgls84}TxQJ<~PSz{oPAxQiH# zW!gq+OWbLwD~CPjD24S*)AJU?igiQw1QYBDGX~ghG;9%bhb0s?i<5^j!^jAip;@#I zYZlE5o5j35+MD^xyR$LpE3LRCTHYRE)b_OoFDC9qx4Bo5)O>bE`G)I_azD{SxLlsqDFc8qmJvIDn{ znUQIz-4T#wR<^jq%t*rbqN-4BV^H^mNbis^+cN(eOL51BGVuI+3L|6YVm3)?9b?u2 z(6;ah7XeB6ff#O2b{gO%NOsgYaZcGfN3wH{s`uFOC9VVXj+3ORBo}R<^`ad;Tc;&; zqZ%vrt)#H0I2SxL&uYFZ)$H$S_tO>~xD!S9fSEbY1rUiPY)pi;R?($sczwCpn82U&O@s_xfKtY=zLR# zbsom8)8?c1Hl-yAC!45k1sT!A*b~=K8xZ#ll#c7qJ4qP&S(ujcP+HSM-8}0;BV7;HzSBi%(mJnz7g+3=Yq=!M zdJlJ3H8J<6r-#zH?nF@8z20^|M~ z-**3`(}46z#~`~JobS?6&s0l`O#UXfE4nxeTU9UMGN zl$&ZT7I0RbgP?^ZxFi-dWgdp#iJk+JM$8d(rLJ&2am0Lq)gC?oA5l2hx`iYD7(!aD z@Q#dkrxmgLxbqx=t}T6PtSB!ry7a?r73D#UNzZ@goEO%6>={vs^5c?%e?|;L?5M=C z&xk5wy`adGgwG|NRqbdL3E9)x?Qp@eYnGI#$)e`_avMs$L3##$ee(rtw&jA-ik3#Z zBeGBf8!{-2hKA}wxI$9GZ~#N7_}WyB+-tHX>yuaQ zu|eSr=qEHq=kgKs5*lX^KN1`z*GfSAP%z_K+#RGfM%rlpTfq^qBZ5L(aJY@i)S&Qk z&G(Qo%P0p2g@f;>5yqSd&;dpP#sYEylK~n4p+NZw`c*38c>wq_+zhXRzeEdv$p9~v zijtsiU<~0n9hvaJ&>R&krJ#`B1dGPTA~sC+E>OAGud7i>7wG1o(V5ZiYLG$fCP>b* z?kOR?F}iDFNM{W1$_?qFiY}^)jzv95%n9kD-C`zUDy&1Ncmr01+A247kCIm@DEP4U zbd`d_kHL|MsSx`vXtb&2df$pPMzz#$L_JDrOxNAF!a?DaU=l_^bZaiwAQ0 zJ*v0L`YY9AHPkMagk`v!Qk+yG?p{oEiTPC4(fo?*@|jRR%WL2-2nx%oZcY6+Y1ID{ z3mZ12MSX~R540#P560LGyb|-j9ehA3L$_-}sVvt@f(Bj%?@3UY8Dw;qeRMxqw91F? zMD`2Ov4J-AiS_%>fx=yl&{!m5ZE}p}`KA86RyjuV2>66rE8AgiPOdtrt2-7YU*TkpQR%7Hb|)hl41Mx)-BjV%0OLE()MuX6Tz_i5{y z+V(#u*qhcF{+4q^n$`;0m`RzC$$v`I) zL|sFeao=j4-RQ_|)Y(|sR{K>5y?dRUU&E9&O+Y7xCp6x93@^(+9gVpCE*3n~hTIHG|`kJ?pH7hb1 z^FoAzJFYmcsINV}8Gb~~7+W9JX!|MWsRQ(zUKT8UDI523W>BaFRjON2dbzyOA%9sj z2Zav~QJQAZECLY3p_^_>;?w9?Fk9rkln*{`Vlo6TXGp$RVOQFcYMHDK=!JBinKxmd zrx4wEgj#wxmFK)u=b##XwC%RV3~#xjU3I_nNtNap(R~&g6&(*Te&%5JXa0@SE?pd# z@|vaf1f#p?BcU-2vrjZsyC> z@Tr7maVKnrR}j9A@bSE6@x{DsKCvJP+@tA9=M&|%h3xIg<_8uGL@tBe0ioQU?!Ft* zjCJhnj3m$}rI-x??z1a`_o;?tWy7YxIxRPVhZQ7@uAL1q3vs$fj?f(F zy3+fR-erG*)=p(e^1br?hcz8?ZT+AFR~?Q4CxR~_I|@r-nbc;ZN6MSXGct>&bA0KL zhzvC!b-b!xRd|Y6{VW$>7GGuUaOK389d(}KdcQ_}U4I*e-FyZ1Rv82B@TxmYib@&pl#+2ABwnbI5YuEvsg4d74kQ^ie&VjOs=BZaB^94RUE<|@1TMc zjuXyl9eZYl7oh3gv}xu1Fb7@8hovYCoc25rp+&lDA-lT=y#!1 zk*0<=S|`tq+G(T561C&(K!QJ=nGo~8-qXyI*US!IU4hDwToH~J44fm<5$~{;*~?1G znjN1xj`pn$J<9~306^yCx~CeV_|19+b}zQOY6zDeg|8HhpH0%IJ7JwBlAvJkrgo|h zF}|B>J;fc}Ws0^@NQ+$+RrG9Bl;t)SceG6FtDn=Y)&Jt7ty58FR5xb5{Ci(tIn6QB zoY}{GYWD#90O$%^#63D{#lDDJrMTmU70u@};&fQ!{W|L1dQ&Rys4j;z>o7x_HFas= z9R@FUN6KNKD?$#ty27a~E8Cs2Evq2rRj^~#x!@=0@M?f2q{}M`Ib%tUY!PleMUK`; zf~SevJXH2#ZWY>hG<>PGV#q@Mfu6QD&NL7!!ui?os8~Atv6=W5Vh;-I8fYpvQ}EGn za%Q_6nlmr^XnA$M;GQuf&B;APOvJy#WiJZbXs9i#{h*bN`-(hyfN7n^#O7^N6Jwdn z=z6|(W&=qd?>)Od67N>d))3=f2N^N-G)bS`lF?vX&*Vs^v?j@T>cFD)#+tWVKZ+^# z6E6QdW6hyd&Xn848PzCP8^6BDu6yoc_PUCiz3V$JeY6g5kS=Mv_tu!cefyGPUB{*T zbu?{j@+0;k>OPdLy7YL)Q;7X=ark2EM)8tw-6`_dOE0fGTeB6pee`r_`H|dG`)%i? z!*VR@*bf&|)yqQ|+gs zi)yu0OS+>gJ(PEQ57icE-n^b+p}c##452({7mM!&LqKm)57j#^bW;uW%Wl??0{?AL zxUGlPCu4+jpfgFrU!g~hf!(`Eq_rODrdsV^y885}=ezr~+Gph4>|PDZUXiF)JMHSN zIXf*+$$kMz*x1u&5!`bKvW>9vx$V~zpd~<8#CZseD;gJCg_NG0&?*e<>DLu6i~YLd z1?Z1`bN^Y6(RrS}miI$I}rOoWJ6l$XnYA8Ag{s6 zZdRXwHE0RYH5e`hh1UT;L$icFBF}a}Xbm=p)?g!65k9vbx<02cU#dyBQeOjP)Nk?% zHmeYPUuEaAQk>h=T&Jsqv~Ff<<@XxNPCPC#iMV44vSTy6BGK zUuiOreE)z3Gtc6c&>=sU-myg5H76nqQ=C)sUQ8KK{?yi^;@C8L12iKP8b7Qdv?Jto5(8dK&u#u@WclwVDzKYv<@|8}yTiYohr;c-q%ZTw5mL z+HNJ(XMwu}qm6wOy;tmZHn&F^KS{<})ZZ_T(8fO?--V$u(r<}T_W2tMQ%hIw5HoZu z8mQ;$E74)!!>Nyj7kwaB+mVVi`xWL75YER-hrX9Lecu!57{H$l^*7PpN}Y-I)NfC3 zX#>LU&|YcXVMXwII+XS#XFzx+Sevha1|ATO1V~dact$~_p|SOEd_UeD5S|XE*i(w& z&sZ4{Zjh4g$py*Ihb)6(J-=X4wj|o_x3DcqXuCHske+qC?=fGgur#+vayu+>_V~iY zrcsVMha^^jc}?=ehJw zqI$flsK4$Pfv`|3ZLm)+Y8$UYKkTnBx;5Z71*93J{ANI5*pp}}kVXWA{ecnHrZs!3 z&4s6|XRD7c-s>=}X*Lbw?Jd#A516L3D39+qSzGE&!&@{M^AS^zcgKCkYDOnEf+N_k z4yZG(9B-MVHeEi>ynvUIe$sT$XuLfJoEY_c_-iO{FT&w??}ZnXCndqsb62&It~0!k zbk!K+gJNdpP%QaMK!@j48}Jpg=3< z6$G@pdLR87b&I6ay)Ks<>lxD?Z?rwSkU6aZPR8E4d!q^l$SpDh19Urm7qOmnpQzp^ z%SA{??K)}bVXQaZH@atZ57SZadAr~)T@#s+%Eug!%!tR$9_MG7IMVk&hb)$3CSWzA z%OL*9&6&X6u8P_>FxtJqT;>x6XAvqho&2LBm2Wpl)>Pt;*nFmTv*nd02GU{M0nh1e zMcWFE&TY`Ub?FkcnY8o=XEckmA|FJKITE^~sadRE(2R4_EILg`MY9PWNa&Jg#2uly z!$crkwfw}>t7cTxW;jzANRvdaBUlk>g*?gaE5)~&K*sOnO!MipYnVyVrd(NbQ74$j zkJpb6{ne5)wTtX!Gj`v%WMy1B^awVsDNdg@_>}KV?QKQVLpjr-y$vMrX$K5WFG}VX zMTANsP_lru`Sz|+n8J=TlQ>hj+)G00u<4gf_`)xbIPCP9+L<;*s15yEeq$gH9^8GP zRbczA-z!M-HchCWMt{LKvCvwOSDbB(uZEUZO@`y`+mg(1e4`3HUVW@O48B%;qY5-w zw9Dr%r>}S~dC$~lhuUZBGi;xL;y7DaC?`yR1nKPSdTc=2P&6S_J^{1YLmu*_!p>l; zE!C|4Ro_s1d2MM$ov*Gov53vs)G_6)6SWDU`U$9if~-K9G9#`P+m?2)FS>E;9-X7E+TvzA>=iHzRUAZi=G)%oa2nbt7`?{ zcOHlPIpy%Y*DYaQe3{VW?ea8y6@Z5eFAGz|#gg!+uAaq;oSyQFK6-9wzjzmG&6_#h z{%4VS+`s0XHCx~fw%Av!wG>-tJy!icQ^H%Po=L@Hw3mGcmR}*5&Np|t|rEPhBK-2QgH4?yz@!4Q*_U(yQlbkChS` zb__XO%w^QM*wS+!FTA8N)Xee!#JfspAj7`)QJeh=)`;5ebe%FSMCX@f z|B8d*p?7m?SRf>h-Y&OYdn-rZA!yBzS)S0H2s?N5`S$#(BBS$;sym%W#l+f91#=AV zXyA>gR@s_Loo!)>$5Iy4yQ{g~YGv`}evgZJhS_zy?MQn>lF}ceETHSr!;UulA9l^N z(UcT4ItTg>#|&vY3VYGgv8SBcjoDl#{Q0(&FI4Jd#RZT)DZiI9&XkW;T<4AD_a3tq z5@-M!+N1bQ#4+S$R^&D16(*PMb|$Lm=qdt6j8&Zi?F2Mz2yFei|Tqcgpn>QaV~E(MJUQqhHZ5tBTYB&sPLgR9@g8eUs`ckg!ko^Wnc%xikm z`6nOO83#S+WY~+xBB0^Jx7Yn#D389IraFvYL+xxeq5Fss6wna%P|!fm5Bi4~aMBlyk?NdddDXY(2b zJdugPU#N9lG?2cX#*KHA6c7mDvtSTkevHnP^(plM;TxouAQd%Z!D0q-WoLds2zD8r z1h+ARZ-HqzJ&zzw%ND>hOw(JtF3D$|-ix~DRgk4OAELgNk0sLdgP7R@K7_BGI~`$P zFwZ(mkri%>uv-%AELV?)7qX{`(SqNVsO)u?uA}(sC06HCI{(>kYl6^p{ccA0@P2$7 z5CcL(S5SC95EOC(N%o||BN(5R zK@LL}CjF#gI6~ZML>-92ZtXgx_%?tO8JarvS?U%K;=r zkNE?=wm;BI(){=6UB%dL1N4z&$glBzCqW7-Aq0=fVXfZnbUz5i;ytCm);Shl*VrfS*J%7vh| zva)7n)zU>-**S#leR=|$70l}txHx_h$Cs%F7?A>bk+ zBclH2Wt8N9^{3|4MD~#|ET>S0sqk>|ctpzjZmRRI08M)wl zV({%ck5rP?7+wX!0t{~@!WHB$43g&Llex&vLyMVYJ#uK?IFgOClZE(b)VdMzyAT_T za01F`-dL1nBbJRgtv?>&IJ79kVL0F&%M(9w2WHeeM2{_no?!mGe}calUZjHOp83R&(+x@A!X67yp`d zw?_Z)am&wdt^C*X?}?9^=gnMyaog>Q&7+@tbY}6UL35YB{Lz}X%DyuXy6LNwBY(T= zZJXYQPA{(P(L{&d@_t8KYr@gp_!KN|AxmnDB8(#n({?mkvBt@4YG8uy^u$;n59 zV@Brg9`a%EE%gIcyWIaOewLp-=61)T?i=&Adyg!oa*nRWH_5i7qz8Z8|Kjlhi#Fbz z|K_$stH*8g&3y6JznrgZ-R&Chu z{YTyhBDcJ7CXzF!Y`=K>*0WiU@ryn=+xXF&gPJ#-`tYBvlhfZ(?w`EUnDn=ag$G<$ zcaL1~WbE1P&Hvib{TTcDUz_LbZ;$$ke}cU6(v-EI7^ke8a`LIx1%anuY#2T2ojYDl zFdR2NVr;a0dW+?z)XIBx9{!Pr*<;7Y+|R`3#@J^J`TU pandad # - plus some buffer print("startup times", ts, sum(ts) / len(ts)) - assert 0.1 < (sum(ts)/len(ts)) < (0.7 if self.spi else 5.0) + assert 0.1 < (sum(ts)/len(ts)) < 0.7 def test_protocol_version_check(self): - if not self.spi: - pytest.skip("SPI test") # flash old fw fn = os.path.join(HERE, "bootstub.panda_h7_spiv0.bin") self._flash_bootstub_and_test(fn, expect_mismatch=True) diff --git a/selfdrive/pandad/tests/test_pandad_spi.py b/selfdrive/pandad/tests/test_pandad_spi.py index 9f7cc3b029..da4b181993 100644 --- a/selfdrive/pandad/tests/test_pandad_spi.py +++ b/selfdrive/pandad/tests/test_pandad_spi.py @@ -6,7 +6,6 @@ import random import cereal.messaging as messaging from cereal.services import SERVICE_LIST -from openpilot.system.hardware import HARDWARE from openpilot.selfdrive.test.helpers import with_processes from openpilot.selfdrive.pandad.tests.test_pandad_loopback import setup_pandad, send_random_can_messages @@ -16,8 +15,6 @@ JUNGLE_SPAM = "JUNGLE_SPAM" in os.environ class TestBoarddSpi: @classmethod def setup_class(cls): - if HARDWARE.get_device_type() == 'tici': - pytest.skip("only for spi pandas") os.environ['STARTED'] = '1' os.environ['SPI_ERR_PROB'] = '0.001' if not JUNGLE_SPAM: diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 42aca22008..3d97135f60 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -29,10 +29,6 @@ "text": "Device failed to register with the comma.ai backend. It will not connect or upload to comma.ai servers, and receives no support from comma.ai. If this is a device purchased at comma.ai/shop, open a ticket at https://comma.ai/support.", "severity": 1 }, - "Offroad_StorageMissing": { - "text": "NVMe drive not mounted.", - "severity": 1 - }, "Offroad_CarUnrecognized": { "text": "openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai.", "severity": 0 diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index cf22040e84..e9c62fe327 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -21,7 +21,6 @@ from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert -from openpilot.system.hardware import HARDWARE from openpilot.system.version import get_build_metadata REPLAY = "REPLAY" in os.environ @@ -122,13 +121,6 @@ class SelfdriveD: self.state_machine = StateMachine() self.rk = Ratekeeper(100, print_delay_threshold=None) - # some comma three with NVMe experience NVMe dropouts mid-drive that - # cause loggerd to crash on write, so ignore it only on that platform - self.ignored_processes = set() - nvme_expected = os.path.exists('/dev/nvme0n1') or (not os.path.isfile("/persist/comma/living-in-the-moment")) - if HARDWARE.get_device_type() == 'tici' and nvme_expected: - self.ignored_processes = {'loggerd', } - # Determine startup event self.startup_event = EventName.startup if build_metadata.openpilot.comma_remote and build_metadata.tested_channel else EventName.startupMaster if not car_recognized: @@ -299,7 +291,7 @@ class SelfdriveD: if not_running != self.not_running_prev: cloudlog.event("process_not_running", not_running=not_running, error=True) self.not_running_prev = not_running - if self.sm.recv_frame['managerState'] and (not_running - self.ignored_processes): + if self.sm.recv_frame['managerState'] and not_running: self.events.add(EventName.processNotRunning) else: if not SIMULATION and not self.rk.lagging: diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index 422183c5d5..32aead0b4f 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -29,7 +29,7 @@ UI_DELAY = 0.1 # may be slower on CI? TEST_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" STREAMS: list[tuple[VisionStreamType, CameraConfig, bytes]] = [] -OFFROAD_ALERTS = ['Offroad_StorageMissing', 'Offroad_IsTakingSnapshot'] +OFFROAD_ALERTS = ['Offroad_IsTakingSnapshot', ] DATA: dict[str, capnp.lib.capnp._DynamicStructBuilder] = dict.fromkeys( ["carParams", "deviceState", "pandaStates", "controlsState", "selfdriveState", "liveCalibration", "modelV2", "radarState", "driverMonitoringState", "carState", diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index 4f8bd7ddfc..9ec61b9b81 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -54,8 +54,7 @@ static void update_state(UIState *s) { } if (sm.updated("wideRoadCameraState")) { auto cam_state = sm["wideRoadCameraState"].getWideRoadCameraState(); - float scale = (cam_state.getSensor() == cereal::FrameData::ImageSensor::AR0231) ? 6.0f : 1.0f; - scene.light_sensor = std::max(100.0f - scale * cam_state.getExposureValPercent(), 0.0f); + scene.light_sensor = std::max(100.0f - cam_state.getExposureValPercent(), 0.0f); } else if (!sm.allAliveAndValid({"wideRoadCameraState"})) { scene.light_sensor = -1; } diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index c2bb5f77a4..c4a2c0ca11 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -105,10 +105,7 @@ class UIState: # Handle wide road camera state updates if self.sm.updated["wideRoadCameraState"]: cam_state = self.sm["wideRoadCameraState"] - - # Scale factor based on sensor type - scale = 6.0 if cam_state.sensor == 'ar0231' else 1.0 - self.light_sensor = max(100.0 - scale * cam_state.exposureValPercent, 0.0) + self.light_sensor = max(100.0 - cam_state.exposureValPercent, 0.0) elif not self.sm.alive["wideRoadCameraState"] or not self.sm.valid["wideRoadCameraState"]: self.light_sensor = -1 diff --git a/system/camerad/cameras/hw.h b/system/camerad/cameras/hw.h index d299627ce9..f20a1b3ade 100644 --- a/system/camerad/cameras/hw.h +++ b/system/camerad/cameras/hw.h @@ -13,7 +13,7 @@ typedef enum { ISP_BPS_PROCESSED, // fully processed image through the BPS } SpectraOutputType; -// For the comma 3/3X three camera platform +// For the comma 3X three camera platform struct CameraConfig { int camera_num; diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index fad93601da..1048acfe0a 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -6,7 +6,6 @@ import struct import threading import time from collections import OrderedDict, namedtuple -from pathlib import Path import psutil @@ -334,12 +333,6 @@ def hardware_thread(end_event, hw_queue) -> None: # to make a different decision in your software startup_conditions["registered_device"] = PC or (params.get("DongleId") != UNREGISTERED_DONGLE_ID) - # TODO: this should move to TICI.initialize_hardware, but we currently can't import params there - if TICI and HARDWARE.get_device_type() == "tici": - if not os.path.isfile("/persist/comma/living-in-the-moment"): - if not Path("/data/media").is_mount(): - set_offroad_alert_if_changed("Offroad_StorageMissing", True) - # Handle offroad/onroad transition should_start = all(onroad_conditions.values()) if started_ts is None: diff --git a/system/hardware/tici/amplifier.py b/system/hardware/tici/amplifier.py index f6b29ec0ce..bfdcc6ddaf 100755 --- a/system/hardware/tici/amplifier.py +++ b/system/hardware/tici/amplifier.py @@ -61,18 +61,6 @@ BASE_CONFIG = [ ] CONFIGS = { - "tici": [ - AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), - AmpConfig("Right Speaker Mixer Gain", 0b00, 0x2D, 2, 0b00001100), - AmpConfig("Right speaker output volume", 0x1c, 0x3E, 0, 0b00011111), - AmpConfig("DAI2 EQ enable", 0b1, 0x49, 1, 0b00000010), - - *configs_from_eq_params(0x84, EQParams(0x274F, 0xC0FF, 0x3BF9, 0x0B3C, 0x1656)), - *configs_from_eq_params(0x8E, EQParams(0x1009, 0xC6BF, 0x2952, 0x1C97, 0x30DF)), - *configs_from_eq_params(0x98, EQParams(0x0F75, 0xCBE5, 0x0ED2, 0x2528, 0x3E42)), - *configs_from_eq_params(0xA2, EQParams(0x091F, 0x3D4C, 0xCE11, 0x1266, 0x2807)), - *configs_from_eq_params(0xAC, EQParams(0x0A9E, 0x3F20, 0xE573, 0x0A8B, 0x3A3B)), - ], "tizi": [ AmpConfig("Left speaker output from left DAC", 0b1, 0x2B, 0, 0b11111111), AmpConfig("Right speaker output from right DAC", 0b1, 0x2C, 0, 0b11111111), diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 35f9916c31..6a9b98af82 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -425,9 +425,6 @@ class Tici(HardwareBase): # pandad core affine_irq(3, "spi_geni") # SPI - if "tici" in self.get_device_type(): - affine_irq(3, "xhci-hcd:usb3") # aux panda USB (or potentially anything else on USB) - affine_irq(3, "xhci-hcd:usb1") # internal panda USB (also modem) try: pid = subprocess.check_output(["pgrep", "-f", "spi0"], encoding='utf8').strip() subprocess.call(["sudo", "chrt", "-f", "-p", "1", pid]) @@ -446,22 +443,20 @@ class Tici(HardwareBase): cmds = [] - if self.get_device_type() in ("tici", "tizi"): + if self.get_device_type() in ("tizi", ): # clear out old blue prime initial APN os.system('mmcli -m any --3gpp-set-initial-eps-bearer-settings="apn="') cmds += [ + # SIM hot swap + 'AT+QSIMDET=1,0', + 'AT+QSIMSTAT=1', + # configure modem as data-centric 'AT+QNVW=5280,0,"0102000000000000"', 'AT+QNVFW="/nv/item_files/ims/IMS_enable",00', 'AT+QNVFW="/nv/item_files/modem/mmode/ue_usage_setting",01', ] - if self.get_device_type() == "tizi": - # SIM hot swap, not routed on tici - cmds += [ - 'AT+QSIMDET=1,0', - 'AT+QSIMSTAT=1', - ] elif manufacturer == 'Cavli Inc.': cmds += [ 'AT^SIMSWAP=1', # use SIM slot, instead of internal eSIM diff --git a/system/hardware/tici/pins.py b/system/hardware/tici/pins.py index 747278d1ec..bdbea591fb 100644 --- a/system/hardware/tici/pins.py +++ b/system/hardware/tici/pins.py @@ -1,5 +1,3 @@ -# TODO: these are also defined in a header - # GPIO pin definitions class GPIO: # both GPIO_STM_RST_N and GPIO_LTE_RST_N are misnamed, they are high to reset @@ -26,7 +24,4 @@ class GPIO: CAM2_RSTN = 12 # Sensor interrupts - BMX055_ACCEL_INT = 21 - BMX055_GYRO_INT = 23 - BMX055_MAGN_INT = 87 LSM_INT = 84 diff --git a/system/ui/reset.py b/system/ui/reset.py index b1bf5a6b6a..a5cf1731dc 100755 --- a/system/ui/reset.py +++ b/system/ui/reset.py @@ -13,7 +13,6 @@ from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.button import Button, ButtonStyle from openpilot.system.ui.widgets.label import gui_label, gui_text_box -NVME = "/dev/nvme0n1" USERDATA = "/dev/disk/by-partlabel/userdata" TIMEOUT = 3*60 @@ -49,10 +48,6 @@ class Reset(Widget): if PC: return - # Best effort to wipe NVME - os.system(f"sudo umount {NVME}") - os.system(f"yes | sudo mkfs.ext4 {NVME}") - # Removing data and formatting rm = os.system("sudo rm -rf /data/*") os.system(f"sudo umount {USERDATA}") diff --git a/system/updated/updated.py b/system/updated/updated.py index d1e745ec1a..a80a663ec9 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -243,12 +243,6 @@ class Updater: if b is None: b = self.get_branch(BASEDIR) b = { - ("tici", "release3"): "release-tici", - ("tici", "release3-staging"): "release-tici", - ("tici", "master"): "master-tici", - ("tici", "nightly"): "release-tici", - ("tici", "nightly-dev"): "release-tici", - ("tizi", "release3"): "release-tizi", }.get((HARDWARE.get_device_type(), b), b) return b From 9fcac06297ebf9357d8b1fbf5a3ab573e9733190 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 28 Aug 2025 11:49:31 -0700 Subject: [PATCH 048/188] op.sh: fix switch on fresh install --- tools/op.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/op.sh b/tools/op.sh index e83b46b2d2..54ff8e97e9 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -365,6 +365,7 @@ function op_switch() { fi BRANCH="$1" + git config --replace-all remote.origin.fetch "+refs/heads/*:refs/remotes/origin/*" git fetch "$REMOTE" "$BRANCH" git checkout -f FETCH_HEAD git checkout -B "$BRANCH" --track "$REMOTE"/"$BRANCH" From 76e91da3adb48777fe10b8933172e43c19e02691 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Kacper=20R=C4=85czy?= Date: Thu, 28 Aug 2025 22:11:20 +0200 Subject: [PATCH 049/188] process_replay: use LiveParametersV2 in custom params (#36080) Fill LiveParametersV2 in get_custom_params_from_lr --- selfdrive/test/process_replay/process_replay.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index 56a0fdb61a..1144b7955e 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -591,7 +591,7 @@ def get_custom_params_from_lr(lr: LogIterable, initial_state: str = "first") -> if len(live_calibration) > 0: custom_params["CalibrationParams"] = live_calibration[msg_index].as_builder().to_bytes() if len(live_parameters) > 0: - custom_params["LiveParameters"] = live_parameters[msg_index].as_builder().to_bytes() + custom_params["LiveParametersV2"] = live_parameters[msg_index].as_builder().to_bytes() if len(live_torque_parameters) > 0: custom_params["LiveTorqueParameters"] = live_torque_parameters[msg_index].as_builder().to_bytes() From 6956c6ef0595e90286b5bf84e612bab624caea26 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 29 Aug 2025 00:57:24 -0400 Subject: [PATCH 050/188] no --- .../speed_limit_controller.py | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 7ce750449d..235cd97121 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -52,8 +52,8 @@ class SpeedLimitController: self.last_valid_speed_limit_offsetted = 0. self._distance = 0. self._source = Source.none - self.state = SpeedLimitControlState.inactive - self.state_prev = SpeedLimitControlState.inactive + self._state = SpeedLimitControlState.inactive + self._state_prev = SpeedLimitControlState.inactive self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise self.offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) @@ -70,7 +70,7 @@ class SpeedLimitController: self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH # Mapping functions to state transitions - self.state_transition_strategy = { + self._state_transition_strategy = { SpeedLimitControlState.inactive: self.transition_state_from_inactive, SpeedLimitControlState.preActive: self.transition_state_from_preactive, SpeedLimitControlState.pending: self.transition_state_from_pending, @@ -89,21 +89,21 @@ class SpeedLimitController: @property def state(self) -> SpeedLimitControlState: - return self.state + return self._state @state.setter def state(self, value) -> None: - if value != self.state: + if value != self._state: debug(f'Speed Limit Controller state: {description_for_state(value)}') - self.state = value + self._state = value @property def is_enabled(self) -> bool: - return self.state in ENABLED_STATES and self.enabled + return self._state in ENABLED_STATES and self.enabled @property def is_active(self) -> bool: - return self.state in ACTIVE_STATES and self.enabled + return self._state in ACTIVE_STATES and self.enabled @property def speed_limit_offseted(self) -> float: @@ -216,7 +216,7 @@ class SpeedLimitController: int(round((self._speed_limit + self.speed_limit_warning_offset) * self.speed_factor)) def transition_state_from_inactive(self) -> None: - self.state = SpeedLimitControlState.preActive + self._state = SpeedLimitControlState.preActive self.initial_max_set = False def transition_state_from_preactive(self) -> None: @@ -224,44 +224,44 @@ class SpeedLimitController: self.initial_max_set = True if self._speed_limit > 0: if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting + self._state = SpeedLimitControlState.adapting else: - self.state = SpeedLimitControlState.active + self._state = SpeedLimitControlState.active else: - self.state = SpeedLimitControlState.pending + self._state = SpeedLimitControlState.pending elif self.v_cruise_setpoint_changed and self.current_time > (self.last_op_engaged_time + PRE_ACTIVE_GUARD_PERIOD): # User set cruise to something other than 80 MPH, permanently disable for this session - self.state = SpeedLimitControlState.inactive + self._state = SpeedLimitControlState.inactive def transition_state_from_pending(self) -> None: if self._speed_limit > 0: if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting + self._state = SpeedLimitControlState.adapting else: - self.state = SpeedLimitControlState.active + self._state = SpeedLimitControlState.active def transition_state_from_adapting(self) -> None: if self.detect_manual_cruise_change(): - self.state = SpeedLimitControlState.inactive + self._state = SpeedLimitControlState.inactive elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.active + self._state = SpeedLimitControlState.active def transition_state_from_active(self) -> None: if self.detect_manual_cruise_change(): - self.state = SpeedLimitControlState.inactive + self._state = SpeedLimitControlState.inactive elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting + self._state = SpeedLimitControlState.adapting def state_control(self) -> None: - self.state_prev = self.state + self._state_prev = self._state # If op is disabled or SLC is disabled, go inactive if not self.op_engaged or not self.enabled: - self.state = SpeedLimitControlState.inactive + self._state = SpeedLimitControlState.inactive self.initial_max_set = False return - self.state_transition_strategy[self.state]() + self._state_transition_strategy[self._state]() def get_current_acceleration_as_target(self) -> float: return self.a_ego @@ -277,9 +277,9 @@ class SpeedLimitController: def update_events(self, events_sp: EventsSP) -> None: if self.is_active: - if self.state == SpeedLimitControlState.preActive: + if self._state == SpeedLimitControlState.preActive: events_sp.add(EventNameSP.speedLimitPreActive) - elif self.state_prev not in ACTIVE_STATES: + elif self._state_prev not in ACTIVE_STATES: events_sp.add(EventNameSP.speedLimitActive) elif self.speed_limit_changed: events_sp.add(EventNameSP.speedLimitValueChange) From 6bea1639989bbe8746235d66a174b9cd94b9ef93 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 29 Aug 2025 00:59:03 -0400 Subject: [PATCH 051/188] no need --- cereal/custom.capnp | 1 - 1 file changed, 1 deletion(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 432f961885..f5cbd10156 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -197,7 +197,6 @@ struct OnroadEventSP @0xda96579883444c35 { speedLimitActive @18; speedLimitConfirmed @19; speedLimitValueChange @20; - speedLimitPreActive @21; } } From f2fe63fa5f8d24a1bda50e9159e372a4199010eb Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 29 Aug 2025 01:03:21 -0400 Subject: [PATCH 052/188] use existing --- sunnypilot/selfdrive/controls/lib/longitudinal_planner.py | 1 - .../lib/speed_limit_controller/speed_limit_controller.py | 5 +++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 6bbd21ae86..97dac727c1 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details. from cereal import messaging, custom from opendbc.car import structs -from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 235cd97121..3354877466 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -4,9 +4,10 @@ import time from cereal import messaging, custom from openpilot.common.constants import CV from openpilot.common.params import Params +from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V, \ PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_CRUISE_SPEED, \ - CRUISE_SPEED_TOLERANCE, FALLBACK_CRUISE_SPEED + CRUISE_SPEED_TOLERANCE from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Policy, Engage, OffsetType from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.helpers import description_for_state, debug @@ -118,7 +119,7 @@ class SpeedLimitController: return self.last_valid_speed_limit_offsetted # Fallback - return FALLBACK_CRUISE_SPEED + return V_CRUISE_UNSET @property def speed_limit_offset(self) -> float: From 54174d1ef0e84f198d6c98be212c3e1d1496b807 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 29 Aug 2025 22:23:58 +0200 Subject: [PATCH 053/188] agnos: split launch for c3 and c3x to support custom agnos (#1186) * refactor: skip AGNOS update for tici models in launch script * back to stock on chffrplus * feat: enhance launch script for Tici model with error handling and fallback * empty new line pls --- launch_openpilot.sh | 17 ++++ sunnypilot/system/hardware/c3/README.md | 3 + sunnypilot/system/hardware/c3/agnos.json | 84 +++++++++++++++++ .../system/hardware/c3/launch_chffrplus.sh | 92 +++++++++++++++++++ sunnypilot/system/hardware/c3/launch_env.sh | 11 +++ 5 files changed, 207 insertions(+) create mode 100644 sunnypilot/system/hardware/c3/README.md create mode 100644 sunnypilot/system/hardware/c3/agnos.json create mode 100755 sunnypilot/system/hardware/c3/launch_chffrplus.sh create mode 100755 sunnypilot/system/hardware/c3/launch_env.sh diff --git a/launch_openpilot.sh b/launch_openpilot.sh index d6e3424c34..d4841b601f 100755 --- a/launch_openpilot.sh +++ b/launch_openpilot.sh @@ -1,3 +1,20 @@ #!/usr/bin/env bash +set -euo pipefail +IFS=$'\n\t' + +# On any failure, run the fallback launcher +trap 'exec ./launch_chffrplus.sh' ERR +C3_LAUNCH_SH="./sunnypilot/system/hardware/c3/launch_chffrplus.sh" + +MODEL="$(tr -d '\0' < "/sys/firmware/devicetree/base/model")" +export MODEL + +if [ "$MODEL" = "comma tici" ]; then + # Force a failure if the launcher doesn't exist + [ -x "$C3_LAUNCH_SH" ] || false + + # If it exists, run it + exec "$C3_LAUNCH_SH" +fi exec ./launch_chffrplus.sh diff --git a/sunnypilot/system/hardware/c3/README.md b/sunnypilot/system/hardware/c3/README.md new file mode 100644 index 0000000000..f74a210191 --- /dev/null +++ b/sunnypilot/system/hardware/c3/README.md @@ -0,0 +1,3 @@ +# C3 specific hardware code + +`c3` is known as `tici` and comma three by comma. Not to confuse it with `c3x` which is known as `tizi`. \ No newline at end of file diff --git a/sunnypilot/system/hardware/c3/agnos.json b/sunnypilot/system/hardware/c3/agnos.json new file mode 100644 index 0000000000..941a4956bf --- /dev/null +++ b/sunnypilot/system/hardware/c3/agnos.json @@ -0,0 +1,84 @@ +[ + { + "name": "xbl", + "url": "https://commadist.azureedge.net/agnosupdate/xbl-effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b.img.xz", + "hash": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "hash_raw": "effa23294138e2297b85a5b482a885184c437b5ab25d74f2a62d4fce4e68f63b", + "size": 3282256, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" + }, + { + "name": "xbl_config", + "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c.img.xz", + "hash": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "hash_raw": "63d019efed684601f145ef37628e62c8da73f5053a8e51d7de09e72b8b11f97c", + "size": 98124, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" + }, + { + "name": "abl", + "url": "https://commadist.azureedge.net/agnosupdate/abl-32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6.img.xz", + "hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", + "hash_raw": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6", + "size": 274432, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "32a2174b5f764e95dfc54cf358ba01752943b1b3b90e626149c3da7d5f1830b6" + }, + { + "name": "aop", + "url": "https://commadist.azureedge.net/agnosupdate/aop-21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9.img.xz", + "hash": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", + "hash_raw": "21370172e590bd4ea907a558bcd6df20dc7a6c7d38b8e62fdde18f4a512ba9e9", + "size": 184364, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "c1be2f4aac5b3af49b904b027faec418d05efd7bd5144eb4fdfcba602bcf2180" + }, + { + "name": "devcfg", + "url": "https://commadist.azureedge.net/agnosupdate/devcfg-d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620.img.xz", + "hash": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", + "hash_raw": "d7d7e52963bbedbbf8a7e66847579ca106a0a729ce2cf60f4b8d8ea4b535d620", + "size": 40336, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "17b229668b20305ff8fa3cd5f94716a3aaa1e5bf9d1c24117eff7f2f81ae719f" + }, + { + "name": "boot", + "url": "https://commadist.azureedge.net/agnosupdate/boot-0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4.img.xz", + "hash": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "hash_raw": "0191529aa97d90d1fa04b472d80230b777606459e1e1e9e2323c9519839827b4", + "size": 18515968, + "sparse": false, + "full_check": true, + "has_ab": true, + "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" + }, + { + "name": "system", + "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img.xz", + "hash": "1468d50b7ad0fda0f04074755d21e786e3b1b6ca5dd5b17eb2608202025e6126", + "hash_raw": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", + "size": 5368709120, + "sparse": true, + "full_check": false, + "has_ab": true, + "ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7", + "alt": { + "hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", + "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img", + "size": 5368709120 + } + } +] \ No newline at end of file diff --git a/sunnypilot/system/hardware/c3/launch_chffrplus.sh b/sunnypilot/system/hardware/c3/launch_chffrplus.sh new file mode 100755 index 0000000000..45cc950537 --- /dev/null +++ b/sunnypilot/system/hardware/c3/launch_chffrplus.sh @@ -0,0 +1,92 @@ +#!/usr/bin/env bash + +SP_C3_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" >/dev/null && pwd )" +DIR="$( cd "$SP_C3_DIR/../../../.." >/dev/null 2>&1 && pwd )" + +source "$SP_C3_DIR/launch_env.sh" + +function agnos_init { + # TODO: move this to agnos + sudo rm -f /data/etc/NetworkManager/system-connections/*.nmmeta + + # set success flag for current boot slot + sudo abctl --set_success + + # TODO: do this without udev in AGNOS + # udev does this, but sometimes we startup faster + sudo chgrp gpu /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 + sudo chmod 660 /dev/adsprpc-smd /dev/ion /dev/kgsl-3d0 + + + if [ $(< /VERSION) != "$AGNOS_VERSION" ]; then + AGNOS_PY="$DIR/system/hardware/tici/agnos.py" + MANIFEST="$SP_C3_DIR/agnos.json" + if $AGNOS_PY --verify $MANIFEST; then + sudo reboot + fi + $DIR/system/hardware/tici/updater $AGNOS_PY $MANIFEST + fi +} + +function launch { + # Remove orphaned git lock if it exists on boot + [ -f "$DIR/.git/index.lock" ] && rm -f $DIR/.git/index.lock + + # Check to see if there's a valid overlay-based update available. Conditions + # are as follows: + # + # 1. The DIR init file has to exist, with a newer modtime than anything in + # the DIR Git repo. This checks for local development work or the user + # switching branches/forks, which should not be overwritten. + # 2. The FINALIZED consistent file has to exist, indicating there's an update + # that completed successfully and synced to disk. + + if [ -f "${DIR}/.overlay_init" ]; then + find ${DIR}/.git -newer ${DIR}/.overlay_init | grep -q '.' 2> /dev/null + if [ $? -eq 0 ]; then + echo "${DIR} has been modified, skipping overlay update installation" + else + if [ -f "${STAGING_ROOT}/finalized/.overlay_consistent" ]; then + if [ ! -d /data/safe_staging/old_openpilot ]; then + echo "Valid overlay update found, installing" + LAUNCHER_LOCATION="${BASH_SOURCE[0]}" + + mv $DIR /data/safe_staging/old_openpilot + mv "${STAGING_ROOT}/finalized" $DIR + cd $DIR + + echo "Restarting launch script ${LAUNCHER_LOCATION}" + unset AGNOS_VERSION + exec "${LAUNCHER_LOCATION}" + else + echo "openpilot backup found, not updating" + # TODO: restore backup? This means the updater didn't start after swapping + fi + fi + fi + fi + + # handle pythonpath + ln -sfn $(pwd) /data/pythonpath + export PYTHONPATH="$PWD" + + # hardware specific init + if [ -f /AGNOS ]; then + agnos_init + fi + + # write tmux scrollback to a file + tmux capture-pane -pq -S-1000 > /tmp/launch_log + + # start manager + cd $DIR/system/manager + if [ ! -f $DIR/prebuilt ]; then + ./build.py + fi + ./manager.py + + # if broken, keep on screen error + while true; do sleep 1; done +} + +launch diff --git a/sunnypilot/system/hardware/c3/launch_env.sh b/sunnypilot/system/hardware/c3/launch_env.sh new file mode 100755 index 0000000000..593aacc078 --- /dev/null +++ b/sunnypilot/system/hardware/c3/launch_env.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env bash + +export OMP_NUM_THREADS=1 +export MKL_NUM_THREADS=1 +export NUMEXPR_NUM_THREADS=1 +export OPENBLAS_NUM_THREADS=1 +export VECLIB_MAXIMUM_THREADS=1 + +if [ -z "$AGNOS_VERSION" ]; then + export AGNOS_VERSION="12.8" +fi From ba1da60c25c50ee70437dc25f4c2ed310d4edf1d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 29 Aug 2025 16:39:25 -0400 Subject: [PATCH 054/188] NNLC: compute error in torque space (#1185) * NNLC: compute error in torque space * bump * sp happy too * bump * lint * update path * oops * test entire loop * bump * test gm * bump * bump --- opendbc_repo | 2 +- selfdrive/controls/controlsd.py | 2 + selfdrive/controls/lib/latcontrol_torque.py | 14 +++---- .../controls/lib/latcontrol_torque_ext.py | 15 ++++--- .../lib/latcontrol_torque_ext_base.py | 11 ++++- .../selfdrive/controls/lib/nnlc/nnlc.py | 40 +++++++++++++++++-- .../controls/lib/nnlc/tests/test_nnlc.py | 10 ++++- 7 files changed, 75 insertions(+), 19 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index ee25c18829..004fa8df07 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit ee25c18829fcb229fcf6576194ddd8638b33ba55 +Subproject commit 004fa8df07479ceb205691e0689b42180270c45b diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 56a3258a5c..9e88a37690 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -95,6 +95,8 @@ class Controls(ControlsExt, ModelStateBase): self.LaC.update_live_torque_params(torque_params.latAccelFactorFiltered, torque_params.latAccelOffsetFiltered, torque_params.frictionCoefficientFiltered) + self.LaC.extension.update_limits() + self.LaC.extension.update_model_v2(self.sm['modelV2']) self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay) diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 7e4ef56023..e4554ae464 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -35,7 +35,7 @@ class LatControlTorque(LatControl): self.update_limits() self.steering_angle_deadzone_deg = self.torque_params.steeringAngleDeadzoneDeg - self.extension = LatControlTorqueExt(self, CP, CP_SP) + self.extension = LatControlTorqueExt(self, CP, CP_SP, CI) def update_live_torque_params(self, latAccelFactor, latAccelOffset, friction): self.torque_params.latAccelFactor = latAccelFactor @@ -73,12 +73,6 @@ class LatControlTorque(LatControl): ff = gravity_adjusted_lateral_accel ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) - # Lateral acceleration torque controller extension updates - # Overrides stock ff and pid_log.error - ff, pid_log = self.extension.update(CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, - desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel, - desired_curvature, actual_curvature) - freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 output_lataccel = self.pid.update(pid_log.error, feedforward=ff, @@ -86,6 +80,12 @@ class LatControlTorque(LatControl): freeze_integrator=freeze_integrator) output_torque = self.torque_from_lateral_accel(output_lataccel, self.torque_params) + # Lateral acceleration torque controller extension updates + # Overrides pid_log.error and output_torque + pid_log, output_torque = self.extension.update(CS, VM, self.pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, + desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel, + desired_curvature, actual_curvature, steer_limited_by_safety, output_torque) + pid_log.active = True pid_log.p = float(self.pid.p) pid_log.i = float(self.pid.i) diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py index fe54f46ca7..6e134276e5 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext.py @@ -9,23 +9,28 @@ from openpilot.sunnypilot.selfdrive.controls.lib.nnlc.nnlc import NeuralNetworkL class LatControlTorqueExt(NeuralNetworkLateralControl): - def __init__(self, lac_torque, CP, CP_SP): - super().__init__(lac_torque, CP, CP_SP) + def __init__(self, lac_torque, CP, CP_SP, CI): + super().__init__(lac_torque, CP, CP_SP, CI) - def update(self, CS, VM, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, + def update(self, CS, VM, pid, params, ff, pid_log, setpoint, measurement, calibrated_pose, roll_compensation, desired_lateral_accel, actual_lateral_accel, lateral_accel_deadzone, gravity_adjusted_lateral_accel, - desired_curvature, actual_curvature): + desired_curvature, actual_curvature, steer_limited_by_safety, output_torque): self._ff = ff + self._pid = pid self._pid_log = pid_log self._setpoint = setpoint self._measurement = measurement + self._roll_compensation = roll_compensation self._lateral_accel_deadzone = lateral_accel_deadzone self._desired_lateral_accel = desired_lateral_accel self._actual_lateral_accel = actual_lateral_accel self._desired_curvature = desired_curvature self._actual_curvature = actual_curvature + self._gravity_adjusted_lateral_accel = gravity_adjusted_lateral_accel + self._steer_limited_by_safety = steer_limited_by_safety + self._output_torque = output_torque self.update_calculations(CS, VM, desired_lateral_accel) self.update_neural_network_feedforward(CS, params, calibrated_pose) - return self._ff, self._pid_log + return self._pid_log, self._output_torque diff --git a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py index 644f28573a..1965d50b51 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -7,6 +7,7 @@ See the LICENSE.md file in the root directory for more details. import math import numpy as np +from openpilot.common.pid import PIDController from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.selfdrive.modeld.constants import ModelConstants @@ -43,9 +44,10 @@ def get_lookahead_value(future_vals, current_val): class LatControlTorqueExtBase: - def __init__(self, lac_torque, CP, CP_SP): + def __init__(self, lac_torque, CP, CP_SP, CI): self.model_v2 = None self.model_valid = False + self.lac_torque = lac_torque self.torque_params = lac_torque.torque_params self.actual_lateral_jerk: float = 0.0 @@ -53,17 +55,22 @@ class LatControlTorqueExtBase: self.lateral_jerk_measurement: float = 0.0 self.lookahead_lateral_jerk: float = 0.0 - self.torque_from_lateral_accel = lac_torque.torque_from_lateral_accel + self.torque_from_lateral_accel_in_torque_space = CI.torque_from_lateral_accel_in_torque_space() self._ff = 0.0 + self._pid = PIDController(0.0, 0.0, k_f=0.0) self._pid_log = None self._setpoint = 0.0 self._measurement = 0.0 + self._roll_compensation = 0.0 self._lateral_accel_deadzone = 0.0 self._desired_lateral_accel = 0.0 self._actual_lateral_accel = 0.0 self._desired_curvature = 0.0 self._actual_curvature = 0.0 + self._gravity_adjusted_lateral_accel = 0.0 + self._steer_limited_by_safety = False + self._output_torque = 0.0 # twilsonco's Lateral Neural Network Feedforward # Instantaneous lateral jerk changes very rapidly, making it not useful on its own, diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 77d46ace34..1738a11e49 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -9,6 +9,8 @@ import math import numpy as np from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction +from opendbc.sunnypilot.car.interfaces import LatControlInputs +from opendbc.sunnypilot.car.lateral_ext import get_friction as get_friction_in_torque_space from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params from openpilot.selfdrive.modeld.constants import ModelConstants @@ -30,8 +32,8 @@ def roll_pitch_adjust(roll, pitch): class NeuralNetworkLateralControl(LatControlTorqueExtBase): - def __init__(self, lac_torque, CP, CP_SP): - super().__init__(lac_torque, CP, CP_SP) + def __init__(self, lac_torque, CP, CP_SP, CI): + super().__init__(lac_torque, CP, CP_SP, CI) self.params = Params() self.enabled = self.params.get_bool("NeuralNetworkLateralControl") self.has_nn_model = CP_SP.neuralNetworkLateralControl.model.path != MOCK_MODEL_PATH @@ -57,14 +59,44 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): self.error_deque = deque(maxlen=history_check_frames[0]) self.past_future_len = len(self.past_times) + len(self.nn_future_times) + @property + def _nnlc_enabled(self): + return self.enabled and self.model_valid and self.has_nn_model + + def update_limits(self): + if not self._nnlc_enabled: + return + + self._pid.set_limits(self.lac_torque.steer_max, -self.lac_torque.steer_max) + def update_lateral_lag(self, lag): super().update_lateral_lag(lag) self.nn_future_times = [t + self.desired_lat_jerk_time for t in self.future_times] + def update_feedforward_torque_space(self, CS): + torque_from_setpoint = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._setpoint, self._roll_compensation, CS.vEgo, CS.aEgo), + self.torque_params, gravity_adjusted=False) + torque_from_measurement = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._measurement, self._roll_compensation, CS.vEgo, CS.aEgo), + self.torque_params, gravity_adjusted=False) + self._pid_log.error = float(torque_from_setpoint - torque_from_measurement) + self._ff = self.torque_from_lateral_accel_in_torque_space(LatControlInputs(self._gravity_adjusted_lateral_accel, self._roll_compensation, + CS.vEgo, CS.aEgo), self.torque_params, gravity_adjusted=True) + self._ff += get_friction_in_torque_space(self._desired_lateral_accel - self._actual_lateral_accel, self._lateral_accel_deadzone, + FRICTION_THRESHOLD, self.torque_params) + + def update_output_torque(self, CS): + freeze_integrator = self._steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + self._output_torque = self._pid.update(self._pid_log.error, + feedforward=self._ff, + speed=CS.vEgo, + freeze_integrator=freeze_integrator) + def update_neural_network_feedforward(self, CS, params, calibrated_pose) -> None: - if not self.enabled or not self.model_valid or not self.has_nn_model: + if not self._nnlc_enabled: return + self.update_feedforward_torque_space(CS) + low_speed_factor = float(np.interp(CS.vEgo, LOW_SPEED_X, LOW_SPEED_Y)) ** 2 self._setpoint = self._desired_lateral_accel + low_speed_factor * self._desired_curvature self._measurement = self._actual_lateral_accel + low_speed_factor * self._actual_curvature @@ -128,3 +160,5 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): # apply friction override for cars with low NN friction response if self.model.friction_override: self._pid_log.error += get_friction(friction_input, self._lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) + + self.update_output_torque(CS) diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py index 01ddec68ab..009e3d96af 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/tests/test_nnlc.py @@ -3,6 +3,7 @@ from parameterized import parameterized from cereal import car, log, messaging from opendbc.car.car_helpers import interfaces +from opendbc.car.gm.values import CAR as GM from opendbc.car.honda.values import CAR as HONDA from opendbc.car.hyundai.values import CAR as HYUNDAI from opendbc.car.toyota.values import CAR as TOYOTA @@ -41,7 +42,7 @@ def generate_modelV2(): class TestNeuralNetworkLateralControl: - @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN]) + @parameterized.expand([HONDA.HONDA_CIVIC, TOYOTA.TOYOTA_RAV4, HYUNDAI.HYUNDAI_SANTA_CRUZ_1ST_GEN, GM.CHEVROLET_BOLT_EUV]) def test_saturation(self, car_name): params = Params() params.put_bool("NeuralNetworkLateralControl", True) @@ -57,6 +58,7 @@ class TestNeuralNetworkLateralControl: VM = VehicleModel(CP) controller = LatControlTorque(CP.as_reader(), CP_SP.as_reader(), CI) + torque_params = CP.lateralTuning.torque CS = car.CarState.new_message() CS.vEgo = 30 @@ -77,17 +79,23 @@ class TestNeuralNetworkLateralControl: for _ in range(1000): controller.extension.update_model_v2(model_v2) controller.extension.update_lateral_lag(test_lag) + controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) + controller.extension.update_limits() _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, True) assert lac_log.saturated for _ in range(1000): controller.extension.update_model_v2(model_v2) controller.extension.update_lateral_lag(test_lag) + controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) + controller.extension.update_limits() _, _, lac_log = controller.update(True, CS, VM, params, False, 0, pose, False) assert not lac_log.saturated for _ in range(1000): controller.extension.update_model_v2(model_v2) controller.extension.update_lateral_lag(test_lag) + controller.update_live_torque_params(torque_params.latAccelFactor, torque_params.latAccelOffset, torque_params.friction) + controller.extension.update_limits() _, _, lac_log = controller.update(True, CS, VM, params, False, 1, pose, False) assert lac_log.saturated From c990515eaff7e638514b7073b863f494ecf534e4 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Fri, 29 Aug 2025 14:16:52 -0700 Subject: [PATCH 055/188] update RELEASES.md for 0.10.1 --- RELEASES.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/RELEASES.md b/RELEASES.md index dacf0eaa17..d89ee24628 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,9 @@ Version 0.10.1 (2025-09-08) ======================== +* New driving model + * World Model: removed global localization inputs + * World Model: 2x the number of parameters + * World Model: trained on 4x the number of segments * Record driving feedback using LKAS button * Honda City 2023 support thanks to drFritz! From 205863b71f238e32a6ea8e0197b0d155deee5e98 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 30 Aug 2025 07:26:31 +0200 Subject: [PATCH 056/188] ci: add deploy strategy extraction and refactor publish dependencies (#1118) * ci: add deploy strategy extraction and refactor publish dependencies - Introduced `prepare_strategy` step to dynamically extract deployment configurations. - Adjusted `publish` job to depend on `prepare_strategy` and use its outputs. * what happens if I do this... * cleaning * other test * ci: update auto_deploy logic in build configuration * cleaning --- .../workflows/sunnypilot-build-prebuilt.yaml | 117 ++++++++---------- 1 file changed, 52 insertions(+), 65 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 4f52e7fa29..60ca05f070 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -9,12 +9,6 @@ env: # Branch configurations STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master' }} # vars are set on repo settings. - DEV_C3_SOURCE_BRANCH: ${{ vars.DEV_C3_SOURCE_BRANCH || 'master-dev-c3-new' }} # vars are set on repo settings. - - # Target branch configurations - STAGING_TARGET_BRANCH: ${{ vars.STAGING_TARGET_BRANCH || 'staging-c3-new' }} # vars are set on repo settings. - DEV_TARGET_BRANCH: ${{ vars.DEV_TARGET_BRANCH || 'dev-c3-new' }} # vars are set on repo settings. - RELEASE_TARGET_BRANCH: ${{ vars.RELEASE_TARGET_BRANCH || 'release-c3-new' }} # vars are set on repo settings. # Runtime configuration SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}" @@ -46,16 +40,16 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} build: - needs: [ validate_tests ] + needs: [ validate_tests, prepare_strategy ] concurrency: group: build-${{ github.head_ref || github.ref_name }} cancel-in-progress: false runs-on: [self-hosted, tici] outputs: - new_branch: ${{ steps.set-env.outputs.new_branch }} - version: ${{ steps.set-env.outputs.version }} - extra_version_identifier: ${{ steps.set-env.outputs.extra_version_identifier }} - commit_sha: ${{ steps.set-env.outputs.commit_sha }} + new_branch: ${{ needs.prepare_strategy.outputs.new_branch }} + version: ${{ needs.prepare_strategy.outputs.version }} + extra_version_identifier: ${{ needs.prepare_strategy.outputs.extra_version_identifier }} + commit_sha: ${{ github.sha }} if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} steps: - uses: actions/checkout@v4 @@ -77,61 +71,12 @@ jobs: scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_C3_SOURCE_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }} - - name: Set Feature Branch Prebuilt Configuration - id: set_feature_configuration - if: ( - !(env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH) && - !(env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) && - !(startsWith(github.ref, 'refs/tags/')) - ) - run: | - echo "NEW_BRANCH=${{ env.SOURCE_BRANCH }}${{ github.event.pull_request.head.repo.fork && '-fork' || '' }}-prebuilt" >> $GITHUB_ENV - echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV - - - name: Set dev-c3-new prebuilt Configuration - id: set_dev_configuration - if: ( - steps.set_feature_configuration.outcome == 'skipped' && - env.SOURCE_BRANCH == env.DEV_C3_SOURCE_BRANCH - ) - run: | - echo "NEW_BRANCH=${{ env.DEV_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "VERSION=$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=${{ github.run_number }}" >> $GITHUB_ENV - - - name: Set staging-c3-new prebuilt Configuration - id: set_staging_configuration - if: ( - steps.set_feature_configuration.outcome == 'skipped' && - !contains(github.event_name, 'pull_request') && - steps.set_dev_configuration.outcome == 'skipped' && - (env.SOURCE_BRANCH == env.STAGING_C3_SOURCE_BRANCH) - ) - run: | - echo "NEW_BRANCH=${{ env.STAGING_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=staging" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-staging" >> $GITHUB_ENV - - - name: Set release-c3-new prebuilt Configuration - id: set_tag_configuration - if: ( - steps.set_feature_configuration.outcome == 'skipped' && - !contains(github.event_name, 'pull_request') && - steps.set_staging_configuration.outcome == 'skipped' && - startsWith(github.ref, 'refs/tags/') - ) - run: | - echo "NEW_BRANCH=${{ env.RELEASE_TARGET_BRANCH }}" >> $GITHUB_ENV - echo "EXTRA_VERSION_IDENTIFIER=release" >> $GITHUB_ENV - echo "VERSION=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g')-release" >> $GITHUB_ENV - - name: Set environment variables id: set-env run: | - # Write to GITHUB_OUTPUT from environment variables - echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT - [[ ! -z "$EXTRA_VERSION_IDENTIFIER" ]] && echo "extra_version_identifier=$EXTRA_VERSION_IDENTIFIER" >> $GITHUB_OUTPUT - [[ ! -z "$VERSION" ]] && echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "new_branch=${{ needs.prepare_strategy.outputs.new_branch }}" >> $GITHUB_OUTPUT + echo "version=${{ needs.prepare_strategy.outputs.version }}" >> $GITHUB_OUTPUT + echo "extra_version_identifier=${{ needs.prepare_strategy.outputs.extra_version_identifier }}" >> $GITHUB_OUTPUT echo "commit_sha=${{ github.sha }}" >> $GITHUB_OUTPUT # Set up common environment @@ -226,15 +171,57 @@ jobs: if: always() run: | PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable + + prepare_strategy: + runs-on: ubuntu-24.04 + outputs: + prebuilt: ${{ steps.strategy.outputs.prebuilt }} + environment: ${{ steps.strategy.outputs.environment }} + new_branch: ${{ steps.strategy.outputs.new_branch }} + extra_version_identifier: ${{ steps.strategy.outputs.extra_version_identifier }} + version: ${{ steps.strategy.outputs.version }} + steps: + - name: Extract deploy strategy + id: strategy + run: | + echo '::group::Strategy Extraction' + BRANCH="${{ github.head_ref || github.ref_name }}" + echo "Current branch: $BRANCH" + STRATEGY_JSON='${{ vars.DEPLOY_STRATEGY }}' + CONFIG=$(echo "$STRATEGY_JSON" | jq -r --arg branch "$BRANCH" ' + .configs[] | select(.branch == $branch) + ') + + if [[ -z "$CONFIG" || "$CONFIG" == "null" ]]; then + echo "No exact strategy match found. Falling back to feature/fork logic." + IS_FORK="${{ github.event.pull_request.head.repo.fork && 'true' || 'false' }}" + FORK_SUFFIX=$( [[ "$IS_FORK" == "true" ]] && echo "-fork" || echo "" ) + NEW_BRANCH="${BRANCH}${FORK_SUFFIX}-prebuilt" + VERSION="$(date '+%Y.%m.%d')-${{ github.run_number }}" + + echo "prebuilt=true" >> $GITHUB_OUTPUT + echo "environment=${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }}" >> $GITHUB_OUTPUT + echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + else + echo "Matched config: $CONFIG" + echo "prebuilt=$(echo "$CONFIG" | jq -r '.prebuilt')" >> $GITHUB_OUTPUT + echo "environment=$(echo "$CONFIG" | jq -r '.environment')" >> $GITHUB_OUTPUT + echo "new_branch=$(echo "$CONFIG" | jq -r '.target_branch')" >> $GITHUB_OUTPUT + echo "version=$(echo "$CONFIG" | jq -r '.version_prefix // ""')$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_OUTPUT + echo "extra_version_identifier=$(echo "$CONFIG" | jq -r '.extra_version_identifier // ""')" >> $GITHUB_OUTPUT + fi + + publish: concurrency: group: publish-${{ github.head_ref || github.ref_name }} cancel-in-progress: true if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} - needs: [ build ] + needs: [ build, prepare_strategy ] runs-on: ubuntu-24.04 - environment: ${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }} + environment: ${{ needs.prepare_strategy.outputs.environment }} steps: - uses: actions/checkout@v4 From 62bf9fcc274f40b3ef840101750cdadb87ffdc15 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 30 Aug 2025 11:52:57 +0200 Subject: [PATCH 057/188] ci: tweaking the deploy with delay process + fixing bugs (#1189) * ci: disable cancel-in-progress for publish concurrency * check using var * typo * ci: update publish concurrency settings to use dynamic cancel-in-progress flag * typoooo * ci: update cancel-in-progress condition for publish concurrency * ci: enhance publish concurrency handling to queue jobs based on commit SHA * typos and new commit hash to test cancel in progress * see if this helps? * tired of waiting * ci: add publish concurrency group to deployment configuration * ci: update publish concurrency handling to improve job queuing and cancellation logic * ci: output GITHUB_OUTPUT contents for better debugging of publish concurrency * ci: remove prebuilt output from strategy and streamline GITHUB_OUTPUT handling * ci: refine publish concurrency handling for flexible job cancellation - Default `cancel_publish_in_progress` to `true` if undefined in config. - Adjust concurrency group logic to handle null and true conditions properly. * another ocmmit shouldnt cancel publish * ci: enhance job cancellation logic for publish concurrency handling * ci: add prepare_strategy job for dynamic deploy strategy extraction * ci: ensure job execution always proceeds on success and skips failure * ci: improve job execution conditions to handle cancellation and failure states * ci: enhance versioning logic to support stable and unstable branch differentiation * ci: add checkout step to ensure code is available for deploy strategy extraction * ci: add extra version identifier for stable branch environments * ci: update extra version identifier format for stable branches * Grammar, oh, grammar. * test this --- .../workflows/sunnypilot-build-prebuilt.yaml | 115 +++++++++++------- 1 file changed, 69 insertions(+), 46 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 60ca05f070..344a49e93a 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -28,6 +28,61 @@ on: default: false jobs: + prepare_strategy: + runs-on: ubuntu-24.04 + if: (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + outputs: + environment: ${{ steps.strategy.outputs.environment }} + new_branch: ${{ steps.strategy.outputs.new_branch }} + extra_version_identifier: ${{ steps.strategy.outputs.extra_version_identifier }} + version: ${{ steps.strategy.outputs.version }} + cancel_publish_in_progress: ${{ steps.strategy.outputs.cancel_publish_in_progress }} + publish_concurrency_group: ${{ steps.strategy.outputs.publish_concurrency_group }} + steps: + - uses: actions/checkout@v4 + - name: Extract deploy strategy + id: strategy + run: | + echo '::group::Strategy Extraction' + BRANCH="${{ github.head_ref || github.ref_name }}" + echo "Current branch: $BRANCH" + + STRATEGY_JSON='${{ vars.DEPLOY_STRATEGY }}' + CONFIG=$(echo "$STRATEGY_JSON" | jq -r --arg branch "$BRANCH" ' + .configs[] | select(.branch == $branch) + ') + + if [[ -z "$CONFIG" || "$CONFIG" == "null" ]]; then + echo "No exact strategy match found. Falling back to feature/fork logic." + IS_FORK="${{ github.event.pull_request.head.repo.fork && 'true' || 'false' }}" + FORK_SUFFIX=$( [[ "$IS_FORK" == "true" ]] && echo "-fork" || echo "" ) + NEW_BRANCH="${BRANCH}${FORK_SUFFIX}-prebuilt" + VERSION="$(date '+%Y.%m.%d')-${{ github.run_number }}" + + echo "environment=${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }}" >> $GITHUB_OUTPUT + echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "cancel_publish_in_progress=true" >> $GITHUB_OUTPUT + echo "publish_concurrency_group=publish-${BRANCH}" >> $GITHUB_OUTPUT + else + echo "Matched config: $CONFIG" + environment=$(echo "$CONFIG" | jq -r '.environment') + echo "environment=$environment" >> $GITHUB_OUTPUT + echo "new_branch=$(echo "$CONFIG" | jq -r '.target_branch')" >> $GITHUB_OUTPUT + cancel="$(echo "$CONFIG" | jq -r '.cancel_publish_in_progress')"; + echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT + echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT + + stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')"; + stable_version=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g'); + unstable_version=$(date '+%Y.%m.%d')-${{ github.run_number }}; + echo "version=$([ "$stable_branch" = "true" ] && echo "$stable_version" || echo "$unstable_version")" >> $GITHUB_OUTPUT + + extra_version_identifier=$( [ "$stable_branch" = "true" ] && echo "-${environment}" || echo "" ); + echo "extra_version_identifier=$extra_version_identifier" >> $GITHUB_OUTPUT + fi + cat $GITHUB_OUTPUT + validate_tests: runs-on: ubuntu-24.04 if: ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) @@ -50,7 +105,13 @@ jobs: version: ${{ needs.prepare_strategy.outputs.version }} extra_version_identifier: ${{ needs.prepare_strategy.outputs.extra_version_identifier }} commit_sha: ${{ github.sha }} - if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} + if: ${{ + (always() && !cancelled() && !failure()) && + needs.prepare_strategy.result == 'success' && + (needs.validate_tests.result == 'success' || needs.validate_tests.result == 'skipped') && + (!contains(github.event_name, 'pull_request') || + (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + }} steps: - uses: actions/checkout@v4 with: @@ -171,54 +232,16 @@ jobs: if: always() run: | PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable - - prepare_strategy: - runs-on: ubuntu-24.04 - outputs: - prebuilt: ${{ steps.strategy.outputs.prebuilt }} - environment: ${{ steps.strategy.outputs.environment }} - new_branch: ${{ steps.strategy.outputs.new_branch }} - extra_version_identifier: ${{ steps.strategy.outputs.extra_version_identifier }} - version: ${{ steps.strategy.outputs.version }} - steps: - - name: Extract deploy strategy - id: strategy - run: | - echo '::group::Strategy Extraction' - BRANCH="${{ github.head_ref || github.ref_name }}" - echo "Current branch: $BRANCH" - - STRATEGY_JSON='${{ vars.DEPLOY_STRATEGY }}' - CONFIG=$(echo "$STRATEGY_JSON" | jq -r --arg branch "$BRANCH" ' - .configs[] | select(.branch == $branch) - ') - - if [[ -z "$CONFIG" || "$CONFIG" == "null" ]]; then - echo "No exact strategy match found. Falling back to feature/fork logic." - IS_FORK="${{ github.event.pull_request.head.repo.fork && 'true' || 'false' }}" - FORK_SUFFIX=$( [[ "$IS_FORK" == "true" ]] && echo "-fork" || echo "" ) - NEW_BRANCH="${BRANCH}${FORK_SUFFIX}-prebuilt" - VERSION="$(date '+%Y.%m.%d')-${{ github.run_number }}" - - echo "prebuilt=true" >> $GITHUB_OUTPUT - echo "environment=${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }}" >> $GITHUB_OUTPUT - echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT - else - echo "Matched config: $CONFIG" - echo "prebuilt=$(echo "$CONFIG" | jq -r '.prebuilt')" >> $GITHUB_OUTPUT - echo "environment=$(echo "$CONFIG" | jq -r '.environment')" >> $GITHUB_OUTPUT - echo "new_branch=$(echo "$CONFIG" | jq -r '.target_branch')" >> $GITHUB_OUTPUT - echo "version=$(echo "$CONFIG" | jq -r '.version_prefix // ""')$(date '+%Y.%m.%d')-${{ github.run_number }}" >> $GITHUB_OUTPUT - echo "extra_version_identifier=$(echo "$CONFIG" | jq -r '.extra_version_identifier // ""')" >> $GITHUB_OUTPUT - fi publish: concurrency: - group: publish-${{ github.head_ref || github.ref_name }} - cancel-in-progress: true - if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} + # We do a bit of a hack here to avoid canceling the publishing job if a new commit comes in while we're publishing by adding the sha to the group name. + # This means that if multiple commits come in while we're publishing, they will be queued up and publish one after the other. + # Otherwise, if a job is waiting to be published due to environment wait time, it would be canceled by a new commit and restart the wait time. + group: ${{ needs.prepare_strategy.outputs.publish_concurrency_group }} + cancel-in-progress: ${{ needs.prepare_strategy.outputs.cancel_publish_in_progress == 'true' }} + if: ${{ (always() && !cancelled() && !failure()) && needs.build.result == 'success' && needs.prepare_strategy.result == 'success' && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} needs: [ build, prepare_strategy ] runs-on: ubuntu-24.04 environment: ${{ needs.prepare_strategy.outputs.environment }} @@ -265,7 +288,7 @@ jobs: notify: needs: [ build, publish ] runs-on: ubuntu-24.04 - if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} + if: ${{ (always() && !cancelled() && !failure()) && needs.publish.result == 'success' && !failure() && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} steps: - uses: actions/checkout@v4 - name: Setup Alpine Linux environment From 5d110bcee56eb655db8a7bccd0c181b1396bc7c7 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 30 Aug 2025 14:35:05 +0200 Subject: [PATCH 058/188] ci: prebuilt process improvement & tag of staging prebuilt source (#1190) * ci: add validate-test-on-staging-c3 branch to deployment triggers and enhance stable branch handling * fix long overdue mistake lol * ci: add condition to wait for start on push events in build workflow * Fix extra version identifier * no need for this, i validated what I needed * only care for release tags, not any * fix: update versioning logic to use build date and run number for tagging * fix: update tagging logic and enhance commit message format in build scripts * fix: refine tagging condition to exclude tag pushes for stable branches * fix: add extra version identifier to output for better version tracking * trying to keep things clean and simple * bugfix --- .../workflows/sunnypilot-build-prebuilt.yaml | 40 +++++++++++++------ release/ci/publish.sh | 23 ++++------- 2 files changed, 36 insertions(+), 27 deletions(-) diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 344a49e93a..d654f5ab46 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -16,7 +16,7 @@ env: on: push: branches: [ master, master-dev-c3-new ] - tags: [ '*' ] + tags: [ 'release/*' ] pull_request_target: types: [ labeled ] workflow_dispatch: @@ -38,6 +38,8 @@ jobs: version: ${{ steps.strategy.outputs.version }} cancel_publish_in_progress: ${{ steps.strategy.outputs.cancel_publish_in_progress }} publish_concurrency_group: ${{ steps.strategy.outputs.publish_concurrency_group }} + is_stable_branch: ${{ steps.strategy.outputs.is_stable_branch }} + build: ${{ steps.strategy.outputs.build }} steps: - uses: actions/checkout@v4 - name: Extract deploy strategy @@ -52,18 +54,19 @@ jobs: .configs[] | select(.branch == $branch) ') + BUILD="$(date '+%Y.%m.%d')-${{ github.run_number }}" if [[ -z "$CONFIG" || "$CONFIG" == "null" ]]; then echo "No exact strategy match found. Falling back to feature/fork logic." IS_FORK="${{ github.event.pull_request.head.repo.fork && 'true' || 'false' }}" FORK_SUFFIX=$( [[ "$IS_FORK" == "true" ]] && echo "-fork" || echo "" ) NEW_BRANCH="${BRANCH}${FORK_SUFFIX}-prebuilt" - VERSION="$(date '+%Y.%m.%d')-${{ github.run_number }}" - echo "environment=${{ (contains(fromJSON(vars.AUTO_DEPLOY_PREBUILT_BRANCHES), github.head_ref || github.ref_name) || contains(github.event.pull_request.labels.*.name, 'prebuilt')) && 'auto-deploy' || 'feature-branch' }}" >> $GITHUB_OUTPUT echo "new_branch=$NEW_BRANCH" >> $GITHUB_OUTPUT - echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "version=$BUILD" >> $GITHUB_OUTPUT echo "cancel_publish_in_progress=true" >> $GITHUB_OUTPUT echo "publish_concurrency_group=publish-${BRANCH}" >> $GITHUB_OUTPUT + echo "environment=feature-branch" >> $GITHUB_OUTPUT + echo "extra_version_identifier=feature-branch" >> $GITHUB_OUTPUT else echo "Matched config: $CONFIG" environment=$(echo "$CONFIG" | jq -r '.environment') @@ -73,19 +76,24 @@ jobs: echo "cancel_publish_in_progress=$( [ "$cancel" = "null" ] && echo "true" || echo $cancel)" >> $GITHUB_OUTPUT echo "publish_concurrency_group=publish-${BRANCH}$( [ "$cancel" = "null" ] || [ "$cancel" = "true" ] || echo "${{ github.sha }}" )" >> $GITHUB_OUTPUT - stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')"; + is_stable_branch="$(echo "$CONFIG" | jq -r '.stable_branch // false')"; + echo "is_stable_branch=$is_stable_branch" >> $GITHUB_OUTPUT + stable_version=$(cat common/version.h | grep COMMA_VERSION | sed -e 's/[^0-9|.]//g'); - unstable_version=$(date '+%Y.%m.%d')-${{ github.run_number }}; - echo "version=$([ "$stable_branch" = "true" ] && echo "$stable_version" || echo "$unstable_version")" >> $GITHUB_OUTPUT - - extra_version_identifier=$( [ "$stable_branch" = "true" ] && echo "-${environment}" || echo "" ); - echo "extra_version_identifier=$extra_version_identifier" >> $GITHUB_OUTPUT + echo "version=$([ "$is_stable_branch" = "true" ] && echo "$stable_version" || echo "$BUILD")" >> $GITHUB_OUTPUT + echo "extra_version_identifier=${environment}" >> $GITHUB_OUTPUT fi + echo "build=$BUILD" >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT validate_tests: runs-on: ubuntu-24.04 - if: ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + needs: [ prepare_strategy ] + if: ${{ + ((github.event_name == 'workflow_dispatch' && inputs.wait_for_tests) || + (github.event_name == 'push' && needs.prepare_strategy.outputs.is_stable_branch == 'true') || + contains(github.event_name, 'pull_request') && (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) + }} steps: - uses: actions/checkout@v4 - name: Wait for Tests @@ -93,6 +101,7 @@ jobs: with: workflow: selfdrive_tests.yaml # The workflow file to monitor github-token: ${{ secrets.GITHUB_TOKEN }} + should-wait-for-start: ${{ github.event_name == 'push' && 'true' || 'false' }} build: needs: [ validate_tests, prepare_strategy ] @@ -276,7 +285,7 @@ jobs: "${{ needs.build.outputs.new_branch }}" \ "${{ needs.build.outputs.version }}" \ "https://x-access-token:${{github.token}}@github.com/sunnypilot/sunnypilot.git" \ - "-${{ needs.build.outputs.extra_version_identifier }}" + "${{ needs.build.outputs.extra_version_identifier }}" echo "" echo "---- â„¹ï¸ To update the list of branches that auto deploy prebuilts -----" @@ -284,6 +293,13 @@ jobs: echo "1. Go to: ${{ github.server_url }}/${{ github.repository }}/settings/variables/actions/AUTO_DEPLOY_PREBUILT_BRANCHES" echo "2. Current value: ${{ vars.AUTO_DEPLOY_PREBUILT_BRANCHES }}" echo "3. Update as needed (JSON array with no spaces)" + + - name: Tag ${{ needs.prepare_strategy.outputs.environment }} + if: ${{ needs.prepare_strategy.outputs.is_stable_branch == 'true' && (github.event_name != 'push' || !startsWith(github.ref, 'refs/tags/')) }} + run: | + TAG="${{ needs.prepare_strategy.outputs.environment }}/${{ needs.prepare_strategy.outputs.version }}/${{ needs.prepare_strategy.outputs.build }}" + git tag -f -a ${TAG} -m "${{ needs.prepare_strategy.outputs.environment }} @ ${{ needs.prepare_strategy.outputs.version }} of build ${{ needs.build.outputs.build }}." + git push -f origin ${TAG} notify: needs: [ build, publish ] diff --git a/release/ci/publish.sh b/release/ci/publish.sh index d723782934..4aecbacc4a 100755 --- a/release/ci/publish.sh +++ b/release/ci/publish.sh @@ -51,26 +51,19 @@ git fetch origin $DEV_BRANCH || (git checkout -b $DEV_BRANCH && git commit --all echo "[-] committing version $VERSION T=$SECONDS" git add -f . -git commit -a -m "sunnypilot v$VERSION release" -git branch --set-upstream-to=origin/$DEV_BRANCH # include source commit hash and build date in commit GIT_HASH=$(git --git-dir=$SOURCE_DIR/.git rev-parse HEAD) DATETIME=$(date '+%Y-%m-%dT%H:%M:%S') -SP_VERSION=$(cat $SOURCE_DIR/common/version.h | awk -F\" '{print $2}') +SP_VERSION=$(awk -F\" '{print $2}' $SOURCE_DIR/common/version.h) -# Add built files to git -git add -f . -if [ "$EXTRA_VERSION_IDENTIFIER" = "-release" ] || [ "$EXTRA_VERSION_IDENTIFIER" = "-staging" ]; then - export VERSION=${VERSION%"$EXTRA_VERSION_IDENTIFIER"} - git commit --amend -m "sunnypilot v$VERSION" -else - git commit --amend -m "sunnypilot v$VERSION - version: sunnypilot v$SP_VERSION release - date: $DATETIME - master commit: $GIT_HASH - " -fi +# Commit with detailed message +git commit -a -m "sunnypilot v$VERSION +version: sunnypilot v$SP_VERSION (${EXTRA_VERSION_IDENTIFIER}) +date: $DATETIME +master commit: $GIT_HASH +" +git branch --set-upstream-to=origin/$DEV_BRANCH git branch -m $DEV_BRANCH # Push! From 3408873018c6d947f07c2168ce9f63de3c834f20 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 21:54:42 -0400 Subject: [PATCH 059/188] wrong cruise speed --- .../speed_limit_controller.py | 30 +++++++++++-------- 1 file changed, 17 insertions(+), 13 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 3354877466..b4ec829e44 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -108,18 +108,7 @@ class SpeedLimitController: @property def speed_limit_offseted(self) -> float: - # If we have a current valid speed limit, use it - if self._speed_limit > 0: - current_offsetted = self._speed_limit + self.speed_limit_offset - self.last_valid_speed_limit_offsetted = current_offsetted - return current_offsetted - - # If no current speed limit but we have a last valid one, use that - if self.last_valid_speed_limit_offsetted > 0: - return self.last_valid_speed_limit_offsetted - - # Fallback - return V_CRUISE_UNSET + return self._speed_limit + self.speed_limit_offset @property def speed_limit_offset(self) -> float: @@ -141,6 +130,21 @@ class SpeedLimitController: def source(self) -> Source: return self._source + @property + def final_cruise_speed(self) -> float: + if self.is_active: + # If we have a current valid speed limit, use it + if self._speed_limit > 0: + self.last_valid_speed_limit_offsetted = self.speed_limit_offseted + return self.speed_limit_offseted + + # If no current speed limit but we have a last valid one, use that + if self.last_valid_speed_limit_offsetted > 0: + return self.last_valid_speed_limit_offsetted + + # Fallback + return V_CRUISE_UNSET + def get_offset(self, offset_type: OffsetType, offset_value: int) -> float: if offset_type == OffsetType.default: return float(np.interp(self._speed_limit, LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V) * self._speed_limit) @@ -296,4 +300,4 @@ class SpeedLimitController: self.state_control() self.update_events(events_sp) - return self.speed_limit_offseted + return self.final_cruise_speed From e1d5b9019b46b4ff712c0de8be6fdbdf2a1f763c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 22:00:13 -0400 Subject: [PATCH 060/188] fix --- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index b4ec829e44..41a06c5719 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -234,8 +234,8 @@ class SpeedLimitController: self._state = SpeedLimitControlState.active else: self._state = SpeedLimitControlState.pending - elif self.v_cruise_setpoint_changed and self.current_time > (self.last_op_engaged_time + PRE_ACTIVE_GUARD_PERIOD): - # User set cruise to something other than 80 MPH, permanently disable for this session + elif self.current_time > (self.last_op_engaged_time + PRE_ACTIVE_GUARD_PERIOD): + # # If the initial max set speed isn't reached within the allocated period, permanently disable for this session self._state = SpeedLimitControlState.inactive def transition_state_from_pending(self) -> None: From f0083d6241380e84ed259c402941fa2c78e03fdf Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 22:38:31 -0400 Subject: [PATCH 061/188] not used for now --- .../selfdrive/controls/lib/longitudinal_planner.py | 2 +- .../lib/speed_limit_controller/speed_limit_controller.py | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 97dac727c1..aa30e12f6b 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -21,7 +21,7 @@ class LongitudinalPlannerSP: self.dec = DynamicExperimentalController(CP, mpc) self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None - self.slc = SpeedLimitController(CP) + self.slc = SpeedLimitController() @property def mlsim(self) -> bool: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 41a06c5719..aa8000c385 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -1,3 +1,9 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" import numpy as np import time @@ -29,9 +35,8 @@ class SpeedLimitController: _a_ego: float _v_offset: float - def __init__(self, CP): + def __init__(self): self.params = Params() - self.CP = CP self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) self.resolver = SpeedLimitResolver(self.policy) self.last_params_update = 0.0 From 22fd56e32e4bdf38dea03a4e2fb3cadb5a289145 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 22:40:49 -0400 Subject: [PATCH 062/188] Revert "not used for now" This reverts commit f0083d6241380e84ed259c402941fa2c78e03fdf. --- .../selfdrive/controls/lib/longitudinal_planner.py | 2 +- .../lib/speed_limit_controller/speed_limit_controller.py | 9 ++------- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index aa30e12f6b..97dac727c1 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -21,7 +21,7 @@ class LongitudinalPlannerSP: self.dec = DynamicExperimentalController(CP, mpc) self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None - self.slc = SpeedLimitController() + self.slc = SpeedLimitController(CP) @property def mlsim(self) -> bool: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index aa8000c385..41a06c5719 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -1,9 +1,3 @@ -""" -Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - -This file is part of sunnypilot and is licensed under the MIT License. -See the LICENSE.md file in the root directory for more details. -""" import numpy as np import time @@ -35,8 +29,9 @@ class SpeedLimitController: _a_ego: float _v_offset: float - def __init__(self): + def __init__(self, CP): self.params = Params() + self.CP = CP self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) self.resolver = SpeedLimitResolver(self.policy) self.last_params_update = 0.0 From e6a647674062df31428ecdc1aeadd8bfe5eb7298 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 22:43:02 -0400 Subject: [PATCH 063/188] some --- .../lib/speed_limit_controller/speed_limit_controller.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 41a06c5719..7a4d4b8ad2 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -1,3 +1,9 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" import numpy as np import time From 84abd66bba913464aa7c6eb01e767dd0c346b3b4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 23:35:04 -0400 Subject: [PATCH 064/188] use frames instead --- .../lib/speed_limit_controller/speed_limit_controller.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 7a4d4b8ad2..655fa36160 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -10,6 +10,7 @@ import time from cereal import messaging, custom from openpilot.common.constants import CV from openpilot.common.params import Params +from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V, \ PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_CRUISE_SPEED, \ @@ -40,7 +41,7 @@ class SpeedLimitController: self.CP = CP self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) self.resolver = SpeedLimitResolver(self.policy) - self.last_params_update = 0.0 + self.frame = -1 self.last_op_engaged_time = 0.0 self.is_metric = self.params.get_bool("IsMetric") self.enabled = self.params.get_bool("SpeedLimitControl") @@ -165,7 +166,7 @@ class SpeedLimitController: self.v_cruise_setpoint_prev = self.v_cruise_setpoint def update_params(self) -> None: - if self.current_time > self.last_params_update + PARAMS_UPDATE_PERIOD: + if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0: self.enabled = self.params.get_bool("SpeedLimitControl") self.offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) @@ -178,8 +179,6 @@ class SpeedLimitController: self.resolver.change_policy(self.policy) self.engage_type = self.read_engage_type_param() - self.last_params_update = self.current_time - @staticmethod def read_engage_type_param() -> Engage: return Engage.auto @@ -306,4 +305,6 @@ class SpeedLimitController: self.state_control() self.update_events(events_sp) + self.frame += 1 + return self.final_cruise_speed From a6ea4e31b49a01df785933d9e447ce4532c3f80f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 23:44:20 -0400 Subject: [PATCH 065/188] split speed limit resolver out of slc --- .../controls/lib/longitudinal_planner.py | 20 ++++++++++++++++++- .../speed_limit_controller.py | 14 ++++++------- .../speed_limit_resolver.py | 4 +--- .../tests/test_speed_limit_resolver.py | 8 ++++---- 4 files changed, 30 insertions(+), 16 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 97dac727c1..56b52f6fef 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -7,8 +7,12 @@ See the LICENSE.md file in the root directory for more details. from cereal import messaging, custom from opendbc.car import structs +from openpilot.common.params import Params +from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.sunnypilot.models.helpers import get_active_bundle @@ -18,6 +22,11 @@ DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimen class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.events_sp = EventsSP() + self.params = Params() + self.frame = -1 + + self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) + self.resolver = SpeedLimitResolver(self.policy) self.dec = DynamicExperimentalController(CP, mpc) self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None @@ -28,6 +37,11 @@ class LongitudinalPlannerSP: # If we don't have a generation set, we assume it's default model. Which as of today are mlsim. return bool(self.generation is None or self.generation >= 11) + def update_params(self): + if self.frame % int(3. / DT_MDL) == 0: + self.policy = Policy(self.params.get("SpeedLimitControlPolicy", return_default=True)) + self.resolver.change_policy(self.policy) + def get_mpc_mode(self) -> str | None: if not self.dec.active(): return None @@ -37,7 +51,9 @@ class LongitudinalPlannerSP: def update_v_cruise(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> float: self.events_sp.clear() - v_cruise_slc = self.slc.update(sm, v_ego, a_ego, v_cruise, self.events_sp) + # Speed Limit Control + _speed_limit, _distance, _source = self.resolver.resolve(v_ego, sm) + v_cruise_slc = self.slc.update(sm, v_ego, a_ego, v_cruise, _speed_limit, _distance, _source, self.events_sp) v_cruise_final = min(v_cruise, v_cruise_slc) @@ -46,6 +62,8 @@ class LongitudinalPlannerSP: def update(self, sm: messaging.SubMaster) -> None: self.dec.update(sm) + self.frame += 1 + def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None: plan_sp_send = messaging.new_message('longitudinalPlanSP') diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 655fa36160..63c1df90c8 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -16,9 +16,8 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import L PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_CRUISE_SPEED, \ CRUISE_SPEED_TOLERANCE from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Policy, Engage, OffsetType +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Engage, OffsetType from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.helpers import description_for_state, debug -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.selfdrive.modeld.constants import ModelConstants @@ -39,8 +38,6 @@ class SpeedLimitController: def __init__(self, CP): self.params = Params() self.CP = CP - self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) - self.resolver = SpeedLimitResolver(self.policy) self.frame = -1 self.last_op_engaged_time = 0.0 self.is_metric = self.params.get_bool("IsMetric") @@ -173,10 +170,8 @@ class SpeedLimitController: self.warning_type = self.params.get("SpeedLimitWarningType", return_default=True) self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) self.warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) - self.policy = Policy(self.params.get("SpeedLimitControlPolicy", return_default=True)) self.is_metric = self.params.get_bool("IsMetric") self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH - self.resolver.change_policy(self.policy) self.engage_type = self.read_engage_type_param() @staticmethod @@ -294,11 +289,14 @@ class SpeedLimitController: elif self.speed_limit_changed: events_sp.add(EventNameSP.speedLimitValueChange) - def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, events_sp: EventsSP) -> float: + def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, + speed_limit: float, distance: float, source: Source, events_sp: EventsSP) -> float: self.op_engaged = sm['carControl'].longActive self.current_time = time.monotonic() - self._speed_limit, self._distance, self._source = self.resolver.resolve(v_ego, self._speed_limit, sm) + self._speed_limit = speed_limit + self._distance = distance + self._source = source self.update_params() self.update_calculations(v_ego, a_ego, v_cruise_setpoint) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index 43f78087b2..887f419bf4 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -13,7 +13,6 @@ class SpeedLimitResolver: _limit_solutions: dict[Source, float] # Store for speed limit solutions from different sources _distance_solutions: dict[Source, float] # Store for distance to current speed limit start for different sources _v_ego: float - _current_speed_limit: float def __init__(self, policy: Policy): self._gps_location_service = get_gps_location_service(Params()) @@ -38,9 +37,8 @@ class SpeedLimitResolver: self._limit_solutions[source] = 0. self._distance_solutions[source] = 0. - def resolve(self, v_ego: float, current_speed_limit: float, sm: messaging.SubMaster) -> tuple[float, float, Source]: + def resolve(self, v_ego: float, sm: messaging.SubMaster) -> tuple[float, float, Source]: self._v_ego = v_ego - self._current_speed_limit = current_speed_limit self._resolve_limit_sources(sm) return self._consolidate() diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py index 62a9329958..457439022a 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py @@ -79,7 +79,7 @@ class TestSpeedLimitResolverValidation: source_speed_limit = sm_mock[sm_key].speedLimit # Assert the resolver - speed_limit, _, source = resolver.resolve(source_speed_limit, 0, sm_mock) + speed_limit, _, source = resolver.resolve(source_speed_limit, sm_mock) assert speed_limit == source_speed_limit assert source == Source[function_key] @@ -92,7 +92,7 @@ class TestSpeedLimitResolverValidation: socket_to_source.keys()), key=lambda x: x[1]) # Assert the resolver - speed_limit, _, source = resolver.resolve(minimum_speed_limit, 0, sm_mock) + speed_limit, _, source = resolver.resolve(minimum_speed_limit, sm_mock) assert speed_limit == minimum_speed_limit assert source == socket_to_source[minimum_key] @@ -103,7 +103,7 @@ class TestSpeedLimitResolverValidation: source_speed_limit = sm_mock[sm_key].speedLimit # Assert the parsing - speed_limit, _, source = resolver.resolve(source_speed_limit, 0, sm_mock) + speed_limit, _, source = resolver.resolve(source_speed_limit, sm_mock) assert resolver._limit_solutions[Source[function_key]] == source_speed_limit assert resolver._distance_solutions[Source[function_key]] == 0. @@ -113,7 +113,7 @@ class TestSpeedLimitResolverValidation: resolver = resolver_class(policy) sm_mock = setup_sm_mock(mocker) - _speed_limit, _distance, _source = resolver.resolve(v_ego, 0, sm_mock) + _speed_limit, _distance, _source = resolver.resolve(v_ego, sm_mock) # After resolution assert _speed_limit is not None From 2bd87ff6a0f70230dd99d9f450589764f224772d Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 30 Aug 2025 23:48:43 -0400 Subject: [PATCH 066/188] no need to pass sm --- sunnypilot/selfdrive/controls/lib/longitudinal_planner.py | 2 +- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 56b52f6fef..9224f9f358 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -53,7 +53,7 @@ class LongitudinalPlannerSP: # Speed Limit Control _speed_limit, _distance, _source = self.resolver.resolve(v_ego, sm) - v_cruise_slc = self.slc.update(sm, v_ego, a_ego, v_cruise, _speed_limit, _distance, _source, self.events_sp) + v_cruise_slc = self.slc.update(sm['carControl'].longActive, v_ego, a_ego, v_cruise, _speed_limit, _distance, _source, self.events_sp) v_cruise_final = min(v_cruise, v_cruise_slc) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 63c1df90c8..1d6b03caf3 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -289,9 +289,9 @@ class SpeedLimitController: elif self.speed_limit_changed: events_sp.add(EventNameSP.speedLimitValueChange) - def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise_setpoint: float, + def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, speed_limit: float, distance: float, source: Source, events_sp: EventsSP) -> float: - self.op_engaged = sm['carControl'].longActive + self.op_engaged = long_active self.current_time = time.monotonic() self._speed_limit = speed_limit From 791f597bf6854d82d35fa8e92f91f3dbd6b0dd69 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 00:05:12 -0400 Subject: [PATCH 067/188] fix params --- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 1d6b03caf3..33fc6a45d4 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -61,7 +61,7 @@ class SpeedLimitController: self._state_prev = SpeedLimitControlState.inactive self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise - self.offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) + self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) self.warning_type = self.params.get("SpeedLimitWarningType", return_default=True) self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) @@ -165,7 +165,7 @@ class SpeedLimitController: def update_params(self) -> None: if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0: self.enabled = self.params.get_bool("SpeedLimitControl") - self.offset_type = OffsetType(self.params.get("SpeedLimitWarningValueOffset", return_default=True)) + self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) self.warning_type = self.params.get("SpeedLimitWarningType", return_default=True) self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) From 27fe7aa83fcd4e715e98680dca2879d218cbe776 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 00:06:07 -0400 Subject: [PATCH 068/188] test init --- .../tests/test_speed_limit_controller.py | 52 +++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py new file mode 100644 index 0000000000..849c157183 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -0,0 +1,52 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import numpy as np + +from opendbc.car.car_helpers import interfaces +from opendbc.car.toyota.values import CAR as TOYOTA +from openpilot.common.constants import CV +from openpilot.common.params import Params +from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET +from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController +from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP + + +class TestSpeedLimitController: + + def _setup_platform(self, car_name): + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + + sunnypilot_interfaces.setup_interfaces(CI, self.params) + + return CI + + def setup_method(self): + self.params = Params() + self.reset_custom_params() + self.events_sp = EventsSP() + CI = self._setup_platform(TOYOTA.TOYOTA_RAV4_TSS2_2022) + self.slc = SpeedLimitController(CI.CP) + + def reset_custom_params(self): + self.params.put_bool("SpeedLimitControl", False) + self.params.put_bool("IsMetric", False) + self.params.put("SpeedLimitOffsetType", 0) + self.params.put("SpeedLimitValueOffset", 0) + self.params.put("SpeedLimitWarningType", 0) + self.params.put("SpeedLimitWarningOffsetType", 0) + self.params.put("SpeedLimitWarningValueOffset", 0) + + def test_disabled(self): + for v_ego in np.linspace(0, 100, 101): + for source in (Source.car_state, Source.map_data): + v_cruise_slc = self.slc.update(True, v_ego, 0, 50 * CV.MS_TO_MPH, 50 * CV.MS_TO_MPH, 0, source, self.events_sp) + assert v_cruise_slc == V_CRUISE_UNSET From e454ca42c966d0f1c818ec9b5153047b0f3ecf78 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 00:14:57 -0400 Subject: [PATCH 069/188] use frame instead of time --- .../speed_limit_controller/speed_limit_controller.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 33fc6a45d4..c06f90b46d 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -5,9 +5,8 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ import numpy as np -import time -from cereal import messaging, custom +from cereal import custom from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL @@ -39,7 +38,7 @@ class SpeedLimitController: self.params = Params() self.CP = CP self.frame = -1 - self.last_op_engaged_time = 0.0 + self.last_op_engaged_frame = 0.0 self.is_metric = self.params.get_bool("IsMetric") self.enabled = self.params.get_bool("SpeedLimitControl") self.op_engaged = False @@ -67,7 +66,6 @@ class SpeedLimitController: self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) self.warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) self.engage_type = self.read_engage_type_param() - self.current_time = 0. self.v_cruise_rounded = 0. self.v_cruise_prev_rounded = 0. self.speed_limit_offsetted_rounded = 0. @@ -205,7 +203,7 @@ class SpeedLimitController: # cause a temp inactive transition if the controller is updated before controlsd sets actual cruise # speed. if not self.op_engaged_prev and self.op_engaged: - self.last_op_engaged_time = self.current_time + self.last_op_engaged_frame = self.frame # Update change tracking variables self.speed_limit_changed = self._speed_limit != self.speed_limit_prev @@ -234,7 +232,7 @@ class SpeedLimitController: self._state = SpeedLimitControlState.active else: self._state = SpeedLimitControlState.pending - elif self.current_time > (self.last_op_engaged_time + PRE_ACTIVE_GUARD_PERIOD): + elif (self.frame - self.last_op_engaged_frame) * DT_MDL > PRE_ACTIVE_GUARD_PERIOD: # # If the initial max set speed isn't reached within the allocated period, permanently disable for this session self._state = SpeedLimitControlState.inactive @@ -292,7 +290,6 @@ class SpeedLimitController: def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, speed_limit: float, distance: float, source: Source, events_sp: EventsSP) -> float: self.op_engaged = long_active - self.current_time = time.monotonic() self._speed_limit = speed_limit self._distance = distance From 0ff8e3be3c39d17366fbf8d76690f5d6f88e4a95 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 00:32:11 -0400 Subject: [PATCH 070/188] track session --- .../speed_limit_controller/speed_limit_controller.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index c06f90b46d..b419ac3f88 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -58,6 +58,7 @@ class SpeedLimitController: self._source = Source.none self._state = SpeedLimitControlState.inactive self._state_prev = SpeedLimitControlState.inactive + self._session_ended = False self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) @@ -219,8 +220,9 @@ class SpeedLimitController: int(round((self._speed_limit + self.speed_limit_warning_offset) * self.speed_factor)) def transition_state_from_inactive(self) -> None: - self._state = SpeedLimitControlState.preActive - self.initial_max_set = False + if not self._session_ended: + self._state = SpeedLimitControlState.preActive + self.initial_max_set = False def transition_state_from_preactive(self) -> None: if self.initial_max_set_confirmed(): @@ -232,9 +234,10 @@ class SpeedLimitController: self._state = SpeedLimitControlState.active else: self._state = SpeedLimitControlState.pending - elif (self.frame - self.last_op_engaged_frame) * DT_MDL > PRE_ACTIVE_GUARD_PERIOD: + elif (self.frame - self.last_op_engaged_frame) * DT_MDL > PRE_ACTIVE_GUARD_PERIOD and not self._session_ended: # # If the initial max set speed isn't reached within the allocated period, permanently disable for this session self._state = SpeedLimitControlState.inactive + self._session_ended = True def transition_state_from_pending(self) -> None: if self._speed_limit > 0: @@ -258,10 +261,11 @@ class SpeedLimitController: def state_control(self) -> None: self._state_prev = self._state - # If op is disabled or SLC is disabled, go inactive + # If op is disabled or SLC is disabled, go inactive and reset session tracker if not self.op_engaged or not self.enabled: self._state = SpeedLimitControlState.inactive self.initial_max_set = False + self._session_ended = False return self._state_transition_strategy[self._state]() From 0f2db833d5ee096f0650ca0fc2ccc7d1a0922277 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 01:05:26 -0400 Subject: [PATCH 071/188] some tests --- .../lib/speed_limit_controller/__init__.py | 2 +- .../speed_limit_controller.py | 4 +-- .../tests/test_speed_limit_controller.py | 35 ++++++++++++++++--- 3 files changed, 34 insertions(+), 7 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py index ecde484372..9b31b07d24 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py @@ -19,6 +19,6 @@ LIMIT_SPEED_OFFSET_TH = -1. # m/s Maximum offset between speed limit and curren LIMIT_MAX_MAP_DATA_AGE = 10. # s Maximum time to hold to map data, then consider it invalid inside limits controllers. # Speed Limit Control Auto mode constants -REQUIRED_INITIAL_CRUISE_SPEED = 35.7632 # m/s 80 MPH # TODO-SP: customizable with params +REQUIRED_INITIAL_MAX_SET_SPEED = 35.7632 # m/s 80 MPH # TODO-SP: customizable with params CRUISE_SPEED_TOLERANCE = 0.44704 # m/s ±1 MPH tolerance # TODO-SP: metric vs imperial FALLBACK_CRUISE_SPEED = 255.0 # m/s fallback when no speed limit available diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index b419ac3f88..91d3da83df 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -12,7 +12,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V, \ - PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_CRUISE_SPEED, \ + PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, \ CRUISE_SPEED_TOLERANCE from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Engage, OffsetType @@ -178,7 +178,7 @@ class SpeedLimitController: return Engage.auto def initial_max_set_confirmed(self) -> bool: - return abs(self.v_cruise_setpoint - REQUIRED_INITIAL_CRUISE_SPEED) <= CRUISE_SPEED_TOLERANCE + return abs(self.v_cruise_setpoint - REQUIRED_INITIAL_MAX_SET_SPEED) <= CRUISE_SPEED_TOLERANCE def detect_manual_cruise_change(self) -> bool: if not self.is_active: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 849c157183..cce3406802 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -10,10 +10,12 @@ from opendbc.car.car_helpers import interfaces from opendbc.car.toyota.values import CAR as TOYOTA from openpilot.common.constants import CV from openpilot.common.params import Params +from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitControlState, REQUIRED_INITIAL_MAX_SET_SPEED +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP @@ -29,6 +31,9 @@ class TestSpeedLimitController: return CI + def reset_state(self): + self.slc.frame = -1 + def setup_method(self): self.params = Params() self.reset_custom_params() @@ -37,7 +42,7 @@ class TestSpeedLimitController: self.slc = SpeedLimitController(CI.CP) def reset_custom_params(self): - self.params.put_bool("SpeedLimitControl", False) + self.params.put_bool("SpeedLimitControl", True) self.params.put_bool("IsMetric", False) self.params.put("SpeedLimitOffsetType", 0) self.params.put("SpeedLimitValueOffset", 0) @@ -46,7 +51,29 @@ class TestSpeedLimitController: self.params.put("SpeedLimitWarningValueOffset", 0) def test_disabled(self): + self.params.put_bool("SpeedLimitControl", False) for v_ego in np.linspace(0, 100, 101): - for source in (Source.car_state, Source.map_data): - v_cruise_slc = self.slc.update(True, v_ego, 0, 50 * CV.MS_TO_MPH, 50 * CV.MS_TO_MPH, 0, source, self.events_sp) + for _ in range(int(10. / DT_MDL)): + v_cruise_slc = self.slc.update(True, v_ego, 0, 50 * CV.MPH_TO_MS, 50 * CV.MPH_TO_MS, 0, Source.none, self.events_sp) assert v_cruise_slc == V_CRUISE_UNSET + assert self.slc.state == SpeedLimitControlState.inactive + + def test_no_speed_limit(self): + for v_ego in np.linspace(0, 100, 101): + for _ in range(int(10. / DT_MDL)): + v_cruise_slc = self.slc.update(True, v_ego, 0, 50 * CV.MPH_TO_MS, 0, 0, Source.none, self.events_sp) + assert v_cruise_slc == V_CRUISE_UNSET + assert self.slc.state not in ACTIVE_STATES + + def test_speed_limit_at_initial_max_set_speed(self): + v_cruise_slc = V_CRUISE_UNSET + speed_limit = 50 * CV.MPH_TO_MS + offset = 0 + + for source in (Source.car_state, Source.map_data): + self.reset_state() + for _ in range(int(2. / DT_MDL)): + v_cruise_slc = self.slc.update(True, 40 * CV.MPH_TO_MS, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, source, self.events_sp) + offset = self.slc.get_offset(self.slc.offset_type, self.slc.offset_value) + assert self.slc.state in ACTIVE_STATES + assert v_cruise_slc == speed_limit + offset From d9b11dec9a2046965d2afcee1e1728f23754f72f Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 01:07:43 -0400 Subject: [PATCH 072/188] too limiting --- .../lib/speed_limit_controller/speed_limit_controller.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 91d3da83df..2c4a668388 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -181,9 +181,6 @@ class SpeedLimitController: return abs(self.v_cruise_setpoint - REQUIRED_INITIAL_MAX_SET_SPEED) <= CRUISE_SPEED_TOLERANCE def detect_manual_cruise_change(self) -> bool: - if not self.is_active: - return False - # If cruise speed changed and it's not what SLC would set if self.v_cruise_setpoint_changed: expected_cruise = self.speed_limit_offseted @@ -220,7 +217,8 @@ class SpeedLimitController: int(round((self._speed_limit + self.speed_limit_warning_offset) * self.speed_factor)) def transition_state_from_inactive(self) -> None: - if not self._session_ended: + # if new session, wait for + if (self.frame - self.last_op_engaged_frame) * DT_MDL > 2. and not self._session_ended: self._state = SpeedLimitControlState.preActive self.initial_max_set = False From 20ff0b8d7ba8c02555671a3de6e7c64abf73f990 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 01:19:22 -0400 Subject: [PATCH 073/188] bump --- .../lib/speed_limit_controller/speed_limit_controller.py | 2 +- .../speed_limit_controller/tests/test_speed_limit_controller.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 2c4a668388..ed69401fd5 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -217,7 +217,7 @@ class SpeedLimitController: int(round((self._speed_limit + self.speed_limit_warning_offset) * self.speed_factor)) def transition_state_from_inactive(self) -> None: - # if new session, wait for + # if it's a new session, wait for 2 seconds after long engaged before transitioning ot preActive if (self.frame - self.last_op_engaged_frame) * DT_MDL > 2. and not self._session_ended: self._state = SpeedLimitControlState.preActive self.initial_max_set = False diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index cce3406802..d0b7414550 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -72,7 +72,7 @@ class TestSpeedLimitController: for source in (Source.car_state, Source.map_data): self.reset_state() - for _ in range(int(2. / DT_MDL)): + for _ in range(int(10. / DT_MDL)): v_cruise_slc = self.slc.update(True, 40 * CV.MPH_TO_MS, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, source, self.events_sp) offset = self.slc.get_offset(self.slc.offset_type, self.slc.offset_value) assert self.slc.state in ACTIVE_STATES From 5d2fc14a24e0510cc0d7e293643f979ab6f523da Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 01:24:13 -0400 Subject: [PATCH 074/188] always reset state --- .../speed_limit_controller/tests/test_speed_limit_controller.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index d0b7414550..679029c69a 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -32,6 +32,7 @@ class TestSpeedLimitController: return CI def reset_state(self): + self.slc.state = SpeedLimitControlState.inactive self.slc.frame = -1 def setup_method(self): From 4eab2b01e491a81f05b1db1f43cadb2679bad9f8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 01:27:19 -0400 Subject: [PATCH 075/188] end session if long_active but slc inactive at any given time --- .../lib/speed_limit_controller/speed_limit_controller.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index ed69401fd5..b9bd6dfb74 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -247,12 +247,14 @@ class SpeedLimitController: def transition_state_from_adapting(self) -> None: if self.detect_manual_cruise_change(): self._state = SpeedLimitControlState.inactive + self._session_ended = True elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: self._state = SpeedLimitControlState.active def transition_state_from_active(self) -> None: if self.detect_manual_cruise_change(): self._state = SpeedLimitControlState.inactive + self._session_ended = True elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: self._state = SpeedLimitControlState.adapting From 3f3c29355993be02ab081efaacf3d5b802bff58a Mon Sep 17 00:00:00 2001 From: Michael Date: Sat, 30 Aug 2025 22:35:14 -0700 Subject: [PATCH 076/188] reeadme: Reorder tables to show -new branches first (#1191) * Update to Readme.MD install instructions This commit changes a few things in the installation guide. I moved the the tables that have the branches and install URLs. I also added a TIP that let's users know that they can install with sunnypilot/staging-c3-new. * Update README.md with proposed changes from DevTek Added header text over the legacy branches to bring in separation and let users know they are not recommended. * bit of change * Reorganizing a bit more --------- Co-authored-by: DevTekVE --- README.md | 47 ++++++++++++++++++++++++++++++----------------- 1 file changed, 30 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 805c4b73d8..7e686cef5c 100644 --- a/README.md +++ b/README.md @@ -22,34 +22,24 @@ https://docs.sunnypilot.ai/ is your one stop shop for everything from features t Detailed instructions for [how to mount the device in a car](https://comma.ai/setup). ## Installation -Please refer to [Recommended Branches](#-recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `release-c3` branch. +Please refer to [Recommended Branches](#-recommended-branches) to find your preferred/supported branch. This guide will assume you want to install the latest `staging-c3-new` branch. + +### If you want to use our newest branches (our rewrite) +> [!TIP] +>You can see the rewrite state on our [rewrite project board](https://github.com/orgs/sunnypilot/projects/2), and to install the new branches, you can use the following links * sunnypilot not installed or you installed a version before 0.8.17? 1. [Factory reset/uninstall](https://github.com/commaai/openpilot/wiki/FAQ#how-can-i-reset-the-device) the previous software if you have another software/fork installed. 2. After factory reset/uninstall and upon reboot, select `Custom Software` when given the option. - 3. Input the installation URL per [Recommended Branches](#-recommended-branches). Example: ```release-c3.sunnypilot.ai```. + 3. Input the installation URL per [Recommended Branches](#-recommended-branches). Example: ```https://staging-c3-new.sunnypilot.ai```. 4. Complete the rest of the installation following the onscreen instructions. * sunnypilot already installed and you installed a version after 0.8.17? 1. On the comma three, go to `Settings` â–¶ï¸ `Software`. 2. At the `Download` option, press `CHECK`. This will fetch the list of latest branches from sunnypilot. 3. At the `Target Branch` option, press `SELECT` to open the Target Branch selector. - 4. Scroll to select the desired branch per Recommended Branches (see below). Example: `release-c3` + 4. Scroll to select the desired branch per Recommended Branches (see below). Example: `staging-c3-new` -| Branch | Installation URL | -|:------------:|:--------------------------------:| -| `release-c3` | https://release-c3.sunnypilot.ai | -| `staging-c3` | https://staging-c3.sunnypilot.ai | -| `dev-c3` | https://dev-c3.sunnypilot.ai | - -### If you want to use our newest branches (our rewrite) -> [!TIP] ->You can see the rewrite state on our [rewrite project board](https://github.com/orgs/sunnypilot/projects/2), and to install the new branches, you can use the following links - - -> [!IMPORTANT] -> It is recommended to [re-flash AGNOS](https://flash.comma.ai/) if you intend to downgrade from the new branches. -> You can still restore the latest sunnylink backup made on the old branches. | Branch | Installation URL | |:----------------:|:---------------------------------------------:| @@ -59,8 +49,31 @@ Please refer to [Recommended Branches](#-recommended-branches) to find your pref | `release-c3-new` | **Not yet available**. | > [!TIP] +> You can use sunnypilot/targetbranch as an install URL. Example: 'sunnypilot/staging-c3-new'. + +> [!NOTE] > Do you require further assistance with software installation? Join the [sunnypilot Discord server](https://discord.sunnypilot.com) and message us in the `#installation-help` channel. + +
+ +Older legacy branches + +### If you want to use our older legacy branches (*not recommended*) + +> [**IMPORTANT**] +> It is recommended to [re-flash AGNOS](https://flash.comma.ai/) if you intend to downgrade from the new branches. +> You can still restore the latest sunnylink backup made on the old branches. + +| Branch | Installation URL | +|:------------:|:--------------------------------:| +| `release-c3` | https://release-c3.sunnypilot.ai | +| `staging-c3` | https://staging-c3.sunnypilot.ai | +| `dev-c3` | https://dev-c3.sunnypilot.ai | + +
+ + ## 🎆 Pull Requests We welcome both pull requests and issues on GitHub. Bug fixes are encouraged. From 45ee58b1f6bc1ca2462924e9aadf120a9d6edad1 Mon Sep 17 00:00:00 2001 From: Nayan Date: Sun, 31 Aug 2025 02:27:08 -0400 Subject: [PATCH 077/188] =?UTF-8?q?ui:=20Favorite=20Models=20=E2=AD=90=20(?= =?UTF-8?q?#1168)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * init model favorites * fix fav buttons * fix blank favs * switch to ref * new favs at top * remove debug prints & add some comments * button style * fix current selection * !@%#$%(@^%$#(@!%#^ * add last update date to folders --- common/params_keys.h | 1 + selfdrive/ui/qt/widgets/input.cc | 171 ++++++++++++++++-- selfdrive/ui/qt/widgets/input.h | 26 ++- .../qt/offroad/settings/models_panel.cc | 62 ++++--- .../qt/offroad/settings/models_panel.h | 1 + sunnypilot/models/fetcher.py | 1 + .../selfdrive/assets/icons/star-empty.png | 3 + .../selfdrive/assets/icons/star-filled.png | 3 + 8 files changed, 224 insertions(+), 44 deletions(-) create mode 100644 sunnypilot/selfdrive/assets/icons/star-empty.png create mode 100644 sunnypilot/selfdrive/assets/icons/star-filled.png diff --git a/common/params_keys.h b/common/params_keys.h index 01f6f04680..4a78575de6 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -167,6 +167,7 @@ inline static std::unordered_map keys = { {"ModelManager_ActiveBundle", {PERSISTENT, JSON}}, {"ModelManager_ClearCache", {CLEAR_ON_MANAGER_START, BOOL}}, {"ModelManager_DownloadIndex", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, INT, "0"}}, + {"ModelManager_Favs", {PERSISTENT | BACKUP, STRING}}, {"ModelManager_LastSyncTime", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, INT, "0"}}, {"ModelManager_ModelsCache", {PERSISTENT | BACKUP, JSON}}, diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index 4f330dca8d..efd330587a 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -336,8 +336,8 @@ QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStrin return ""; } -TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList> &items, - const QString ¤t, QWidget *parent) : DialogBase(parent) { +TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList &items, + const QString ¤t, const QString &favParam, QWidget *parent) : DialogBase(parent) { QFrame *container = new QFrame(this); container->setStyleSheet(R"( QFrame { background-color: #1B1B1B; } @@ -375,6 +375,9 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QListaddWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); main_layout->addSpacing(25); + iconBlank = QIcon("../../sunnypilot/selfdrive/assets/icons/star-empty.png"); + iconFilled = QIcon ("../../sunnypilot/selfdrive/assets/icons/star-filled.png"); + treeWidget = new QTreeWidget(this); treeWidget->setHeaderHidden(true); treeWidget->setIndentation(50); @@ -396,34 +399,49 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QListviewport(), QScroller::LeftMouseButtonGesture); + // Create initial list of favorites from param + const QString favs = QString::fromStdString(params.get(favParam.toStdString())); + mapFavs = new QMap>(); + favRefs = new QStringList(favs.split(";")); + for (const QString &item : *favRefs) + { + mapFavs->insert( item, {}); + } + // Populate tree - QListIterator> iter(items); + QListIterator iter(items); while (iter.hasNext()) { - QPair currItem = iter.next(); - if (currItem.first.isEmpty()) { - for (const QString &item : currItem.second) { + TreeFolder currItem = iter.next(); + QString prevFolder; + QString currentFolder; + if (currItem.folder.isEmpty()) { + for (const TreeNode &item : currItem.items) { QTreeWidgetItem *topLevel = new QTreeWidgetItem(); - topLevel->setText(0, item); + topLevel->setText(0, item.displayName); + topLevel->setData(0, Qt::UserRole, item.ref); topLevel->setFlags(topLevel->flags() | Qt::ItemIsSelectable); treeWidget->addTopLevelItem(topLevel); - if (item == current) { + if (item.ref == current) { topLevel->setSelected(true); } } } else { - QTreeWidgetItem *folderItem = new QTreeWidgetItem(treeWidget); + QList folders = treeWidget->findItems(currItem.folder, Qt::MatchExactly, 0); + QTreeWidgetItem *folderItem = nullptr; + if (folders.isEmpty()) { + folderItem = new QTreeWidgetItem(treeWidget); + } else { + folderItem = folders.first(); + } folderItem->setIcon(0, QIcon(QPixmap("../assets/icons/menu.png"))); - folderItem->setText(0, " " + currItem.first); + folderItem->setText(0, " " + currItem.folder); folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate); folderItem->setFlags(folderItem->flags() & ~Qt::ItemIsSelectable); - for (const QString &item : currItem.second) + for (const TreeNode item : currItem.items) { - QTreeWidgetItem *childItem = new QTreeWidgetItem(folderItem); - childItem->setText(0, item); - childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable); - - if (item == current) { + QTreeWidgetItem *childItem = addChildItem(item.displayName, item.ref, folderItem); + if (item.ref == current) { childItem->setSelected(true); folderItem->setExpanded(true); } @@ -431,6 +449,39 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QListsetIcon(0, QIcon(QPixmap("../assets/icons/menu.png"))); + favorites->setText(0, " " + tr("Favorites")); + favorites->setFlags(favorites->flags() | Qt::ItemIsAutoTristate); + favorites->setFlags(favorites->flags() & ~Qt::ItemIsSelectable); + treeWidget->insertTopLevelItem(1, favorites); + + // Create favorite nodes + for (int i = favRefs->size() - 1; i >= 0; --i) { + QString item = favRefs->at(i); + if (item.isEmpty()) continue; + + QTreeWidgetItemIterator treeIt(treeWidget); + QTreeWidgetItem *nodeItem = nullptr; + while (*treeIt) { + if (item == (*treeIt)->data(0, Qt::UserRole).toString()) { + nodeItem = (*treeIt); + break; + } + ++treeIt; + } + if (nodeItem == nullptr) continue; + + QTreeWidgetItem *childItem = addChildItem(nodeItem->text(0), + nodeItem->data(0, Qt::UserRole).toString(), favorites); + if (item == current) { + treeWidget->collapseAll(); + childItem->setSelected(true); + favorites->setExpanded(true); + } + } + confirm_btn = new QPushButton(tr("Select")); confirm_btn->setObjectName("confirm_btn"); confirm_btn->setEnabled(false); @@ -438,7 +489,7 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QList selectedItems = treeWidget->selectedItems(); if (!selectedItems.isEmpty()) { - selection = selectedItems.first()->text(0); + selection = selectedItems.first()->data(0, Qt::UserRole).toString(); confirm_btn->setEnabled(selection != current); } }); @@ -465,11 +516,91 @@ TreeOptionDialog::TreeOptionDialog(const QString &prompt_text, const QListaddWidget(container); } -QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList> &items, - const QString ¤t, QWidget *parent) { - TreeOptionDialog d(prompt_text, items, current, parent); +QString TreeOptionDialog::getSelection(const QString &prompt_text, const QList &items, + const QString ¤t, const QString &favParam, QWidget *parent) { + TreeOptionDialog d(prompt_text, items, current, favParam, parent); if (d.exec()) { return d.selection; } return ""; } + +/** + * Handles the addition or removal of items from the "favorites" list based on the provided reference identifier. + * + * @param displayName The text label associated with the item to be added or removed in the favorites. + * @param ref A unique reference key identifying the item. + * @param btn A pointer to the QPushButton associated with the item. The button's icon is updated to indicate + * whether the item is currently favorited or not. + * + * If the item is already in the favorites, it is removed from the list, its associated buttons have their + * icons reset, and the favorites tree is updated accordingly. If the item is not in the favorites, it is + * added to the list, a new associated button is created, and the favorites tree is updated. The current + * state of the favorites is stored in the Params object as a semicolon-separated string. + */ +void TreeOptionDialog::handleFavorites(const QString &displayName, const QString &ref, QPushButton *btn) { + if (mapFavs->keys().contains(ref)) { // Remove from favorites + for (auto *itemBtn:mapFavs->value(ref)) + { + itemBtn->setIcon(iconBlank); + } + mapFavs->remove(ref); + favRefs->removeAll(ref); + for (int i = 0; i < favorites->childCount(); ++i) { + QTreeWidgetItem* child = favorites->child(i); + if (child && child->data(0, Qt::UserRole).toString() == ref) { + favorites->removeChild(child); + } + } + } else { // Add to favorites + QPushButton *favBtn = new QPushButton(); + btn->setIcon(iconFilled); + mapFavs->insert(ref, {btn, favBtn}); + favRefs->append(ref); + addChildItem(displayName, ref, favorites, favBtn, true); + } + + const QString favs =favRefs->join(";"); + params.put("ModelManager_Favs", favs.toStdString()); +} + +/** + * Adds a child item to a given folder item within the QTreeWidget. + * + * @param displayName The text to display for the child item. + * @param ref A reference string that uniquely identifies the child item. + * @param folderItem The parent folder item to which the child item will be added. + * @param btn A pointer to a QPushButton associated with the child item. If nullptr, a new button will be created. + * @param addAtTop If true, the child item is added as the first child of the folder item; otherwise, it is appended to the end. + * @return A pointer to the created QTreeWidgetItem representing the child item. + */ +QTreeWidgetItem* TreeOptionDialog::addChildItem(const QString &displayName, const QString &ref, QTreeWidgetItem *folderItem, QPushButton *btn, bool addAtTop) { + QTreeWidgetItem *childItem = new QTreeWidgetItem(); + if (btn == nullptr) { + btn = new QPushButton(); + } + if (mapFavs->keys().contains(ref)) { + btn->setIcon(iconFilled); + (*mapFavs)[ref].append(btn); + } else { + btn->setIcon(iconBlank); + } + btn->setIconSize(QSize(100, 100)); + QWidget *buttonContainer = new QWidget(); + QHBoxLayout *layout = new QHBoxLayout(buttonContainer); + layout->addWidget(btn, 0, Qt::AlignRight); + childItem->setText(0, displayName); + childItem->setData(0, Qt::UserRole, ref); + childItem->setFlags(childItem->flags() | Qt::ItemIsSelectable); + if (addAtTop) { + folderItem->insertChild(0, childItem); + } else { + folderItem->addChild(childItem); + } + treeWidget->setItemWidget(childItem, 0, buttonContainer); + + connect(btn, &QPushButton::clicked, btn, [=]() { + handleFavorites(displayName, ref, btn); + }); + return childItem; +} diff --git a/selfdrive/ui/qt/widgets/input.h b/selfdrive/ui/qt/widgets/input.h index 76f87bf32f..3fb1ebfe1a 100644 --- a/selfdrive/ui/qt/widgets/input.h +++ b/selfdrive/ui/qt/widgets/input.h @@ -8,9 +8,22 @@ #include #include +#include "common/params.h" #include "selfdrive/ui/qt/widgets/keyboard.h" +struct TreeNode { + QString folder; + QString displayName; + QString ref; + int index; +}; + +struct TreeFolder { + QString folder; + QList items; +}; + class DialogBase : public QDialog { Q_OBJECT @@ -75,11 +88,20 @@ class TreeOptionDialog : public DialogBase { Q_OBJECT public: - explicit TreeOptionDialog(const QString &prompt_text, const QList> &items, const QString ¤t, QWidget *parent = nullptr); - static QString getSelection(const QString &prompt_text, const QList> &items, const QString ¤t, QWidget *parent = nullptr); + explicit TreeOptionDialog(const QString &prompt_text, const QList &items, const QString ¤t, const QString &favParam, QWidget *parent = nullptr); + static QString getSelection(const QString &prompt_text, const QList &items, const QString ¤t, const QString &favParam, QWidget *parent = nullptr); + void handleFavorites(const QString &displayName, const QString &ref, QPushButton* btn); + QTreeWidgetItem* addChildItem(const QString &displayName, const QString &ref, QTreeWidgetItem* folderItem, QPushButton* btn = nullptr, bool addAtTop = false); QString selection; private: QTreeWidget *treeWidget; QPushButton *confirm_btn; + Params params; + QMap> *mapFavs; + QStringList *favRefs; + QTreeWidgetItem *favorites; + + QIcon iconBlank; + QIcon iconFilled; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index 93e5ac80a8..baba7d3a17 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -259,6 +259,18 @@ QString ModelsPanel::GetActiveModelInternalName() { return DEFAULT_MODEL; } +/** + * @brief Gets the ref of the currently selected model bundle + * @return ref of the selected bundle or default model name + */ +QString ModelsPanel::GetActiveModelRef() { + if (model_manager.hasActiveBundle()) { + return QString::fromStdString(model_manager.getActiveBundle().getRef()); + } + + return DEFAULT_MODEL; +} + void ModelsPanel::updateModelManagerState() { const SubMaster &sm = *(uiStateSP()->sm); model_manager = sm["modelManagerSP"].getModelManagerSP(); @@ -272,34 +284,31 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() { currentModelLblBtn->setEnabled(false); currentModelLblBtn->setValue(tr("Fetching models...")); - struct ModelEntry { - QString folder; - QString displayName; - int index; - }; - QList sortedModels; + QList sortedModels; QSet modelFolders; + QRegularExpression re("\\(([^)]*)\\)[^(]*$"); const auto bundles = model_manager.getAvailableBundles(); for (const auto &bundle : bundles) { auto overrides = bundle.getOverrides(); - QString gen; + QString folder; for (const auto &override : overrides) { if (override.getKey() == "folder") { - gen = QString::fromStdString(override.getValue().cStr()); + folder = QString::fromStdString(override.getValue().cStr()); } } - modelFolders.insert(gen); - sortedModels.append(ModelEntry{ - gen, + modelFolders.insert(folder); + sortedModels.append(TreeNode{ + folder, QString::fromStdString(bundle.getDisplayName()), + QString::fromStdString(bundle.getRef()), static_cast(bundle.getIndex()) }); } std::sort(sortedModels.begin(), sortedModels.end(), - [](const ModelEntry &a, const ModelEntry &b) { + [](const TreeNode &a, const TreeNode &b) { return a.index > b.index; }); @@ -322,37 +331,46 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() { }); // Create the final items list using sorted folders - QList> items; + QList items; for (const auto &folderPair : folderMaxIndices) { - QStringList folderModels; + QList folderModels; + QString folder = folderPair.first; for (const auto &model : sortedModels) { if (model.folder == folderPair.first) { - folderModels.append(model.displayName); + if (model.index == folderPair.second) { + QRegularExpressionMatch match = re.match(model.displayName); + if (match.hasMatch()) { + folder.append(" - (Updated: ").append(match.captured(1)).append(")"); + } + } + folderModels.append(model); } } - items.append(qMakePair(folderPair.first, folderModels)); + items.append(TreeFolder{folder, folderModels}); } - items.insert(0, qMakePair(QString(""), QStringList{DEFAULT_MODEL})); + items.insert(0, TreeFolder{"", { + TreeNode{"", DEFAULT_MODEL, DEFAULT_MODEL, -1} + }}); currentModelLblBtn->setValue(GetActiveModelInternalName()); - const QString selectedBundleName = TreeOptionDialog::getSelection( - tr("Select a Model"), items, GetActiveModelName(), this); + const QString selectedBundleRef = TreeOptionDialog::getSelection( + tr("Select a Model"), items, GetActiveModelRef(), QString("ModelManager_Favs"), this); - if (selectedBundleName.isEmpty() || !canContinueOnMeteredDialog()) { + if (selectedBundleRef.isEmpty() || !canContinueOnMeteredDialog()) { return; } // Handle "Stock" selection differently - if (selectedBundleName == DEFAULT_MODEL) { + if (selectedBundleRef == DEFAULT_MODEL) { params.remove("ModelManager_ActiveBundle"); currentModelLblBtn->setValue(tr("Default")); showResetParamsDialog(); } else { // Find selected bundle and initiate download for (const auto &bundle: bundles) { - if (QString::fromStdString(bundle.getDisplayName()) == selectedBundleName) { + if (QString::fromStdString(bundle.getRef()) == selectedBundleRef) { params.put("ModelManager_DownloadIndex", std::to_string(bundle.getIndex())); if (bundle.getGeneration() != model_manager.getActiveBundle().getGeneration()) { showResetParamsDialog(); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h index 8586d862bd..93edc4de1a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -20,6 +20,7 @@ public: private: QString GetActiveModelName(); QString GetActiveModelInternalName(); + QString GetActiveModelRef(); void updateModelManagerState(); void showEvent(QShowEvent *event) override; diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 60a222a36c..358c65fe34 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -66,6 +66,7 @@ class ModelParser: model_bundle.is20hz = bundle.get("is_20hz", False) model_bundle.minimumSelectorVersion = int(bundle["minimum_selector_version"]) model_bundle.overrides = ModelParser._parse_overrides(bundle.get("overrides", {})) + model_bundle.ref = bundle.get("ref") return model_bundle diff --git a/sunnypilot/selfdrive/assets/icons/star-empty.png b/sunnypilot/selfdrive/assets/icons/star-empty.png new file mode 100644 index 0000000000..bf60dec374 --- /dev/null +++ b/sunnypilot/selfdrive/assets/icons/star-empty.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3731604f80e83a1fdb7c258baf6530b81190eeec82e6443172e84a35b7a74c02 +size 1088 diff --git a/sunnypilot/selfdrive/assets/icons/star-filled.png b/sunnypilot/selfdrive/assets/icons/star-filled.png new file mode 100644 index 0000000000..3667231bf0 --- /dev/null +++ b/sunnypilot/selfdrive/assets/icons/star-filled.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:0c2a513b7f2da004f145b7d689654cc65137f1b146d484fbce7ce727a297b62c +size 861 From 3e9545670b245534327db2762bc61c22b15a5af1 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 31 Aug 2025 08:29:45 +0200 Subject: [PATCH 078/188] feature: Adding support for copyparty (#1116) * feat: add support for copyparty-sfx * feat: add toggle for Copyparty service in developer panel * feat: enhance Copyparty configuration with additional volume mounts and options * Update system/manager/process_config.py * remove f string * lint --- common/params_keys.h | 1 + .../qt/offroad/settings/developer_panel.cc | 4 + .../qt/offroad/settings/developer_panel.h | 1 + system/manager/process_config.py | 14 + third_party/copyparty/copyparty-sfx.py | 512 ++++++++++++++++++ 5 files changed, 532 insertions(+) create mode 100755 third_party/copyparty/copyparty-sfx.py diff --git a/common/params_keys.h b/common/params_keys.h index 4a78575de6..0b08d8c0cb 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -146,6 +146,7 @@ inline static std::unordered_map keys = { {"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}}, {"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}}, {"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}}, + {"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}}, {"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}}, {"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}}, {"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}}, diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc index a4a6bf481c..9c36097f1b 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc @@ -27,6 +27,10 @@ DeveloperPanelSP::DeveloperPanelSP(SettingsWindow *parent) : DeveloperPanel(pare enableGithubRunner = new ParamControlSP("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), "", this, true); addItem(enableGithubRunner); + // Copyparty Toggle + enableCopyparty = new ParamControlSP("EnableCopyparty", tr("Enable Copyparty service"), tr("Copyparty is a very capable file server, you can use it to download your routes, view your logs and even make some edits on some files from your browser. Requires you to connect to your comma locally via it's IP."), "", this, false); + addItem(enableCopyparty); + // Quickboot Mode Toggle prebuiltToggle = new ParamControlSP("QuickBootToggle", tr("Enable Quickboot Mode"), tr(""), "", this, true); addItem(prebuiltToggle); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h index 42b3bd83b8..7f67512b5d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h @@ -16,6 +16,7 @@ public: explicit DeveloperPanelSP(SettingsWindow *parent); private: + ParamControlSP *enableCopyparty; ParamControlSP *enableGithubRunner; ButtonControlSP *errorLogBtn; ParamControlSP *prebuiltToggle; diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 67a77caa24..6130916887 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -65,6 +65,9 @@ def use_github_runner(started, params, CP: car.CarParams) -> bool: return not PC and params.get_bool("EnableGithubRunner") and ( not params.get_bool("NetworkMetered") and not params.get_bool("GithubRunnerSufficientVoltage")) +def use_copyparty(started, params, CP: car.CarParams) -> bool: + return bool(params.get_bool("EnableCopyparty")) + def sunnylink_ready_shim(started, params, CP: car.CarParams) -> bool: """Shim for sunnylink_ready to match the process manager signature.""" return sunnylink_ready(params) @@ -178,4 +181,15 @@ if os.path.exists("./github_runner.sh"): if os.path.exists("../../sunnypilot/sunnylink/uploader.py"): procs += [PythonProcess("sunnylink_uploader", "sunnypilot.sunnylink.uploader", use_sunnylink_uploader_shim)] +if os.path.exists("../../third_party/copyparty/copyparty-sfx.py"): + sunnypilot_root = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..")) + copyparty_args = [f"-v{Paths.crash_log_root()}:/swaglogs:r"] + copyparty_args += [f"-v{Paths.log_root()}:/routes:r"] + copyparty_args += [f"-v{Paths.model_root()}:/models:rw"] + copyparty_args += [f"-v{sunnypilot_root}:/sunnypilot:rw"] + copyparty_args += ["-p8080"] + copyparty_args += ["-z"] + copyparty_args += ["-q"] + procs += [NativeProcess("copyparty-sfx", "third_party/copyparty", ["./copyparty-sfx.py", *copyparty_args], and_(only_offroad, use_copyparty))] + managed_processes = {p.name: p for p in procs} diff --git a/third_party/copyparty/copyparty-sfx.py b/third_party/copyparty/copyparty-sfx.py new file mode 100755 index 0000000000..2506c39a93 --- /dev/null +++ b/third_party/copyparty/copyparty-sfx.py @@ -0,0 +1,512 @@ +#!/usr/bin/env python3 +# coding: latin-1 +from __future__ import print_function, unicode_literals +import re, os, sys, time, shutil, signal, tarfile, hashlib, platform, tempfile, traceback +import subprocess as sp + + +""" +to edit this file, use HxD or "vim -b" + (there is compressed stuff at the end) + +run me with python 2.7 or 3.3+ to unpack and run copyparty + +there's zero binaries! just plaintext python scripts all the way down + so you can easily unpack the archive and inspect it for shady stuff + +the archive data is attached after the b"\n# eof\n" archive marker, + b"?0" decodes to b"\x00" + b"?n" decodes to b"\n" + b"?r" decodes to b"\r" + b"??" decodes to b"?" +""" + + +# set by make-sfx.sh +VER = "1.18.9" +SIZE = 846007 +CKSUM = "53f9b019dbba5e9acb44f1e5" +STAMP = 1754082544 + +PY2 = sys.version_info < (3,) +PY37 = sys.version_info > (3, 7) +WINDOWS = sys.platform in ["win32", "msys"] +sys.dont_write_bytecode = True +me = os.path.abspath(os.path.realpath(__file__)) + + +def eprint(*a, **ka): + ka["file"] = sys.stderr + print(*a, **ka) + + +def msg(*a, **ka): + if a: + a = ["[SFX]", a[0]] + list(a[1:]) + + eprint(*a, **ka) + + +def u8(gen): + try: + for s in gen: + yield s.decode("utf-8", "ignore") + except: + yield s + for s in gen: + yield s + + +def yieldfile(fn): + s = 64 * 1024 + with open(fn, "rb", s * 4) as f: + for block in iter(lambda: f.read(s), b""): + yield block + + +def hashfile(fn): + h = hashlib.sha1() + for block in yieldfile(fn): + h.update(block) + + return h.hexdigest()[:24] + + +def unpack(): + """unpacks the tar yielded by `data`""" + name = "pe-copyparty" + try: + name += "." + str(os.geteuid()) + except: + pass + + tag = "v" + str(STAMP) + top = tempfile.gettempdir() + opj = os.path.join + ofe = os.path.exists + final = opj(top, name) + san = opj(final, "copyparty/up2k.py") + for suf in range(0, 9001): + withpid = "%s.%d.%s" % (name, os.getpid(), suf) + mine = opj(top, withpid) + if not ofe(mine): + break + + tar = opj(mine, "tar") + + try: + if tag in os.listdir(final) and ofe(san): + msg("found early") + return final + except: + pass + + sz = 0 + os.mkdir(mine) + with open(tar, "wb") as f: + for buf in get_payload(): + sz += len(buf) + f.write(buf) + + ck = hashfile(tar) + if ck != CKSUM: + t = "\n\nexpected %s (%d byte)\nobtained %s (%d byte)\nsfx corrupt" + raise Exception(t % (CKSUM, SIZE, ck, sz)) + + with tarfile.open(tar, "r:gz") as tf: + # this is safe against traversal + try: + tf.extractall(mine, filter="tar") + except TypeError: + tf.extractall(mine) + + os.remove(tar) + + with open(opj(mine, tag), "wb") as f: + f.write(b"h\n") + + try: + if tag in os.listdir(final) and ofe(san): + msg("found late") + return final + except: + pass + + try: + if os.path.islink(final): + os.remove(final) + else: + shutil.rmtree(final) + except: + pass + + for fn in u8(os.listdir(top)): + if fn.startswith(name) and fn != withpid: + try: + old = opj(top, fn) + if time.time() - os.path.getmtime(old) > 86400: + shutil.rmtree(old) + except: + pass + + try: + os.symlink(mine, final) + except: + try: + os.rename(mine, final) + return final + except: + msg("reloc fail,", mine) + + return mine + + +def get_payload(): + """yields the binary data attached to script""" + with open(me, "rb") as f: + buf = f.read().rstrip(b"\r\n") + + ptn = b"\n# eof\n#" + a = buf.find(ptn) + if a < 0: + raise Exception("could not find archive marker") + + esc = {b"??": b"?", b"?r": b"\r", b"?n": b"\n", b"?0": b"\x00"} + buf = buf[a + len(ptn) :].replace(b"\n#", b"") + p = 0 + while buf: + a = buf.find(b"?", p) + if a < 0: + yield buf[p:] + break + elif a == p: + yield esc[buf[p : p + 2]] + p += 2 + else: + yield buf[p:a] + p = a + + +def confirm(rv): + msg() + msg("retcode", rv if rv else traceback.format_exc()) + if WINDOWS: + msg("*** hit enter to exit ***") + try: + raw_input() if PY2 else input() + except: + pass + + sys.exit(rv or 1) + + +def run(tmp, j2, ftp): + msg("jinja2:", j2 or "bundled") + msg("pyftpd:", ftp or "bundled") + msg("sfxdir:", tmp) + msg() + + sys.argv.append("--sfx-tpoke=" + tmp) + + ld = (("", ""), (j2, "j2"), (ftp, "ftp"), (not PY2, "py2"), (PY37, "py37")) + ld = [os.path.join(tmp, b) for a, b in ld if not a] + + if any([re.match(r"^-.*j[0-9]", x) for x in sys.argv]): + run_s(ld) + else: + run_i(ld) + + +def run_i(ld): + for x in ld: + sys.path.insert(0, x) + + e = os.environ + e["PRTY_NO_IMPRESO"] = "1" + + from copyparty.__main__ import main as p + + p() + + +def run_s(ld): + c = "import sys,runpy;" + "".join(['sys.path.insert(0,r"' + x.replace("\\", "/") + '");' for x in ld]) + 'runpy.run_module("copyparty",run_name="__main__")' + c = [str(x) for x in [sys.executable, "-c", c] + list(sys.argv[1:])] + msg("\n", c, "\n") + p = sp.Popen(c) + + def bye(*a): + p.send_signal(signal.SIGINT) + + signal.signal(signal.SIGTERM, bye) + p.wait() + + raise SystemExit(p.returncode) + + +def main(): + sysver = str(sys.version).replace("\n", "\n" + " " * 18) + pktime = time.strftime("%Y-%m-%d, %H:%M:%S", time.gmtime(STAMP)) + msg() + msg(" this is: copyparty", VER) + msg(" packed at:", pktime, "UTC,", STAMP) + msg("archive is:", me) + msg("python bin:", sys.executable) + msg("python ver:", platform.python_implementation(), sysver) + msg() + + arg = "" + try: + arg = sys.argv[1] + except: + pass + + tmp = os.path.realpath(unpack()) + + try: + from jinja2 import __version__ as j2 + except: + j2 = None + + try: + from pyftpdlib.__init__ import __ver__ as ftp + except: + ftp = None + + try: + run(tmp, j2, ftp) + except SystemExit as ex: + c = ex.code + if c not in [0, -15]: + confirm(ex.code) + except KeyboardInterrupt: + pass + except: + confirm(0) + + +if __name__ == "__main__": + main() + + +# eof +#‹ð,ht.7?0äUåšë8 íï<ÅY¦2.Óðefr¥õ7Žk)ítŸ~g`™y3HÇGйj[)/ÛÁ†ÒjÊ\­{Y±ì§J:ˆ ƒÌ§Ó¦-f˺•ñ|6ꌦ“Åt1›Œ&³Î0ÓqÃÎ_ 5‹ò@ç*ŸS¶rpE‘x*²[CV„é¢??ÇUý%?nmˆõW£K-جÈ"wkœÊµ]¢ð®D7QÉ{p•hg•1[„Âmx@V¥A *żq>ï©ZV¶<*£´:¬DªïkX‘©¹æF6•Ê»-Î`;?r™2%d³m’X01‡ýøÔÖãOß%ºÀä½ó†ÖdÞ€¥—âíA"ì¼ßv±u5Êš¾¶A»f¨òR[Íâ•8·Tªfj7ŸhÁ MÞIôeXšãðòÞÃçW®ï|qåùÕ/v®í=¹ýðö½«OvjïÉÊŽ³â¹Mò„ɯuFüä>¥;F‡É'7”W% yÆ`}mèv`ÿJCþ‘ýr+Æ·öžïÞ¿~kƒÓâT0(~ŽÛ??‹ƒwÄÜ¢—5±ÜÑ%¹: ¹MÙÏ`Irߎ%®_þ(6Ú 8?r¤Šuö£ñðY»ãá‡0n©-Bðsi-Ñ÷gñ„6)a­Y7qÒ,Õžx¥ü1H²V͵35qùµëw¢>ÔÖsô}Íí2”ͱVFçÈÈ‹.t¦„8 jOß‹§@ F§ñô§ÄÁ—?rM_–®4€õq ûÔ|‰œ?nUæ•è´'ˆáw’w~Ø'¹‹çß(+÷­ÿíÔ™²Xê5AáðîÁ¸"Ê‘:Ç‚œ*²ñ6p–$ÒÊ$†~g…‹ô[%TêêˆÅÝÑÛa–2ÑÎòSˆJ7??>²BÞ’à†+½h?n“…óÍy>ºPYRLȌΎñøÊ×Îñ=¾>;ñã@²+•è,^??9I°x±öíw.‚¬u&´%É*Ó¹ÛºÛI\ÕD|}{:¤¼\Œ}Ç+Ö‘;KIk¢óçÈ·ßÿó^©‹0û—¼ÿ¡3ïÿh´Ç“a|ÿ¿–±‘ÙhýOàîéfa™(Ë ÎÐÀâÿÿÿ_ñ' ##ƒˆ’a`?níég8ñ6óÊì\Uoù4£Ïw*£Oäž“ðU92kêK¶ÕYG·<;·òÖ×ø›²bso¾’8ø#nÕ\–œŸôœS¸Õ??k®É-²Új8ÙtásnÑæ„çy;÷.a`°¦§OGáüŸ™™˜ ÏútËÿÆ& üonhb`hdlT, é—ÿGó¿ƒ(ÃG0æÌ™À"?0È€”ÌR?0$ÏÀ´}Nýîâ>5Ÿov7Þˆ1é yà¡6ûQ¦œë:‹õ¦Ó›Ûžk›¼äÿ)2ÄêJ[ɉ–·.”\šË3??àR×Y£âc}å…>»¦L2<ÛPiy7xyVwzЫÒ<Ó_Ë_ýµk¿¸Å£ïjN¥;*&Î8~bËóï'=¾–š=»Â¼0r³¤°Ö©vOÆB­S+[³–y*S<¬¥ýÍÒšEºÛ*«-töUñ4­eOµï\ñšæ½G|åö­™–D—3£ù??©(¿ØÕË(ÉÍ¡Wþ7442æS`Þ7722?0Õÿ†f£ýº?0E?0{߽߸­4úwü·ØNT,y½{Ž×Òž’ú;i¿lÊi¹Zˆ%®H‚ €’{oï½üs{Þà–ïE¾W9ðÍ?0„H ”-Û»Î&GN Ì €™f0(üíWøâ}‚2ïïœà‰i:êy,õú;ÂhÐßyë$aŠL…dªçå*lþÂÃt©˜õÏΈ~ /_ž´M’ÅÁùT§ Óž÷ûæ×¿nþ–'U8Oó´ÛÌR øñû=Œ˜·@KiÂzÞ4b³Œ UœE÷†¦¦~i(TDã¦ôiÌz­_4ÎÙ“(ÅÀ¸¾™õ\ }—õˆÖˆ<ÇáÎ%óák1€ƒ"àÁ©v^kHlÿíÚ?0ð­¬ÿ<<„õŸ‡<ìÀbÆ»¶ë¿7ùÛÆ5ïD+Š­: @3¤IÏ%MeS2…è ªàlÈEÀÄq';%’㮟;¿üå/›TSˆÎÃÿÈÁ㌸Ýåø€e§H‚ž‘Ä`àñ¬ÈDÏ|LñNÀÔŽ‹ÖªšÑ1»ØgÈÜ©K5t:ä˜x ÑkÄÿ®þÛ<ú‡àå„Üâ׳©œ¸ÙÉ!²ÚÎÕSù)Nñn0uAT£¬v–âÎLZ­öŠrdwµ‘PG_–&'ÍÕ',ðÕMŒÎÎvÞÚ½¿KÞµÕÁi!?nݾÃÜ ×#»Ovõ^/üR‹.£…ÜykóiÖMg:Žnvju³s¹nŽ»î gI6å‹iÿ}šdw È?nÈ¿`d½¯Ÿ¿óµõÿüÐõü^»ÿ×étŽÐÿƒ§n÷¨ÓÁý??G­ÿ÷Sóÿä àþ›æ®_­Æ<Ñ.†Ó©©ù1xv¤ê&’ÒO4|Ø/6g‚II>8ÂõÁÐ)v¤”y¨¡³¾>V mfF)ÔÊÒˆ¥>3>]]ÌG2»Å~/ÌSíˆíí°q•ˆÄˆÙ`Äoæ{»ÐšÝý–nN«h?ré‘F1^6ÈÑÁÁ¾yÝRœJÕ⓽Ã)Ò>iùá€O?0nöBнÝ;âVôÝýj0c;-ÿîÿ 2Ç?0¼þó_:ÜÿuØ}txø¨ûHïÿ|¸ÿ_Åßvü'ðW%%ø+Y!”ò(?rØiƒØí!·½MLK™|•[Åòèò]bõv±\‰­[÷}Iê‚S„Z š¹CŠ¿Ê<ž¼~1…ÖÓgRùƒô~Èň)œ”ºy¹d¢>g$xžÉå<='³³[;µµ™g÷´ÎÓéÕoÁg8-±ÝÊ•:îôD‚LÏS=qôl ìÒ!R®Í9ù¶=Õ”SÍÄ4ÓÎ6mF¹ÅÃiçðG0BƒlòqðA.Kd äŸúc¶¿RjZ?nÆ“¹dѬtsvÌÊV¯º®¥ÇåE%»¤Ô[^Pgæ’Õ›­Ý`axú³EÅH¶ìÊ–Zú??×]ËYZ¹Â¢ˆ³&âÌÊolÿ“àöç°ýxþã° ‡À=ê¢ýïlíÿ`ÿOôÿ74ù¸Á¼aqÿG¯Ôz_n\¯Œž.º?r² Â]W#×­§Wk,¿kø“´Ü%U&Ú?rdú-ŽFceöÍz‹Q¾??â$ b‚| šÒ©â£QÌ*€ã(`2 ¤Ó쮤SVi. ƒ!•¨cë"üˆ« ƒº¯ˆßë#Ž¡[’ʲp„è\èh&ê‘X§á\ÿ•¡\!’ºT È©c)q‰Þýl©v@×ÀXÒåf8Îciw²b‘ej…múÝâZ 0QmÙ+€P¼RQ8':ƒìéÒ'û«¨‘bb§Q3åi“J??ŠŽ æRe±s·š®ÇÆÜ<[ÅñÃÑz”ÙŠÄH«XxõC_-ˆÕSxÂ¥ £œ)÷úŸÜ1‹Ή?0¤z‰J8ŽÉtœ ’1ëJ4õÙÝÏLŽD§ÁºD@„J‚gâîgC?n0TJÈ6— LR^îÌ5:@öô^sÍÔKêÔ5îSñˆõC +#Ÿ-J›cžä$|‚ž}}Á0æ¶”ê=¿¬¯¯{€ 3tNB8câ~Ý¡ffˆÀ]¹T0Z¤z„æ€Á“,f?ncla¨WV³žc¡Õ%i(–ï1¾›²Cü&…)=öALƒ|‚cî"é}dÆ|’ð ÌùÎуÓ<QªÕ#T ñµ†°#Ïbl0¾Çê£g¤mËG¡Â@óC,JЮ1à_ýú7Ö€áâD??Uí€áÀK›Û$t3q”R±ÓiÙF Q3-鶈óYÛ°hD üaKó뚎˜$Œ·y@E@ 4‡ó&þjíÙ?nÓÑ.ÌÆÎÖÖ/øªÚñß3ü·BÚ9????ß9o6á_ø}öÎ;Cï¼óìÜ>aê RÐ)üÁ³sû„©ÿïK%¢ ûûÿÙyùŒ9ÏÈ3Ü—??ÏÎͦþ黽;FìMûþ³óÕ”ªrÞ.´’àŽa??ÂÖ¦#ÆÉ=ðÿóÿ=i›çgç5‰çØp£#RO.ÏcÝç¦ÃÃ@Ä&@Ò\üÁË1þ/¥8ž2bÿÎÕŒcÁØÎÎFÅmRÔ…Å`‘™rç­w?0â€ì¼Õiá«þÅw“iòЬ")ö?r ¦ï†éÀ£}îâ³}9Ôð/r®€vã˜ïØ_ÌBùëí+0~„<DfÔg’쥅A2¼@Æ’(-kr¨6Ñý,þdtj)(ʸcà,X-H Švƒ»æ]ÆjwÈÆTªA›ûáØ®¾ÿõÁ£íþßíøÿ×±ÿ;Ý·û—î ty°…Ûp°¿Ÿeö??n¡˜8&îå^º°Pg{æüÞ=X¹ÝËZÙ¶Ÿ?0¹¶BÊ?r"ãÁÚý!×Ù‘Œ5©ßu<óu,­Ï¡£5´‚HÔg?0CØ¥›MK°!îà×µK)¹ò{KûfŸË¤ÄÏq¯eÖÿ„˜âµ2›ÐÅÿlví¿=Š€8·gÿºÝÒþ›øÿ£‡oØþß­ýßÚÿíùSÕÒö—žâE^‰${œoè-i?rüY#`1SûŽ¥Ö¯úV²4Oðª @=è‘XšDi{,°%Œà¦ x=óퟰù³•fõyÙ¬>]ò\øõ8È¡úŒ4¼ê™(³E.¨Ïd§Y$X}YÈÂúŒ±XƒAƒ@³ûê'°&?r’ÍdšÁ¯h©úP–:€ÿ:›Ì*Öù¦ð#ÇÅÃ~ž¼^ÿnξ:®×/ìú]U·k‚T?r«;óæLz:3+–fšMoíɱºû@¦Yq,­òra‘b®Í•?ns¯{®M\˜Û¹(׋Òг§Ø?0Ô°#`4ÀTL9¬‚M{{ðÞÄ÷}Ò&öõ.AÁó4Øë쿦bB?0h“bnå\nFxº«ôâ¡é\ßïfD'C5{û?nvLÂ_²Gl‚§esk3…í:Ùvý7-ÈñíÎÿ`Ó<÷??<|ðPßÿx°]ÿÛÎÿ~rë¶ÿ¼yS¹ÂT˜d4Áöf£ž;³ç“hèN÷4ÒÔEò—°|ó±VUVA›ÑÅ¡/ä{g×ÎË4U˜€ù,˜©(!e7¡qÜß+ötaKb>±?0„³??kW.sHÚw*œÍz§ÕûÅy®¼¾ù-Úº¸–Óîúš±ØçP÷!õ'b. uS‚§#ìeó?nï·Èr×T/XýLähåSD‚rÄÅSÌÊån±µdòL.Öœ©×R¨”ŽÌu‹Çv9¹¸ßÒ2cÊciwà•óaœÝÝÓÓ:|Òßÿ]¼1EÏQP„©«Áì¥øuu6h‹)”ÿì Û²TÞ ™Û;;UP&uܤ®›tø]ý…’˜÷?0ó<wô{ý£Îxh3ìm¶Ö«h—²îúzqñª¸`,Ütû½õ› Œ¥êªÒ{eb â×+F#=7éèa??¬ûÑ:aÿâ»W/j?nâ*·vèÏË(+Äk¥Û·œ"'o7›dÌðÒlÖ(@š'…ü-ŽôišBO¯6Ǧ¹M²Xc*ÇÍULy±AÑÑ2<$\?0ž¨l.?0†Mê+ ¯»E®% gÊA«pÞ¢\©¯@ŽUr]”2§e·QTåò?nÊŸd¨ý¥ÀËo«˜œ)RÄ)yP<þ)ɾszІBtü]º€×&—Z¬qmÃ¥WOå ªšyü]íe­v;öì?n=£¤T&j{g+;T)J©´ßc¿¥+ ±³Z¡ŸW®n`†¹¨±ç‡`NÍ3gDÛÀžŠj¯Õ€—ƒ¡àZññ¸²ÿæ1þ,kAI?0Âë ðv6CÕóø‚šÏœšÊš›»´‰âW~öcU¾tXËêïõóªj‡ËD±Êê1Ú/•!|zW¼{ýRðôÅ¿¦e/A¯¿Ý”EÝëе{@»Ùî^(Ð3‹îgÙ,XÙó@,°KʽŽ=Ç¡©ÚG[™Ózl{Õº®†\@­9½@1qFŠ:kÒm‡›•Î ¿Ê-ê4ÔK©<–¾ãXÿMO+}ˆ:jvíä¯Aô[Ñ»U·’÷ÇÙlEî§•[ZÌ5‹ÊÖ ûöÕ¨©(’ï{}Ž—Þ©ò0t@wÎ¥•@pd$ÉÔòȈ´éÈ«½>Q_¹Oæ:=g\´µ¬/þm3eFo¡¬† ·ø¹·nñV6/—”Ž…O.ôï4²’*óÜ’ÇNɾ_2åOzˆÐKÁbGR;døŽu {~.KU<‡:`V°Tm÷w ‰.)d¾˜× hqº¶4¬‰-ÎQ$}‚¾.A—ù¼ „þ³nú,Šc¤PD:𼙀b# MxJØ”‰9ù諯¾ ?0ÝÀ{Žü±6¹ÅAz"yÂ̽=ÄO#&I(xBFL)(µLܞ̱kÇó"êaαáEÓr¿ANXÒbÀ~I¤L¥h,9ÁO­š«D$‘xªM úŽXÊ??·lÝe:r£øàjÉuõ¸^D)_Q„”_[¨I9µºò*?rJm@jVl‹(I3ëSÚCk·/<^B*_¬ÊVËës:˹dÆ;ÿü) 1‰ó˜ Bñ|IJq‰E;ØÕ¦p)YìÕîDvH°Î•9ð·ÚVcí×cÄKŸDi~z!|Bý?n<¼}þÔ/œ¥•×—ý2¯4ÆNdqy!¡Yƨ <-¦5Ð]s…é’P"Ø(!÷£÷Þ{[”l™X^ FCg†ë›&‡,æ3²'¹PÚïªÞ¯Ú€\©@âBª}=¹™Â[(ÿý¿ª!^—„^X)*ô•H ÿ!Ïú• ?n?nA/Å®‘t¯ð}tLN|€ã=4nåì@:³™÷† ‡:»gc rK]‰Úÿö¸?n«]ýŒG©Z*yƒúÚ–ÞF“ìuvj²êËK¦²™‡Û—‰`4^ŠUj± ¨{*ü­·!™ñ —b >³÷²?0«XsÌpÄKd<-‡_ùÙÇŸ}¨¥±Ôà)—0ºâ—šxen€7¹ŒYª"s¸A$/00|B0St=UÃ~?0b‹¼Ç2lG:*ïE F£`’aK@÷%FÊ«}9Îû‡ŒäX¼âÈB&40JšˆB±³S qÌ©bžÆ€€½QgM¢)V5mBÓ$?n²üB¼ýÞƒõ?r‹Š¶ÈæÓW’±rDÖ±¸ãv{©q>lA+Ú¿¯½ÀÆ©ê°=æÓ<ÀMyÒ¸/Û )¼´ˆ 4 Ó¦™‚uÀ:`ýÜÞ«zá¨?nÁLù?0HVu\¿eÃ÷~ý ö¹†«c¤‹Ut6¤¢qì20C™µ6m ·½¾IÀ¼ë?0/ö[ÂyÊZ\ŒÚæ±Y&šfæ'›4 >h}AÑ@ É"ÈRì5Ê á(@?? j,Z +#&˜;Jb…ímºzM©ž6‘…F8¸¶ÓúC¶„,h<‰¨Xù£“dʠʢÚy?0uó™$¸”3–ovÐIäÒ €.zÝù{'ÃêàäníM¨¤Ùœ†²©#«M¼¸—è3%ÒƒHéú(È‘\máñÂ!vp^5®÷0N¼fЕuuÅpŠÈÍ?n¼S«µ/†U >ùõgd ò<—?nû·Ù¸Qù( >*!vp&÷Ÿ±f3åM}³U³£ÉÚ(‘[ÙõœÅÚò8?0a?nA¥xX0û ).?? ºc`k??ƒq H3[·lÆžD-ØwëQ„zêûÚŒBãGíЃ’ñÖˆéÂ6€¥¤è$º[¢÷º‡Oì&¾ºß0îöë.ëºÚ´m pÔRåõ—ß±·aMKøÔTæÁ#òiôãªFß3G ^¾ŽÑWHËÕ™õ_}ñW7­väJ¶ðñAÀQŠ—àÖ+s{±`üoÌ¥ê-Ü\’q¯eå0?r…11.ì’¹vY;ôÑ”§sèܲ¢!DŲ§/õ}-^.6èSòZ5we$/ÒWŸúbcÑHÍŠ'ôHÍéjlÈ…«™îIbÓ>«l+:jO;ÝV§ÕAÅíjë$3æksÇó[‰o¢¯[}ÝL©Ü¡|Ã’Q#=þ]yDuÖ‚BZ4ËÚž%|ù`ÉW¤‡‘ü9èa½›UþUtó¹äéfÊùe1®ƒwÏÎp#"¨÷Ë—»Pø­¨`\kÔÃè£n½-\ÐÒk¸^I{­"–r|5¬’©QÂ;o·‡QÚR9vò²YÏQw'^®”ÊØTðî^€âwïý¡y/¹4ï}tïÓ{O÷[Y:ªA³­!à^’¦$MAΉ•€—¯šÄ» w-ñ'¸ Ôƒ”ûД» ×?0{*ÀFÃÍõåÕ\ýF—¾Ý£ÒS{ýóöïvÎ &ÛQ* ¬•±¿cï,¶ÕÒ?0\cžâγzá ´ï{`3,$¸OßçÔm—Z5i¿¸lÿåÃiÿuÿÿ¡.Ôœ¢œ$0Œ ??¿ÿü±úÓûßÿŠîgŸ$Åüáê(>ð„4!úº1EXm˜°¬ÇGý”Ø?rƒÀD?0\jÙV–nJ€^ùØÎó •’8œÖöžeˆämáëNw´œµ·òÐ??.gNOÍ&yRƒscË®ÝüÅàuó8– ¦q‘ß…º³1â÷3.ž:r¼óOêÈsæõ­{³"Ôc\¹ÁÓÇV|VaŸW8Û$1:ñ0=Pñnnäñ’rê•r(gÏEä|^–EA‰A¯i½$P+­þ\g›Fo‡éµk–.Í»™ß×~fÄâàJ€f`*™öÝÞWç:mÌ«åà=}}•¦°0žîšMŒÐÌtòœÐgþˆ8æErH1W\ß½v{} Ø¹š÷wIÕºŸœù‚õuáË`uÕ‘Ëw¿]R*6ÜäœU£w½"Ypqñ`×Ai@?0®ülí-IÇÈÏÎz^õ|j ×t ÐP¼ÁP­o•6ã`“"ŸàHð¦¼"ëh”Ù›²ò¤=?0Úóõ†rßJ½4_£‡ti…õ3m·UßbЮ3yKQHN’dó„€|W6Öª×4šŸ…MZõ†•¿ó|ÓMà ¼ §s¹Ÿ8®’+â?nžÁb&ôeÓ˜xò¶vpññþ§CqVn)(ѧÛ^£A'«’ss+B7ÌsäÍ?nÏEåŽsÔ³'l¼ò Ÿì“LÏÚúŒ7ÌæÃ[q\f¯ehÌ+m¨ÉåtK’M¯ÏéHCÄzáž(½ÄÜ,Ì6èï`»?nÌÍr•°×Pc¨ƒno¦([ƒ¥Í—lêL™/fÞätAài¥òÆi…lRÓ7¡??¯ÝFÜžŠµØ‰m<ƒ‘{Ù!§öà|#™·i>„ÈM{±\ ’¦vòîÁ¾'^0,£õ‚!–Šœ`hb+TníÙ<†S4{cä@:ðåœyÅy»?0 i)uŠu4”%Ó{qópnõº}-rº±ÏA£ÏdàîrÊÔ¦æV¢Ä„ Ö(ȺWGo•…¸[ɰ¦-R±ü-4õ¢ôÜì `6¯×l†²@ Öò©}¤Vh¼HÒ"-5ÍØÞÙz”æƒT[ ýyyäM—Ê>—ÅE³—õvÂñåPîɳ.æÍA2Jì÷ë°šŸ­Ì ?0·à¼sK!z?rß½T?n`³œf»{7?r½äã‰cžtFvÊä5ÂìMäs-¬DZONCäk¢½•D=›§=;ž¤q(>®e²ÎÜ)óÅ»Ïs.Q+:QœïÄ?r»ÌR·ó)ë‘Ê9ÆÈ¨5Þ÷–SBÌ>uÉ&Œ@ —kVu=DñÒ¢&?rz ¨ÑÁ#õ%ºEÌeºÉt’P9Õ¨'ÅF\Bn„x4Öì(ùþÈuk5tà[úU»zœ§éLHoµî•‘_ŒžhÍÉnêtªY¤rm¦TBUÚØÌõƒÁÑâãujÞÕCY=õoón3½/ááÖ¾znØsh›£¥Hð=.éTàÊH?0‘¬r*)ð ¯]£]ë¥;~•”¿ŸŒŠìÄT¶?rõ~8¼œ Er~ïXʺKj“ë|µ&qÊX,®‚Z›j#7X~Å–=v¿—(ñxMK©¡‡J8fN“Œi3o2 NÆqÊ—c·Ôœië…öçCx‰hÆ0¯¹Æb£¥¢_!_¹#˜üß²èÏ8Šާˆ?n<áœâMãÀƒ(Á¦@P*· à}Ûl§ëµ'äfD/7Šˆ—vM…·#Uëcå±f&°6Ø´JÐ?rPK?0¿ ðap^ͼ¥İ4} ChÍ5“›-?nU¬pñ!íÌ&"ý=ö@ˆ¶·yõ·\ÿæ þ˜häš¾μ!’ñ¾mó`8Äùe½úD¦LÏï¦I^É{¶ßW­~ã«Y‰< Jöï}zù÷qmpîoùaϨ»p£mÕ¢~ õ–šúfžo3Ô{PxCP7Ê©uÛkÖ…Ÿƒ|gY5éÄ¢™>Ó[æ…3??ÙóëOBÌ]%݉Ò+vüé/`ÔzÀt—ù’Wãzî{N‰xr¹pÛ0FL¹sâá#Î694ÿ› Dþ7ÀÌÿ†ƒ:ÿæÿUüŸéEƒu~üâ@Ñ=ÌÛ®¹ýw??ÖÂãÀÎËë!û÷âÌR?r‰»û€’vì *B[\ÒßÝnjºøÔPÇaòñÀ&+„,TWr[”ìô¬ß*á¬Ã †v£ Ø9Ä\ó-ØÍÌaj”Ùç.kp!ÿÕ׈+çÜ""ïæÓÈÿFC­þÍÿk`íÿÝ‚Í.뵓ÄXýîtH²†<8Ã{¡Ñ»@Ü䀾ṧSbY¥g<äЫXX6??-ÏoÓñ·“¶Ï4¨»+i×ÑÚ°›AüŸvÂÉ‹ý üЇO?n-àšY7³AàƒxÅÝõ•Ý\'bÙW4}ß̸CŸåœz?0§î²Uy{Î9:<ï>®Ïr‡õŠÒ#ÅGz)!u??¸W0ˆ4¾b*l=ðéÏB‹9Ä~0Ú^Î(Ï;rÜw 0šÿããAÇ'ÇÇWÅ3Ð5ÿ›™€Ïÿ6622505åC?0{×¹ä8?n„ÿó*]’¶|š<—ƒw¬½ñžÓÙÞ(,!f”?nЄ}ú£Ám¬¹mî¯ùjÛÚ¢›“÷ëÿÿøñ—Ô•ôKÏ$ô&æúJ³Vµ‚Sêå%·Yˆ´¶J0©h®ëqZäŠ VHB6ÄZ¢ÔLA‚_Äòn[„Ðo‚ýÞ~HQâîKsÔ‘Ó³óøìÇáÎð<21ÂüŽÇýƒÌ^j »ˆ6щ4¯2˜çG½P—þZñ·Pì†d“2T·bzÁÇó<¨¥áº[m@@ 5Á¬tu§805o)Zî*ƒ?n%Bž?r'ƒé³4Î+mÎà64¹¨o½¼ÚZ „‘l´u??òÃ7¦Nž9–†âeºþ&Û´oX0ÔUBBž.uÔ‚§9:̳’¹]Á#0±¾öj±M-#^]碮¢5×]N¦ôl:šÎá$´F&›XBÝÀÝ>ü‘A¾?r‡aUjš•*­[å’)u¸2Ý}UÛ€¡wG±åP€†ªÅVüöïÕÁ~ÏÓ¿ôUá_½ áÁHòB3¼ov«@›h"èã•_j 6¹[CøÁBÆý³éBWûuS˜?0!ñóX“Vu]Ú$L)h"<¶¨ßñÊïY‡áÎÛ{6ÇÐtÁ«@›T&<¥P;Ë`k½ ö5ݽ̮°“¦¥&?r/:ƒK7M§5ô¹-óC;¿û!<î!!ï$L®ïðÞºÑy Âx'Ý„–F¹bë–+ÅWõmt)­Ê†:E‰ìb8!ÖQtù‡ŽdNv§y×yFvÿ—ÁÑ.8Þ'@pકwQÙ9?rgò®L9µ«¼ÜÑ•L\ñ´£rvBŽž¥O3†u•7u–9m#rY:¢S]Þפ]–LÓÔáŽê‚èCÿ,q•öâµyÚ8£æ9+·öt}€”/Q{ ”šï¨¹« ?09r?r”G´„:EXßÞ3†D„HEŒd¼>¡‘ŒÉxWÉa‰½3ÁŠÍáÕV­òå›èbÓ,»?n%WÁ;ØÃDÞ¸í÷uåÃ"'怨W×3&X)ƒzuÉÚ…œòÌCÿ)¼ÈBϳ%ð‘‚mþá‹à ì––ujrW«×©ÕkÁu’ÿÂÿ+Y^ýïþßÑéÉgû‡¿ðÿŽÞûÿË}н•œdçÕµ×Ü©‹º:"ÿš[è3ŸPÊZݬ Þfá¾øÖ}_'×QÉ¿(Í%†w¸ð…VyÂ+ÉmÕñp ÄVÿqêyë˜2±Ö*ÉC>ÿÓcDæ”8ÿ¥+P ’'j‹ê䊫_sm/g`¹Ž¯‹²` _±ä?nm›§Ä7Âýmk]3ëæôœ»`EížXܫS+÷ψè&Y`+»?rÅ@œ½ ·CBwG¥Ç0ˆ'ýqÜóôá“áh@KŸÆóÅp:ÙT„ù–â+ñÛ†U)µ9N(Äô<³½Ò$[Ó^œf-XÊ?rÎJµiF#l"+Ø:aJB,O!æÂRäu¢§Y‹ëä¼]mŠ`¿¿gÌÙl/úº|¾´¬Ï¶Ò‡Óå¢AüˆÆÏgŒ—ñ.8·à¼ÿ4¦ÃÙÓS ‡ãÙNèÇß´óx8yÜ“X8Žcœ£þ|©“W¼p…ZÄ‹3ºpà?nçý1í??íG.§Ë>˜ö'‹¡‹ŸFÃeìª>™Ü•ŽÒSöƒ³ùc Œ—ueeVäëŠ*¶²puzÌ«ÄÊ)Oy¥¬|Á$\Ö­H¸Õ@<Ï=¤Ðæ·VnîVyeEÁ5³UÙç*.JÈžn\,o É3÷=&Xµ†ª[#ì¸Üštÿãe6_¾ “)]Ž~hY¶‰\r/6Þ·ù¾"n.‹Ñî®jY8o½CůÎvçÏl…<õ´êÕí—ç©–`}GðÇqF­¨^ùfëKÌóÄÄb1¥¸pŠÎyÑlÕ¿å_ô¼º‚ôôß®nŒø3q_¾Þ¶Žìùÿ}?n¶æË”PòžEnßþN'9Ë—Í_œ^›!‹GÉ&(Ë>ÛKÌô+Î#LýP,‚„HÙíÌÌÍYb“À…B¡6ɆËa™gÿp»sþrVï‚ÈÎ÷ç»D—4Ô«Œjç{Æ«UD˜áíèà¦ÐI«p9§I\“J®Jzù³ÊM*aägÚõPé_¨òãüàÙÒëV°Š‹±Ç—Ž–äÒ@K«ùawia8’gÅÇ9Ô€k€ƒž~®?n»®}¦{„ ?rê£ôlo‰iѳ1&Ú•ý¹-µÒ¦qµÚg:=%­~&ý쟲qð2‹!XU·§ØØ¤,Òßß|úxùñÓå{²œ0h§$5òÏ‹C¨i.7x”(Ù٥Ͳéé‰m}'÷…©¹éÕ¤oVùË—óóÝáËã‹§K“gðÑfÐX†Unxje ¶#¾Ä ðJ©Â´ ŒH{>?rÕ-dŒIƒ~L£¤_6Чýg)þ=J <¸B~w]Î/ƒ0¼\Ï£B™7Y•Eá?rùË®yL!äÑï‘0>‘–MÁB@ ÌÇ…úäÙd2%›ÓçAªQpëSº†ØlÌ¢j” ¯?n¾îEf¹Ÿ÷¾è'X–/ËŸwý_¿Œþ@zUpôù‹T²Zª<(T_F4W^« ŸÎ½ 6â_úè̘Žäà†D`ÆÓ9Q?0ýEl´Q1Áð›cf4uãd5B°Âma¯¹wlX¶±ò‘/è¾w³iùFw›,ƒ±3Ì(×P–rèí?r6q.îb¿±…ΉХ¡€6_i說+S†Q"÷­z†WjoïÅóC¶µKÝ6ˆöÇòØAï6;BèMkÔÒêFª6O{¾fͦ6Ö·í>ë׊Ÿ“$ØhõÿO§lÉ¿÷¼AHl¼ºË£vd•Æ4Ò"eiÖCÇ"—Á.©?rÞ??¬ÅØÕ3ÒTÁ˜¥wƒ`ÛP?0u)|ÀŸˆ{ÊŸ†7,@|ÐNpÔˬhŠIŸ’MÕldžìì-Ñ?0Á)[ÿッ¥÷ó¯¥+@sÍÌ¢b ˜†oúoÞ0(Óa’*o¬†…çB¬¸ £¼ßXÃ, x??¸z¡ßô>‰Ç}ý%Ï??|ûÃw—ß|ÿ¦7ð7:?0mÄ1^]D~Ûqp„æÛñ??¿??}ýç{´{óþô^hn#º»S,3¾í* •çš=ݺ‚›Î&íNÃ@}¸çËýt!Ü´Í0Â÷úÙÌ'šªÑ°¼Ådœs/WA[ûlFp5×¢Åð˜÷îe#$ƒ?nwVö{¿qøÐþ‡–«ˆ?n:•…NHñC??kµ `ßÚ?nÃû4óëo¼l›ô‹4¤9Û2Y!Pщ,:/Ûë†1ÞõŸÌ;Îé ³Â,ÔÇæ u«whüÍߺŸÎMX—&^quëåʰä½J…*—Ã{ΦÂZ]E׸ƒ—ia˱“¼À«vÒΦŒÔ…·ÒdÞ#غim­Cæã{Õ¦/oì ~k·Ã³.ìDQ£q–Xûlã&±6öõMׯ]eæ8ŽWü÷Þþ]+ž“ؼ Ni^½È6J ä-qòk9ˆ<̲+v§=°ŠC#泈h?rÌ«…?r†j+z½å?rgØ«]Arˆ×\TÚŸ5›ôÒ¤§./b߸+œÅR-ˆM¶Ú¡Þ\ ®†ÂÎv–Ã?r«2îßÅ•FûíªLÆw,‘è!·E0Ñø{“[ØteuOå*%{MÌŠ\•ôŒcªãòú?nña?nvïëæi1×Qr°ß³}&4RË:Q‘JŸ¾éá vã&›]Ü¡ƒ{wôM›€!xî8Œê 0äãiwZ­kINÅ»h’ùíÎi®f*WÉTé–´ÚÄkxBàgu‹ÿ•o|&—ìKžÐ\!Ç Eºoz¹‹¶E¸}‹À³??g¤ ³Ùed<&©Vì±¹D ñPKçpâk…ךîƒZ©a.‘Ó-\?0Ù\f?0˜#€ö.ÆIŒ™»Kx¢Ô¤ŒÈ%îUЧœ–€Y/˜_”$ô{ÈvÃÿµŽï,Øåph±Ù +#×þ‡4Q>‘ä›Ôç@âîíó— :VHU×($Ž•)@*Çû½4b'qÄ0£ÉÁ¾J°4Ð.«œ¤7]öƒóñó‹Q¨Ìž9„²•([2Kçû‡GÏžŸ\ØœA•tXojѼù²é÷Ö“Þ?0Vqæ8©#SåƒÙÐ.oB›vbã)w©­ˆqÑ7«ä{ Š…$8Ü* ô8À Àÿ´¤œà¯3ŸéÄpeÖ6æ¸kZ2K%'‹|W_Øe¡ÑfSÚ’·•fÊœ-&—:ýéó·„Œ6šå°MÚú6;ø&¹"£2¿üS!}V4y@¼èÝÙjoÿh¿l…=ߊð¤FZåôÊ ï^]~óî›2œo¼ßB-«ÞŒqsãÃkÚt å’•€Î@ÆlòŠP7/ûU¡ÿ%!Î¥¹ñƱ!à-q\ A÷è xqK”?0…ÖBˆÂ<Î:£Ieº±Ðõ#¥þZ[?rNoÐ.¬ž\ϰ瘚ÔÞP-J@ð g7>̵ºQæo²Ž¾?0»®øB‹‘{ч:•°Á;çðÆãßÉwÑcbmõE’®‰YÞx5ÑAXøæMÒ í‡X¢GTÏÓ5"¿N[à…êWTV›5=3ÈÇ*Tì!¬Ù*Žå =r U»õ¹®Dñ7ä'«(1D:)~¹ºpjlˆëød;QFœUßm7D˜Æ×*Kå ±?0öË!ZÇ8_î±Õ‰hQ¨\›»¸Y8ÙsÎ-áÿ®1`¯Ÿíñ%´ÿ5þ7²ÖØí/.Ì–bãà¡Ñ¤WÄzoxø;²çÿóã)Îûtøø ®KȳÁÀ29žù¨dB傸ÜçÝ|ä³0óB^ ” £©©˜aüm0ʧ/•cÒÃQUÛc;ýàšöò@TÜœ??_ØÄ}ÑìUOB?r¨°Ïh0„ç½ Ž{Lx#]—˜FÌ@Ì1O¾¢ÇÐ(=êmšØ¾ðyÕ?r±b·ärÉ$CÕ¥q8v7¯‡ªºŽyà"¦R’jÚÔôFËR  ø` ¡L5’âOjæXi .Z£ÛÈþåˆÒÄq»[æ#dYE‚N„ݶ÷?rÑé '÷6¹çŽÓÚ’ùyËüÑÀu(ì†zPÔλ€X·Ø”ŽŸw%óµ7~qó«×ÿyï×AeÆ> 2ÚtÆ4Êæ´t?r½1-ÀlþjÒey†‰š>.¤[§«Èò´*0_œØ\Âv£¼©§,óZ{ähÓé›åRì×sCß¶[áÄ-£ânöx ï7åËëé ׯ ¨˜@ ˜…½ˆ²Zm ŸŽ'ÄcîW›6tKœˆ£÷ãš`ºˆ*KÉ©óº´8cnëvE>J_tô6Ók÷t-&7Ns#ͺ"¾Iúx+?n«Gß»OêEm} F8XÕ.Ä's£˜á{Ã^ëIø‚ûlšÏÒÔ,‚ò# ô…¦–9³uèÆ°çŒ?0jµa°3?n¯¿dcKì§¢½»WgG$·N`æ¯E¼Ü0YS Aⓦ!ze›“´Ê¯Õ^Á±à‰ÙƒT;qT‰d1Ç´«âË‘=Gªñ^à<öçeù?ràK3-Ð9O!]a©/G’éÐJõ‹ù`иGV÷ªšÎÒx]ê‚~¡. ( ÈÅ•bÔ8« #?retIŸõŸ"}VØ0IŠY h˜ú•iq›)+%ðïr—’†|¿r²ÕtÒOžXþï[Û¬œ#‡*ϱ& otlèâ’.§ì‹»gÒÝIã =»ôÆô¥¦éPÅ$/ð|—íóúÝ;³*=?nÍSÚv`¾‰^£æâÍn5œ°côîôõ_>~zm¡O??þðáó›Oýª…¹??¨ˆ}§Š³"üÞìˆÆ1÷£þÄÏzœ`¥±zŸ†­Îº[ucG ]ÔÊÒøý7^¿{ãoLk`Çz?ncX‘™.M–n…Š#üèìMþƒB—OýáÞÖ€º²Fîí6 ЀMÚûsÜÆkL—wân…‘éqQdÅ'·¤«úcà?n C®ƒXò#åPiN†9‚Åc¦cý«M‹€äæcÖ*4×^ðµe ±Ø¶ýÓª™­í??¹%%„ä¶eëQ®®"|­Š§á³íó…J;«±¾€ÿfЩÀ­gãuÔ§Ž•”¹ÛmÖ¤ú$Jÿ­ô¯›ZÄÂ$©‡¢()wsYdéBõ‹4ów—Ÿ~|û¦+??˸•„çÏf 9Ú0󽌣eü†ôÊKÐjÄ Â·3\?rC‚?ng ¶E’äMÛ nÂÔžjæ5Ìp—û†LžÂöâdt?rÔÌ÷úÈ??ò§Ââ0ÿÞpï¿øX÷QŽÄ4 •›s¬xI˼L¯iüÚ^5Ô±RYÿù‹½ýƒj%)GÓ~û>®/oäñÆòWOÿFO#UÞš•ž¬fÄÛÙŽAGœ›LìÙ®ØÛ<}ºäd®A?0§‡zƒ¶@¥d[’zø¾Ï5>«]>Ì♇‹†ž?0n7òäõ5-*’ïZÉtŠ‘z“u^ÍkUìlÄG!£è4á²þþÆ"öœ2.º>X£2ôŠ`¡´ðWk‡Ze\®ÐŒä¥YÚä3âÆly|/Mް??œƒ0¤µÔÀJÂ4çŠ>ªÓ”K¤ß¨q‰Tå_tJ À”?në>Ó¹OtzãqIêÀ[ª !ü8¦¯1~xæUc7k »#óO‰„›<MS±€~x¥wè*!êºööŸ¬½?nË"1Juˆh¾õ¶?rg¯FÏWáø{/÷G{Ï^Œh;ZÔ k´üváÈî+Å4óàœ:²Ñ¹®‘¶¤gÞÁËýi²*äËÆò ÈzÓ»þáá_ëÑ$}„¾$Û'jÎ%92óZ?r³<½¡zT<Öt?r“aK^¤?r•V?r©å²Ú%õ¾¦Æ™ÊMtL•a¤ùÊåæ;…XãçÏwÇëõz,ÌE~GÏ—üœ÷C ¬?0l"nï å?n—‘6‰ÙªÏ.?r_6j®?nD?nßó!Šé1Q±ŒøPi¶÷ý*b|ëßÅ¥ç÷áÆª`ž>¯n§´2·iâä ‚¤œ)IÀ±7 B3æ_I "S§f¤ÑüòͨÝNò=È](•ioïlH[ JÚ®’S±©Wø:œ×³]Y¹äÍ~=•!;f)°cª“¥»ör¿Ú¬,DSeUí•B”NóQ¯#õ»,X05ï9Ð=¿ýº™ÍuSt¯hƆ«&à œ¬ñ©Ðuš‡~³ÝuÙNçÓq¨‹jѰ?n%cöÚ.î·]ü0 ê‘„Žëû­×36æøÄ뵌ÙÃZõj[Û—)ïU??íû„×ëtÇ¦Ôøá²–Tqʇ¢åâ4j8¼Ê3æ&ëÃöbê=±Ü¶‡6Ûqb¤8”K¨é?rIëÖt?0oË™øVp¬ù5IëTãšë÷È÷íߌ¥ïy“ S R?rÓu‡•=a`M?0¦X«Ì6<æ#ZÙª ÅÜbeI(p°?rø1 Ϙ ÁÀAá…ö{†.Vhž‹‹•! s𩾏‰ì\?0ù«†gö¾1МþÇœ\n¹PßÔ*c0sªÐ«Ð]ЉŨD¹GÃu1iN`xI°j`2?rq©σ݌ÐÜ…??i¡?r Ä-ƒ^`¼xt×ÌV5·?rbB Âe”?0F¦Ã„«œ.í—[k˜+\tûCýÍáõg}h–õ2 F¥Lî¹Ú8ìðÚí˜w„>×sNÉx“áþ•ýÌ2=W¹ò?r…Ã!N8†æv³·º –Y¬Æòˆ¦½)ªV…ãù?naÀ>êh<ÎÇùÚW¡ùm´%¤[WËl¼æË㩟¤á*Sb…ž1¦û%è2ë…Q®¦EšßB¾©?nÑìiÛC”Yîõ«ÒspïαöÒvÍŸòåÖa+<³èž?rØWŽï´Sx‰GÌ¢°2¨ö¨çú¸6›ëUV4Íň¢æÄž¯6M pY6,jH ~­œ5§Òì+5üÅ\?0…Üe™æôC¬nà???nY;LÄ1¸2_åkRC¬ŽW!ÔxxKú;šz¼KÀÿœúGÔ·5QhDc0©ÆX¾Þ9ÁØðLÄ`Èþy˜W"Ýy=ø´€aK¦0*Œ eë5FC{>ÿöÏJœ`@8î9o5qòÀ:1úIJ¡¬Ü?0aVP¶<íãeR˜lþ·ôj/ (.?0·KBù@O„ÅÌ*¼Á´o‹èúíßþOàD·®¾žÜàÁpÛåa”»—øó”K§ª³}=íßKrÊ®?ráAԘū«(1z—›yS•£, +)*PØê16u`¨¬Ðá1jx†©âgÔiwêÂÔ™5€mçƒzg¶ŠÄ3…©HÐĘº[N£fàZÉ]Š¡[×wuÉ$L§óJùDÉuº ­aÞCh™5íÜl!íKô#X@ŸÎÓT+™’æÊ£ûÑ¥NèZê\|=9??f;\TåÛ.'3,Ý¢4`yŠ Ç7"ŸuÀQ)F¡¹‚˜v4«LEX•¤ä·rLâtÊCÈ;Õ¢™q+à Î#ÉŒjïG&ãêyø¹Žuj46´’.½óGƒJÂS‚V%]?0??pÖýÎmúÃqË"-NYH3²ñÄm}©4Åe's@9÷Ç9vpÏ <,£÷0b¦¸ú§;öÁDu’7_ûvpgªbIÿS^ôc÷lÔÈ ÅMßõúÈî||;§ê ˜{>~.ÓøÎ™ól†šX—¦\ñY°ˆ  “ Úx£±fH¥Ä!-Ïÿ¹'Žˆ‚wŠß¼D­e.&€]ÚÄ2<ÄÈ;SÍÕÆ{öM°µ“+¢ä‰¡aãÙz´–ót&ß²•$çí³Û&宨Á#€[ð'xgÎÁ¡NÛ*Á\,J3Α#A‰¼ †rÅy5åp«HmØ#Û9¯² x}𞣲ã2Ãê%‘ÆË9xPjn•Ç„³t“sðÓì™5¢•wúñìóƒñ ‰6T‡<¬{òRñE´›+íêÞò¢g0aHWµÛb•\‘˜>-ƒÊÕ² ˜¼?nŸl ‹½ É›–7ŽHå|÷æs¨)†¯.?0Õ\i’ =_‡ÇšâI®m˜þÍòßÂ4¾{ñ[aÁ€ûÃbæ­§ÚØÒlÐ[ADÙÜf6lÌÍlKkr²UuÕ¤^§yû¬ø^£ºÚej×}tÏB˜÷ÉzÒââñ)“V&›Q¹cßþ|êíïî»Î&¾ð±CTWÝ´ÛewƒDðC!YeÓJ]$kÉÄu’;‹r]ðô°Œ°aájªB‰#mº IÒû’P_”ír/(|n­4oÂÎõî3™Ð±¦îë,ÔMæêD¬ùþøÎÿöÝ7ßu¸kÔ¦ž/ª²·üÕT/ÐÇ$ȸ“¤Éí2]á3|!ŽMcÝL7€±Ž±ä¹0äÕ¯=??IãôËúY=è?0ÅLj¨îù®Óúºjš`3Dà+F–DkŸƒ'û„Ÿ›{¨q"°z ¿ôí¢…@6W¿Ëdm_'h&˜ê&"´=fn=½šÎ-6œõ…8¹àÓК6WŸ{V*»²ý¼ÅA6rôøÉ“ÇöÒÿ`å!i ñA[–Úg.‘ï?0óŸøqò˜Á8Þçà}û¤-3·õ÷3??,ý9:áÉzx}5ò=à!Þîùõû5œ„î†E¨ô›®ò`áU–ˆ¾ G¸wq¦HTœY³éùŽÙL‘IÙkÔ‰~ÿúµ>æ$“Æ3äOI”=!„Gû»¶GàýåõþîÙéßÿêõÏÞòöGG=ïèpw7Ï–Þáb2h*4˜F¡H•yË'Et‚µF÷‚’c#ZdøeCSr‘ËâX3Çpì1 ð?r'¶†d'(Ú‡Õë ®†%Zo¼Y ÑGs«?nïåËÑËG^ ™šád¤¹*¢iÀ_p˜¢Ã’é®~¿Q„NÛÓTÑNù‘2FW!ó­?n”C‡ex§çÀP+’ã@jš(çjŸFcAÂÙÛý=™ ֋ȓ٭¦rÌPeQ1êµ³A‹(ùÖešs’+¾m'â6ÓM*öFGB]ö:4/åtÀØÁ Z…$À%c†Y@Ú’M<‡]‘^ІM¦º´ÄéÚØW䈭¸k v&_m (ÎÆîK‡†¥Í¹ÈæªÊs”„?n´hýáöè æ9ð=vW†õg*¤hŒ?n¤ßvÞ¯t4/ñSŒ„Îó¯WÓù2ÝP |‡kIëU´Ó qÁPíPÒ =”Œt£¤I™&—U)qÒöC%–<ɰÃãçvØÃù&ÔX)EŠoý?rŠÂE%r «ÉDTa_Á>ç¢Ù±ÀkÕÔ0Œ®¢‚¤¶ MVxÜŸmg`ßËðüè¨:‹°×v7®ÑÇ «kª˜Žš£sÊÍ ÇŸ”Qy︶ª¢[ìw_Ý94´ý¿Ãó¾[?0®pß™~”×?0>RÓ?n 5x›ƒ½cpô†_ÏZnܪ5v­Ê°nåÐ^Lä9šÖÞ{m½oìElc©–“Zw¬VUßÛŠºß‚º6P¹æ µ÷A[ï›–ÞU¦®ép):tÀjí|ÔÖùf³óTH…M_-r èë­s¶F•kžå?rn>ø?rTl(mK0ñ•V±4Dõ¹}œ5Qe» š[ž9ö„2]Õ‚Dç0ǶÛl#~é…jT;ÃjÒx¥$ž5Ô?0¶õRȧs°!øÂ§·ãó!)%C¦¢Ø—¦eЦà?nÇmLÛã§«—Â$¶’ã×u@ƒ4æÞÁoǵ‘…õ¨AšÓ0ì‹XÑt^¯!µØîÛ¡l…Ëx;h˜&¢Cæò|¹O^Ž0¾†àï=óéEX%ÜÆ›¸a2‘Až"¤9ð=êé½þè{‡åÛgµ/¯¸§×eyýÝ›½ƒë½ÑÁàŽùèi~›_7Æð÷hBþ šÐÁ~ׄ¸%OÇLeÿÉ“=LÇ‹H?n¸zEJÍ)UィO°¥ÀÎ<çV¦;W¦üâ`|Á'ë½>ù'åÉ$^èŸ<“—oïäÈ<øÊõ‚¸º&ã½}f·­ñ Ab½:f,жC"õË·¡Mpþ²;:„—?nÆA~ÞƒI`™ðr Ùab*mtùÛ??W"b:œÎfQs‘x¹¹“¼o½Ù|à¤#ž$ôªï6|½Eøi£rfùúÃéèÉêêêá¶?0R?0 p­H§iLÔóÿ4ÐÅÐnù,4üî¿Úq<*Œtàžá‡U‚Uá*mâ1â‡|$™Ó¶*XPä nGV¡›( 5ZrÞŸ%•‡ê¹à>0É=GÃ!ç+4"Rkj +#Ã[d˜÷8Ñ‘¢{ÒˆˆœxþàJ^ýeebTp 3V}ºuµTÂ(OïWfÉÝë0`eG£1o1mðÁæ”ìšÀæFX\$ä%×á¿;8M9™‰ÅǸí"­s Nõ°{¸7ÀÏéLã¢çRÆ“ Ý¨þs_ÂÝ6ŽäÿUø2‡åQ²d·“ØÏ¯ÿ¹ºÛÿMœŒí̵G%R;É!$;î=??Ä|Âù$[¿*:4Ȥ³Ù}ÝI$D?0?n…Bå åçKU?0\„1®Öíúí3Ò`™/‘Øp”e=ÕX ¯4ôvu¼·ì4áæ–ÒAǘ[l†Šö>Ý$Òè÷n¯œª$Z<“¯Ò9×ÌR·è¦”É›|!9?rH/A¸(&ÖÌ.——/q]wø»#Ëÿ, h[’`ÃuŠ*5We1 ¸šGÓ¬~à”þCÅ‘Ì&”û^o-§À=¶×P¸&®*”™Ô7gú\r]˜Îuþ¿ë™ˆJœ¹egZ«0/PòŸ4ù¤ŒVƒˆ¨ÍæÃ *@Y¡SØ~‹šªûŽÍBƒêÕJñ5;»úJ£5Uƒ˜!°¢_GåɃ7O.¤ï¸ý??Ù+_Þ“„I—‹¾Æ±›aéÁ½}gÎÏÞ¾Ò¾á+¦§¦“l®¯£0õéš¿¿ãŸÜ~Ÿ½>qÑÚó䣎‹-a^b¦ïÌ»BäBxòìÙe/¶è1¯6™¾½x¡×±6Mã›'šèT·ÊúìHºÜèõøx??Š×.Št9ÛC±¡×^œ??»‹ßó‹Ë»??é5­‹²©qF°=ïÖ??s¢¦,2c±ËÕ<%±míf|cîy‰]²8:žõ#œFM÷Ù“W/\*u°&zvvœÞý‘öøûGà™#Òy>œEe÷X&ÀU‘çÂÖïª` ïCHoøMà.±D¼ª‰`³ëÞ#Ë]v 9$]Mº_ÚÄã`™ò=|UÏ¿Î;èõOw†áÕëç/œQÐõ÷@]G•¯û1£§–)qÇÙƒª1¤›Uâ¢uù§Ë6¬æEü¿à×Peñ’­T­«€»rŸ9Ãûë´'”T{4éÀB'j<÷ 5»ÖucYÉú;â§`Påqg',BmÁUÅ¿£¬Î¬®úFq6±xñ§Ë“W§¯^t/¦uTð”±CE¯VŒH2½gÒgè²zˆnïaéOÅÕ ¼ÿT$WÊÕìɰéG-shø!_hMQxfx`”„P:÷‰s¨… YÆhæÜš+MSqcCØ–Ñüº—:n[ḻèL¨ZÍ,?r5“)¢ÎŒzN%³8ºæKù¤¬—© çâ*ý¿ŸŽÿ­Zš>l©¬M=z­Zº½:ÊÍ_KÖk6›Û>úÌ_ËnU&ükÙ< –üþ<ä€gIh¥›ŸÙKP¸¸jjŽ/_Ÿ·jŽ{ì&²¹•úA“C¥ác˜‘Y|Ìg%‹þž¢¯Á‡Œ§@jè©iAýÈÍz"7>V|®*øâù&9™~ûí®LÑ?0òâåËvÝù@AÔGÊá䛇!Ól•SUNí©ù[âGàçÆ¡ ý?nÜòw¢ü=™|p4Ì©ýyêþ¼g޶4:TÍw‚œwÍ÷Pò J+Ÿ†äSÑľ æÖЩQETE~¹~WreP^'ߎIgxàT—by{㽞-(”¼u®º0>oY„–rÎ&ØÔLØ3MY„œ(söùIÙKs¶5·Ã:•ÃpCŠèšÕí]Ýà‡c%™xm«ó;­ŽÕæ&€u7¯wÜ…WC¹þ‚Äkê¨ÿ ᝻‰OóÙváRþôÏ—÷œrɤÿÍdoj,Ó}:~P‹¤ q¡r\ùN7ó0çÛþ7l$Ži‡HØ„]á’ ±¾e¦«ŠGœx>D^×—Ú“Çû6¿+'Fvë“‹XŒ6ªÆÆuB÷N…M4C¬Ë‹ÏZÙqh…G™°AÉñÖ¥é@¶Ó;CG)Ö›œë`(²È^n† ™†rB2Û¦Ñci³â¼)•N69ñ·ãQ§™–F?ryb–ÄòZ¤Ä3©ÊzÒ,÷õbD‘dZâÖfϨNåˆã¨ÔCG2ËÒåâóÓsï–Ό넜¸ÓrC^ý.°B?0VÝ*Õ9äõcÌmýJ‡¢Æ3ÃïO_¶-+þ)žê|ˆ!dõL°ƒ}°Z7޲o‡o½ ¹¯¼;eÉ‘»ÊV–&7’ŸýdŸ\ÊMãT}Ú¾;Nì*ÕM L®Ók›^D*$e¥’ÜVl¬KNAïˆÚî}•;úØa½‡.i¥:›+ÀÛgqtë>ÚÛ{T??»I’÷_ûµj¾´k¬"ñYí·3ïHã‹"Î70©w #†HGI¨f+eP‹T³Ã¬ùjC.š³ÀZ5’¸Û”Agû4 ÓÂŽÔ\çYÓˆÒV¶¢) z1 äè.¨)º'˜À¥oUùGFѵhè$1Ñj£ÇguÁ@û´ðѳÝô4z<@Ó¥íbt¸ø÷ð_ç±9𥙥Yqe!ƒÌ\u0K”H½ûÒtAÄoì»*væðÛ³Ó??]’½‚ë?0Õª‰P¨?r ³êAuÍkÒÔã<%g~‰–3»õVOݾc¨왿oØ›_þЬuò°ÌP24¿‚¸çˆh~óörüÇdöüÉ”;GðÀëø[ú›zšÈ??¤<ðôLfü׊ìòÍØ¯¦IÔü½¤Ù¶øâ$QŸc’ðé<Ôþ‚$Qç$m³"7›>?nÈ>UÃúf2Uš®P°ŠNšs\©ëÐJm«–ß6VéP,MP"­\oócºÄÐkmgw˜.}dL£é0÷2îí‰Ê2 Ëo·ˆ@ ¿µ¬€¶ä` iÄ8U!¸;j;AŸHˆ‰ÝLªJ/’ˆ,hÉ®»ã8o¡=+šö"¶vÞ¿*Æt?n{¤F‘ÖfzÓv\<‘ÄO<9ñõé´Ûom§²?0¬ç[5 Þ5„ÐÝð¦ß”ÛÛsEdåòܘy'&W½¢%2`Hr\ZÁ—eÇUšTèQGÃ?nC‹Ò¬'è>zÊä*œGE÷áÿÑ%Goîä^['›êélçNËÛFQE]2m3îX½Á‚‰ú‹C­YŸÉ>Ež¯:¦°äVWxKbruÅù±H ;®0 ¹Ñ7y¡w1A£—t¦f‘$#wòPp N©š‘ÍØ2Ó¨íþk“ô!?n¦¸V’T†ô7'ñ2Ã.j4ÕØÛ¤õˆÅ¹ =ÈkdLCqÿáµWqGð#àŠÐöÆ¥¡'Ø€z?n% τիÝûKÁj¼ÐÐËZ8ÐkŽx`­<<ÿcð;Ȭð\¥ˆ„´´4ü“¶ü“ÛôáC§ébKÏ.U ™*6¨›q>bŸÃJ'~ปUÊnVõ§??ö.‚4îHáÄ%\¤±8b~6~Ê‚é‘=„Z8¡¿âœ¤È†èÆòp6‚Ô‹ßÕgàLJŸ/†à•AZÛ¢Ïݵ†?0‹§¤Š6~}•Ê…F?r”öED¥ÂîbÏ·{îíó<*š+ìZ)“Õ"á.þ?rC÷uRM.HÏN0¯9³ò[GѦhfƒSäÞBŒ’8Á­àú×´ƒ´NúéÔƒ›½8/•õBnuóˆXÔÛº2£-Lc½NcÀѼ¤Ï?r¦Æ[¡ôA›Ë„u¶™GŠ Æ«‰`dérŒ¼ë(N>F ÐüªÉ¢73Ѷ›—FðÔK;Rñ,ÿ£ÃO‡œ]¿VBÐ??ÚíeûšÆ1'>@7é¬bÊL3«kÒ‹šTYëÀŒ4›,é3ħRû ÁŽÆ©‘1§Ei·Àßž5H‡Kͨ$Å[ÓO’„“º&N»ˆe5VÍÜèÁ%xvÑb²¨è§ú¡¥YÂA' ?0•·YU$vôšåvƒÛþc©S¦!d£%1b¼-¦ïGèKF ÙLócvÜ,·ÅÆéP}‰ç«$Êøw¤à9i€€ÜîŸxÄÀÑÁ­¢"çˆÑ¸‰z»•ݽ"Q%ÎlÁo9VŠ3n¤#Íaä]!#ç=vÔõzÇ’:²tµŽ—ƒ…’Bý))•íôØ]×›\(JØ(ïÊúiàälÖ€{2=XzU“éÆôqfºë[Å%-6¤V¹‰š[ÚÜšhå@6Ž}Îf¶µ?r¥¯;ÏuO|NçîÃí‡[÷üë÷1+i²÷9— ]¾¦8® )¹ñêè`‘\(M@ëýªFÏ}ˆp}¬%öJ¾%M†êÈ.¸ê{Ô½!Ð!L6Ua´L¸Î؇t,q\a¾X [Mz?n-Ä«-*9ó¬ÌbWèLÚߺÇ$—g­68ê는qQ—¦Ú€Œ¢Ú±&J†ò2K??ÒÅjEg—šÜ çfâ¡}YÌ¡ÒNb>Õ¬¶n'^7æ"ê3ì°;#èð\8NL©ÿ21†7“ƵÌFϨµ³=/]ÅÑ£Ç< ä1y<Mö÷釩5 xå$f¿øöE‡sñĶ=n%.\1°áu›PvLsK>?r­k›²2IÝüŒ,#>Œy>þ8$ÕF%È>}ÿEÉÖ$ôcWœÍXÛ´ÉnBu÷µ® Ž8*R,lÉ•/œL‘¬g µKðl– 7ÍÑÈô!ïkf&üËu‡LVé‚ï^=yöúÂO/,XPüä2þ9$ƒi&ÄB6XÖ¢Ò*gTõ±"ä–÷Ö}•‹Ñh¼Æ"•Ѐè%=¤øéìì‰lÝ;*h'ørt´Ãg#|šìà y«•­ÿ ïè„ô@,p@o”ׇô×;K¬"¼_v½jf¸ “Âô_VåTaoYbQGÐŽ\óB?0ºVᄃ ïh¡z烛Z¸ ç(Ï>g^rðµ6áÛVŸKœÉÔe!Ö…sËÄZcµhîè,o[€¶]³L<€5·¡‚ÛšM"M ißoéËáŒ(G94p\tšy¬¡±7p‚uŸ*kÎô7¿ *»INºœ—è2›v’§}Br”fÊv`hÕb㤙œœ<áN@ø‚÷][{¿·H¿ÙäØÐøà6\ôª "Õq¼tÝ”¿„.ƲIX'´UÑoîèýœF.PxÅF·N…ßVmŠ_V'å§‚üiS~Ãê¶ w÷²ÉïF_¯6Nà›•A”5‹äx™>ñ‹‹—ãË—=â&X\Agëy;nVãÍJïÆçN˜Á?? ÓˆÓg?r†`z?0€7=¾e¡Ägv°?nÓëiHD??LÝÛ™Ó‹ËŽ˜ê:B+ÿô2À5Ä¡±êtZ\Ysê9íy‰Dý„'Yý´ôn¨Ö¥ä­÷1.-–Ôì“#ýô¦FÁúùÏ:¬[(3)6?nà€ÚÙ=н¬¿^À¹âøŠ’¢„ÖË5Ô"ü'H‘(Ú>Tá2A$×ûÆâŤj]½ü=NK'x¾áÊwP7í^çw&n 9£ˆS+_=|ÊM¿‰ìu‹¹ M\DsOx „™¹s-ñYÑyßÿþùÔ>7™],[f ¼»ˆuà“| Šûø5?0n*âyTÆõ yï´Vµ?nn¥R¥w7ò´è?r|§€–ÊÀ=L÷µé«$²–õuQïâÙ}!òô´Ž7‰ñ4¥§PmúÆÓÃ<Ø¢”nnÕÊVc)¦$ñHLm=6K”ì?n[S6Õ¿“Ínµœ¿kkBf5Vëé9”ûÝ`~T ö@ÇöD¶>ZóæÄéENÍHæ‹¶òM¡¿çø8FŒN•Wsr¼??YÿñÉùÙéÙG?nO¬ÐnÄÒPR]±Ã˜^ÓD¥›'V‹ñ¢µÚíáFi’Õ5Âêèz# ñ#º6Æ4Ô;k¢ŒšöÕ]ƒœ‚ýFüÔ>‘ãhÍ“‹™ž»Ã:ÇqÅ¿HÊ$5iæfÚõûÿóÊÚÄpÊ|ÕI)-ÖO T“æã?nÚÍEÎç‚«mTÆÇ|€h²[¼3ˆh»¨R}u(A~ܨ4“»å‰jÉ?01j:—3NîtÑ|¸E™"¾ƒ'W&i þyë}Ú¿ÿnhùÒ^󛇪•%Ï™_Ö+Å:ö©ŽŠëÈNvR%%()]k½râY#ŠˆÕõ›xJ–ŒYî–?n² a&V²]ᤆvw• ½ qºàY½ ž??5»d0*Ý^x“¦ë3ÃQŽ)??ôR¨ÝoNJ—ÕéÁïq”— ¾œ$l¥K¯·âûœæÊDr2 Ýûá{ñ¼†þ);{}ùâ(ø#&¼m&âÎŽÆ)Ô¬>ù¢.ãi«F%¸DL7R>1acô6Ã}7„“w¶Ôh0ºÜ¸au£ÿpý¥PÁxŽ¿#œ‚A’÷ ®!ºJ?0>x{Ú¸1Ã>Õï*©îl°6ãœ=ôfRK%8iÌwžÔo&;*Š$ñþ(a¼„çíYKƒ©c rpxHbzØ[ßOÌæ$õ·e¡.×­Äs ü.ÃÔrú¬9%¾O©Ñ`rU‚´ŒË2ÑtçµÅ$¦ŸT¢†~©¤Jœ-4ø•(X%YãûÇ'çíޥ΋  ¹rÿo˜ßè*._$UÓ½ñø`÷›ƒoýÓ˜ô›Ìʪc¦T-¢îÄM³jüŒ÷ö~{÷öþ„Ê w§ˆÝM‰»Ç•AËi¹¬Ç^Ȇ?nwžB±1VGurië[m7Û“‡¸»Møêþ$¦Óú??7ÍiËâ¦wþ-4ýdò¬Ïy˜V³`¿Ãæî³?0õ¹^f–bAœ:¿S?rí?nÍ ¦”¦ÿº‡R%#¡ „XÑGE÷òÉt brýdD5h!¡~u)"TLøµ–GоF†(Ε˜èrÝ'ºcª@H3Ÿå»??Ò7mi”Jú¨Ç?n‡lW´ªƒìa튑úÍëæÝ®Édpd‚Fu7m*‘¾`™Æ*þ9)sàÞ39ç_ªæºïgîϽ¢­€”¢ÀâBk™.bÔ¯2&öFmÿæÍùñâò§))P ðõÙË??Û"pí+N]ÏÔçÕº”ˆ½tœfqò!1TâïûÛßÿö_VZ‹ÍfI{ØÍ*Ÿ ‘ôÖÕ½Ú3j2×(rc‰÷öŽqtt4~¬Eý´/ý‰×ÍÌ¥þ‹îEq¾|ßËÂnò¶Ö¹uáQÉØý lò¦]Ϥô&ÈÓÕÝ>-}¤®çá2/úÐêdÓÄéT+âÖ‡Pð%â$Åà”ܹI?r$©‰¤ÞRœšUÌû›¼ªSÍ.]y¡¾æZŸ½À4½{'3*·{úÕ™ íä'—Ù4\[ÞK´’9vöƒ·Ïß8e‘R)jÆÎ9äYZª£â Ïœ‚ ùnSvÍßAP|¢è?nÁ•_Ǭ»™T~ö®ºµþh‘$,,=Ø?ng&€[FcP;}?r/žï|,EÚSëeóÏ_ ƒ«Œã½ÖÑüõ'òñâ÷™"H}AqèEñ³%ü?nøøÇ¾ürÒ C,QØÒ…,!'¹ÛŽõ¹õ‚;ìwÈà¼ô”kX.þ~úuTv÷„VW25É*¢Ã¤,ó’¸(T,k_ñW\%0b??×Ý7Q8éw¤»#Di&.Ó¼„‚_í??|Œ ¤„P‚'xºc4rc±ñ¤ ôüX5®»âåÂ;Õuã†+K5??*˜oñIU¾byÂ!‹“ým(Øj†Çì¶FR%9©©d©~ƒƒúpCoTHY€É¶É ÷1þ¢!Q‘ÿ<^ç2¢6?rÍöú€Ðrêûš„6º3}Âé›Ô$£»Š??Í_©l"­s;w3-W<”d¿@—_?n(˜ÙäâK\ô…h¾Dð^×YW:†uR^qD”ê2Ü­ÊwÍo ‰@L´Ö'šE–~Q²z¯&?nûN€-&?rÌÖeÀµ4«Ž›*G센¥‡W× Ó¨Çèªera£O5#“[ÃIÇ^Ø,\Ø%”³äªŸ„<#ÝSÃÔ¼[$Ü1‰cð#bˆŸìɧ­)ïÕ˜a­Cn2MaªÉÂàÛ>F:ed~i~c(i«¨`?rÌaïÚï&—ÑÂ?0Um«rwuS_??U7½¸xþFuSÍ/×CªMéü#Ç‘ø¡¦– _IË4Ÿ­eɯ€_O?rØxûéUìhüÝrÞ³à‡¢EfŽB?0LŽ+T?nÚýÄjHÁà?nÅ\Õ#?0ÇÜVfš5ÔU¯í6˜„ò ÿ¶­_qbåJ$,uÅ?0»V»^¦Í„oOŸ·ðÍ”ƒo/Î+…IÜZè©Üu•µfí›MOA°óý¥]þÁàò}A®=½{±i‰ŠöØt) ®ïÅñzÅäæP[kÀãéäó¹è\|>÷üøô[gþ>:N„#'ÂDÿKqœê Ô㊠+#¹«0ÍH!O7îS”mš_- ÜÂI2÷\ê¨IÄéæàØ#¡f(ù±4ŠjsqøA÷Ìþ°59=æ«ÍÅ×’eГC<»í™??ü¿?0?0ÿÿì}Ùzܸ’æ«ð¸Ë£T™©Åå²­V»å²ìromɵ×ч$‘™´H‚E©”Ϲžýrnûj`¶÷êGü$Si‚ò2kU’SI@ ˆø~¸K±¬Ó¼£‡Ža¨1?nŽNM䳉)@C1ÓÆKÞŽÊónÔpðÒYµÆl@ïab„ûÐ%Áæ9Cïì»ûêwg–¦]áÀ‰šØ †p¡°‹a¸sU’Ð0Á ÂŒìêm?0É:öÞ-HÑ?0“À|P”?rìxpY"ç\GQª)êÅ÷ǯ~xõäÔI­áÕ qªÉŒ3%$.ZXd?n$·¦r"¤D§á?0qìý$ªÛü‚eˆ1o%…Ò@h@Q#×Ô †¯B‚,ÊÝŽÛü5pFý:Íë¬6LËÆ?n×°¿ÍÂöGÉˆÓÆ„1¢;,»8g—•†ýÅz¥{ÿØ2ˆ³Ù€Óh”\.îÅ@Êl%nJ?rÚUzXÓ|@ý°}ÐJϤUA“ËKì.Mú1M@šeÐðË“)©ÂóÔ‚üÂî‹?n”$2­QG“RÌSÉó„sVLÙ\Ë_òõS“U1£Ä µ¾©²sC<>ûRoÇOÎNPïOrQ&èàKõ×Lò%ÔçS€˜bXŽPãCQéȳ³3‘îê¡¢t3‘Z°íˆCÿ 7oI=;›ÉyŸ·{q)y2;Øä$0kû´ÍÍ–…sd¹C±(Y÷ÙÐ5ƒWnXDt´¤*m&÷(l?r†mnÍ­¬ÐÎÒ$?n ëævGR”ï’µb»¹þtS lqÃ;ܼ˜´ð–ç|EH%qx~Ihñl޾§×ؼ²·vvÉz||j_ .m` ‚J´Œ"ìèæy·ØO;&ûë‡n“½üP›ýôöÛw½úº(Z–óí»}¸—ŸÚì-ÛâŽB†Q8ŠÑ$$ÎÈ??Ê6£¨3']$ræõ7~ýu|ÅÅ»DnÔåªÜ¾oë'ŽkyÌÌž!失Mk!åmI3þyDôåN=n#ç¤)'ÒG%¾‡ÜGHá{‰Ä¿tæèTf":æ‘q¼Êd zîvû|Ï´9£3¡öÌŠÉX·½”W5â’ñ_6]C7]åGÚç˜×ís‚¢/Õ=rÆ‹ëYæ2œOž=˜|ûäÑd{Uè Ã\ÝG¨:`çôô®Y".ób½ßð… z²j…· ž+†#€®ºçd›‚îàx˜"Ç_œÐ{ŒÛ?r"*ÖòÖ­¯‰‚•ã¾,"Èn]Á¨íáÑóÇǯ^¼>AE—ûïyô™ É?r³¡AÏúÃ^¥Úîtw:Þ(O4âg$`R÷??ð|W?r„åžOö ÏìW»Þã¿êAK´Òzk° 5&g˙ƒP}|MÙWØP£±«½g{1¦n®Äâ¼D¤×À„9Z0à(\ý‘!:øêL*!Ãí[ç¼Ñ_y™åÛ´¡éÇ*9y#LT+Þ½°×&# ìÞ°~5U¬àCD¼€Þ<SÙ_ë¯J—¶¥' kàRPóе›üz IÏ\ S†èA³ˆ!zk ìXlÏéE¨‰mÊ??ÓiMká^¼ôŽûÑ–ÊøxÃ?n¥ ,F–)žÈÐ?n9pž7·{£Vè–ù²ÏøÙ­[Wû'wÐ6ì©'51#_ñ°ê¬Ô/2Ó+Æáþê÷??¸úý­~!J«%Ï?rœ°.ÍýöäÛ¤´!Èé]ì´Âp‡/H;T}‹(éð ,«Ù¬ÛáBœím½×÷ðD·ÃñMoo¯¦ÝŒ‹gÝÚ&õ6ÖœzÈh—ØŒÙÝ0¶½x±+öI… x֮ͮWŒø³ˆ‘$ÖŸMBÀŒ0GˆˆzGäj~¾‚ÐÛ1áçëWÝÓâóµ;K>)´¯³·ùÅçkþ@¦ÑçÓ­©½?nˆ>Ÿܵ§Ÿ²rêV)Ù¼_Ý,û”•¶öôs„C¨â3pïåµVªŸýIU$}¦ fð`˜Á@ów_X²rè2/ØŒel’9¼’ÐC‡ðËF¹ˆ³²o³ˆšmlÉn?nf¥7‡y11_h??l”Æ™1e??}J°ˆ¢av1xâ2•*É£‰f?rÒ¶u-‡òÒäN?0…¶²¬JÍñP)J–n¡ï-S¶šNð;s?n Ëk €NiI £nêÐ9–\¾¥È-ËKWÝQ•Tt•v][ó\b>ð`³kÞW…U6VaßV3µ›Š»ï¸Z¾?n¸u€ƒDŸÓ¡,?nŠ;µg‡unEÂËo¢F?n›È¸¤}"/¤ÈÐ}ØÊ&"‘í#°-RÂm°Õ#6º›¼w]¡lFãLè;™>¼1¨äúVœ·B,Ö€3ÆFâA"P)?n}œ#æsÕUq¶}@ÛxÚhÆcsÔ[zßž¼zÔÇVËq¼Ôß8½ÿ,½ûXoQžÚß´ýu¿¦·gç!}½B*}´“ò!jlÒÛ(´om~ …ŠK-?n•--‹¸’ÀPÄŸ]ˆ³æT6/8Éìá’ºXågôx7ñG䥨Ïüêî@ÝÞ3Ô3‡ÏŠxoÈeŲš¤DÆ6Òg“)ÏÂEÊŠ^ŠQ?0 ¿ót2›y¯”›1<x—Ä%Ö¢)ÅH~7wFÍ¡{ƒàíé$?0ÉMÎBéWbÜ ¬ŠRòH®¢J"/Gâ˜Ùi?nÇÌ3¾aS…-·Ÿ£P>p9ª‡zÓÐ_n:PÖÆ‚^v8pÁæ`P¡™ë7Ce™p ÛgPqTÒr3ãR‹êoõC×Sqì® Ÿ¶^¸ššQÇë'}ÅGƒŠ'»¦’l~Ý?n¦Îòs$Ëð(˜^ë~Z0?nñp”¼sØ4ã~fG?nF³‡^“(F4»ºñ\ÉÉP‘Quà)€²¾KçØeqÜ@Øx÷›˜_eq¬›·íÔ€ìÈýü* æÙ d[јl‚Ü~®å$ÛòzÊxÖÓp'§eÝT·6\Z]àžJ–"¥SNÚ¼õjPůKTº%ªú߃.L­î-¬La¿}G0å,õF‰ÏeÉs«HÃHjbÇ€3«¢ ˆj"Á1‘¤ÛmZ{ˆ°íêØx Á*ËB‰‡ºP.ld-ìû(©]9þvYàÓ` ’u›mžÁ“—µÛBüèy…/D.dY[³KŠÞmø­ˆë½!´2çífœEÓ³8ïîä?0}Y^ÜÈÑÍnÏEÛQ§½l76ÄÔY–Q*õ^‹O´J§™R¹˜(±Rh#¾’T|³òBsÍfiÎç“<Æ{1YÆšxGzVÅsæpħ÷/ðÎZÐŒ£$f6Êô‚ %÷h\Aú˜–lÌfôG!¦BGt?r曞]„”ŽHѪÐ^Ž+:yYÐÑÛÄÙÚAú­Í%m’è½öÁ©{Î3,+ýz»çãàÔ0}øˆñFŽ×ÇÅ^m ‰4úw<³3¬õ±ÄI +"Ô§>.Ù¼âªÆÃþ¥G¬cròÂÉÓòøž„Õ¢:¢$F?rým“žÙ@Óò›Ðgò|xê»Ã0σ›7R??éÍ›‘úç»›7ŸÝ¼y‚ø¼ñê­º??‘‡_~éé'™Ÿû}GôÝ\ï׿øßÿâ??úí·þÔHDÏKÐË i‚°’?r>¦…ËÌè4´Ê$1[o‚ÅæŒ¸ y‘ø$Á`9,éw¤í˜rã8cFÖ¿¡eA;eÚy ãùee?rC8ÕûŒ^ÕhKvÈ;6fiþ:ÎùïFz7پȃ‘v´¿ƒe­ŠzWÊÈí&…S)©ío3|ˆ<Ò*œ€Az»-‚¶`žwçDòêz¢¸æä˜‰lx»þ4š±e¬¤ûu¬~¿# +#«ƒòñ¾x÷'Jf.E.ÜôîO¿Ž/x’šÜ£ª]¤zˆF“¹e\íªwÐKòÓ4³"¹Dyά;f’Üc gq1âR¶©ø± ô¯Ù«×׳NŸéFGU†û÷*/¾TCcç"¢?0Uo`·éi!SeÉkɳ'Ïûðƒ»ÍÅqÞ©ªƒ‘Ö´–N´|íž,‚¨ÔQÍ=÷t¼Kà¦*Eû‚–­ó?n*á®Ìb,‹È‹z*´®êbT ¡¬;"vz-Ý"–nbáÛ®§# ëÀXï之Ë2eìòÔÛ߬sXø½rMmtK}iG÷‘¶‚o‰®)8Òk/Šxg«Kè¿¶Ê àßL#†‚C “çiÖ=Zk³F¼7™ìŽ÷Æûck¸+Vl’ªå€2× §H|éQA5À=!mp ªÒÙýµÑÔÂÕxò2Vô«1¤Fæw|ô°_›Ï¸RT$u¦ôE£3j—˜ÍÐ,o´õå&†f%ñyk³ ÑÌÔ¶%µƒ©ˆµ/ýL !žÖÙ9>cX›l¡?? ;,H°‚‰s\¬s÷=}¼Ic?0O´Ú¢?0^€îÃ}§÷¬–ɯËÞç+}H¸Ç³&Ë^ !2Õ©šºø–{ª°¾OdÛ´cÈ ©APW`8-LÝØœÃÝèm.tÓ Û8zº)R¡Öö†ãëƒ6a˜éd£ÍV6+šd™å‚Í»„/èuK×»\w?0쾬3E½4&?0µ joèäôá“ç¦ö#¾}±÷†&„IÜ¿$'¬Ê`?ne”P[¯R]LÈJ¢æÈFL;[i_4ŠÔ»Àd›'ìÒx¢i¦cÉ ?n“ÙùÐa`_CqªãŸëýøxh8T’"m·TY^ð(uî*YD7 ½çò¾FƒŒ®?nh7“$e)mÖúÑÍ®•æå…á¯Ýšór«e´÷EÛ²â|`£nÔs~C”ï@N€ñA­¡EW’µ}|Æ™á;¶GXqL\p’Øã^i²”£^À0Dݙɣ„7Ç@¾—qFö†!z3±-¢3kP¹vêà12½ÑCØj}ñãÃÇgß¾xþèÉã³ï^<;6g.FÚ?0óÈÙœÙùgiÊýßÑœè35'ú<Íé®ú‹ª„->4€nD;ÏŽsª~½`g7x}-.·dëø¦›ã/ÅRÙŒ"yc€XH´øt¢pm‹Qsõí&WqgNúá»ã~.S«þ®ÏÏY·9“žÉ\iYÒáln-d2V4?0’uš›âhenÂá¤>]ÿЏRRÅÌ3¹>!TŸÊA¸{ø{ç¨ð|^6ŸTÈ&ŋק&#¯g$âzÈëbŽÀ÷®Ð 9"ì«ûüÁ==f?rßbyðÇ?0)eäÆúæ8«e™|¯cä†ëàûS廼þž¨º›X:ž-ãBd(>PŠˆ ü…šGž¾xÕ# áÆ@3õqGÎó×YRÉÅ£??}#EPÏÕï ,{ŠÄ:ðÊ8»ô¦ØX3©®õW®^†¨J‡Eza’²¡,‹:~Åfè??W ª2T[%ÑÈ%: ï­ÈTÓZÜΧ??CçÐ5Œê×§ßz£µ†o;)#Þ=Ëé%†ª…iްZ¥ÈïMZ kÍ?rªà8e‰t0eQ~?r§ÑÅÛªÚ'\çCÅ™ña7öù=oçP5fÛnCÙá{{‡´¥qH„9~~™YçÃ<¨½>©œ»š»(óáeÃka /ØfåB$ˆ‹Jú¡¹âï¦aÃ'q¼s‹ò’·Lõ¨×:lì?nDOwYŠ;n™Ãþ•P|¬82™äŸLžd÷µ‹Rîëɯ:ÕøÝ/÷ÿÕoåâð—‹7ùoïÔ—£³w¿'gï ”æïô[¿åÑÛíéªÄ ¥ýe¥PíM¬Ùͪ ÿÿP«Mßëå,ã‰{Ý¥@“D°)Ð÷éæCeax#ú\nדऎ) }› ~Ûî d ¸Õ™*n,G²dáùÐp¯7²¬Žúo騠Êe³9ªËª®@ÌrÚ h»ë 𜣚&ËTbøžlÆ“aá×£¥ÏO˜?n‡³xš<ɹqÉ›qi2$ÛÕÂä“IÒÓ"t•’ðƒ[Ôí/Ãe.Å?0ø mi¢u”è: ÆÙ:WLRÇmñhµ¦Yµ¯´Ë×fÈ@ª‘ÈYÊFu73U¹8+Xª&«Ñ«£ggGß=y?n§þ8}qz¤ÿ¸»í}éíŒowˆUKR¶aâ£/}ïö¶¯nÜÇý»;ÛÞDývχFª³¡¾}Ø ŒyÁ–ÞÆÖ_:Þáå5*‹¹Ø\ÕrH]ì:ª¢¸[œ e!æª d—ØYu¹ØÖÙõ®°û²».V±R‰ƒVЦ³?nÞjf¹@9ŽªÒ²ƒ»òâÕñf¾s}ÅTH0¯¼P)¤?r¥i|—˜-eô ?0l®réÒíëã…ŒåéÀ g–bÀê@—îPê¢@½4m??xæ¥"!ÔSê™Þ\Ê=ŽlÓv¦,é<Äêtf½ääü­à rè¦ù€¼ÍK;ð@S£îš[÷ö1_àÀØ‹.ª›¨yЦx0;PJ/6Î1½K73ì¥aQSkêÍZöÚ¥Yó§^¶/fk³¦ïPŽ£ëVûÔ“u ÓI=Úõ´¿jmÁ>J—\¾G½Šà²W¢ý.ﯺXÛ…>}òà¤=i ÈÏãÄŸÍš ŒƒV=(ôA:Z+Šˆv¡yÁ5!zØ??F1S¾É2·©qÊŸW¥³Ì >ÍC7äƒJÍÜ’Bóm9Ñç핱„5JÔjÖÓúèò­¼i'°D]‚È‹¢_̨ê&.µ;­5-VIJ‹‰E¾?nŸ˜6ëù%T¯ÚÇ‹›‹s>„lf¿ñ§™pd/aSž±H´_6ŒH‘ÀLLÈ‚z?0•Gfá^ª™Ÿ!¶ÕÌÙ…À‰YÉ3åŒc–M8ˆ4ñŒ¶‰ú©*§³Ò%,ÏwäXrÙŠÍiôöέ;Mê9­O¨FötvÉ)¨ŽgZ™tÞœ$~uUdûuMQ”_9ç¥M‰ír6úºž†1ŸÊnÀãF7>5hœgs¿þ¨^EãKó o±Q?nݶ¶æk’¡Û(,¬ƒ_Î÷L*:µÉ0á6ˆ3 øP쿈ï’8&yê1ŒÛÒÐ4Bnûž?rªÂ TÊÆàB¸iKØ%½ ¾L“äåq(‚Ä(×£†^†§®ÀIn¯EäqÒYý+#[Æ3¿¤??Mršæ~8}ëGáÊ"éGñÔçéÌç¹ôgq©~%¡ú‰ýY¾òçêÑCýKâ7þFQq˜á—ðãÔ³—«ŸsÿM¾§~ø\ýÂÏÊϧ©Ÿ«zò¹ú7›«õo®~däÿ.b_ÎÕOûåœùe<ÃÏÌÇTï_(‰VêéUž®-d™rUhM«Aª) ÖaX ‡êŒ¯?nh¤ÖJ)IQA¹ÒØ7«ÄÏâØÏg©m0+—ónã†6èº4›ÅCÚaš`?0›éèøÍ?n°=>K{z}C_³·)óÙ­œå?0åìÏsŸI¨fWý„P‘z–þbïö-üúZµ\}ÞZú)Œßê×¹úS¨Ÿü–Yô¯=ý»”ê|¿ÕYUúbžªŸ¥_¤¾úf)¦huªÆùÒ¶˜,m12˜je¸ï³x†õ+a!~]ø,-|–㯜û¬ò§";÷£Y~‘úÆnœËÔ“ièÇ¥úþVýZá×[¥#æ§Ñ\}ˆVø¥~ľú‰”2 õ9ÕO™*Ũ/*T)æLýÌ}q®Ô”WÒ/˜/÷Sõ£?n’û+üzë—ìÜ/KæëG.ØR©ŽùK•ªÿÕ«t…_ê'??·*…õôq*Å??³Î8Ú¨Óu]lÖÆ†?rn€1òB,Å9÷LThY°pLm‹b>)c@ÔOvw¾¹û7ZTs S¤´yEQžªÄQ¥×ñ_ôtVÈ??oëàøðÑøÛMzP=T¥ßƹïAú/@…@ú€„¨–*]ÐmPþÂmŽþÀmJ‡«ú.h à&( ŸqRöa\Ò=èü{ÐøŒ{Ôdw¨ÞÚ±ú·«ÛÉÆy†ÀN ¢/Ìê|{Rs×·R5ª†Ÿý›'†ˆþà¥hëþ^nößìîÝ1«ù(§±ª¬¤n·ÕRSPêfûØi1ÁÜè$Í;[ñ|}ôôÉéO›FÁï&rÌJò{Ål6H‚ya3µ¯"9oð°¾}ðªKÒûûNëâ??N¾}õÈ÷~ß9LÙª®Mýy÷P3*pYnn­ÁÓcðG×Û£žÓ}æLÊY¬ÈS–»{]î~³í“5ZIކ›M_¿$â‚}$XcjQH„;Ú€MXÉ‹„aú¼–»´¨¿äé5ŠžÂV¾Zb¶†ïÓÇT€??øÐÿ!gC¤…P?rw”Åí[› ©(¨… •ÀGŠ‹ëç.ñJˆ7È‘ni?r¶Z¶£òœCó`c‡BÑk:…@N¥b?r(õØŽ/yÁº?0]"^˜=R¯É†j²³a’)þ²¾óÞòB?0 ]ܨÖÐN@ò>Q‘jI?0GXêç]Â΢aÂîZaI‘Øú˜4yœjr„bgÑ6®‘‡kw¶C[È¡˜È¯N†P¤ræ½.&Ü(aŠ%°¤u"§Š‚¬=5Ù¬wóäôdóì÷µUlý•?rê"?r£c?r‰à¨*’ g°¾·²Ù–K„ï"xûÇýÀHô b0äã%Ï$T Aê„ØŽ$¨fkû€r??@K®Ñ‘½xõp£áE.½|ùôøäèù™:ÅÂK§9‚=žHîݸá~oêò½hê|}àýôö ±£ÁÞ#$]’4 0Å7:©²Høˆú.tî Ò…º€Åº/÷Ò8^fi8èQµñšÚÙ’t¾$/å&_o¿ Ì-„ì¸m{«”êtÓ‹Ä3âÛ×§ óB¼!˜$‰n:ÌvìKYQg·FÓÞZ+g†® ¥v<ßóô‰ ï 1REø]€û{«Ì¯[eβ8DË?02_6 –}Œ-±,¯³^c +# ¬12&FÑÔ¯OÖ¶-'-^wfv˜J"ï×cTûë?rÃ´ŠøiS’]bpK/õ4gåâZ²Ûj Øfj5=DÙ„oJkz§!xâ*b,$5+»ŸÄsDË ÿ?nw鳿Bò®¤~xUVÆI¸õ,€½7Ö&‚hYäp"!’N0Y„ ì˜iº!.§í¥ºÀ€£­'åÆaI–{tCãMíi“KÎE\J·—÷›»wï¶³HØ?0ê*Fl—‘?0_̸²6¼Ý½¯Ím–&vWFˆr‰ÈWaòÆ6«"Zç(Õh]l2ÂÌA%\‘QË›‚ªÈN[:PʆIm½c“úk§m“Nø.s9ùe'¸û›]$04QE ùxËÜ¿…WzK??ôðÉã'§[Íû‚öŸ¡!½/(q_ÿd 7±\q¶® S¥¼çÕåyï¼LÔŸ·×<è ¬ýœæö›rÃÆj^ðÌsËú1-ŃC YN4ëÖTᤉ‰ž¨÷úFŸT7p€£ –:xÏn¨ÜÊÛëJ1Ò~Aá´?rºˆ­”^€‡ôJ?r y2Ôi~ß Æ0$â´*Aãî}ô+--jêi÷Z4)DÊ4¡á²£Vâ_bE·ëfekì!d'ÄÒ³«¿d³¾ýÕXŠaQ¬íÔgAAÕV”;êúĉ*h6jżãªy p§ÈJ>6Xz¤¥eîÉjªZ¯-Ä*\8ë_Hw¦â¹¥¢ë‘81Š˜/zRZ¦­Çaé«ÿÜ<̪˜ µf TÝÔ«Û´äÚ|õŸ­« ™£Ûh>›Ô¨RÞ¨Û>0˜»,óp4\ð)\_ BÒ^‰(ÿë$=3lÜHn„¯fBà‰É”­íÁÃãGgÏN{Ú³Ü"b™^šÂ×Z„˜¢y6±"_õÔA{f:Zñ»ónÿ2ó{€¦«‹1C`€[ i H¥H1{YÚø_häYâÙ°*.˜ÀÀãNÓˆœÁîÔ ù>g°H"Ïä—iç^q®³ˆÁß*?nZu̹ºaÏ‚O{ðÞ¦gNóaö‹QsžT€Æ„ÅŸqV’²!°ñ½i<÷~TG»E,Ï{Áƒ†UÛ¨b•3}@ŠŠÍ”öç??KžÌÆqþ—¿Ø)û`ÞÕ]ÙÓ«ï¶û%T£µƒŠé«ÿºCÕ¾ÛÇ??¾4’ëùb¢ÙÓ1k0»2§½Zð…É.Ô.Z¬d״赦ÅETœ™©q9c©ë´ó,\MMæÿ ›jyE ­µ¹+s2ßvQ F"ü̶b`òßB”p¡Åè¡:‚eDÅa”`Í`r9ÇS”L A-¨91f”r3åÆU‹(ø¦/Z£Sôö2°„˜ȤsöÇð†VMÀ‚ˆá h6…ÙQcèQ`¶ÔïC0i }V1‡Üež ??6l†H÷ bT +šVݤ]“th½äoâì?rÛó€”0l§JÛ?0Tm› UÎìí¥·uüãéуªOÆía£hpw‘êð²nÉ&fe»£Õp‡jp&©„tPÖMØR…¡ UQEÌkš¬¿EYçêAF¸”[§$F=VñM¥“k”$?n½dtåÏÄJt´¡kR>z¶Ñ†y3Û߀@QëÞ›C—â½™éA•!­:ç¾Ú_û.Hà ö½‹CDk·^Wë —Àp#\‡ix¦4>hŒ~²~[?nN𼆂?nßmòØCŽ®(iˆœsfnüó??ýÇÿ¡ €ÚKâ) ÀŸúqõW7_€¾6š7ÄÏÙe³)ØëQÏØ aÝi&›CâÐ ¶$X|}ÁLׇ?n;“$Ú€³O#$Ib9æ°ß¡E;¶ŽrÐ?0†øuÞƒæ¸6G ‚ÐdŠš³ÎͶ€Åîȉ]jŠËk½¬¸Ÿ˜üé™4©ü¯'«ŒÁ¸'±³D??ÞH—²¿¿ßÆAÇz«õF㶺´R’Íãq=)»$ër1àóbE ëd"?nüSˆ\J…hGó0½fãïÜ…ÌÉàPzïÌŠPçÈP–?0­¬f%ìÄÝNfBLƪøÉƒ£“ãÛ–TKTç¦û§j™µ]±^Bûjãò/d‰­p¿yѦŽéÜ(%¢\Œ½£ÎªI{©¨#/FÙ&d¶*F$Ìârˆ‘ZÅìÐyG<þ¸mÒ$cw¼0¦‹qO_Ï%OUQÒ·=[@‰¨*,‹HìáySJë«MÃ⹉°íL&ºp—mðà[öÕIJy'ÏãèùF¾žÍmµx¦bsÞ¦ B?0lnXõŒ—é“ÒØíHWet Ÿ¿~Ö,k['§w´3³íªG©ÈfÏ5!Ýô°Ö.¢u¢þ,IÕÛízvð°±&D‘‡ BÓÃ>‘úbwgw{½©”¤¨Wxs´‹àeUe$ëÛßúê¿(ïŸõ„mû†ÛÁîÈCˆyOJ"Ç%z¯")‰~2ñŠV„­` S…asUCÅ ÊfÐ@/Àol<ùÖ+¥^’'GÉ´Jéã¸Ì®lž hoànô>µ“(ceU°*0õtúMíI8…Ñðh‡ô¹DYà†Ì}â¼wuâITZT ïÄôÃÓ8jœ°nžâ¨Ö>¹$çú õZZRuhf)ŠŠ ;N«lKÙ?0ƒ2ôvvv{Ó±µåø•nã+~8\NÔ' ä\í;U cïžæig¿¢æÞ‘lé;8M48—ãEPŠù\Cͣň8üŽ* ê5¡ó4_þyo<þú/ׄÎDñžMÃØ´¨wºÊã,ã…{ø©1öïÿë•¡ÅSñÆ&Æ`¶ßû¡”æÚÕ÷æŸÿé??ÿ'??WòÀÞB(¦J è=ìˆÛ¯ñZ‹¨†x—¼oONºs¾=b« ×®¼9Þ|1þáã¥åâË€M31¾çý<¤3žÂÁ;aL@¨‘Þs|Ru'¡˜¡zÍ¢Mµ#÷P’¤ŒC£÷í¸°á?ný{8PQD槺<(Tû[q›o¶¨S>3zYnÞÄÃDiî1yÅUkç´×é{ŒÎ`}ó:['/ÒÉ}\#-2ëD&ƒy™n÷œ”A³KdÅÐ5Ñ?r¯Ÿ˜#}ïþÍÆ€€nMVL_ÓÌaŸìXÁ'ßoêïrUúÙLøQüÖ+î[vyÒ†¤J€ƒ@V&=‹šß'̪\‡ýcüÀ‘V®°Y–‹Ý6Ëy#ÃYŽóÒ9/¬Û=ao/JVTÚW/O,]8Ô«Ûœ!ü½GÞ «*rôML˜¼¾h?r°xí ¨ïuR±NYÖê¯ ?0tµ£:Ž„X{¦2xN_>ú½{ÅTßž½|õFŒYOßüÿJýmêèê1™k=ºF»–±·h¸ç:2jÁû{ø=àUˆ4V?näÜF­?r¶íê¼ÏÓ„áJf6XO_N¾#ÞæÉËÆÝ¼I^þ?r‰åv–SÕ‰ƒh ÐŽQ\|œPTF-º]ýå}¥ª=‘^¤&²ï‘Þ/Ó#6|“PÖNV4™AÀ’¨Òñu™e…wÀ=”Ñù±<\a_ó½P},yý•ô˜ 2αu«¡Héb¤Ò.}âBEӛū@?nÀŸJ_÷ybê¬ñ¯Àwºî«°m¥?0knqeæESÇ’@™M6£lM2jòˆsU Á¬<#Ô–YÈ?0Ù§œ0Ø]eÒ‚„»‚H¤§Õtê® Î¡·ÉýEØ×h0|¥¢KŽîŸô#”EªŸ>-o‘g8+ëñæÝhó®aÕrhÆYNjâQõo4!æQ@¾ÞÞq1:.?n9“n§3§k³×Œë÷‡Ì;êŸ_ï\)´Š!ZJ#iI{˜ !†â1Mìé¦S¦ƒ?rþvoÄêœjœèm{‡6©×EÃ΃Y»ÖÀ_?0/õ–3f惫À?0N‹"rMBQU®O˜Uvî×L‡¬bþ<ïÝP¤é,ÈB ¤˜EÝKQ\8;‚вEo‘Bئ߃”¹gÜ’R“UyFBê)V¶Ä÷ÚZî.dëcG`ì·Å]?rõY·«ÕE°¾Âû‡H4LÒaOþÜýÒ;ýô1zòûŸNߟȓ³Èð´ï\ŒÒb)uÖ rÕ€«ñ{>ÉÂnƒ€Æ" a<•xžà¸ÎPè^µõÒˆ3©?0$?0ì7­¼¯vÃ+Ë{ðÚžýìôM^»ƒ??/ͦöÅ/—·&ûöç1ÅY÷ º!¢†¾Ó'¤n¶Ðh’­n„‰¨Š5šgÅøB«–9©¦wÇ`6Š9^´déF*m9SˆàªØôX®âõøŒ®×ák¸(µ£áZTL®<àÝHÂ)Ó”JmìC S©G1ÇGª¾yÁUßíƒî??ÊèQýëܽPÊÏèÒÏ8¦O†kŸU£æžÓ—åm‰4Óâ`åŽY‰cVV‡ÍKšPúÙâ§ñ=âm>Ë=PÉ,-”OÌó©OÁƧÐ^–ï/W_îךwTÿÑ^¦ðãùLó—'jsyB ÷…Ѹt犴Wq{‚;4tXMSR“84@ØÓ³jp¯ šÑMŸêü¯lF©¨ŽÝdlÏš{TüŠáD¥A›ÆàÚØœÃÊJ+M‚œCŸíM)#ÈÿaO€ú–´æ… ©2úb¨F“ :#ûaŽx Ã’ÆAý.Œÿ0ŒIÊNª?nÁ#§Žmö¾]FpɇN%7bÓã ªU…ø†žu¿õzPʲB(Sˆ^ Yj14lÞI8(‰Ñv…¯)¥ÒúÓöÀ¯œÙ¢˜@¦ß{JKMoÞដD°„ÈBUEY6?nœå^„^á}¨ºU?ršx/æ¦ý ¬€iX¨G~*ÐÇ·^ºßÔ—x¯#,’'ÃtN½.¤:0=zV‹Bt`FýÖ .õ H¿±ÈaæÓ/Št859'­ŠTê³úiyDX éBYªWÃwÔÏpÿÚÕ~=ðË:Õ&d}$ôÔ—?r扂-®ýÒbKÁN”Ä*¸$‹š@»ÅÑ']’øí|!Sœ!ÇÝÆ9óòå‹`$"Áõp6©*‹t¤©ç6q °›Ðàë˜ÂMµ/Š=ÄURègŽTap“dQ¿‚×r€Jw Ÿ3œòÛ¾ÆO'+Í%ŠêvnoþÓ»;E_çª??Ø#!-É´D\™]5š‹Æë-78@JB°ŸÓô‰¶äëX2?ròÂ+‘hôÛ”—~¶K#ú‘ü|vÌø^nzµm?rC4›j“g®.ÁQwò­7û­9ª¶cT CÆ ë§ïu”tõqPe>henVHÎ3gù–åƒ{7#:ט6Eå ÎP©ùšÃ¬E0KÓEØf%‡ 7K3#.§UerÞê6ÓÇñúB ¢²°??þF–d?rÓ2W˜º¦«I’”¶šãâe3>8Ø÷_µ÷møÌê)VÓ9¥j±"ÃKH-˶Àõ…Uø&yã^, ±SÓK®gØlcdF¶³qo5Ûõ6Ç®«~-zUŒ¶IÆš$Eä0TM¡wÃÂ?réËûÓ§gò£¾rÔ³Guä??y?nË9Á<³xä^þ "¿¾<üÁË#¶,)ïDøa8Sé¦kÏ—šXÿ Ù?n??l}‚’÷$O.ÓVë勃8ø$ŽÀ½;WŽ'óYÐñ²P½±NÕÎñ°&á+V²«??؉›‚T£p¹úzÚÐí’yQ1òÁÂUÔ*'çYÍ|ðú<ólWˆJ¦7aUMóH—Ó¯?0•)É6iÖèRúݲx}£©åŒ&Š€«jˆY.AóDŸÎÌ|Ð…ûql„ŸÏZÍf­4‹À°ÌqñL³üšÎ·i´ê@ ÷kï‚ʯ+¼;˜P ¢*1TÔöæ'[_ešGLð?nHJ‚ÀpÀ5ONŠÅF!¹¢Ž®)î4Ÿ³bZ4ìµÑ>N??|~úöøŒf=¯ã¤v­7݇³˜àeëd;°kEŠªØÿôñ߃½–¯_ØwÇ??wåéçŸ !”t¢Àׯ…W~[4ú”ßQñ‡×XD•ƒ‡¨c˜tQ’\N!¸ô!À›È­S?0Ëlë"òWH‘°ušo?r#gË?r…zõuUÄ'S‹‘š¯jÕË+b’Ñá¨IbHY}!œø0ÜP½c-5òéðV鎣åªm??$Î¥‚,«`œ‰Á¶AŒÎÛÐz¡Ä“6G¥eŠ„Šdtz¶H%­¸S&…èŸFÃŒ¢Áôx~ÿðü?rä·õY.U"Ä“KMwŠ»6±EŽvÚÖ×Bi]ëOý‰@…¥«"r•abs»Õ¥Mða‚ƒ1’¾áåq'll³ÈwXäO³È}ºõzï™óUj&É÷%°9”+M0ÕJ1G“ÅUZ¨§bZˆŽ½«ðØ[?náêùVØŒåýhTPË7,ö4sG.D´] ILÊuþù·6dèõø„OÛI)y/ó‚zQZIV´Xӿēœ•»üi¬~'óÔÝu„æ`T Õ•Á"þˆ¯ÍÄ}Aö#lcßFŽG´Sˆh~±³ Y½¦Œ.¯òl“Ùz°®Šìbܶ‚ßý£òèjOó”+p>’•+ô?0|ž)*sàjOžêb rt¿·½[]è¾$ÁÿŒé%¬«+"M¢¨Q¬ôRrÒ“’RRbíQJ;RÀC¸gÿ¦ßf]ð;)ír–”Åí³Ç_“~GøÛzqØtíïàÅá³Öa“h6ÛÏš­öÑÑ‹gAóÙÂo…0žýý}ƒ¦¬/€X-Çõ—µÚ7vy–T7la5^EµJ"¯ÛG[°‘$jßðÊ-°ífû0Mñ#tJ±'{ѰÍa>\„?nF»?r£qÍõ­—W‚ˆ[ŒÅ}óuó y¨ß·^7ÛøÛÖ7ó€??0˜ÒÝRxŠºaNtý¼ŠL–6€ãPVƒÓçs}¸&ªö¿@þ®ýc§XknûÿYíÿEëh¿Üþ[­ÖEûÿÿö¯ÏWK9^-aóH^QуQ3½“³ !ÉÚŦ½ªÕ,WØkߎ؇Ç>f“ öþ®òŒÝ¹bWÁ‘õéÛû–æÔ2Í,³Õø9>Î7ûk&¸!%¶Ô\¹Ì‚Y ë„&ƒ¿}îÊ·ïºoÿ–c¶µâ k#_äŠã‘Óiy›‰ø:?0kT¬¯È°ÎõX^Pg¦#v¬Éa,®Q+Ìãó/ïÈgƒVËÉŒŒego·Ž·<ï´÷5ö´ËZÆz l_S¼¹²¡`ãÎÝÍ2oæk)goãÓÉd´4ÎÏéÅt82îá…ÂëÕÖ3Vif°mŸŒ›Nj`’—/D/¯Rev}À¸ód¼B·l|µ$Ý|ÉÞÙèjjÜP2óŒguqt°I˜ôôö’I¾d§ÎÄ&k 4Ö‰‰Á"X]^?rj[‹…`öB÷òµÅ ¬šù4-ä|Á±¾×„‹] i²:»*vQPöv6Ñö!ôÖ$­gåØò×?n|ßí¾ÿôIž¿ÜÓ#tšÎò\&4z?r•¨Ñ¯×íÊ÷Ÿ~D JS¼³Wý`$Wá=„@˜Ç%\D@B2Ð??2fRˆj¸w÷í@ð$ÞάÞ`¦»ïÞÂ}†–‰‰î÷•??É¿|Ökº?r| ‹í¨Bœ{~¿?n÷ú¬úyü1:‘†ÿøü²Þ£'MÞ—ááù·÷ýÕå ÿ°70o‹èãyÁÛñe÷&ÜÌÏ1àr†ÿÚ ó Ü+lCqš€;$7qÃÕ¤[gü ÿ5Ø¢ëÓÙUØÊ¨¢'鬊Lm´Š¼x„ŽÀ«èúu·åY,sFZ«TŸDwcN¹F¿Íõ²Š¼Îg—)ö[$ðLƒü’È0dg¾èÁÌÞ‹­ÙaÕÓõ8å£{~ÔAd/Èk¶ånf*©; '‹h7àâ±?0ªÀþó¶1¨ÿÝXE®EÝ…êî‰]įÎÞš¢šZµxb‚Æ®Z+²—ŒC+8`£Úö¡^Š©³‚1Ò+ ‹½ÓAΧ7P‰™û¡¸øîŸØûòíÆqœßÿë)t5_²ºgéÝÕ陞}ŸùöÅíñ‘-9öÄ–}%Ù•¤¦Ï¹1Oø=ÉÅ0,?n†%“¾ëW§bË"‚ H‚ zJ)º‚#nÛ•«??^xÒ+\‚Ü‚j4À«¯êÒr"æõTïœQâ€EÕzÍ÷ K7\2²B°€ùëúUgÌ5äw??i,ÕuÏiím ãéýFûXK9ì¿âODo½éÇjÓšC˜M??)o{B?n  ×!òÉ}åîXKJ¬œ@cOóZ(?0³ýŸ_퀲‡GÙÎa_aþÀ&'Ÿ •ðP¡UdÞ,2ç"™Vlº>·Dúî;,ºõS™fã¦kS9Ñ6†ŽXey ê†Õ|¢xØ-¤+‰Qí+v‘Âïšš#vˆ<8x|,nò/%ÒÓmŒ=)>0ÐJ° £½¥?0êé¹.Õn?r¨<_Rín¶á3WNg}] à_ËŒé½WAå?nPXD§–šDhìîù'`zĵ3¦3ôÅùg¹7¾'sLßmžÃÝ,·<¨?rG'J9ãvøó0tR;"??§íâ…»À|ºn5?n>?nö 8,N Ñ+Åz-UºX=¥ G‰Œ§JÚ´ØÀ3K~Æ–]â ™j{ AöËŸþ“5"ÀÀFA™,‹THíÍ«î÷)ºßÁ‹í`õêyÔ á$^ú»%Ùl§dÕ_1s‡¥'=˜zËŒ"?0*wéÅFº²öBÕö±áb3êsÈ^ƶ}RÍÜè®^ÎQMÏħ'%¾A*¿\Š!}¢HßE&Ì0ïë㕚x•ÚNà”MEA·€9_³Em]?0†k†žÖæ!R'ì??$N·Õz†éÒcgpV[öjßµ9¦ÈÏJ]ŒHÉc Mêm]÷k¤Õý:dM|õ%I8–ä…ÝKX½2štbàÄfàäQN<NžÂÀ‰Ï@\*XD w'ÂÝ s×f° ƒâ9©-xL±¼ÇÎ/m0éÒÌz—ìeV­ÞÛ¿ü\m …IøªóN¶-ë_ذVæÁõû·(Þ•Þ/ 0àöï=4_K¹”&ʾOݵ$]Hš²gƒ¸kØ|ÍT^3µÚD»–ùB-;²ar_§™½s[«db¦¥Z¶¤ŽP+Ö-VvB9/Æe1?rÚSiC¶Ä¢ãtšG÷V©ð8‘4¸q²ab‡#Å^¬WÍj$¼{f&aGÍNÁ.›™€=73aÓš‚:;…wílÖ6¶??—ãei¿§r Îk°ì™ØØ˜ F‡ô?r^÷ôÂÐîÓÔÊîê82Ÿm"VØ÷??˜vÀ´; 9þ-J¾À+i¶"ØHùØ×Œ•7pÎ&&†³X6tÄŒT'4¸×’ ¹ídeâí(µ½‡Ü$^2œ¶4ÙLQ?rÓĜ&@Ï(Îh7ÙI!ãýo58”‡ôeÙe§[v§1{Èîtƒ¬z«úhôÈÞµ†¯÷²C–7ŒË<ëÝsþWÿòtT_$¥mRãŽWÿ†´4^ !sš=e±”¹?n*QS ³Ï‚ ;·î×?r:6î뛓½\urA«{:¿lïîó¦¿<£x8½;Òoü˜-¼®‡†t}MäD±›oô(˜?n¯,œ0>nÊE’Åï¿j8«ê®«BÎì@~=_Œ½ª¹ú?rÙÂGÌ"ƒ”Eù{³i*[[š¥eÅŸ?r!‚:¦ÎN"8ˆáM¼©V!•@À­€|Atí›H­ÙqV‚É»Æ'w5äwŽæò‚ëÀ°6° ø&k¥h­Ùû0CdõÁ??ª¶+ŽËTñ¼0>µn&Ä:E-¼ýn;j#Õeë^pŒKíxrÇñPõ—bÙ £'ä¾àìmù•úÚ’2†‹úØ?rž= S"Åɾ7ƒàq]s¦j!¨ ¹±Hº“·˜á-­BA9dæ“ñ«½€¿Á??/ãÅ©œ{éVyµÀ‰|¹®~??Ê?nW®(&…?ry:²VÕQ¸ª9Z+~Ë"/‹‹ÚṪÛA°ó#¬xë‰`ã©—ÐXdÙÃü”Ò¶˜1 Äx€Dx'7‹Ü/j¦2Õ‚Ø#zÈEŽØH7**ÞÁí!“oZ¿‚šÒ8›TcÐC1@™éÈ^¿ û!^´Ê¬©¹yh|XÓiÅdY»½š>_ˆ&š`s9 §E»þúð)b}ZNË—«&»s3ÍľýwKÂg"†šÄXૈp9‘qæÆ5¡Æ]$J9ŠCÔ©îiN#x+4ð/0F¦05H‰ð×D€Ã8Ãú.ƒ3¨Îïû?rK½Re?0Ñ®?nˆ½¯·kª?0å|‡å º#(r‘ìeåå0ûì§z P{šëEþ‰0ÏÈ/T@#¢¼5Ñ©Eõ÷N$†×Y.+ÑyAìIW†C¥Òù}1Z–d”ßdZçü(¡¿n_ÛMÏ?n é'»E¨SßQ¸­Ó†V aOê7N›ÊªÒ{Eò–ó:U¯ß«÷‘©0ë9ÒîñÇW°‘•è]NÒÎþô ÁñΘɋ™š¨±[ë"Õ6Z¼‚$H«á 9øÑƒÕ6"µ)8RÍ­”½)i…ÿ+Z`QSý‚wë4ã¥]Ôô§8Ê®Á|Žº6I=¤9§´K?r{²Þ†+m?nkoE4r5—£fþ–= æéw—_yŽkœb-0ÍÈÅV®k”Vk¾æ6TyU{øFÅ+¨ê”õ­´ÿQÒÃcl,=×pöáN4J<±ÂˆGèƒü…¨2#skSf®Dt¥²šR+£Vƒ!@²ÈÞ”*öuP FgêÌ8ÆKî¹{.³­½. »ÄD¼ôž&"LoH°’ý莘HôÈ£Øß&á…»Ý:ø‹ÎçÇã;* G¨Ür^ÍÙÊ´y^a5Ñ4Êô«»?n…=2@ Çëaú}²¼m§—í§³,·ñ–!›JÊ«tWz¨×㺱ðö,÷NˆA üðü~µ??øsŠ·€-á„ %páo°^F^™Á×Ú§ðk÷ ÚZ‰<¢ŒÓ«æ†Š´hÝVz$&Ô—[À:ßbL?rœŒüðZã˜EÞ,¦³¥Bnr˜_ä7µc<À–1ÂSoŠl7 Ežqs©<ÏÁ¹ZÑkË>d]˜R¡øÐXþ†ª™g°Â»4Û…Íb½'©UïÕºguÉîzÄw×c²äH½ ½º¡lK5–t·Hçtsùu}F4Ù4Ôw¥w +#?0x]È#ÏÙ¡È+÷Dž(T3{µ“mö¥Ú£Ú\½>Ç ÷ §7Ša¼»iIF–]ùÑt”Šm}“»³juJÐ5ifZ𣭼äÑ:””—F}…F ÷=d0¢ô!7…§,Cyn/ÈÑÑûÿ8þÕ??þôWÿ?0G?nÌ%cÄþ<44F´c9…á•"Ek›…y˜â~‘-ÓzÐA¯XÖÝ…1Åœ·â”ù~!†cÎÛ­‚–uPi”¦Ð®÷ú¼'úàvޝ·ÜVŸ¹}+9VœcÕ1‡0ž;R=yj:b‡;vR!ýÆÞE~V 2á5à¶¹Ó'_$»ecQ{p®É<í^ÐÖí­¶Ê_²¡òÇy‚O²< ŸÌðî|f} JøM–wÝs@¿i¸;W't&¢ª]9âZ&¥ íW¶"7PŠ(ã82³ÜSû–æAá>ÚYÂj½ñ]†àˆ]Ë<#ãÂptàøpèM#zYs\ǶG»ŽcüË.JHÑîJeàz·•5Êò;YãÅRGÔ|¥ÕíT/©‘“3êÙ¬H[g2£¤+=„§}ºË–zœ+ðñÒv)ލ¢®Œ¢ÐøâÄÝìPÔVÌ©œêWoÉ+ÏA ¨{ª»k‹±”_0¿UæEÁÕ´r«çÑJ=ˆ‰­ a&,Þ#Ý M¤+?n‡ÂÝ&œà!¡‡²¢‡œfÊï?0Hix‹òQÖ¨m,­hãÄaÇá¯*ãøM…½b[+F(Úæ*·Kr¯¼üì‹Ï??)Âÿá??…þ”Ï/bÂ+?n]ÚÒÞE:j¡¼=Ü¥‚3ð½Ý7ÇA€# †pdâóì»4zª|¥¦|¥‹|¥¶|]0®Žª#0¦˜%]W­â•6ÏM%rlŠoœT÷åIÅQkXï•soë…•KèNs¦·ŒEr÷d‹±…±É¦…ÎÍ'¨ŽÊ´<ê“}—èŽè´ÊLÕ§Õ??ÖöEÙ· m¿j¥WWý¢á!D=På©}+åÒʇˆä<‘ÕJ?0*[3ú¬kìEX#?0³F?rØ%.+@è2ì½¹ª‡ð!ÄÇðu1Â&0??=a#”ÄÍ=¬¸& ý‚ê£>{,DwXš×g/ŽN5­‹úØ—œÑ8žrv.@míZ™ü£÷¾?nMÙ ?rTm’?nßE ³ábõlßf?nµÓ ò5S5C?rÊü+›ÞŽ—4ÈàcwEAÇ&ñÆy‹ë£-AÏêêptìñWvÀ½R£o†£Ü`°ÂÁòó§ëÓŒ~·‰Ù[t'³?0ZüÕX´É¦ÔE^î–?r]ôèÐ8ëО{Bt??ÑÐæZ–¶áùkšV…Á~ :¥o*Ûîl¦?n(èô?0¯ õ&¡Ôp|cmöb>‡¯Ï÷)–ÁÌî0_Ä?r)súò3Ð@ûý™«»¢f#Ndzú†‡ƒ)­æÙ¾‘Hºø|¼çH‹!³5äÌÝg9™Ýêëmˆ=ô®E^<£Q ¥ Â8m ƒ&3»cgwØmòtŸäüº¥’bE¼\`XËî.+3’Wo.å1œ/Ê£¢ß®Ö¶ Ï$Øëæ®ÎQ¾†çºÊ¶ì3vß\¬ÆJà,VÎ\˜]¥AvUÑß.4¯5b,OqÏV6.*V‘:3*ÎçWi8R{Œ?n Ò?0Ò$õ•ºj™’qäIuþSo˜œ=|sö£ÁðÛoÏ)ô`ôÑ_Üg4èÉwÿ£!+eTkGfg¬ã@o«ù??»çeGý†Cĺw1(±ChÃðâJŠÇÂ2'p"à€ô=ÀG*(ø¦×fBºOáïu¡Aìð¼M?0UkU#…m‘gãiUŸZ_äi–³û¯J ÛóÝtO~Ÿp¯úùÃ9O!ýC}6G>2#ôÂf'æfÒtjŸ³^´¦$eËA{zŸ˜ $3›÷fÊM±±q‘ìnÞ§öAzª??ûÌŒ”!K­óð“$%°Ë–àËt㟠7R©ö6aHÜ–Åøfngž/Òñ4!EÀ̽ÚVùIÔWDô»žÞö"]yT¬÷}FõÅm*VL_ºnîǸ5c_Ýè8›?0 vyZ*Ä»‹rŒuQOº~˜* Õkud‹?r‡ F'Vc?rÏ3Ôe7»`ýøè°õ7z<"¬L×Ëuêq£Ï¯<Ÿ+´;«wÚB-QAc±:7îÛŒ"*›iuïãd{~¨3jásŠºC†;z¹Ä˜-¹ͿŮ®>?nº)3Qµ]®h¤°è]-VÏj9oУ+0ÜŠgŠCf*ÚÒ­ÊêeŸ]c«[«’Ë6EZeo7ª7št"^ÔÌCY<µ0¬Ãw2N§ÒòEßé„n$ÝŸKI#+kE†é`‘,P4Ómœyö>Ài7ÞîÿUúÇ©œÅ@ÛT P¿>¥.PGÀ¼jᑌ\*ÙVEý8¸!ç ÷}ž»rä^;ª^®Út¨žhER²^Vë"³wö<‘àZnX!}*ÏI;êùhP~ä…FÂ|ŸNúô@…ªá'Ñ©ÄlJ¤·??ÛÀµÅý„Y‘ý”÷‡a,íEvæEë,ÞkdæÓ~Aï‡ñ#É,Ú<» …wK7ñN—ë2“w '¯j~¯’͘{ð¼E䣸5¢Òß[­·9t /‚>Ä>zQcKo|“€"í¿œ¡Òv‹’+Õáv@Û…rÖ¯OÚ¿j–ŒÄG0‘GJ~†ŠdùàR)…®& \wUëi¾—Y]Ö¡(- ÐÏQ=5OJîMå²WÑÄÁášÃHNÛõÈ"áHÁ‘V'–Õà¤ã¨NdËÍŠÔ½õvƒë§­tú¨ßñë·¡sæïNàg NßvØ“bËÞ??r .·9ÊÞ8Å9†WƒQ‹y¼Âltà®u( =×–2©Eúæm¡ê€–'Åé«6JQ“†C˜?n´DË—ZÂ?nRI>µ5'IÒë.¹×]Ô?r÷w¶ß9ï'°Á5‰ÞèBúꞇ˩ÛÅÝ QÇÀ,¦= ?0"ÍF®¹@lû€L¼?0´f :»:Š,Ã$cðëwƒúÝ´ó„YÒ= —"Ÿ«wÓäˆQF{¤[ìÐÜhgÆb† z×I€í-2wv}Y&“lÉ'åD…›X^ä2G´â'´‹ºáP(-IÊë7Yª(1Ï‚±›úâý„gñÔÔsÅÐCºŸ£Ì!›EYO æ’XBc`rýŽàêï)+¶?nÓnØRWä[°Ø,3v ¸/IM8ã}täÀÈšTÁB_Œð·Òƒ|·iê#-ÎXÐ^•ɘó×—ÑÉ›Ùë+êTß•šØ1#¸Jª&ŽV©C U^®Ùb\Â#,R ŸÛ*DÔðÖ·Õr ¾™vÇ¡¯@7ÜBÁC£ácîà©_d‡ª›Ü÷>îÊ=½0I[”é‚…l`ÂTˆ+Mµ\¨ûØÝ%û«Ô¢¬n}b)Ò%úß$Ý™ÿš]IÒ¨ÕÏkl¾Ú/¡Ïqm¨v•°V»©8È5¬ æ2KcA)âJá›JÛxF§°2ºkþÂ$Z)õt:t·G}K—ÿ/V~¬øÜû//V#.TQ|ýõ×ÄS¿íjò™}!ùò¢y®R®ŒËuoÏæï…¿„þ+òlj‚MÑU>¾?0œ×ݹ¦Ÿ¯> >ù.Àêï+W7H6ÝñÎrñŽxõb‰ð“A_†ÿ¯ÚÇÈSˆ©±/Gàô¾1üf€KSÏÛ¦4¶Š×¿Ù€ÝjB—¤b¥˜¬×hqÏ*é€ ¶ù?0 I8¾«u§ŸÑ .åëlé}x(Éú†Œâ¹¹?n‹~±;2xû¼Œ…BpÔ°%„SER7§#G6¸Cò›ewv'’ìX6ßä£G”wÙóØÝåÊ›m?r;óo‹Â$§ ºµ|'†Rr¸s·ï’XuñëPŸ„­)©0!è˜ý2V¨ºÌ8DÏnL4Ôðe$nìN?0²œT¼ÒävZFb&7“ tÝ$‡5ï¦X#XD–FèøoªÏn„Ëâopÿ—|Išñn¶ªêš×uSLPÛÅù&)Q?r¾¸w‰I”YÑOºn&š¹ýªÑF¸+áuzþ:uk”H”\EFÞcy_ùl…ÅN´š Y-pu¼ËBh‹ø÷/Õò†‰ì&å’rxeåä?rÅ5S|!IÔ¶)½¸Á›áÍr=I–£FJâRh-›i„˜óÁ8¥Òî‚QvÐx=ÃkvokD„ÐÛ•;ã¶µ¥’O§a(­‰¹fr³ë¥êÎØ©ÎÅ´??á?r)[<–%¿†nÂv\n³ ¨OCÆt¯‚o‡ý¾?rþ.Ä̪‘Ôt âè>–2ýý áVÿ`}ì8}¼rož²“Z¤mûFìÚ?nËê×Û,’\<²éÓrŸ‹^Us¬òŠ’Øº@ÞY•¼´£%½Ð;OÝçûƒÈfï¢ÖTójžTr‘.ÆZˆÔjÎn£]ó¥{€›m\Í/;:ÑF¤W'Û ¦õ²CÖ Cnj¬ñÐEO°~’ÝÄ`;ƒZ Ü??£œØHý<è%ù}5??}=;›gËÍ;ÔÊ9‚ŽõŽ›É2?nÛ %åÊâgqPEF%}B“Á£h>…fX#ÎdL?nöÍ>R†«'ÂCÛ£E¨žuÞHbÃ}ùÙ7Ž]¥ZDRЕäf×¹=B[ÏØðg‹Ÿ-‚Ý)ÒƒÆ!N ¸óŒ”«ÚØ??’eƒ?rvÖ€>má(ànÜuî@Û®shm7Ý” -òŠW=ä9œ¸½ŸJ/dìm%9¯gì% ù@Xº}…ʘwÂÿüë_ÿó¯ÿC†¥`èÉ:BÒv“³î¸ÙÃYñtYà„*¯c+%OXàmã`ã½”¢TÔËOOŽèÒCy˜äúÅNã~¿.ÒfõP¬Q9Qs‡[¬7ךÒß`Réå"eWZ‘”Žt˜åCŒ·Îç@dø¦Û„îwÂçt£bÓ½‰°À§ö¸üÛZ ëþ6 À.(âóÛ£¥›œöÞÑ[AqÌÓ [߇á˜írºlwqíËÒ¢’e§²‚y!¸¦—WMKb\‹ ês»¯+þê÷ûß§ü(?rWéÀƒV…›ÇWƒ§bS%Õ„ú¼£´}¼ÙÛÝ×eu%+Hí¨’¸àñæ¾[\¯??[]€úÙº²®"ÚùÚµqy¾×Ïn +#É›>'Çú„[dYÚ¦­áÈ'hаx1 |tM}òP>ö)'ÐÓPîñ]ð}1zÞ„r÷ˆt‰m;¡a퉂–|Lß_Z!,qõή±þV jÍŒÍôpÈ!çÉ7ýûh¢H*]a½TíD•ƒuÂEcX”%6ÜÜ1Ûw¢Šu8_+µ!T½ü@4EZw‘ŒùR?n‚‚õŒe]†·`;+³MÓbú"£è£Þ.psÅÉu ܰ1HŒ©2n”àÖt¢í jŸ¾“÷“>>ô“ÙÍÏîN(êgá-&C×ü¤ù¹±eNyS —žThBy¶ÓÀD‚Åöš@÷âÙÿ©½ø³Ø^‡µ÷ㇴˊ‚cUŠ45êʘ?05(Ukãß$¡¢¥“!û\œ}9>ó†–5‰æÊ‰å Æ1CÆ6«Û›˜¼Oe·¹u?rîniÄqÒ»'¶[8ÿS;VÍ?r+åQâÄ€9­'M1|ª?0üñÞ«2É…õ™ $@÷ªmNMÌl éíUÍu!ƒo4öåTðûWÔÒ¼‹¡fÓ)! ä¥>Т`|9Û5ÿÝÕ8TϽÁ@ƒiou ’0†ÚÈxlùåž}ºìX—íG³Ââ?r_c{=c1`hñ€{JG1ŠoÄYÝ ë^Ïe£,Ó9Gø‚!¶Y¤0,üH6Kä¥\eªÝ28U´aæ.©ÄÅÝ>6?rš??²öx×t‹M?0mÚ…î$$ÒqZb1$ ¢â1N'Ø»Y“à3@\I¶¹¹X–œGŸþ™a<ëd¡’±w1R6µžÒƒ?r¶·.P­¤¶ùpÓOô¨þÍG²)?0véD%×¹ìÖÂ`ÔKsi^í¼ÛÒßÝ}˜˜«ýÁþu¼Ê`Gÿƒ 3Ó4³=ÑxÓ$w­O¼¾½T‰™™‡^nØI6M³µ¼ßËÚ%ªBBy£cûµmDWÞùï¶FµyïTï›nõ¿¹â@ò25ç4òâ²N9¡v|¦ew32ØcsG`Q£ºRRðL¶•«ú:_Þåv³q!;Éwœ¼tDÞ¡_”Ò<0Êvã‚%ùýtQN“"í±ŒG§a¸DÁÔªJ;½„£¬íQDq NÀQ‹.&~§P»d:q¹šªÒŸ·ùm™ÕAíÉF·RIåI Ÿ³1CÍ:\~ˆ¹éfF±‘ø —ø ;ñ$BuKúßhÃ'\ÜÓìE _½FD?n½mÛÌ€bïI°NAY޽¦,+uø<ÌÔÌ2//3‘›PÑŠ¥PS‹Ðµ$ß¹`2ï½@¸¨&ÈJ@…Ó„H"Í–&@ÿ?0°®L€D?0ÜÕ|mÍ,Bh¦ßŠ›€_À¦?rb.ójµ4%ÉC?0ÔÉ›k§³vmÜ4õL¹d€¹øŽ¨a¶ËŠ{ZPŽHÙ>\éXOýÏ.ˆwÔ>\~Wdá·màÀ#Ÿf„4†??2­ò®VäU–pD].Ðl?rÒéÌä ­làu[óJ‚š?n/hÐVj€Q?rå]¯ËÜÑå1æwÕ»¸4é]ZH¸¯yñBs^ߟMâeø ÷‘mŽNæŸÁ|C(ž':‹”¸º–ð·ªx[E€±›Òs,†y6DB®/B²] º&-@Ž”údo{ÿgëŒ ×X¸JÝ[«Íõá?rξ¦eUתÛLì­’»ÞEìïwXÅ_E´$ ÷­Sùú³³KÅ ä„ã4ë"Ðæ|¾‡>ÿtPƒ†ˆ½Ó¸ø­tçV=è4¾ê>mÉÉc|M‡–AàY'•)“éµ®B\¾ûørõmžaùQ?0J¨„˜pM—[ÔuTä][ÐîÌÑéÖ Ö“ý>.¿£ïCý*¿QÀÕ*ÊyPÇâ­¶?n¡;GpdŒ"uVŽÆ{àmºÞÜ÷"+ït¾yÏ9uû5MÊ,e½W÷zl‰ï]¯/¢£X.‚F¡Âö9ì&ÞÕíŒ@Hñ•õkΣ­œÚºã»-uK,8RÊìµ´ç/ç²ÅŸÚØæ‡jnY×þÍ^g{7â9ŒY–KÀôT"v"h™Ì²AðÓõ¡dÓ¬¢­b±’¼.ûÁ??Í¥ÀaUŽlˆy ýnKc¶‚ !—Ò*Ê?08. ‹”€i³`¹.Ý®ýàß×[\Ø`Œ-ˆ³¢œ›mR¤|&?0îK.Œý9v?n€ï€æüý9Eá‡A#kÃÙé 09r‘”ºúÉ&\V èUýÁ xZÆ'=è ¬¶pÚ†¢0_ïky°.ÓÖ»a ãH©ìªåØíƒµnß,ï©îðXf.‹ýÒG޳®¶vRŸÝ%+XJ'®cà ]UmÊÁùù?rÍ[}óüË‹‹Ësô??RªûóÉr=9Ÿ¯wÛ”ÊL¶«sâUy¾GTž3çÎ ábzÆ??ÎÔ}]f5’>zlì7í£PÆ:±´6ÿÕ[ÄlÓ#A¹ß›Kƒ»ªiŸ¥¡½Do¢>+ôd??Ø!®¨†×zÿGv0ÐHëc\(r–.H?0«uqÿx_³ú”ÑšOjŸ:¼|#Žx,ÓÜ݆u´yhª\bpGVüzcRµµ²™h{)_ó…{­±F@|ŸÈ ©j)é/óF„?ntkêšÞµž<“ñýlÎ=óz™¬&iÜ?rè0ZŠdw.Z´>­ïJ!„Öƒˆr ®)úêšs1¦×JFE5Ùãt¢ÀŒØóû¼Ü¦íaçUêÕ‰db/¥îN5$¼ ˆEÞ BO”9ÁG¼Z.ç–e°ã´ -Hñ¹Û¸çŸù ŸÛŸÕ…û¤9åXý©è?0üðî~?nY°(Vï§M,P®^—‹×ë‚0|âÂÅ"ì 1KEoëz"¾‰IÔ¶ Àj~¹˜Ùɧ—W=€ëî¢~º¸ÉJŒN&m…ºïl'Ÿ}Bz“høÉàò³Q??Í_RN Ò]N앆dt_P ìÆ®¥¤V@ͼ\$4ÀÂY‘Áܽ!y??’ãý“s¬žœ#íòÜoø€É¹„­ž³È(Þó°Ów3ö’Gí#ÐŽ»3Gc—§6øDG³`Àê͈ò8$“ž@¾U04¦áa„÷ •D??¬m§±¦†_cütýsLú¡™aY?n첤‚6™¯DȄް“á½x*7#u6 ÄI—ä92ŽeÐk(f«7/(W¤‚n²ý­vî¢@^?0ÂuŠÔùe@• 6ø€[bà<$ié]µÜzGÍ=&?r ò´…I¨M£œ¡û´k+„G4$UU¸éR%e™š UœÐSÆöÐd‚³‡uÙNw!híKM{\¼SOzäг›3FèÝ] |>gºsGsˆ©AfLh=ès¢Äð¡i"Æßa€–¢WšCB*UÕ]Ÿ?nˆuñ&@)U˜•¹#â»\óÁì«åó&ÍoT’¬ ÖÓ<Ãtª0??›¾½Aľ\ת¶%Äv’ëQ-ièg-IÔûZR¨±[R6íIè¿-I®W·Q±®l_d[ºSªÚ ­¡õò”\þ77AC0ñSÖõ»ÆÐh­d£ (›ñÛÕÑB¥Ñúê{£d SÛƒu8¡}ÌyßÂãç=6c™{?n¦Ù4Õq Ž.˯\E}¶+6/“e7óîï:¹`â-ÐéõgQWg»ûõ–½AÎ’À ¬cy +#Ú|Ø5Zþoö³ßþáãŸ~óÛß~ór·j YHRÜÃL@°»,­‰°ÜDÛ8¦œå›÷©r¸ÃÄ%‰hëA^(ÁdsUB”q04(??Ì“®*‰ÔCP°wb??†œ×çŽLÆ«=[!cH£µ€­½{c4?0È‚k`{¥™$üÑ|ñ‡Ý}X½’–!¤qÈ´‡Ñé¥\ÛþÌÓ^0$mm$ç^.܇՜”¡‘½M Þ.X ©|FIÄFiè.«??ȘÝBcÕ¯(ÂFägŸô'_±½?0 ~½Ô—c*…ªÎPJ–E¥èå$¼y½UûO‡ÔzU¨ë.À×8M10# ¨iMò°*·ó=@ÚŽ!ש€ßW"¸â~Ç9=o×™¹øSÛšÀjënùt^ǨǠF’7éñ‰e©Hޏá“o_ûÝñ»xûcá(u3VY;lŠKúÐå™FÃí]}&^„ÏÞsPmz"noiA¾šäX4nÉŒ\X@Eü7üf”dÏ`aÀâß{}å½MbÝò×!QEf&??ŠèàWý^-P;û?r‡Ãš ŠµNŽF?rñè?0¤ËâI(˹áY% Ï—PÂàHª•N`6d¯AšÔ©M2%}Hß/$—ÀèK¥E$¿° ÊÿíbèóÑBPBH¯Ÿ¹±Ë¾¡_ö†áÃbC.œ÷e8zÂA߇Ò_´ oGÝÏãHnÙ¼ô.Õ»5·uùW/­m[Ÿ–Ú‡¶³Úø0së&¸’áhÕ«E`*ïB× 3“f[}DËØwÛ#eÐ^¤4ŠäìÏågV»X5=8¢VùÑtª•j¾.·4J·š?n?r}—OÕE„ÃXÏøÊ5o7“Kuµ°`À³Ʀ­£Ä]`†5_ŸÓ*Õð¨¹âDPqç³Õjõ®X•ΖÇÂù(Ëʇ0:fΓ۩Ä&1??‘øAWö¹“Š~Xø•ާ¯XWù‹PD±<-A!í?0´w%mö2Ì˪1uk1ŒÇ/RlN˜b~¢•ËKp„N^„´É´ HÓâô|?nw/Fâ4>£Ãí^¬ýv¹MA׃Çxúl9Œž¯I*'ëJ(ëî¨ßØ@±‚±??>UÓìÁ Ý¦5ÛÝÏ®àÎ-yNΨ)ÃÔeŸÂ—tÀ—<ŸÅ¦g:Æ!±›O¨Ëë^×g×CÃ.¬?0»ÜñìVôˆç¶+0hÍnÃH%TrÔ)‹"zæ¢tq?0RùÚa¿àT ZŶn ·3S™è×ÁçW'váÑ÷q>üøh¿<¿©æ„œ²¼“ÅV>uÀ¼×©wÉã|q3Ï?nìxçÁågÈ·Y1KlxOª÷QÇ{^•ᇪrJF{ŒÛ4©ç¥F˦Բ?n¨´ Ê#°{ ìþX?0€Pß>†Ü޻ǎ¤ÔÍÀB×ÔB@+¨8¤ çnS­×äT°\‹äqÌJûÞti j«F‚8Ub*|Ðq¾û.ùîó”XÐÒÛˆ3wëò…??5‰˜€œÆÏCº’æÛ>GЫGÏÙí¸L–U4h±:î;O?0 Fô†c7éõ6{i@¶+Ù¾?n.??´G0”Íx, ^•¥Ö¹ìoµ:Å0§b—bÂ&G©ž=Û‡›Û°qîÃcÞ<ˆ§N¹3RNI2PZ—Áa9Mò°32—ŽqôþäÉô"ÓœÜdÝ?n02ÖÔ׉Nˆ·9N Lk‰XUYÓ°éb–”Ç3z‘¦URªÉ^Æÿ"›á˜¤J|N›{ý2»J¹]Á‚ÚáZY ?0•œd í… lbj†ïÜì?0·±ó ÎÌÚþåPŽÇpâ±ÝÓ(¶ÁéCáÁ"3Ï6 ??˜0e1ÉB±\ýcÔ%*5×BóD[8ªœïC ¤xÞµÛÍ¢Î.‹ŒÍ¶"Þ$æI7dxlA²›QcðD[Ñ瘖ˆ´m:ïJi6aݰ¥aw©3m‘Ä©ë¬+´ZO͘ãåd¼¼ayósØ*0’ý¤?nÝêi¯Ìª=|W1ñ³}%°ŸÍOÍÿËÈ…eUl—MƨM)Ìb…‡¨Û¶…Ь©­ù”%yûjfv½P…5턺[¦¢‘éê8÷ G7ÝDvâ^—T?r·)öæY ½ÛX­¡¬‰¢öö˜ÎWëtì†?n~œI›¨ÍÌáœ÷šdQP¦aíÖ¼OU,O¬?0û‰½¦ðóO???rÛQt¤neŒêp^@Q/jÜšæóч¿Ãš†ÔàS·ép¬ µk+ÄàOghHë:#ì©õS„?0âP¬åv:‡ûÒj<‚Æ2üì“OF²Ò` ¥²¶NÕö)ª,{Ý7E;nŒJ|©pºç ® ·øÛCÛóöø‡È.ÖTu¹ÕØ ¥¶3®j}99ˆ…vL6_á£1??Œ38ó4–uÊI•‘KÒÆ7‹¦™‹ŽÚF”C¤–(†ÁQwY›(.ªÍ´)VçVstßáãîaµgC(“U—ÖL˜v¯u›´—q£À¹¡Û÷\ß(Ž©åÕö¿gm‹Šp¿ê*ŽÛF’Þ??¶Ä°×q­%î—`mX­åZûÞ,npRܨ¦dÞ*øæëúÞà›ß$ª+MŸ8Á ÷.dÃ;Š÷qåo™Z‡d³­Æ8­§Z߀¸â½ü²•ýIöQ#< úaôˆD4CîMdÞI{ѧLô~žŽèO¬M»êRê??ÿþÅKµM5é$-ùÞ"ýKù>Yþ°Æ†m2AYÀ9dµ<óSíŠ@¯TÛ!«¬C@€è•‘†ë®Ò¦êwúŠ_­åuÒð€Î}”Ñ ýº‚vM_s|Ñ!?0ZûÉ‹v{¡¾OÖûm' ‰ÛÙ ±®ÓÙ¶DõMôqpb1_¯oÀ<åªY?rqC ˜"«È}*Ÿð­*‡ŒÁ[.¸Ó¯6¤½ÚÉ5EŠ ’Åööž!sÄ)o#qºJµöyÞª²þ©”rd2àƒ> í˜ç3Îm&®‘ý¹]òÿ?0?0ÿÿìý}›Û¶±??ÿÞWÁ°'µ˜HÚ;i³®ÒÛ±7‰¯øéØNÒœõV%Q»ÌJ¤LRÚ]÷ä½ßóÁp‚qéÔ'==¿¸ÍŠ$€0 ƒÁŒüÓâ #Óéñç-W ÊnáiA†õÖ‰vý ß•èÐäfö\^¥5ía9Ië6ÏT–6¦||qŸ#fáœ`ζèÆÿ(†š#¨ õ[âÀcpKG9H¯]Äñ~Íû‡¹³P«ƒä÷Ki?0??õNéëY;ÆÐukõO0ÏLåãÛÚîOá]oª©BÞB(Þ¶º€,.âBc&ãŠ@ü?rBvÓWvTlÐÌÞ_¿¿[f('õ%‘«àUñTŸì¥p?06æ·N& ùøF ÞátV<»qJì©9zƒšlpÜÝ9 z GWµÁ™^DÓ˱„ÎÊ{š61QØíÒ)ÉG¼¬âˆ ïá½à÷BÞËt24Z»˜Þ ×t¤+‹µ€LK:X€ . áín(¥—U× Ù2ø¥zò??ùAü¿¿=Jò9NÐsÍvuÿÃw=RË¢›ZB{×Õ::ÀPJ}!DÒFÉ´ù'Ї'¨ÇxÝ¡Cᨈ±™Uw‰r²­¥E3ˆà ð09;œª#«m¿šWÙm))Ž`ùÊœì˜{JŠSs`#!îÙït Ibùûnée-l£ÁhʪQ4ËÇhŠ+ê@¦ba‡÷Y´»?0yÒ®?n³tå°€›3ýZ@™?0¤ †!é}x´Z7Whá¶ž¥³õ¦·êæ¯k”‰‹E°_XÕeI‚EO/fs×OY}‡Ñû´™´ä$?rŒç32Wµl÷áL-rãf2o7x»v,¢ëH ¥€ MAÇi¨¯`’GKø7RAk“÷pµcâö}çÁá'*Û2M¹#8‡ÀnÛ^"J}þ(žj[ùP‘|YSBQ-KüÇ]ÑêÙÜ6*6O÷Á‰ÔÕÃ!È!ɤËMû??|hù’/ʺã¤. ÈÉ+EK osGáȽeT„¸~ˆ‰b¢²?r¢Š æLwMþ?0CëõÒŒ]øS¼S?n¥ç[7ãÁ{¯/}ôBe~Ž•¨Ã´É·÷žüæŸã52`*§q‡Hh{‡º³1X^*¡R€ÿ¥>âiÅÃ$V¤ë\2É á?rîÈx˜d|dxZ‡F},Þÿ¶]zÍÕÄÚV¹ÕâF…JÁβˆ2vñ +#LïâÚÙ@†¦ÅÖ¦€ì`—[ÅïQ£:ÃÀ¸"{UHí>,ö}{ØCÏä!ñ£?n¹ÀÜV‹¶#‹†Q>?rWQ?n˜¨?0€¸è晫r}Y(°ý¿ÃÁ>îãAùβ Ä”Z~y€²h<“«)ÖfÍÂAEfúÿÿ ?r¬·‚ hÅ)Y2¿ÁQRæïÓïYp[µÆ”«µ^´Ý9´›…Wÿ¬}%@4H•>Ö;èSm,P´õ@?nPôõËm¿3Õ?0XÒ@XÎýú‡'mLŸ?noYµgšàõ®¢ do×eP§€yË`¤¤!LRT€u´[;èlB÷ø¿œT:p1L¼ŠKÕ†}0@¡i˜°Mø$b®wu%ÔÔðb.Ι&‘ß©QÝ ¢}g)ŒÂÉÜüþ2èd`Ór«öÒÈÐï(õú?0S¯S??hzÿ?0%ľØg‹ÙhE6Î<'7b·'òÉXé„…d„¡Kj œ?0Ý€2­¤³Îò]yìu÷Ö3ì™~mÏ9ËB'–Žhñ#m©£¬ëŒ‡I´Fþ79ü½ÃJÞ¼yxå[Ó¢.ûâ\Ò°ŸÝJRj¾öª‘1&ë mn9ÒK¯Æ_±é®óWI(ô„Y4[¯Ië$ç’h×m¡*¦s*Á—„rc"¾ªßݽˆ"éä]…Y2‰c°³cãÜÿÅûL?n꾩tº[OK¥?0•Ošùn» ò÷lÂŒK ]fç¿Þ]nc£Þ!®ƒ'`­¸&þ?rbÚeq-4JwCܺ™<¯&??é¹LsáAÏÄ—qL&zÜ“’ó¤Å®<8ã5yð«çY¯¾)ó¬vg2L&<è™Øs<2ñS‡koäÄ£iŸÆƒ¢…š]K<‹Jx‡5ËÂk5³U9ùL.Ÿ-a–s3Ig7Q—Up£?02¬ŠŽm­7bM…¤f_è2C?rj¨^!dÊžü·GJ‚c¯zÍÐâZרr‹M«•e“õ]QtQìaT &£&Ú)bÅüÍš)I²^•Æ:·þ>ܰ7ܽ¹Ø÷…Gc…hŽå…_z÷t&êÜ/O³õruܲÅl ®¹4ëìÈŸ3N/.Ç‹‹GÁ›€ =¢¿+Sê^Ù@°[6ú&±T»Ú¼óüDh©Å’ù"NŠãòÚ#»‰ZF³µYt?ré§G+±‡ý".Ñþp8 Xï"F½Îün2Û=Ó¤• dÿ…›4†[ö”2.sÞiðéÀ~FʼnϗÁÞ‡ÞƒùÜ„Š–Ê»¡[Æ\ÅGÔ«™¥«2W‡!äA«# ø n2êCy™5~…??ò +#èí:J¦Q¼ß¸¾ÛôáHÂe‚õ±¢”§‡rˆØ´O­ˆ(š÷s»£)ANß¼1{púì"劆:½™]»a?r"›¡oÙE´´þîk>é¢*[•Á8ö4“=5ì`½èc0úâ‹/\teÒæKjÀO¯f½À9ÈÝÒ‚[k†ëøÀˆÁ??µ7îÆ@Œ[¼Ñä2œw(î¬Û6$òÉÆ¢–DÈn†@ÂfåDÕÁvÂigJÜå‚·’G±çrÃÝ.Q>ò^¥Kæ°¢ôŠ“éb=‹v³_×£Fѵ‘]×EJ q<¥Õâá :K"Óœp.±?r‡ÞëÚ«uÆ1è[í<4!·‘ƒød /qˆ(«áCUQµUlJN/*}?nÙ*Ìä6“ƒ'AêhĈ´añuÕG¹ƒN"?0¾aØ)‘A?nw1eˆø”…??1 avÈÁгHý¯K܇ޫ“‡ßÓøÉûöÁ=xù裼i·`½ÇP.q)/7qaÂq³±[‚vLR¾Žš»paŒðY>qÁÐ'D*†¸_ Yt½2[H^:¯*MéŽÑëÇOäº Á(;iÆ™ýRâ-Nr"?0t1î /D84Ñß²¡÷òäáó§OOž=27—޽ŸÒ5‰Ö˜YåŒ4Eql‰F¹ˆNR(¡¶8Þ¯ —cñ!Ò„g©“h¹6žrÐ@Yfh&jÈû@®‚‡¥ß“…Òtè\ lspôYÜ}+N/‘âíèc!¤-E\¥Ùå}FœDÆÅ=È)6\!/°6<½â´€c=Ž“yJ:ìÞ]jÿ½à¸}/ŠMQæ­nh¯œx%±-H¢+béúü‚OÕ}ê¥ù_@øwÂÉQÿ××~·ÚŸ¿’Ÿ˜RÞ%t{΢–û§†ô??nÂ,š¯pˆj~Ý$2j,ü¸ q í›cïÒš«Ö‚þâ‚Û§±¯1¶Ü$±ú§ZWðqœGi¡²ïR¢kÓ©—=C$ÐÑ"\Nf!$?rŒÆ5iP„?rÇö"@”ÙæÈf€ºis æÁÅ—¶ŠŒ¾t~»­!Xýê+‰3ù5Ü“5E7”ê6?0Z.Ü[Ö¥!™ÚE•Q¡†*OKkç`÷˜*ÝjS¤dT‰Ž[si’ä:}‡Añ~øú•nŽrìþöªôe×wЫá×Àé"Ík'þHP¿Ï&ò¹¶Õú™6hµhUòì}ܶïˈ#›k5ª‡®w Ä@]@n³s…z¦›†¯'úhûÑ€åÃêñÖŒ†Àô•ñõRˆSÙ"òÁð}¥{%=¦DГõSÊCÀVXÛÖ.Î)u^B»ñkÍvo÷Y:oVúêÅãgÏN^Rµ¦¼ŠÉ´&·QNæ ¾(Y?n?rá5Ç?nmê¹Ãi:ðºÙ'ì*EÓËi½ïÊJîHðø¼«Är£ ¯»òÏ¢…R?0Ÿw¢(Y¥ sïØâÏÁ®RÑråtšÞ•¬ìUÂÒ õ‹º!¶§SáræR!¾èT¸œ©Tè  S&l!O‡ÖÛJÔ-ꚃ‘‚>`á«87ëcåñÔV’úp|1M`˜¦¶‡ÖÇG°s–¼×4øŠÓZù¨5í-Q“´ŒçYlF¿-Œûœ !~ÿ/³Mê¢-\CÓÕWϋƌ/T:YÓÆ73±h¤y-#w¬ù“–wMÒÅÆmµù¨d6¡äŠ…ËŒå£º.þìd¥w5WþÎɆ¿r•…7hɃÇ»‹oûÁ˜Åô‚è?n@ôeûnm §Óœ’£M©§¢ûãó I·ÅiÁ]5°‹ÛIÓTq]z3”‹h¶ÃŒbe\b-œ^¼§N€›1rBÊg¹Šoh°rj‰–'g&_p¦”e’ KÃ÷©é?0(K89“­…WóÒ]"çVLøÍó|k¥œq¦\&°µüâxÇe—áp—ÍϬìã¬Øƒý£Ú`ó ¾ïA}ÖÊáq˼UÃìåu.Ë©ô¹éúe{Nì&©³¶ aÂÑ£ìÔ¨1>îï{÷:­»×È‹_J¸˜g»óne?nàgO™?0ŠE²Z‹_ÍUî¶p•»¿‚«„³GA„鎰‰[XJÂ¥Ì]ò”I–^F™×/iÉð©ÚK®sIí°”± SC®À-Ë߆¸ì³Èáu~`Ä¡J»ÇjÃm{;³AahåhÓ¤¼à*Œ…©½æôw;…0YºîNÖ5êåsn!ràPæ*ɼæOs* ŒÍUAùôbu¥ò­®ÏEzMDY?rírÖæÏ£Þ#q¬þ\š––~˜kLN.®¦™Ëô¬?rXk•ÈMóæÜT¦€(·´ë¸bT¾–Æ6ºÄ¤azêV€¹¤ý¥^%(\c'ñr½4Ç{d%8ó8ê±DH §E”å~KÛø «QW¿z»ßØÌ‰Ù¢¬¡‰®)«úVWNgLÁ‘]’JÞÞŠ*XfÞÉ·Þ` h{·$Ëåõ,ò뵸ænwù(ßZ§À7fÓáe„AïÀÖäˆÍEÞl¢YDÊœ7äE®°ÁÜnÒ¬t«‰Ÿ‹kP?0°4^ó??oÏpÞÍwÇ‚]ÝøhÞ u=“ëUŸÐœÙ¼‹Ç±QJgáv¾•#ßǸÈ*ã`8]™zc…Ô÷¢„ä ²ùÔµÁŸý?0öCs ¢dé¡ú¾7G+"û"Íòépß$?rjITª/ŽYDs¨ˆÃEüŽj##!‰Í¢äœoBBç³cš3–1kÃ-ëØN_õÀ¾0ë&abôÀ6¢+KûÏ¿U*k"«™'Ⱥذðòœò­ÆŽn&”¬œ]¨^çñ˜“欺ïEç]éC%h½ƒZŸ‹æÇô²þMn#õ©715`™Îdv8àj†á̹Ԥ]ObA½Ói/J2Í33 Ñ·Zé‹^³Xzª—°ÕÐ×½ö!oZGRg»½-Œ¿Ûú¶×0=@OPªV‚D‡i˜Í£_·ùX«LDªž–r$"¼(‹1òÊÊKQÿ®]Fß+="lûu,NDpÃݽÆnôÛÒ›½ ´ž.ósW…„¦ÚE‹²eÒ.v´¬Ý¼Dô¥P „FtlHeç?rDx‚‰¶?nxE P2a©WÏUœºÙ€á:vË‘'»y€ªëœ~uyn'¯ züâÇoMN¡¹@çö^€6Ó(}<]Ędηsmºó &ž÷ÕƒGÞÃçϾ~üÍ1^=£u°—7ÇÏFž¦¶ EG±áIÎ^Q,7Óÿ?0ˆÃÎpÂ÷ê??Ÿ<~}2þáäåé€Ü:ˆÝ˜™xH3QÛ/ú¾’ùhWfrÃæ™ŠÎÈ\öãHr±2‹fë)½p›htqsß09øþ?0“¬â¾iúÆÏOà½úöù÷O™›€q™æöàÓ??yì‘^?0 ¦i–­?r«ã»4"×úr°³/‡­}ùññkjÎk·uè_g|ñòùWtU¼l¸4ð#´°ÞŽÏ—Âýq-‘CJƒj(Ä™)æpb<öòèÆ ª²Ù™MÎÇ‹¼A%|ðâìqyõìÞ3ê%ùR‹ây¦z{±@û\œ_ôÈWª97&[ÚÔ÷ÞÍùtYë¶tDYm¦îƒP7ÀÀ'¾ÒC1k`ÜÙˇ¸ªgÜÔíiz?rÀ©y8«mÀ/LBsçà(øP);©2³ÜVꀸ?0¶,1­žË§À£ï¦ÍìgM”&us`äÒ»îB¡ß3mgƒtÕãšëð~ ;tWîK›x‰¦8³#õn:Z‚XÔ¥vªsm5ô\Mb…©Ã??ÌpÅÌ@À+ýà?rTŒWóûK¸ëÏ>V*°‚àëùx‚£.~˜E þM‹Ü<@S‚_xä'vÚˆG÷”?rÑž7*ô%tƒkHìÀÑýnƺŽ9_ûìFEO1½Ó“Ð]=…ú¿#!-ôBž°Ú™טj?n£ÖM:»EúdãxÐÀ¸c³KtCC¦7u‘ y‘ìJ_!yµ+5CjfS+z·p¦T}†¢‰LÍñ<èt+Nm^„y¸/T×Îý´í^4½„¬EÀ½0¯?nl¸Ò º_+??F[J??ˆø%ƒL¯ÿt×ß~©2dLmZ®Úaµ'“ùÓ]·èÎÎU¸¸ìéÝ-jªïëßOÏôïëÙO+¨;ÛQöPÿܰϧ´ÆÅ™š›kx¯$ÝõÒ¸ÿ3f¼ŒôaߣÊsJ6F;úÐη†>‰ÚH!œíðe¾@S%'¯ª‰-Ë0϶á?rxÊð×–P?n€‰™ç,ѨLoE¸-$új³ñ(1õö¨P¸Ÿ?0Ò«t§Äã–Vƒ3z„È´óÊ6•„¿Ë¸by3]ˆW =/çÞ˜@…c:c¨÷Ã¢Ž¿‰r}zxVCxZÚ–èÕBû­E©v‡¥­òeÙ¬¸Doô½ÏÒj½íºrGÛ ÑnEhC#|ÚÚþ;?0sgÐr¾™gì‰ïl÷ÜHÁ(»sU›·œo;N;óŸuñí¯÷?ní¥†w4¬ »•~µ±EJͨÇYÐO *©FcVÖ=«(Â@¿A?nê8¾ŠçI}6Ã??³¹·ˆB㛀Ùc3ž€ÙR6 ¥¨!5·KÉ»0#®´—háé`° æÇí Íòg|Iåëˆâã:-ÜY±T¿^•?nÓÖõMQöÈTÆöx“¨»å_SÅrÓŽXUE?r)·ìï±ÿ‡??xâ;Éï7’ ]Ê"V Èêeœã »&­lg¨»„‡h€¥Ôë±_©›IdƒVÕ VÞ+ä8Í7å ¡{+jÏxšn°ƒ}·§‰ù;Çþɼäx‘`æD8Ž·??õâ 8KÍße±Ü†A¿ËÁÐ9¶:¥!¶º¹&x}2ãÀëúX³Åö\ó]Þ¬šNã•ô $ésŠ?r Èã8öæg9¦—‘´½¡ðsóÊ¥1o¹Aœž¬ü-¼lÉïøÅ{5€ç†õ±Âu¥……R¸ ‘¼™'ôÔ œqŸC©“º3yZKFvÕ¾å¬}Ÿ—í?nhfyåĤiH&UÊ~â’–÷K³º??~úâÉã‡Æ“Ú«úöÕ²X~y¸‰Z–ÄË#ç i€Ž,NAIŸ{ôŸø°Ùs«lZÉI-þéù‚æáâ̵ u0?0‚†áW™2??Œ‡Ù‡I´0É¡ÖÛóÅÈnż ÊÎçšJ¥¾ÇžuÝõ(z5ù‡n·zé¸<ꃋ¯6Í•ðŠN¾??}}ô=¾ƒ(_¾?r¤éZ‡??¼_)ÿ䇵pæyƒƒ^5ûQ-;wÂj!¥ ÒPTE,@o©E›{”£{ÕÉ£‡>??Ô×ôN©S։㖚·bGŽha)âs,ͳ÷Ù±ÇçÕP@3Žt¹ µ9Z®mOÓ6G|š¦Vc«ò¥Ý×dŸ´•Ú(±Þj÷˜¥!¢V®Ú NíF’@Á0»D7<£Ž¼´Í£5??Ø9 ‹\«JDïY”×8fíTm6½ã?nRãܧ:g*9 ?n€=oÕXØÜ÷×÷Gº£d÷ÕŪï[–˜ò5W覬›vå`S©nN÷V‹ûÈÖ…1K×+µÍç Œ¥!­Z@]›ýq~,F7?0ãÄ:v@®DJýjÀ¬–ž‘¦ñÌoqm©œ§rŠV¯-¯¹uVCØîðl’ùǘªßL½B*+͵ä%’¡8WSgH…ò\KrbZh‰ç&ñU¹Ö]ÁæâõÙL$½›žMzîœ (@¾–½ 8mDª[£Š»‚˜ÌJüwÊ|Ûê„5B$lÝ¡£?nF_èÄ®§ËÆÃ½Â9ÊlÂZ‚¾I?0Êíè}÷p‚`fòîô0õø†é^Y—HlÖÉ%KØ…Ðq;Q??ÅCP}*§ó|œG+†€À[†P;,¤ô­f9–,NUL.•HÉÆNët‘¨aá¥iÔØ“—¿l[”fÕjjÌú²/˜i1Dc.s¶Mª†Bzܨµ»b?nЦ?r¾ï•óÇý|=ášsúJëÜy’fÑLºYx|‡ÐX•ÂM Nÿ|t ІqÏ9M{E?r»Jg¬1{Ëv‡þAb|{Û+HÐ?0Q14\o´œ3JáÏ—G"6 A¢£{ͱ›Ÿ¿ËkФyhÍ[nǘÌ“ú¢…œ?0b&^Çë LšÉßÀrîKkÝ‹BýîÍT™]g¡Kø R´cŸéÒÎÝßÓiD'4t²8ÔÉBë5: *[ÃÊMˆÚÓ¡‚^Rú€ 't³?r¼•Àâª??l,Ï8cÏ»»fPærm×*Þ¥³Má´´ð ®­à ³t:ƒû¨¬î??ÊV|fËÈÊ‚RƒçýÁxwSWà??qìª)(‘¦ÜAŒE4ÜûĬøè¶> ìBݺÛg‹¼ÿÇôyÒv?n‹ ‚"–šÜäîsE?rÞÍÅ?nˆým-?nndu‡øbõiùÚqN[2Zƒ½Ú4~Nfí)†…¨žûbˆ]cŒ8ë¤Oߨt’V%ÌKNuæ»f£+ ?n_YË–¤"ÛÁtY Àc$Ø»+ÿÖ¹‘,b"SÚÃİîÉsz ¯¸JI\¦)–{“ˆúÌW²þ?0ØÎ2*< 3R )é%Ìóõ™$›„¸ƒ%M”}êê"]D¦¼D(£dT2'²HFÇlDü/¿Äšƒ'B†Daž‡Ëx‡™žCUš‘à_Fà»pÅÁi%O¦6¯¢)®¡&6C¤|穆]×´ÉeÂ>7ªD:OÐ…)«Ë®så²`3¸,¢ò‚ÖƒL63¤ùÏÂYd¶3óeQ‘\٪ĸÀ›É°R¤×wyad??-cœ3§„MÃÈŠ&OÖÍu”·)TØÇéÉ|z£¥`Û©&©írXñé~)ºJ 0!ʉÓcó|ðæ’3ÇîÚÈhÚSÑÄi€Ö²,ÊÜ\Bî÷F¶‰ñ3 ÞÀ;¤ªdB3)Tf#ày§t¾Æ²Å椿9ò+B ÛÓl?rÛ•˜½—뤙ÿÎ`°™ÞŒ<ŠdFn"l.¤Ær^…IAù„8¥BÓ™FM??–&w?nÙç ± 3/dºÃ‘Pâid‚?n†yßs K>±ôBŸrhÛ‹˜#4*óϹ"ÂÓÃ^?nÅpZv¿YÙ#˜°ò€æ¦:ôQ Wí£8õü¶‘š·ŒÔü×P¿º??t„…ÓaúÜЃOJ%LînÄü_Í^/71;Út$nOZ*PÆbçœ:=>¹K:ë²ú,|?r­Z!3‹fqé×(­?0«‚ OàÑ+ºûnQœËÎ'Œ÷óE +#[úœºÕGϸür|ï—j/¨”ƒ÷ѤvQæ±¾½h?r´¹Œ9ÜêqE‹w›äÂÕšHWùMRZ¸‡ àû«0—=Œ•´ªþ0Ú#ã.FþàÔ§´ã:4“:ÏôìoÞ²–z‘†Ã˜÷jN”`óع=€íî©êÊþÿ÷û¿ÿ=ÿ¶qì÷'i¾??Ã…âx<\Ý|À:èßç÷îá÷ðOŸØ_ûïÿ~vpïîÑÑgŸý¿ƒÃ£??ü??ïà·@À²çáñ÷ñ§ÿdè»ñ¿ûùáamüÿtïO¿ÿoñï;h=öŒƒÖ=³Ççk¸ø½x¹J³‚VÍ«&,èb*á±' æ·Ì˜Ò£1®‹x!å_ýôô5…:'íY>‹¦ø‰’i™Qò˜cŒ07¿{{O¿{4þÓgŸÁ?nÀŸ^,ÓÙv»)}û…‰ônsé{/x—PøRƒ½± gŠ,§>ò@Î*sÉ#å#ï6¥^Ï€ïáÔ™~ÅóxEË”9‡éQop¾`[:½JLéu<£¿çôWl2›¼F΋WžM_qjÐÚ»®j í)³À?rÎJpËð2¢„Ü•z}x`AmÓK¯´?r°a'Št–Zm̤ Âà%°œÊÍTÊMzÐf§ÚDVžXðÕªë§)’ÍêR?0¤á·\zèÔ`ËKÛ´=ÍË4>2)ŒLŒ+!??ƈHŒ‘y(£§muGï&ø€ã¢Ù±ãJoÊ»‰PTMXF?rʾQ¨ 9WLãÇs<ên¢ÔQâú;1Ñz¯æ£X(}±³ÈñLV¦çÔgt´È'!ý÷Ée¨”GžJqÉ) p„‰«Ó¦^˜çn'ω³“pz¹Íþv­#Y1Dû(•>|þòä™þôâdüðÛ“‡ß‘uT™uK¬W’ùéêÇ4£on†Êd•þáâ??Q-îÄ'×üŒØ¡¼ÊòÕ‚Âh‰N/WÝö0—á2ùfz±žl—¼ÍôÛõdooºóÜ{ú"K§Qž÷h~•ÁñV†”ž÷œK}DpŒ=0=T??ÝÄÑÂýV„ˆ˜&ÄŸ?r^ëÁ?0Ö«(ëI›8¾O0ܶ‚áŒø‡í—FøS‹%V¶jT6ÏM”ö¤¥{‚НÌÐ<]õÒÉÏÑ´¬T΀°ª3À×Ò7Qø3žÜ$ô¡LÃãž›º"lÔ®ÙpQÞÈ’öðI:½lDc£•†‰/®hi㎠’õr|j®9¯þÙK3žÊå¦xè°&iZlj?rôdŃåöì¿VuÓÐÃ~³AŸzu'EBè9‘7& .¹º™>¿W³Gc½‘- º/ÅûÛiÞ÷ši•‹¡õð<…¹çÓt@ )Áð—«ANBÈ z4l´£/0D":)¼ö³µU«&^LwÊ•Ë[5?n_~±.fé•â\Mlä6£"~m$û¦Aψ†ÐÜÍêÙnÔ†±Ô¨¡Y?0:øxFÿ7¦ç ÛÛ’:v‘T2…áj]ô?rDBfï l§ibŸ&X?0Üv‹Ì&³ÒV.ÉêÌåDÑAr麆‹¿ž’ŽdPRÇ›H¹üÍkù"ŠV½ƒáÁg-1˜?rf×[•1åÐ^ÝF”óÒÖFsü^£Y ‚€æ!p[çÆ–f~èêÝzíØÎL·¬ÏiˆØÊª×8C(+‹BMv[oÇвϢ¼\⼊6®÷ ƒñ®DÃä·ølþ„WÈš•B?0êT €ü•eÂÉè´Û9Ù?r­•ýAýJ 3h_Ý•ªlB»ßÕ§E[ê—¥W½_?05oœ˜Î‹½ÍÐ1¼RáÊzëh­††?re‘­·LSŒ¢ûÕzwêG"!ÆZ ê÷­ÐZ.cÊ#Êc¡*¥eJØf3eô©ÕÙæ—<˜‚ÊŽÙ[²úxtƒ®xwqùæ"zϥͺœ-¢€KŒÔÕºæi¶÷WºBM·‹Öw”ËÿÛ[©2ÊÏÌH9É'­“ç´Š‹4x¬-qƒÂYfp›;(ѰZ-§:‹uØßEQ¬òlcN£DøXG¬+<®–Œ©!dÈÚ °RiLã$*fÑ&ÿ€-²ÈÞÝ‚éd\6b¼^Õê–Ù8t2Ap¹nk°+\Ò‰s¶¢¤©‰š«ìADG»Zk £.ЀØÝ$:ðrö-ò5o~}»ÇÌÝŽ…¹H%ßho1o•x›aƒ¨¬ÏøGmê{¦ ”ÏI•&‹ëWz ´ ¹Íõ®ÁJ 0uHþ?0Ô³E…ñLfpa€Ó°ýéÿ® ?0ü?rÏ>ûü qþóÙgÿŽú¿ßõ¤‰œòÈžÉSŸÓZ»}»É*ÃnZÁÏ~úññ³21\4á¶iôú*ÛìÖ²îé!&}›:°PNg)ü-½ZèU°_??øîdüôE«*üöi8†¹¨¼DkÑÛÖïh½>¨P”~òšìV?0J³?nÀ`ïŸÑô©J7ü¸ }Möå??Žéy<‹óp‚ÀtbžýVü¬ðë"5Ki¥D”˜P9×Yè˜ MÔ$ƒØ ã”'ÃðÕão??{Ý÷ìëë“—OÏ´ð`Lö.4( óý«—µ%K2cQCJó`1Y”Æp¶euÒ1AF2óŒÔ$Îf½T=¼BøHo$S¤ç\Ž·ºÎ“átN-ð9¿ô½??×FBfçH&f)Ú&;4oË0NŒÖíj€G??à*+"•ÛS‡8ΠC¸ºC6®RŸ5Ö=Ë?0¦†ö?0A>Vˆ 8ÐŒPaS–Š:J™]*Ò[J?0$(¼[D×L-W¥gm??fU„vDuå‹å4¢°q?n)š”©*¥]¨·[” »ôH"??J¾^ ç3#è§—Þä&q[×!iJ`Y½‰¢"t#S ˆýŸ½ÿ1²BþßôþÇçGõÿðî½»¿¯ÿ¿Å¿rÍ›†‹(™…™,ÛQ–%©¼üœ§‰²¿—Îhuù™‘½û^X¤Ëx:Fˆ¶¾g¶bY”§ëlJ¯q™å¬ï]ñíâ½½oü@ ÷ׯ^=ñFâà;J6q–&fmô_¼|ýÓøÙsÎãXíó²¾6ž"Ê?0Æþ±÷lî–Õ7NËèí`x(©òþ‹\ÚÕ`8°~‘K‰I ?n³ªgÝÀ:N_Eãé" Ç»a@ò}ZÙ£)"»ñ¥€»UÔøØä 6aS+ZvN W0~ ë¨Ý622l̓:_äÈ8ŒÕ©„œµm³ÝbÙ„,ñ<ž†»V¯ùTgçt¶N×Pœ?0lS© W1²Ec+œžE í„’5¡LÖó:^ñ/=ôFHã° ß{ñòñ^Ÿxßü4ð+ùŽjùž¼|ýøk©9©æ[Ÿ±D.+L‘hæ9øÍ)PRA…79eøü@þÝ£??C°@®àXŠL??Ù;p%”@RXŠF&7œ+E”¡-eÁiB…lždj}¥ôi0ÅF¢’‹…‹s¢ùËü]µ4}u¢,YôÖ “ë??|æS¥ýÊ—ôå¾t›^ä‘ðÍý¢oB}Æõ´‘X*Pˆ0h=pû|x 7àÚT ˜1”vúÿ¹iÒ/gœÊtGlQfÞl½ +#\å=ê a+×óÊ ]¬±<Jc.µ§›¡7éóç%]1gaÆâé¡”7€JYà¤:??¡ÿJÄ‚ƒôѺý‡8ÜD•Zæa¼à`1¦uµË02xoíW}›šª#T¡´¨ð“0Cy¿‚£ÉÙ-K*[ˆ:ˆ0 FÛVù4ÏÊ\Mª,‰ñùwôrTÙ&i+|³ï•G ž÷~,”ÀÔy¨™_Uî䕘ÚJ?nÛBžžUDy›]‡ݹé[†Ër}ÌóWëÃxè\?rÔ?n’4^¹ðc˜Ð 20?0dW¯&^I×ömÌ·‹é"µ?0ÚÃ¥màû‹”ï‹47ƒHg Ãúß!^Ž‚BïÍE‚ª”vš<ø´¥ E™ÚíçVúQiœ4ªÚ`Û¿4CÛ+_ô–V»ŽÔâÑýãúØ;¬¡î—r˜4ùº"ºh‹:Î×ê{?0úJEbi ä-{Ã$E,oØ6 šZ·N` æ6"CÝ!1ä»$†Ï¼/Ë.T(ðniVº˜•»ÃáLJ¢qú¤jèà“?nN3î´Ó+—í1Õº‰¼ÚL”â¿^“ЮMø »«]Û«0™9ïéÛ-+ãuSþ:Ü»/YºÁÉ+Ãy¿ŠÔèZ°ªŠ$ÕrV.ù9s]䜟»2!¬—aŠy,%¡Ü–: òO¹ê¤’c‡ç, JC€5lèI¬!›Ž(Cì|+wó8ËóÏ\Híþº”×*d@¼B¾ò]+$¡Š8JH¬‹£}M¤ÏÿGDúµèÝ"P³£÷‘Å+}?n=ÞºáR^ñ3Xe© Œuuuå3¨•UÁ¤Oý#´|dâ÷«1;1l¬|­ú€»Pà~Ð&ê¶ ñÙþKñmýê.¿£“ÿ†|wÞ!ú}A›†îæ€uÛ§È)© î²bª¬¤Ê’ÜÎùLC>e`ŸR¿´…®¶6ºÛá­»»åÙc7”¡ã4Y¨Š›-I!\J¸k Ì–ª±”J+¨K·¢éRÊ8»¹Z÷‚N"‡sVeF7Å „ADÏ„ÕtÆù±+“X/ãHLÊ(‰ ½ =t-¡£«6Æ"…ÀÊê‘âæñµ‰eRê¬÷÷ÏãÂ\eO—ûÓEºžÍÄ÷öMáý,2!ãòýš\x^Ï|ï{[6IŽþ7hQt‹$Õ6º¿Gù;ÿŸŸãøÿ7??ÿ¿ûÙŸŽêöÿ‡‡Ÿý~þÿïeÿÇaÉI>ô¢£þËÍŸþø/72ú»Á+üY{e]’–Èû™X·œfÞ²ˆè¿ úoIÿ­¼dâ%S/‰gôß…—PJrå­¼·^îåøîm¼wÞ;’ÂÒ$š±¥y!Ž–HWœˆmæãÉ2D,ÕÚ)<–?nWŸ…›±Ù ›góØw“³¢L¤‡JR43ŸÓ"¯~½3ãRÞ¬{È ð^͈UoA=Až$åG7Þq2??֒׫²0žj‰ó9™ŸkV±”ÆS-1K'è–IæçZ†‚DPNÆS=ñb½œÜ𓛺±É-=´éå£ìuDïri”}•1Š?nI6uÆ‘(´öš×ßC÷CQ{Íëï™ûaS{]×ÞWÎûµózžÅ3ç=QU¿-ÃóxZ'‰É8^Õ¾å“ñrÖü¶8w¾å©KÜé¹û6¦R$Ö¾æã".ùda2sÞ£y½éY£íYî ÷*¯WFí¸t`_“xæ¼×!_oRqïâÕ2¼–¡pCŸ^â.áe-h™å ›Ž˜!nÄÓO5ddzkNØ1µt)HͧaRMÌÃy4fÂ>æ·•cD.ÜDV(àOõ’Yj ã·–t}—Kݽ¾uæMh”¦Î?0H”Ƨ¹3A'NfP5Ò½?0I&Ηb¹hႈpâ?0_Îä‹“'tó(_Hv¹d”×GéUmÌ¢|ZŸ´RÖ>õ˜5Ú·q¨~ݨ_ãú×Õ¢öeí€[­‹ÚpáK:“Œ?rÞhåÎØæ·:ËÀJ0žÏŸà>²ñ± Òo|¤ˆôηiºH3g?0g¸CNjíX×xõºÆ[Ý×õ*G½5¦¡|¢'ÍOyóSñ«¸Í´•Û¨SPV’/Ç—åÌýRÔHpY¸C^Ouf"'º¯3÷5s_×Îë$LÜ÷©û:s_3÷Õ…»¯Ë÷Cô*Ê–˜®ù–}û8–™û8'31fQf®ùGI‘÷«]œ´ëEkÿŠŠÝ1z–ûÞzeÓï{IÍ?0¶²säwÊÂK&U•Å_.‰s‘²vBµxaa þã$„0.XîŒêG¯Â„Z»¸ñÌ7¢´{(+5;G??Ch5.Mc)’”GI\6N€‚lËž£,qéû5œFn]?0‰‚èr‡Rþt{½æ?0*¥MQ?0Ä¡dV>Î<ªªŽ¿ ?0ÂRЀ#Ý•agq2#Ã*!ºB¢EœÜ—ª¹´ûñ ê+ÄâD—‹=0Å•zï ®q8¼ãõ@Cû†"ö1¨û<û‚Ѐ`Àú?n!æ§aQ¡À²s}¨Û°¿rŽnü­?0ÀÞf¼2 )¤óˆñï™,‹xj(D•mq|j-ß“M㜺iޝÒDêÌÉÇKœ²¯Bìó<&¨Z²÷1ÀYy)IÒ j#µ£À¾ÿ&A :/ôæëѦÚ*)¢×'²¥ÛE|yÏÚJ;tÑŠb¨”úÏoÆ`Ó<‚y´X²²h\¦ÝÝdÃA =§äˆ‹›mùt3ÿ!,%FOº¡õD{¿}ôÈ……ÒÌÉpe jJž¼%'èAHŠiäPÊ>ª§_Ì‘e p#DI<ÆŠ_ÀcbsÚôzclH÷öâ{#ÒµGÃê‹ôè³??Aò$ðH©eÔd›_ê¿2€?rC)wy}ñÅïè9¡?nŒ]òá}Š ýü‡“—??¾|üúÄ|:¸÷³eŽjÝYO5Xl áÓ8ÁLAW„Ý•¬ÌÐIÎÉÛ›ÜÔSÜ`*DQ²P $TJÎ¥?nU‚EcdR>v*=5üC,º§Ñô2_/ØÉE™@ØÛg$+0° ê?0ƒríSЧˆówåÚ’^år$c,íÏI¤$Ö´\eO t.å™­þ•ŠVe¢]€ï–a; kÐêÒŽ®¨£ï¥åEhì½ëw}™@¿ô«Ë›—­A??ÎÒqzDZàþ甂w^·J±>~F‹´K1דÑáyÿ®-tè}ÅÙ‘Ûëåë9mà\eBÇó4©hqí{…Ãå6% ?0)Ò‚z@Šƒ5 ¶ÊžÛX2ºw)-ºwðÅçžR¸¤e4‹C³ÚV)ÆØLδڬWG—[LõŒÁPZ˜,Á.5?n?01Àlä¿ÒO ç3ëˆi!]8tÂÌ/ã`Îê?nËñ–. k3™u`d_Ð ÆFP ÆäFîK4v×£g€¤Áðpæþ¬¦¾ç¢wÓ­³ú«w0JRïpÏÌ‚ŽFáâŠÌkê[ÅÑܰ Óù™éÏéôÌ,¾ƒE˜ZÌ蘎ÚEˆ??]ŸT?r`‰¦ìAG‡Û¹çáƒiÌšÉ$&.0+m[–Y<-¨]ñj0½¸ôîŽÂé´À£³?n½^î.-lôdš“¨Q\EQB´úÑ*þ»O㯜Õb^9Ÿ±•T=ϢȃĂåg혳iaV‹7I"­Îã @45NO½ZOq¤¸óÀ%£Ãƒƒ¾Yãïz mFÒ§•„Ë–œˆ2yQBÈáù…Ž&€æ#scqK¾-5ÅÑ€OP 3Xça¿‹]¢YÄóC7º[²é (?r;Šš[!„&šY?r/Ï V*²ºh˜íÂ,™ï#s¹²‘†¤—6>õ8§,Kƒ5ͺJZ4ƒfm7ÅSŽ ¡Ð„¢Y¯îÓ0åØ{¹7P”Þ¡€äñüçÀ•/izdžUñŽ‹/}'!X$iH\ŒSežç5¶Ïî„–ü-­+ê 2»Q6½]FEh…ìGÄÞü”E¨/¨CsZ6+[VºãÌ-,„2±²J­Õµ3?0»gÀ ŠÎiTo*ý§„È€âv7Ìè7›Ô?0Óì[èŃi±_ö…Ž J¼^˜çX‚d›Â Aõçi: êçÀͧ©iÍÛ5?rçö,ÙEÑÌEQš >Õ–úf‘ÎHˆ`ZÊ??©!ø6üÖ?0µò§<:éQvce¡”ÞÔJϜҠ™Â4:´t׊;¥¡8íËÕþt©beî™Ã«øâ?r?0]ðs‘r»\½8 —š º”]P%Ë0»ÍÓ´Ò@m•HƒÇ/Ê…äîgLz{ß›†Ùlž×ÎcF§á4žýw~.þÿݤ‹ô Pí4XEÑl0[gá$^€q54Ï›'r|°”çƒRø­=ݾ¨!ª»Ôú91œ‚Ö6Jf,u¤„’Œë&/¢eÞ8ÿ“‚Ãת°Q¸)µ"—gÄ¢”³c?0©ëDeå"ª[/êGâ•rÄ$ZÊy½RvºŒÂ8dšß5q ²xôýË'–ºaÀEÉçѵ+¬Ü±ÂJ¸žÅ©Y9ß$¬÷/õýøYñÏÒ7®ý€;Á)/ú°€¿ã<”6¢5€ØHŸ³ŽË?rÌ?0•a±2ä·O¤»Z„7µó†Ñ|Yô©cýpÚ, êÛ´y4œ¬–£yßôl@ÏdÍd´ËyÉ|'á˜Î³pI€ß$,º”@|ƒ3Íív?nW^ì¶‚;µ„`eýasÜr=D}²<²ÀìrþP-ÍËp­t¹%wËÇ-å½/—Æz*k MµsÎÛ;1ºvÐÚÞµÐ$»µ&VãðÙ”[aÖ^a[ÙÉìözYr«œÝ^¥Rl©•J¡Ëq©~æ¬ä†¤™§4y“È„Ö÷(Qe„ùèÅŒîúZ³Ò¹?0/û–éi«•r¡H„%Á*QEˆË.Tåô²nYQÕ'ãÝK³™Ë É–„ e‹ÞÍ{ð%™‘,ºQÕª)6?0@½`%=>?rŒ¶Ëp8›áǬp;rŃ¥èÂà ÷ÏJ'/O¾9ù›jÁ3zý·×('t¨ ½‹¸ûdø²•ÛÿïŃ×ßn%ÖQ|hÚˆŒþ0ŸAœÊÓ|4ß¼^èQ+?nSü*šL²ô?nJÏ>dé)a³ïEÅtè?nålhi·f¨YA,ú­š-•˜Jnµî"sÓðŠü%çÄÿTÁuZ—Txqƒ3ù¼^¶Hô5‡`šÙöâ݃ÂwN±?r?0N±9ÊÒmᣅRúªVš•ÄzqÕ´°²×ü™šM3g’ò=mlGa-Àt¡Ú î, ©Íè¤÷£UŒ§çÛš£äöŠu¼Ký?0Ñ¥z?rŠ› ‡)$uNŒÏ×a6à ÊFÎ5O«ÃøÄÔØ‚˜³A».Ö@ÄÇÆ%îì­Äh·¸­s‡3¼rvWðR;"HàÙGJõâ~‰œ« ¯WNQ/"Æ4k6Õa{þ?rr³(*§ 4ûÅ(¥ð Nx_¸µAé`€o}A-o´î î(Æ~?0‚ß,æ[Aè °$‰P;ȇ??Ï]Á–u×÷É8~ßûynþÜ¥¿Wø°ùÒ­¦”ßñA?0Iïoÿðx“æýò‹7À«ÉHoº¡!?0bѰB ¼'øu ÀrNs9slŽ³èŽ¹!õÅêGT«d{t/U3$X0„ᓼ÷Ã.M±¥„à¿Î ÊPÚE‘Jz> D+Ìê†Ò(,×­d-E54’5õ솩c—Õ&p8G»!8£ª>™ÞÑ gx„æ¤ÝÙNu€*5ÄéÎ$1zeo¡¡¤p´³ø_¯rêúh{LÓ÷6#|¿ÅÆßKF MÛºi»žR1v¡Ùä4Ê>?0™Ý²%_7÷¯/¬™¥°µÆÛ«Ðêçí,Þæ"fiíʽÜ~¨®aů·5‘ ½s;¥gœ¬Óu¾¸ ÛY>B4梴%š^̼žÑÈ΂R{ˆ-¨jp;:YX¢Ë…V¨,JéjaæŽî°4²X É «í¬JS)ƒÙÑs~>Ø,qQ?n½Ã‘±¸ËûG#ܱ‰fý»#Ӹɮ/˜èu­HUÊâýX.G†ŽÄ-ßETš(t1qdeêåèÏÐ%‰ž(‡å?0EÊžÀ>Fd1 gaf*IpÇ?r!¬¡ŒE¢5{"Cvª—Vט‚CÊa.¤ÝØÄ`«¥üŽÓ¨°Þª|Ÿ_à"Qxe¨•àŠá|Nãa{¢´Ž·?nNëf*ÊH6þoþ…x›±Å®æxLKfcÀË ¨d=ÁÀwÀõ¢É?r!Ó™M/ä 5ŠOˤÒäPúÖ‡@d=¡2?nPÌt–uÔ8#èQkÒ;î-;îü•^ïu0›–x½èšàÇð¾.åÂ?r•E–Äœ„I¸¸ÉãœåB*Ã+`‡ö’Ó›š‰¼2½7qt%ºt‘[ ÇQ"ç uÓxÆvÊnŠa> ¼ûE˜)ö÷£/.­c½ Èb™Ê?nï ºpÊ£Ô„“ŽÎÛá0im`6‹ ÅÈŸÖ=±’¥üN’ÞAÙ:8ÈëQ)íjÒ±y ÆÁ"^Æ…%pàÀ1i阂ü™ñYPr‡¯º!Ã€Ê &Q¸´ë‘bލW?? ‘Ï4¾T2??'H…ÕAf»ÎKá’Ú­ X½³„\Ã©ç—Ø»Ó±‰5á”öÄVè®[¡Ê]MQÕÀ’g™ÎÐìjä8¨jZß¾™”‘ð¶ÿ&ÁüŽŽX% =±Qi¿1+‘BÓÆL+YT*kšX~o/\Š‹˜‘ñ~vìmJë† –%1šmXÞ ‹±?rR)Ó0.¢%}'¸¿êþ÷ìz¹øWø??¸wtOñÿþoÿí÷ø/ü³ˆ'J o1(ž,"¬G¯é[ÿ“׻ɿøéhƒ#œýíé$÷{Ü?08??yëReR‚+éj­¾uñä#þ^˜ïœ?0Æ0BÛ‡ôŧ‰ ;M8ؘãÌy›q;^©±/`Å1È•r•0°Ê÷ÓirVFŠÙ³·½,f‡ü4æì[bÔÜ÷PL^)©oê?rúžiûÏ®w§ñõ†?0šBï{»#ºÔ»„R‚§FßìpøgŒLK¥¿Cƒ:v‰<6í{í?nWÏ{ÛgJ 9l£¸ŒmÓ{Û§à¸C?0—b­@K¿ZÇXY®ÂO€+áÔ‹† £bT‚Go㜯¸ÅñÔ^ gÎßmá+o=J§ÉEÓÅ·|Â'€°å®ä>I?n2Ǹ=ß÷‰©hÖ5ÿÉ5My’9ÿËh®eßâoŸ0ãÑŸO.C¸uYÉìök¾ªB2ŠƒÊ>ûfçЩ#çBÇgZi¼Õ°Ç,ÐeðÐ*”œÅ`hL…{Åua›¿’Â?n*)ÃyÍP$¨­¤MiQv®Ae¹×Û &Eæûþ_>zôü!¢Wy3ïtï/‘[ŸÇ¯òBÏ'.'áåš@çkÿ˽³/÷þÛ•/ÿÞçÿÿeß¼3(ÐÞöÕO¯^Ÿ<õ|ˆ`ä?0h??*¦ûâ¶Í­£}VÌè0Rº/ÅÃzap0་zßG[û?0),«îÈ ÿœ¡ šÞŽ~|ðòâ}yÚØ¿I>†Œ k]¯ð>ö?0áΈ¸X^ÐÌÏ´ˆ®¯'qÅtWo0¬JIÄ+Ipüü;j³¥…½r)ÓC«4!J?nZ^I:3Ü—æYU 5Zþ% ×d?nÊ”¡É:2%*Í¢”-àÈÆN˜L eÖ¡†x.ebÞ¿ â‚⬖sµ6üFrÐïþŸæ¹„~üã??Ü;¼W—ÿÿtðùïòÿ¿{üG7^TuÕ÷ž>xøüÕŽ¨{’:IsI¡G%æ]1ŽCI=A¦”hû,-D øš†âFõS"?nZo‡2Õ/¼x#äÚs¯Â,‰f®'@I+2"½‰áDX¬›@Ziy6EÜpùnÊ4$??½Àg„›žCÊ ÿâîg6ß¶K;¦w+ÖQWKÔ,s fj€K|==L»îíka3ÙâÛ‘¤W®§ïêÞ‰Û/¦“,±ˆ’ží~@ þàu$?0')øÙaI³jŠ[Bò¸£S‡‘œfžÓXÞ³ƒF8ÂGëÒVÒpT1q†vüãì¾gîq@oöqqÆ· kaÐÜ7ó°uÙ?nÿxLd«Bz;¼÷ðÇ…æåµN=técÏ”ìS d:ö׳e=â$D%§%<}m Ô–Kq‹ôS8«Égº`¦€¬1@l,¯îq,’eëVæÀrÎÒZkG¹ôB¨Ì•» ­õÕäÚkTãúD+L>>êDm¦¥€¦âóTF¿>Y#„ë,{ÆäoÞø6H®RãyLy¡ôâ`Õ3ù9Ï’zëxÓöñ“Êî‘*êcQ ¨aI2ô8žE›Ö1hTÜö~‚mÿX¬ܦ;÷±6õ懨ö³Kàa_þÐ"Ö °Õ?rZW…zýTVµŒmœm0ðlì¨äŒðo ¬ŠÄlÓþ>üä¯ ‡Ÿž9ééþÝ;û4ðÞÐ''æœY¹Î60(YÂè{?0±-Qfކ°øFTA¯ô>í{cÊÀ«uïÔ_þ™ÍA:4ËÌ©%„>º_Ðå",Ä-%õ°ìKo„?r‰Do!ípƒ¢-ìLŽêËq±!Äûô窲¡ñ‘W&¦´]ö9=D°¥"„ ~:?n77z%¹Ñ6‹$YáÑ%öd›˜= œÕQòŽªÂÁñ®õÊ]Òôp .­Àpy«$°Ù}Êáp"ý‰Ç Œ‡ãÎtè`èÑÙæ¨ ŽCAIzX¹¯iŠÌOà°˜CðGaDy‡-èÓÏdg†%??êDS…ý‚€ øC72[˜×¢dVû~c°‡ál¶J¹ñŸôÙÁ9òíàøÁNþ,Z„“hQå¼}Gß"-é2†.tXÚ;,ºÊ„&Fpú÷7oöÏ>©2û¤Oy–Rp3F@L˜L9‡Ú…Er¦i\?0Îíˆ ‹ëÄ~“ 7x3õk‘…”©ÀÇÜZÆ®Þ?nTß^#Ó/IÉ馪A<Áµû¢Ä7 ìµNÄMœÇXß&±àê‚Æ“Ü6*óݨè¥AZÕ`‹¤ŠáNTÓÜB ŒÄV0)oß0¹°'”+ä³D¤Kú.£j•L9«]Ýæ) *ôlªÌê|tÛ"[8·vm§ÀoòL+Лv{è蛤"±¨B± ¨&ˆNðr}š‰ì$sS2=,€§Œ*!UhÁNêáSÖÌ(— Aw'†Š½O{ÿ×ÿ«Ù¿âüÿ©ü>û¿ÿýwýŸHžxà9ÕÕÔ?rbV7p…«Ð'N¿¡?rL³øÌ«fŠïÅ+ÛžI˜ä”"8âÝ­û|­ISmðù4^yE1. µÓÿ„Œ§'÷Z×IyëÕÃcºØÖØaÃöCîùÛ:¤V x·‰`c•D¼o«#i{mÉê#J?n“4¹!5RîK;ke¥¨Ýþ¿3ÿžP¥Ó@ßqšFc¸hƒÒ·g^Ë!??ôÞ™ˆv.N¥¡ïÜ‘­·ÒÕäI…—‚??++^­a>.¥?rZô>Köq¼>Ý>'KR„­@¡ ]Gç‚…‹û·!ísˆË¦Ë&C€6ÑÜ­¤Ö°w^ÖóÕÕ¬N¼çÃE¬l²')1w3©(^¨ÉýkPñÜPŸØÇÑïvN0µËƒ-WKc§åÈT¦åá},RÔ–dée”™@i@©¹{yLnïÓ‘w¨”Ôàú±H—9î~-Íl†2ç5^¬pv“äî/Òe„Ð7=wùp Ùlîû¶0ÍÄ1r«;°jKÆ S:&=ÈÞMvà¶?0òJ¯±òá«Çû߆Ak TÃ?næP-²p6_^=}í» H®‹ùN`H*ð”µ6h„'¸N §7¡_•[¾Î{Uù[Yj2¬Ç9™„ð\BñN»® U1;ŸM¯ñ}fû´uØ–«âƯ¸x%öÝpåê8ïÐíŠûn~ãê«ñÛ¿»¼¹ÙWÍ??Kmn7;ô‡4¹!“x#çS?rìX€/ü¤»)‚Lldö±çÒ 6GaĬ¶•uî–ˆ=é‰ýv¥|[*ßfµoþè޶n¨osU>…n¸ãâË<ÙjÅEggÀ(Jmì¿z™ØZ¶®KZ§1 Vܦc:M¦¶¸€]+Ìï“ò<(šá¼ÈLòÌÈZ§ÿøåÌ×ÅH³Ý*„ç0xœ»«ºXÝ%öèâãû"{)˜4猟ZŽyn+d1±æ<*[ѯLdÊÞ÷®ŒãæY«½FÇ®ê_8"G"Çù@ oÁÕŒÜ(®Y‚ɉë:®1ßÕR®Ëçj9Ü*r%Meüç|M‚€PÌfz“ ÝñúAp«Ñ¢¨èouÒ{–VmfÌ©2ß,}OÊk6€:¨å±»Açs³i!„X¤O±K!qiÑ#ÔÍ|”Gr^\ŽIJW;æØ Pa%q4Ó§—Å€»Më>ÿÜÓ Q«±Ó4üãíÿ±å(»9æ{=š™«òzF„+ñqÔ+û~…÷¥}_â}fßgÿl—DDqO…ÒäD‘Ê E×;Y!&¡õFä&voõˆ,û¯??ŠéZä`AáöÊ EWh…=š‹€J/Š­W#‰•¢ù}“¢ùî’œVS<©öIÎ ò®-ø¹Æ4$Üè·ä|l%³R´OBȰ¸.D¤°ÿ@®1ã¦héz˜/VOâÍPß!<]؇óÁ…D¯0¼êB5@fDKø~µýnØSè¾~jKíYîT÷X˜µ!NizMhñ®œsý«.g¹à`b©¥³¸™kê?r¨e/ñÞÈŽ`YcV·ý¶“I¢·ä ª(îd ×É?n…¾yƒCdÄË_FvÇ0$‚_iѳtjûÓHRHëN(}O¯vÁU;Ñ¥€:hø§ó4Ú}‘AíÐGmœZ;¡Iœ£uŽBûî… HF7ìüÚ1Pª*lMŠú˜•£‘úÑ3ÏYsâBæÙÚ¯ LœNÖó I>>Ÿj´Iöì£Guõm¶Áe#¼oÚidm6š5þ.Â|GÙ`¸’FE~?0§›Ø#tÖ1È8#møjüøÕ£Ç/{Ì€ô]ìÙFïï5í@w,×#óõô‚Ù8qzi“Ö`å‡jtÉK©ª)ÈñJ¡i«;•͆¦è+I¢ÐÓ $]1 'BG°#e½²iª®`Wui¥TÐE ´„¿¼Ô ¿u™cïjäò,!"WÀ¼Ç%Gõù‡*¡¸Ð+u7Kú|Óç4Õ„h«æðZu†LDŠ‹s´-\¹s£;±+ûï)Óºñ"??ä‡MœåDXä=źé¶«k¤¡îr‡`ÔªfÎÓÓ?n.ÎúÞie:;kæ/.²”‡ÚMsÑȃ¹ìQÍQÑ5ñiéô™’YlãC8êê>Ö!v¥öúézS˜’?rkÕ»ªœ¡Æú,W”K½¡4}ÃÂl[úž/GÏD»ˆ©i¿¢¯’:M„Ew½å®·œØm`ë­,Jáb1¦}¸µF”6j¥b**ç\ònËèFàÝÅ$uÑ”jÜ•SŸË..Ä:\ Ý%'ýH'*zQe‰lÙÎRV:cVÚzƒŠAc‘¯‘æóWì?n„Ù­£¡12‘¯yž<{NwÙ[hRŒª±Æíì‰"`ï\±__DÛ0»ü.c§'??†í{ʲU‘a³Ú±hæå‚6…°Ì®•ÏÒÆ8k²%k#kŽq^zºY/ži2Z ;è®ÇÀ6@ðšgS /TOGÚ8Þ-zì”Ý*q”b¨"•E_¹å¦uà¬'ó÷½\>ôY½™’¸ùgÆz¹ik´¯j??ÜèšP®•I£¨#„ÁHˆçÚ4Ó{©rÌå .ÒöÒQôBë"øÆõÖ”ªÓú¥4 ?rúß®¨ºÉ¼Â‘×Ѐ¸C‚/´ðuàÍW³?nV4„Ï^ r »J }ˆ» NE :Î!ét'Ÿ¼Êì¥??2äŽÞB6ø!…"×{HµÓP9|?0„ŠÐpu¼Ê2þÁÑ*´ý¯F«b9Š ­ÏÁ+}„FòƒÖ€µjä(B¯Æ‚quä Vq}°¶ ¼:ŒÅÍxr3^DzVГnbBm¹¥ÍJ·øy[qÇŽ¤4ÂîY{ì’d­uÉZ¶9¡’ºe¬qqìõeéï{lÅíÚé(ö!¶ºyˆ›G5QlT­m²ýg;8Tš.-—†ú%׿—á5U€¶uÓØÒ?r ÕED²GµéÓ.&´fÚ0¢»ZmŽ{?0¨$²=#AB?rlýØå+QŽ0;0Ê×#D:eåÒÿ?0#жÕ‘«1+t;ªU÷¨jNàí{GL^J%†‘2wÕèA$UñLÍ#[!ý?n޼•>l´£?0^Füw ®‰Q4^0Ì…Ñ#%ƒÛÆ$ ‹c?rú2Ìß®#ÓãRb”£ŠFQ |qb-¼O葵¿åô~PÍ<Ì)K´‰=)óèä«ï¿i4pc°´üøÙ×Ï«2ÇÊmVìXÛãx?r™ê—%·³Ò??šÿù.ÐÚ~_?0.DŽ·Ì×-»Ä{¶Æv»Àá«ÛrAÜ<$ÖI{Ç0A³±õ°kd‘ö>4+rXĺñøEàƒØoÝ ¢*¡!s·zˆeÚœSJ…ŠD1*o’÷FFÅ8’HÛkÙ[×£K{½gnÛa¦.VAÐ8nJ÷ÃG{½÷3eäⱌC­ñ­Ê>¹’Þㆠñ§ïႇ/ŽµÍæ”f2棕#®ê΂xÂ*·+ÄRzñл?n”"{ÿ–þ??p[D]¸?0ù-ý|þùç‡÷þTóÿqtð§ÿþ??~÷ÿúR}`ËW 8Eš.rùðsž&Ša’*ÒeÓ¥pÝB¡º)2êžæ\ä–q•4låäyMšgÆ| E0!®ÜŒø—íÆòÜ8ßÛv$N~8{¼’¥œiqØ|þ,*®Òì²²A‹çà4QBfCiÂZ­/_ÿ4~ö|ü俞>½©nЈ$¿x· •íW«ëev-òòäUÍ¿Hß;I6/BÿߎÿRß{5þáäå«ÇÏŸénIžœœyüúänùåõëŸ??*??=yý€Èç«ç¯KPß¿~Èß„Ù,,3­EŒÅ€£ ´úyù>É¢éæäù×üú#êù‹4/¾‹núrÚµMùbÀÀ’Ç0QùV¤ËxZ±6|~oµ:“ám(æaù ÿ9Ilj| =Ôùöã””ÀqYÞè¨æö9_ÍÊ—5ÍÔñ2^JF¢ ûdàŽiv>ÎßÙÏîÜ +#ý¦ùž®3é0ø˜bùÏ¢­£‰ƒ>‰òi¸Š*_ò |+??¬—aòkz×_pN¼ÒJg+†J¶Ö2|²É町H¹¬¸N/YH~1ô]"¼yl5^à®&æm”U>0£n|àiÍš ëd‡ÅN-UVÜÑXC‹¡9:ºXæåã]¡ŵ|¨¨^׌ôe”%µOÀ–æ’ë‹\,Cã0™Ò®ß×Bçò%òbý¯›òÈd0¶”‹‚ÿš#¤MJTÊ ŽHóòÙlákÞ’ðrG Ó©['91M¶Œö[zHï( nŽîË¡öü_¿öøo´l ¶ôô¾7 íÐÁ»'¢Dß.Õ”ôý«“G÷^<ĉ˜¿Î£Ù<¥<Óµ±8fÅô/XA¡Ž’eÉ\,ÿÒëÝí{_¼ŸÅq­§P?r¬‡ð›:xȺ'‚ä'é`Šo>å$><~øüùwO^Awy÷àõ¦õjv}á!­7]‘3ó7÷Km 5ù«þöõ×(çÍÖ‘qç%A† ÔË8ç#uÆ3zÐÛÇž;6”ONƒëù|p1Ë?0XÀ3™0!‚¯ŽY„¾GÏ~}ÐIò·ÔþgÏÆßœ¼xðäñè³ÿpÅ÷Øû.ŠVƒ çKÞ‡Ož¿jä{ˆcM‚÷äù7ßdœžÐ%lòЄp‡á!>G«¸ú™vØ/OÙë/_¾ñïüQò>õº‘õïg§ƒ/ÂÁ»ƒÿeÞ§Z濟Ö3úœý»&ÜmÆ-Ìo_6r9í{úè‡z83ì¡Ö³Oß Íï??îþôèùéòìôÑì, &Ðì~ñà僧„1‰„D¢ï,zÆ8QãdZÙ˜`Ì€±{à‡~»e‡{(g|hŒy¯?0 »¯{m>ÐcQX“?0JÇ›°>Z®‹èºL“W7O.©yí{¶MÈÜ”x%)ŽË2IÆGÏ«fQJªØ œxŽRïÄMÅÉ ”Ås­aÓT鱆AW&¤d ò^GØüBràÙMEP[i:¿èG$‹¼7så™d’W7/c~u ü’W”ЬYëõ">e]ä%^DöQæ5üåã?r¸68>lIÓEÏÖ÷²~?rì’Øv¸ˆ??.ÌxÚ‘šŒi)C’wÎX$W \?rƒx³¤ÕÝÓDoµÏhv‰H«'ÅI¡}NóBƒ´•¯èh6]¤É<¤ëv®jOZ’XØÒn= Ï&’‘#ô:†–Zä^˜žk)³Å8ž)¨;Ÿ×Zþ" cùˆÔ‰q^hCÎGÖZÙÕ•:Ú$iJo¯*Ï!??×Ùj­ù)Àͽ§ï}qViV˜¹ã<œG4ÝÅC=‰xæD]plH÷}`¬ñy8äxú*á"4©_\„…ÙPMÍæ¦Š8^ôˆ… L<\žø&ÁÔ/NÆÒ™î뮦…rL{?0”a?nÈb °¿¥Í~CîØÂ©´øç#1ï¨w‹Õ–Oáç÷ó‡{Þæº ißåŸ9‚à«—Šº˜Ââï;E‹e뵘?rõ[Ô¬8ù´%®Õ†ONžy¸!‘²–‹??:ùrâ²´]-Q-¯|v[:¥k¥§›„íÆlÊŽSæŸóqZÇÞ~Îå?0“ 6CÆ(åjy`làÿÑGþ_A¹øÔâUòJ«QÝû‰Ý³ R·b»eüŽa,ò7à±¹¦ÉÞ˜/l!öˆ‰Ñ¼çàt›nѺýT› GZNÄÔ­:´ü³¥eují¤b= 1Kv‡»VV¬«®•ëaÕ’ÿœ4»qB$??ö½‡F2¬ìª:¤¼H3¬†×ƒð<TÊü²ƒ°hõ\ÜŒ'!/¨2ÝØ!­\dV/¹à3š½¼$ˆ­¼|Ün'=Lj2syßX"E?nh¡Æ8#¿éM|ÊôÙÁAСúåÇF+Ø|Óxõj6B";%ØV´Ï²árì÷òqD哪o“¸¡Êµ¾jlÓÛ{©—&ìËÚ§Ž?0Yé{"Ë{It…R¥U'éýâtÍÖQí×täÞ¿{(S_ǘ1)¦éu§âÒ†*ê7¶xngÄnÌ“•µn˜ÂÙǦqâ”vÂo9¼ªÏlµRÖ&¨•r¶µ§—ât‘ié— S\K‰Ðù‡S\D¦ÿÎÞ$xfc’JNÝågÔz÷ Sô'$™—móMÌÙ‘©½Ë9óoN^s­Úöüôg:n±­Ò÷é8BÜ??–[….z¡,£ŒýñÇÐ1Ä;_…y<5·×–#âÂwŒª^ÄÄшÇCÑÏw¤ÅEºž• ¦<•Æå¾9„Yç£v_0;"ù?n~!ÂD®š´¶î0[éxÜ2Q‰•Æ0ÃlRsÛaÖÉTÃ?0úÞçÁ^ŽX[錤qêÝT8dÎ"?0¬|±þ$…õ‹ªjOŠ#Us"IÖUaC­gº=*àzdŠíÖZùFèÙ p±PõZHó#K›l<†¶Ð7@°–°ƒú0§¥'×bMWa6‹f8E)R·C¦FË4A}zÅ¢ÍRªAB¹cñU÷È o¢!û.ãÅ?ršÂ‡iÃò(Íæ×!kd¥»hàÛÎcÞdråh>jŒ^pz|tÖV)ÁÀ­>;õË€¦rG -³ÆtæÕ`3‰Ó‘Ìœs9ø«‹à©†~ êÀ*ÏÃÖ&$ |ïK-¼7m€y© qDظ¤}??èn‡)׿4‘H¹]m/. õ®ãuD÷ÒtâÑ”¸šÁ|k˜;{S >>}t>€HrW‘V¦e?rA´‘F*w2bè’¥2HœUä^¼Yw%÷BÕ>ß9ßJc yŸ+{²Ê§úœ4]„± ½C[Êy¥t ¢œóô²ðÊ3NˆŽÍ‡à¾iÚMºfú6ÞAûæ}‰Wãéd— žAœ‡©âù|ú›gÓÑÇ9º³Ë¾VaËópæäœäu9‹×‘ð)a— ‹èXšf‘7‰.bÂå+î|A_úèwbÚ‰½V¸^æÐ{à<ôÎÓ.¿ÓxÝ÷Êë)ó8£îÁ™š—¯³d$ïLÃ…{níõ¨ö0¹ ¼Y`‰zJ¶0±ñ7ÝÄ3 uøYòû n*Ñ7·²4Ÿ/Ò ý`ga !ýí!û¨6å`\]ñ9‹`öŽ&¦ wÜé²ië>­??>øzûÌZ†8A øh‡E$i„ ‘JjvÒÐ}é‘VÍæZÓâú%S6¼QbÎÙøŸ*ÌI(‹¢tD˜=:æZõLî©í<윘“÷J@Zu‰G¬¶Ò-ã@Ô%÷–NÓJ³9x`®ãMVØÆ}T…t­Ò,t¬Âõ}/^,¢sb$ß6÷ø>öá½³Àó0úQ÷÷n!*&´pŠØÅaÒƒ#ï!±5‡&RmpNMd0>(Æ ›ž•†ú·ªn´B}èùƒîê¨J‹š}‚[,¹Än§žG²ÔC°éD+áÖ?0Ì….×Òp­CÐ5Uƒ¶N¦¸5³š3%öšÔÝeŽ·ïÏ£<k‹ùx-ô³Y³è¢DšÉ£L2!Ô]ž×bP‰L9è6כзLÃ%û'f•Af.®€€À$±ð–ƒt Eíj. í»…KmБYÄœºÞ6ú§ßž‘}Ìwwï.M„‘˜/EDþÜÁ*Ÿ $V…¬â?0•V¾KÝŠuÅ,ž±È…‰wç¯wÌ?r` P:÷îüñŽ¿c×o#®lûN+:Ür.Ý[â"#âUJÉR V??(U4,Åù·zzWf±°íq¡(©úY‚.Ž,B¤f÷üQié#o™÷ÿZÌV1­fÐmF+ 5ˆ­³-»ŒAëˆQŽQ{§çgÇX¦ÜõˆÔj·ªàŠ­â­¾4è|^8Òx?nQÃK: ;'ÕYŽ»¬+˶d¼}Õ~8-{~@¾}uþkòt:Î@z¶?rþAÞòj}E§œg¶æU‘¹ ìî»Úèse €Ccü‹o%,|ü z±6zX{WXÒÿì’@WˈÚTTïqX¸ÉWS’Ts)†£mj{?nŒS5/ÍïnU5eÏÛL]ëfÅõøm沉ãkëL* ö†F-Í*öBøöüÅkò×ðÊßeù' ê?0 YxeÉuÖT E"4HüÔ_+,‡3bÄXJËPüÒ?0šDm¸!ƒó÷ÞÕˆÞÎóĽš¹‘[©d‚&~-ȯÀtÞQ#z‘ÔÈÖC>;ÉÒ¬VД/Âê±FÐA ÷nâšx¤9;<ÛªÄÂ|Ç~  ì¥íݤ®1ÓMùJÁÞÿ]~æ}J@\«¼ÓããÁáÙNÏPR¼vBÉ/C’Eᥚz>åÞ6ï…ž©"ØÕ?0ínA?nº· «™ÜªŽÅ¤·ºâ F”’"¦K)ú?r.—,•›%eT?0®†‚&¥t½8xËöÓΣÙj|1^ç5†>ã£6'•²úJ)@\Èý<[ÕQ#ÿn­…µ:õJ 5«~h,øRvºÕP·gÝ­gT šÿ¿?0ì½ûÛF’/ú»þ?n,öꘈA½ìxfhË^o¢œÉcçØÎ>®ÂÃ¥HÐbD”d{ô¿ßúv¡ØhØjYãÏ>ýIDè®~UWW×SYÐø5¯bPãÓH¯u-º·l›o KM•Wey??è‚B&dÍÚæÎéõÛ3?nƒj–~n¼"§¢ŸÇE¨âp±kGuÆFiüx\½—©§ìÚ5]4ѳUNKRL*è,ÂS¸‡9ÔåÓ)2A;Ýó.•ξc¬žDE3 c!e"ÔŠÊá)]æˆýæ=·fÞÕMVЍ~dK«!¿e-êoX,`ˆž˜\a¡÷>|¶_=”ØÞ~â¯ï}ä*²È¯R³Q?rˬCˆ¶•Õ[röú¨ÌŒK«zÐŽ‡Úz0]Ÿ:—5¤þN{L½ §¶•¾[çªµŽ‰1°½Ò°µ’ñÿw*©‰Y^iyƒ«¸s´cŸÐÑIF¬ôœ›°£gÚI_ Ûëëªnè“ÅDÜUõM¯±õéá–‰¼ÿ¸Í!ÇL+j¸e²uÄ –]íŽêx’žåãÀ¡%^äÄâ`W(òöZüN]²æ¼|ÿK÷­ß}??dzj¬ö‘Go‘s>ˆØãfK™b`cágGé!¬Ü¹¤àié1Wx¨Årœk¸Ó.šÛÚÐqM@ÔDn,ŸŠöfÚpyä¶×s ã~öây‹ô,7Îâ­ùÕöªÆÛ,÷²Þ>‹—]ÿ5 9À{„.š<6ð70:Ü´™t_¸0£çX¢T»¦sÓeoÄ9ËFÃu±¹¼I¤Bö??Þ]Yw?r²ioÊ™èФ"Ò|??"yR¾h„­1Àu⊂+Š”’åF??œFæ.ŒrÚ±¥‰rLÇÙ‰O>Ò8õÏ?råâh=#êèAj®ÙõYvà;A1¸‰Iðt¾/ávHǾy`.-çùnWÍf’ -%»Xóz¥Œ©µFO#þkÖYœ¼ÆÝÄgc0Kjæ‹0пï~!Ëú°M·Ì‹€Ít¿†¶°þ²Eù‡_ØÊ*_Nˆj~QSߟ¼:yÖŸ°_<¢_^¾ÿîÏáCZb7~Qc¯Þ|÷SX;ÈòöEMüú:¼‘õâ‹›ùù§ïÞ¼?nke~A´*¤}$þüæŸNŒÅΛ_þ5ðL-ç—žÆè&ð@tep”ŽˆžEñ烛øã›‰‹i|xr^Ö:&…Ôbwû´j»e¤KD¾”®‰1’ŽªC•²ë€xˆVU‹ð§??ý©÷õíè–Ì8‹~Êt2ebÖ7wãï¿(»¤?nÿPžÇJbí“£5cv-1Íh8¹“Û‡*Ë×#ÎëÞÖExZ£)ÖŸ™ŽNÌ o p{%½+¯œ>¶zÛ=ÌßìŽ{Qñì`žŠèAäðlZÛœnúýu£†‰GÎx‰ú£v2|?n„ú¸úmñëÛWè.¾š~fó´Þ­–¹³nzšŸ{D YzB¨9búñél£ÎY†Ñ[Æ¿7Bbˆ,UMÄÚR&4jqFà Õ±`c’╹¨ÔÔ„²U$›é­‹öŠ6â×xºT¿\™$!?r3ë´¬gkº´Æï=`Ú^¬d4ªª#·„’|¯!e­{Öè%Nð2J¶) Y­èLÅ})ÖÇ}iöfÓyˆW%ÚO_$ÕÁé$úòÐFMš- lH׈\Öˆob×”ÏW*A³Ú?réέôê¶<ÎÜ÷µ€v„x"JtA•€¶î–Ñ9Žý—½)›ƒÓ53–Ùg·…xcW]œ9æ¼nЬ߳T«0Þ²Ça|ETVpY!•ëä­1?r{Éú·mx7Øú‚½(G}w+³+±Ød¶ÇÆ(Üîb¬\‹ý.ŘíRÌ®ÄÒˆ¯S?n24Ͻ½ƒÉ?r”ªÕUɾª.»¤ûÍ2ÆrfÔøÔA±Ðk.«‹Œ*Ór€˜¡TE/1^؈U5#*óÂ¥3\vZh]%C%PH.Ô‘ì þ´ tOþ$èfšaîáÉÁã??šÒOX¼ÇüIÍS [Kr`’,Ê• ’8ùÁÂ/jç?nr’è9ÂS™"ÇÚ/bµ?0\œZúѺA„¼9Â?r•ËÁht׺°È[û€ÇíÀÝ TØÖ?r¼²ýà’º#*J)·.å,3O騽¸ŒÜƒÖ<ç«‘-XC¢b`R¹Q¼€6Û<Þý‡]ÇTÊ `8>,g±kBŠ)å)?n9דˆ“6h6RÇÆÆ+øKrv/àÝ-€¿Ó`÷©wÐh¤IóMò [¬Ñ;!ž*¬k;›üûQ¶€›!çØÛ;áŒys"…ڽ͆>æj&¿j›¾ƒ¶Ûkè?0³(‹ÝH°T?rž¿m•jan/íÊhuƒyˆTßµ0íUêÐq*Û#?r‘}À7HSŽjÌ’šá¡Ë ‰’ó¦v·X’Zd÷”®¼Ä{àÄè“«QºÉCwÊú‰£—&Ã(rõÓEj† ’@¤À ‡oñw•Pæ eäþ^ñýÇeR?rFU§½¾ò×w%C²±ÃD9úþaÊÊ{QÈâIîåÆTôL,S ¢É?0uújàŽ†­Ï/wº0 ¨¨%§n¹Š?0(p_Ô`Œ‹ò>6ÓSã\då1ËdPzNTž,ꢻ|„×sd%[¡ÅßÞò¼Ú´…Ú¤:^DÅD!’ŠT‰€´`˜33º»Å†7Í moÜî÷G I~]åöî|KCBè1ëRB??þýo%DõÇ´ÓÏøuI¡·ÙCå_'¿sT ƒ- ¨sECl5M¯4RÌmvÃŽß–AŒ©u—[û è+ˆ¥{±%Îö9!ù9X‚ZwÚç'ƒKHQl'±‘g&±ËcœUåÙhS`%*BÍŽÏD+|ê2ì«ó))ªÁfâ®%glAæwÛ˜¯Ü—…ÐoXÉi¦y åK%-??‡ã³ò–â—@ÖnWùEŽU’jÀ¸7P…Ó?rÇ&§ÎCWòjc6@2ÚõiTi͈2ka¹í“Teô=ÁRªŒ6q÷Iˈy€5mL§n·%=pmÇmB¨¹,Po»šéý@ÕiïÀy?nç‘&]A5±¸NAʲ9Óåð”‹iDtëBĪ^˜ ßH`1ÙR4zÆÏ’Ö:5ƒ±u1 *…§ÛFIذ ”bÚ¸o² ?r¤}c(WÆ…ô=¬pø@=Ç5ÝrcÎrUá{’B?rÇÌ??I÷f"ä@Ç3AεåÍx»gÂ÷‚³…NK²ÝJ¦æaà“IMJŽì΀sI(âŸYy±Çq2Ê%ÊÁ6Ž(ÐiÙ¥V°mŽ ,šÑñR¥L}€"??¢ LÔ†½¼u†VÀÝZäª*PÚm ê+®Õ?0( ».œ™¤6dû*™ß `Xü¿È¶ô©\ j«Ÿo–ßÝqF§pÜMÏñç•£;·8éÎ¥­¸ Ð:КÁ›èZhÐ;G—‰R¤È¶)­H<ÂPƉ¹øW¡Æ¢¡®Õôoî[¥s?rO€hÛM†#M]O@?0E]+–/x4œ~8/kÏIë}™¯Ì«yTæµWt>‘•r%F¿íž1É£ƒ#Ͻ‰zÙHÒ÷ŽF,F´\]JLcîIù£ãφåÝŸÇ”ðáùçº$??Û>BÍFÊ:/‰Ša¦ÒjRÇçÏW³ ÆpI”KÔá/Œ4HÎCžÁcó×¾ðعÄ3*ó§NPôvcÚC­UÏçøU>â  =šàe +#?0??Ð!Ÿê“/´Rû¼áP°£Ü?r¦çŽ“¢wü¼Ñ×IAt V™T¼ÝIFÆÂ]¼P„X*à:Á_E¼ãZÕ­ò¢¨3@ª³qï¯qÀÝNM‘òË ïÒ…ú€ñÜéÒi¬I]Ü …·ºàCJ}i¬ôc…Þª\Ó"§U’l½¨üEÐuñz‰Ø~"!Ç€ºRÆáh˜Ó˜pÒ‘OIEÇMìmÀšuÞOÏ9+2ì"Õ³û¦ßÞ$/ö_ü??•Ž“[„üÏé̘ê[[nÄ~¼B·áþåÛ~.$PN/(Œ¦N%h.Îkû]κ#ÖÅw]7•¢‘Q ó°Ü:M•Ót•UGZΑ®HvÝi²Õ,¹k—/wþÁ¨X¹¡¤W Mã¯Å4ùÉSÉf×TࢅfÔ˜óÓø%ÏÑwÕ½D©îwtÂÂø’濺íКÆk®—UwÎAß¹VVÅ+J"kÕJXWÙF²³êþ­©å E?0'PÈMà¤Ëœ^Kè8€Î¼ëo)Þ(frI??¥ï‚²€áöÙ™@³LXîÛÍ‘.¤Æœ¸ðe“@£÷½0Tâd4pçê­!’=H­Ãáv›Ð6 =»îîcsQý{SÕ 0­aß6TÆÿ½ðÔ_+’??5þÓ?nj߉wˆ¸^ Á ó@ô¤pÌŽSÖ—<™ƒ½‡••>Šívj Q3´ÛžW@ˆ£ëvÅÀ?n›b>ûÆM>Z®à-½O7é_Çš£bµîÔSP´³¾ƒ£²2¿KëE1^n‰ÿÝ Ê(é”Ó=Uå÷¬”çÎÙĺp`t0ž¶%cð­±ÑÌ„;ÙÍËxLSۢϗóÑÖ™²EŒYÙ^¬K*a"ºÝ¶D¦\V±àŽÊim+8r‘^‹¿‚½^ T%”¶€þû¼Ø»éÑÁaÍ%! ã«ÕôcÆxÚç?rsT`ò‚¬*O¤B²¦©Â€áG1ýmxðœ~ÛSF3RHööDÇáâà "<·üÁ èu*ÕÛ½pΤ«î:CŽõ¦›>>Ùmé% ìVðþR¨×Ûs§†é2,ŸR<šc±p™s&9¡F56mäT=öݳÛz¡­•êQ)$9b…zŒs¼§ÅNt4ÓA_Pш9u’·àÐGq¾ m¢¶¿ÐˆLÕ1‚<¶ùø÷>uÄs\ð¯;NgN«^öSéä†Ó˜·¡.ƒÞÚ‡SÛ²ÅÕ`L€1?nˆF*??”ó|®€è`½§ñ¹DpñB3Žø6G©ålmn6tô¬¦ãì© [‘—ÙØyÝ"âqÝH)êZº-nŠ[õ†ÒˆŠã¥„çùó„ÂeîãöêJù­ÃÒMd4ùp¸Õ¸!>oØññz{ÊÂ*$Ûr l8[®R¢Ì9Z|4^gø1Œ&žÙd¯ˆÄAºlžCHX_ÅØ—/RrpnD¥û/ÎãçŒx§On2FËfž"V=F?n‡yˆ”•ˆø7E€Vœ¯Ë1‚m¡¶à†0㸻EÕÝ.ŽHªÌ§W3ïÖ«!»p£ð|NõºÐ9!¿ÌÒEò n1˜]L£Ð~Ù´þæl£÷9ñc%ó°ç"‡¿ —14YCà„cµ4™^cÈ?0ÕVCɽYÔΤs$Û?r?0×sè)7ò?0KqYzÄ’«ïØkp¡@úöqcÎ>ºÇÁv7Ú³v8ìJ¹Ê|lŠn:‰ ¤ J¥É–ck"ÝÄNj12S}ÝN@ìr&;í·Hpâ|@„’Ðà@O¾ÜhÙÑ¥Ú?rJ9„[OÎ*ø¢Æ¹jÛxŒKz0LÁS«\VÞª0,5^zugXUSUÃÄ)y÷±^ŽŠÖ¡ž©™»ƒCµV€ãÙÝ¡QÖ©[ßÒj½l5/ï ‹ê4éÇù(Ð<§ã¼h j(>0îxfŠâN=¡ò„zKÓW²VZÊu%¥Šûî]Ô””*ñhâ¿ 2ìð£ä²æiÞìúžcò¯íØ·i„1øÅY¶íz¤Iúj[_;F™ˆ¥¹>¢§¥ŒEQ>§q–dÉH ]„Ó][éÏ/ÿédðîÿ¼úñýÉ#( ÄÛ!`Å_H™=2•'„œã(—Q=hZê¦ñÝ+Ëä4¢eÔ±¦Ã24ÉòÅ[™ íCJìSÖ'n} ¥ÍYßEoDk.¦·yk“uMâKbJ§Yôâ¯}°û ]«ª€O}×s€T>ŸÊ‹xB†yqê$M*x˜4’!qÈ' &ØBs=ÆÒéäw±[¯Yd¦=/8ð\‰¬CCê@·Õ7Vhi¤“O΃ݽýë g]e¤ÆÔœ/ˆ??Ø^[:1θ¢f¢öpÝîLMb‘¯ô$â!t9úC¥ß6³F;rðîÍÛ÷•U›ˆ'Òk*w=4̹!Mèó³,šï¯÷ûâá >_ç#šÍؤ2©µÌã5‘^ëÔ!ZB?rÍó!¨¿Õ-&lçNÒ¢./=Ÿ–˜ "{!c`R?0:ÌùiEû)õ€þ¿tÛ¦¦l{ ?róòf’Ú@•¡Ý_ªP/–WbìU·ïY^±ç1ÿMw¼aUùVV›aŠ­€Ùh\æj÷1ýhFrÝRyyÂ`ú¯H…ÞP§¹*§å¬5Ü€yÁK¸Ud[0õq›gÎÐm³ˆàÄn b˜±oæÍéuˆÈ­JìlO2“óNš~¸QÍÁç+8¿ ­_¿Â¾}gç‚Ûq¦ܙҰÕ# §åŠnqê$–Îáv´Íqÿ¶óìÅ5 ”Ä#ÇñáÞA™Ë=ÉdŽã_ßÿ@×ûÏwžÑ&´¥ŽPŠê‘ÅʰÌçÇzõêêjïêÑ^¾ú°OV{ß—RpZ"mœS”_0ß—ãb™GâìR'öèÔ¬âÆmª.iMÔ§}*»OÐ5 _ö«‚ûñóßÊg£së˜á;ýB??{fÂX6…ýFw©ãkG%yÉÇÃ%M"+?0ü!µGûƒ–Ïw‹gûüŸŽ3Ž'OUž??Û¯ÿâ÷hÒT2_øYÅ ä«ç™V÷èÙ¾}¼7¤ÀˆÆ¸U»CÖ??i3íÆ!BídÁy4F—dÿŸ­ˆ¿0gÙxqüÂ63ZôÑxÁ±÷V²c9s Èç»§—¼`)„_‰'~25Îtšx!Ê{ðò?0Q蓉˜œ2ëîHûI²gŒíòEÖQ¾P@Áf°ã¨µT€ñ-»ªÿ`C8(m|y~üû¤"ºÕÎ$‹`Î’-†“ú×?nÍÝ­üPïWçÇ®c4˜Î‡ð‹j<Á'žmA¹­HµÏà,êhËbjŸF˽Ԙäq²Ôþ0CÎQ·ËPûÓ¯Ì7¥X’êþÃk:±ÀÂtš.V¥Õ§$Hz½€ غzŸM^Uq pãÀÓ ü‡žnuøaP2oŽég÷ÕǺְ½<4‹²½¨o”¨+.)TÊý?09ào†Žp+Løi¾â‡lcÔxä ñÁ™¡ ëgëÂ0QŸx!“8\6öþŒNªU :@ü_çÀ,722$Í}ŠIëL#Ìß?n2÷àäÚ5pŠO@¹€ä8–?0ÀÝ`³§??¬§c³§ñ¥}§ûè‚:Œð¶þ[Ê,×gÃ`ÞËwyGÿ,/ÖtHÐØpFÊÁˆoƒ¾ã}Efø”ÂÌyœqÇk84qž*›jmuÄm¢ê,s=*9äŸí7ŸUÁAéwšx¨âCŠúÙ•@;tÉE*¶‚J=À¥kéÞúWøãtjí?n ¤Æç¡r†»Öñ:’–Èd«¡-Ɔ ¦ørö±ÌxëBW…ÇDÔð-$É-ÑÆš¡¿¿Pf¢ŒHÁîalgŽŽ‡—·Š¾þ9;ûþå??…I¿”äÍ/+Û³¢¸ÒMê#R[:y1nM¤‡rHCÖc¾Úf˜-܃ÿçµÑX6 ¶P¶?0Ífä­÷æ1^*;V  ÍÞ€"0û; nD(7]Û?rF®p •wªõÄDÉÕÕx…W‚C:“p°2;Ò$À@_Ps[,j¼“lÎô•29S ¥£??<ù£†f³¡:¯®W9Á9¶S?r8’M–æs»ýÐ5QMª¸ ¢ÐéXi†È6ÒTBf< Ž`IâDÇh°¬7P’¬(1§ÛA_ Ü4ÀÀxY¡Õ´ZíZ GÛ…ËSQñôPõ¬g4¶Ã'üÃÒ¨{Èÿ㿃ƒùûÇ”€Êÿ‰ÏËÀív]“ÁÈ5Q„Û»Ó£=²žYäbŒÑ1¿öN^¿9yý>6¿ÞÿãÛd‹ìy'Р²Ì—Ć³ÇË%LqP%}+Ê»céŠHŒÚªcÉykjó†SEG濤Å/H?0×ʉ2Æ£›P™´óU#Ï”päI{ð#î^OÚ4‰A¦„ wÙlÓN´½µ¾QÉ7Õðiâ1N É BØV&­£í½üøŽ–»ÃëuŠ%ê~?r`8Îâû.f!p]W©^&ù Ëö”‹½xïæwtTïÎ}gà@Ï€ÊhI§Ó€¤¬ƒ°"i%]#ºT£K5bÛëü_ÅGÐ 0^ß÷8ú7ˇ¾??Žé°éÅÏé¹0,]3ÙY×Ìý>Ifª*ÏÜÅÜ„Ïq$A>ÌoÛ"{–ûOÓ%}ï´T§a*\??¶,~‡é§*;Ì<ƒtÉ«Õpy,¾¶í¡ÿ³™CD ,Ð;¦(í> ¢.~cW•ŽÁcz6³)5uP Ì0é6X¯AÙÌ?nþ¤è›%/ü¬ð/º^È{-Ùé)_¨9í[_˜aaŽÁb®Ê€ëL¦.^ž“nüX#Rrë*êã Rª=U{œ—º2&F:Ágëq†‚Æò®szML劸!çf¯ºpÝ,Žn?0 =áÆú­{ù´:¤/—µC°ð„¾×Ú×ÕÖêKõËM}T䑯µfFNÑä1Nš¸<уÏ7¢Žhô²©IýàÀ$¥}pHo“ØtHæ­Q :ÑÔ^'©UÛçðˆñ‰7½J©$¹ŸÁ;æMFÿ}¾çå.Aß°K²é 7Á._­±TÌ;Ýçí9m*&«Œ¶äY™Ó< Ç•d<é8‚KlXÍ £ŽÞ 㠘Ŷ[ǶþöŒÐt$I}Á”8uÐ’n«z`GÍݽÑ­Jì46!ªÌwŠŽ`’©œ2¯ÓOMÚ]‚žƒÜ6a +#ߦü3͸?0Ò[ÇÈÑ9¥ô->Q݃ë??üðÃ}AŸT>ˆjþÐ*ªšûÜg2†.ŠI¶êžTÌ |4‹$4vN’»‹×µdP·°RŸo„šÓkÊQšÜÔ&þÊÎÆõ3ó–ˆê}º²K‚õI‰º‚ôõ“æEŠ]3ûêŠ{K–÷VâZæ!ðPmËl¼zý“Ãlx–ª £R1•ÃZç“P¯RO„˜iÁ÷½m<’²†3¢OZ1‰‘ nªÌ礇,—  °òý9hâÖ%«¥ÞÒ¬ë›p'?ry[-…ú»…ý\^r°ËÏ"‰Ñî?0ÇjÜsø«eC[ÒÖ!¢—³aQš žSÚ6=GšŒh«µÊ8~$¤ØHwD3BYì ¥mÑ@øýçì˜õ`d±^BV–‘Dª‚‰¯Ù‚ðJÝHø]Az^³`†kQ_Iœwø~â»9ÿ¤Œ<Þ¯5ôüÛµ›nЇ¢‘Ò¯aSZ6ÿª-û)V¨à™ÙîoWÓ¬–¢£ñéä€}±I3»£˜~r6¿°å“^8 v5VN½êÄQ±¡P_BB)à6ø—°þÝßîÔuÙ¬¤¶ûna45æ‹Ä­&¾ñ"Ô‡RÛ0ÛHÌ~E5¨4 Ï%øÔÑ›Ÿ¨„¼1……ÜÄ.Àù´(è´¿ì´|Öâ@ 2³Ý2—4éêj  @‰”زÃm´ïóo@ˆûšÚý† ÛÇÕ>¡–Ó}OÕÏÄ3µB¾P94#ÄÊTbeaKzIæMLATK?r¿!ÑÕ¾sàK¤y$v¯¦TÙ8VE½z'•Ÿõ Oœ öëjžþc¨žÐ„¾;m›šòBggÀÍ$ËüþM÷R3]Ãg ÞH¢j:™eð»{O ~??yïÓKÍéÐ#•;}–øÔz*­ ?nÖh€|ڟã??Fßjâˆã@kõÆ…„‹Yü½Û=ÄËkìÁš’(£éÔQ$+©>«çdÔ˜©=ê¬Ä§ÃÓnÑLIAZ7Ú&ˆ d«‘˜,MàÂïÑ=°@\ÔÑ|ñbR™^¾úÔ|¿7]¢—ƒÚ"›n×Êo —7ÚéU¼·ÿÍÝàÕÿÈÇ~õ®cµ×Ò¤ÐG*Ú0AÏöè¬êyÇ3uqÚ°|–A·‡‘>wÑ™³:›ŽÇÙlN[cß‚Ùwhv«ufÊ‘Wn›&^ÏãxdÖ¦äµ1—‰x®Ò§¨è‚4gWÀÝ©#ÕOÜ•—)ù²Ûî}{µ‡#¹Ì«üG¦§Éf¿‰È¿Í\—b´¤·È­’0 pü_z"…ø×=ÿã€ìJÿóaóIÙe˜®cýzrLbþ œˆ¯ŽnÖr‘Ž[2zþç9QÿçD](âoW»?0ͤ"°×0ê*¦°Àf䡎ë¶„<Õ{MƇ{øó¸ãîb@jÐn6/HTÉ»[%ÔkRCbogO#©c>Ñ1_aƒòu‰&ÞÑ\“2øÑ£Ãƒ˜ªéZ–ÁĘ™ˆS} šI+§ï.8#ZСj[r{äðOÃQI¢$´äe³ Ó“6úF”u„è0ãp—)š¤Bn~c“AÿÝÏàÕˆCC+Z ÔG6jÕ´à?0TL“–ôŒ9®3)°³¥aÑÕYœ€lF7w<©K_ÑtwßÔê1Åç›ç±LcÖY jöþœÅzqÞâ××Ì]üÇ¿`ÿ¡õX¢ºz†*ó ’š[L¹ÇñŒ_??ÿôÝ›W_½þã‹o—Zë¶tÐ?0ZGRÔÇj–F™ErKÛ­"A“À©‘ ü6³Ç_$F!íàßh9¿Ôè7.J­uœ$nç°‘ý·¨„åŽÿïé°ûée÷ÿë??$oÂÓÿ»ßÈa¹QŒ8¿*XñŽÏµ“èÜxêÎyîîî?0þž‚Õ­û"?0æ]7'‹!«ºýýçÑmáxÑê±Mƒ!Õ]°è§ã|ßÊw‚Û1›Ë,¶æð7ïÿ½òÓ˜86‰Ø÷l´Éz!;º)žÀ~÷æ—å„ç²/[ð`œ?r?n›Ù€ª©íæ—mÛm¹æä=^êÕ²[Ò{µ!•ÑМºÿ²ï‡««éb_ŸŽþ(’ÎC…µ©šDôzùË/¯NÞ½|=x{’R½fÐ `ÌýML™{î¶­KÔ„£)Š2žÕMü_ÚËjcTkûi£Tº\z}‡|&¬G|ÞC~²«—BV¾¦ÖAƒ©¹Óõý=CH||Àîô×¹²lõO{¨ØßÂèVo·oººS«]ñ+ç«úޔؘû“é T0¬Ó^2]ï»÷wî1Óh4¼Ž“(ŒBÆ]'ÓÑ»-¤Œ™á¨@bàÅ„hä@4¹£CàÚ$Ð!ñ ‘áÚLRôeÞ’ l*4ÅR·Õ1œOÂ/Žˆôæ—÷??¾yýî+3už*4Ñ‚î›(«ý€ÔŠ“ÓgŸ?néÿýð’a+w«×??Ý—Äßå«î??M‡\w C`|øyͧWÑÎâ÷'¯NÞŸ„/”w‘Â/u·ÆÔzâuéÇ/¿¾¾Î(¼8æáëµ?r/£ã<+ªØWX×”'²M 2HBãc£m¶áòŠù sUP‘+EúË—ÊÛñ”,ìžw«G_R98mêZÙõ’,´6k’XW jâÞ­yEÅ‹7?n8‚}WAÄÿ‰Aý96Ï2:¾3_zJp}¨M'MqΑÜ4~ç…Áß¼{ÿï€àÿ-—Ž“—\ÅÔë˶ÝÚFrѱgÏ €;‘n_ŒVÁcöÁ0’ƒU9Dß²·W‚Yƒ?r [‡¼v¡~/ò…mÆ=ºVµü­Û )窖’IXÿÑt=è×]?nõÔ5(JA%˜;ok†8L'Â3Õ@MÅš=e€äzŸ‡ïBV˜Ì•;=¬Š¥6åÆé€ÞÞ¶™–âS?rjÿñ]~ c¯çËA™s> §­&¡·‰ê?neÞ´ÅG¹¶Š—¤L)}Âc{¦5ºRÅër¯ÕÃ\]5èÑ ßÃ×^©‡5@ «É-Õ½Dz/ÛŠ{ãµÐÁ€åù‡h—N£ÈüÕ¤#ŠclèA¢Åýid^è¶õ‘©¼i­l 3·É'{CÐyG±kÿâ?rAÄYè„è©®KEÏeþØéô¶ÊŸ§[r©-ËSåDµ½ž{ºÆÝ£ºþž8ÛŸÂKžS*掮áYÚ4¼´½Mž­HJ´?n«?níMXIÌHHI 9¬œÌz‹éH8?0Æpyš1¸8ó½áåmÈ??¤H)¬…8Fì"°‚и;ôyºü:}á-è)ªìü”‘ˆˆŸ2ú©£éM+yä7ªŽðï`C\©¸§a"q+r.NTì%u½XBËÄ¥?n\Žóõ&?nÛ&-òÑÁ‘7ƒÆ=òßÔR@¶ ‘ë1€ñ+wpKíã±_½#roÃbþ=ÇùâÅ.^äWÑ9ýOZtîP$Mihr³®[oY1¶+¹ž"þöð¨ÏvOjz‚Ž]˜?nfú%ÿ‹[@ùõ“¨'Q@ÉU©WR*ßñ®ˆj΀?0„‡oìEœbªëªø[uÿq-×&:/¿žrøÃJcõ[ü@ÚPcÓÐuÂzó"_Iü)g>•e$ø×« Ê‹OÎu¢¬ÆÄ/×ÖÊúá!õ—âº+‹ïÎrë% ]ðÔ+t>NŠUÊJÙÈ O¤¬‹…ÊŽPÇ8¦W-"‹,[gd+f³??«ãdtº4£K`«&Ž)+ë:A±>³%–#Úß'9tqÿ¼3³^œÁéªend8j??øà5 l€XpyœëÓ€iYr”HÚ»w$sûM‰ SšJ*îb _Á¾ÿ*òçj1N#ºÇeåe®ÏÖôg¸¶a·‘ f`à5`+Φh]8šñÙ%"Ì™Io¬¥<]¹X#Å-€rçµd|ôrâ¹m,?nÊÉÔq\ï€!µ3ÂÛœŒPØð¿?rôwc0ЮÎh³ŠÝúÛ aƒæ“–±q#R–éš)[/Ê`ú¸„k<¦æÃ‹Œª¦2Æ~Œá›Õª•„ÉáàbÈnÓ1©ÿãžyvã–??Ê«3'}À4'dﻢ»øÃ'?r múyÝxÈùbr¾å…[H î–*!*­{‹\o/Ò\Ž3L?n`ö¢??¥T_\_çÙ%ʸφ³|œhGµ½e`Ü?rTŒ(°úµªŽ7m±Nó–èúTT3†³ËSz5á䌿Pª/†Ù¬ ÂÑByªyìgå}£GdhƒGçT]^0©ßÒ—Šï@9'a{ûø+XÖÖŸêºi´?0¢P;*[ýÖÙÄŒQ­°h*L¶Hß6‹ˆQLΜŸRMERËçËUÆÐ©¶Ó…‘ ¿éˆÏÅë?r(-:Ï /¡êºP¡SC€Ð×­lïÓŸèÔÅ]’$cL«˜(×ßþpròäÉÁA_Ë$¥מ~ÔháìÓ|¸‡ÇHĉÄzÒÞÍ-ë{XO=Ï:ÉëjýáüETžËÉzÖ`ùëÖ´ë -ÒºŸ{{O&7ÝÚ2ÔoÛ z [B»8ígPêsh#'O“¼lPÁE}ÆÄ¨ Pò(îK³t:všM—ǶùZï?r-)Ìüò@h6ù Í/*úÍçU|9‰{¶Ý›-ö}’EÁwGÎÔqv¹XÏfÍIœ†jýµVZ·h €OâgªÅsf²:r´§(MðUQÒkOK?n.1˜ ÀBnäqìL^yÞŒŒ!p.ƒUs{4„P¤¶DQwÃØ9å`ï¦ó?0~A˜ÜÚˆó•Or©¥”áIô1¦ï!A‡Qnĺ?0½×µç@=SbÀ0‘ßýÅ{CÝà†ÝÊõA‰ã€pzç«-±.˜÷ÎLDè…0Á?r‰Š7èá­¡Ɉ°T:_U äàwÔ&Õeļ’ákÇ™}Ñ‚Ñê”S† £ÉðöÑpåz.·‹ôhœÓFG†í´K7)Û#& rqÓ]²w‹8\§³©g]ñ6z飑áù€ƒ¯ºJeÓå$ÕÇ–_ðßBát;úP9åó Ü ¾Ôé­Üßå¦×QFLùÊY„Eí…0»žeÑ©ÜÂÜá¤á¡ÉÒìýòòíû_¾²{K>ç%õWØC?r(6ùÓðíxS«,êê½¹Å5øëñðª’(_wóQ×ÇRâ-w²Q\·Ž©Ts¤QÄŒEkÕµ½Cô&žæ¶âNQÓo.=ož +#vºÏfÃÅÅàªtÇÓr¸Z/DÃRÙýúRŽü“›éŠ|jžâ5 Dly¾žèüq£ ^?n%Ô“7MÆŽ;¦Ä ùØ$©%at¬“ò8áimùE^•U½ÿ}]”¶÷îâx ¸ùøÛ?nžš#|™MÏö¨Hç›o~}wòýãw'ß%Œãð6H(êèÛ'À¨”'­$TºÈŽÀôÅÉ-`¹ðY8Ü‚ ?rÙ’¿ùzÍÈÂ?0Äuˉ5VŒ1ñÙìíJ‡Ø7|m¢/ß0©Õ9½ÂO?nm C+@†Ê×üåìÉcªÎÈØÙ*õ?nÅ׃ÔØ_­Hˆ·´€IA×´¦ïÍòÖÀÈÉ+¹„´r±^v*6-Q/ÏìËTºŸøm¥êèü‚?nw•t™K*ÔÎh»ÂQݼOò@Ûü~zšIÑâv» @m½¬VÎË­‹?0ÎÞæ— 2È œ&™õãÌQs•?0£R‚Ëb®¡'°MÖ¦fÊŠ4ÒÅ™ÝZoŽ©”«ë8¾zᔸH‰?0Ø¿˜#½*Ñœw˜h ãl`™5.öè­:æJ\í‰1bg%Ô¬œUŒ!Øîª'©ÃÌÜ0šÄ'I{gùœÕ>kzÆÜë¾b Zîüñ ñÝ߯9×?0éï&âžq†ÍXÀJÇÃ*"°¬üõ´×}lÏw‡(¹TùÈC–=”Ï%ÔGÛ(õ‘… ™ÓÑ?0ö³ŠŽX^r‚CpwÚo Á‘ ×½ÿ"¢á-¢áJÑÇ\Ôgú4ÑÇð¾¢°ôëKKþGZ.Â¥uºÊýÉ¢láâ‘û‹F`Õâ¿Uë3kÉþÝÑÆ?nÚ\`ÇK÷ŠáG‡Äbc]ì7–îw¸[ýæ²ý±ÎH¢§?0¯vü”ÈR!??áTérê þèüV†^7…Üóñ`½¤©L˜‹›\(Ni6?0JÛôª®C¾tåÅþˆ5Êb0ÑG€kòk©Â„.-ÃYz:µþtüåëýç_sg%€>#$}L¹[»krÌO¿ßðâ¡ý??lSÕóÐêÂ3©МÛGÔJó òüÀ%—$Áu…e.=#P]c\Ü#ŸŠ½Gú[TÿíwäÎTé# r*]I7ÝL1–T¸'×8•S§ê‹Ô·Û¢>MTÄ'ªRQ›•!·9{È'G[Ä3G{&PB§SB??pvÜWàű/1¯,ç’tcºëÈX°m?0tƒmæ­bW GRÕ›£G™TÚÔØ¬u]fªÔz“¥ÇçÚ ‘Ë.yÊ1*¦ðóÒ§6³Å [ßñðÞ¿™QÏ6 t‡¾Ý¡ÇežGóáâ#' µ»Œ¯>5Ÿ?0VÉ.:9›S«Ø ËZ<ÐIÞÓI< ÈÞ·Y±!E¥¥A1eË«´¨2©¨šcÚ¬{¼ñ,kñÖ彃·îôëØ)­æ“6óz؃ã§t[´9{?0!±rï{²ÄÿFœj€Æ)E}òùú“_P½£š›a™]0³Gdl<Ž*ç†Fø"¼âßúÎkªã.ÁpvêŒç!gò3™×BAˇA=ª•k@NkM•:‚OëoQÉÒÑfj ÖëÑ|ÿ¥6ööíaDLøhP•Ÿ¥à/æ‰]!‹£øÊNÊ”@Ać+——vÀ_{u. GtT9°ªÔÐà}?rÍy¬¢|ýó*_|ø…üSöq{¼yiXÎI †O&8à|D_R= ¦À¹¤Œf$J+Hqu‘”¨’F95[¬ç([ž; œ ÀØ;Ü•tÓó?r<ÑgFOH|FŒX`Ѫ4{ € Ј¦‹øÖȦ/aBˆÞÂÛb‘]?ræc‚q.¨¡°VÝL´Ýø ‚kØ€W«c–"•1ˆöΖxu+p\ôv;²¤¾ 2»v ŒÑùòêv (¥Èúç!Ãár­>˜GD}HòÙ!ëþâ{Þ›ž%š Éð4)R4?0n h*­8Æ‘A?nÃh€‰E´Ãá?nžOߪÆ?0T$A?0F"ºw]å+'ÜÞ¥{????½h‰Ö:@>=Bxª^ na­ÞDÊË…ZÐiÊúê9÷?0ö®Žeµ\Ð:Fª&±|™ŽS“€¡ÊÃ48<ª6³€JY¸» )õLÕMd?n?nBȶÖ"«?0+¯*œn¾^Ûrü–zÆ_ìê8ïòe§=Â\y?rÄä¼zÐLÉ,§ P£±?r=äÇc¿kçiÓ­³ì=dåú¼¾n^?0ŠADGg[×ñÅåûÓ½âUÕ;(>Ç*òO½U~!8 5à^,ÅÑqœ´ºûb`c˜À ³??×AvÝ/Q5%NÞüàGôZ?0±iV,O\]€'`þßwo^ǾþÁT„B8BŠ÷xk²?0›¤Ocã„AâK'§%Ú’¤–7mµNchpX8³;®òöÙ(£T]J$©ÇÆEu5•Œ‡Øàcj?nkL¼ît‰¿ˆ‹ˆO"LÇ»+39ÇÓúÃEÈÄ%œv°UœaCc£kw??4>æëý¦…ÞH’óI$ç!Ë3U¨WjVPÌâ6?r‡})»õ¬®‚£lâ6ðeñÂL-î^¡ÌÀ°&BÑ8/+„Â#!€ÒÞ¾iJóç:ˆVdUqJ‹1£k©q™‹¾{ÿöU÷ÝŸüá}÷­áù‘¤wøa8Åùò<¶IwJ­UŸªfÀñ8Â#C<·ÇáSyó·œlT½FB›1·??¼œüüªX|•/kZ˜g³#cE›rÖu¨:ô’øm‡\_g*…/wHŽwh‰ç»Q`Y/Àªd]†ìL¬Aã´exgug 9¶Àº¥$&¨•˜.›CM>àµ,-t?rúõA°·$NeCêÙ£Rõ:¹€×_Ÿ^‰ÁKTY%¸C1m›¡¬oÞqÐí[Å«\‘®Šp­$ɽ{΀lµZä¸?n™/{'/¿ûîäªà³­.ϳÊÔˆFF\ábšh½ +á8ù—ß½WðѺ"Ÿg†±Ž Í0”oBú|Eëa#!I Äs†ÖwŤ¼wŽæëÒ]n¶uj7%±1‡†9CS³R89§ìSÀ1• øÚÐémºˆâœSØ[:®’vH¹Ã¦ßÈår?0>Ÿ„åØdHKåÚб½Î†h1ùOÚLÚÐ]"QP]öë}`èI»¹ãÝ9il<ø§ÂQ³ø@“ø\š¿Gø‹vŽE¤2!8Cht>mŒ;­w< À?rbǨµT‚šç¢‡õÇ&²>·ˆ€úÞü+JºÇ0pP©,Öºõz¦·Že„«¨¥Z ¦Ÿêì]’¼¬RLjõpPØvaôÐÂp:v7Xõ=í¨û;Šæ«*Úx1,ËU‡ÁÁ~@.Gna+ýùå?? ÞýŸW??¾??yB ââ/3"‹ØÏ±~¢\b~=x4õˆ`˜??@®K2Ø‚¨u?0ÌpˆÝ›eC*¿ÊÀàMðÜŸ–—Ï?nWòLu³Ç]¹q²ß±A²©¢;\³#­¼"jzÄ/?r(Õ¡M»¯rkL;q??ÚRÞKŒj—b¡,ì–ú:ŠϤð£2õ;éR6K/€-{1Ü>2ÃÅõ`ï.«1˜"Û1 vmuSFÁÓö @¤¥ê3¾…&®†tÞ’KÍ´Œ(Ãá䆯$óyaMnÒÈ?0âñ-Æ»GJm2ÿªåÔö/­ºåêtÕEÍêêRG;ºÅgK|á›.W»sƒ:Ï’-š¯¿üÞ‹Àf#`!×hœ9çÓ†QL„vÔ¸FºC0²¡j,@¸oIEq^¤B&³FPÅc¦Sð/Tˆø—ر¨Þ°\iá„:Â\Ðß|Щ÷×Ýñ_?rßH§×¢eFRB*=63´u‚?0FO¯wG^ZDß²•,¾×Êvi—ì´ çïÍp:»„¿EbFD?r×ÒÉg²ÿ‡%qèîmµ)œ—™"ì("3â¼´p…`”‰.55X¾52„Ûõhëؘ~OÉj/Š®5¤¼rNùßá@Ž?nAbÕÀô™O<²Ðoóycë7x„ß›(íÌÁ}X7ôz€ ¥a!5q´Ha¢ùšˆC±^Ò(évAšµã^ù™W&:2þ …JðwÝÅ=¡Ë´q[,§«áêb[M¼‹ûMy/©Í5Õ??HÑkœKT9ÓPÕ‚oxv4§×"°¶Œêm¿iS=7 4¡½úSŠ›‡?rò:¢2¼¾õ’.Ñœž!:º#EޤqÕ_)qÐ﫸0üöpñ!C"tN@'©éˆp’´l~©pºˆzï¡©ÖoÊæ÷—ÌAzÔÊÅF`EI¶Q¾—n»¯îÔo´Û®°•à<5?r§SR|}—Ë|QdöÛĪHØÀô•ob<åéÞ'üºX®°fÔÚ#¯™?0XXÑ7f½T‡¸pƒü?r ¿yÉWmä…€ÐNw Åz>˜e“2BTR;µt=T|BI„1_¹îÁî± û—òèn13-Fƒ|ÒJä ýDã¼Æí¼_#xꕳI–0¯´Îþ¨Ç{«¿M¶ÅÃÐÀ„ ÕÓ*3+…lÂç¯ðSlOB'£Â`ñ”ÄÒÝ|’š@ºœ$ÛJÖz²ã#éyÀA½Sq8âAvA›¡oÆDLB™SvâÈdT| +#ŠÉ¨†ÍïU éÂß« ®¯4ݼ;DÛº»ÅénÑï™™çÜGÙ}Xϳî2'ü£_ÐŒ§XQ;óÞ3ÆLF *7o‘‡û«DK¼‘y·&Ð?0ÿMF”/Ìã§Yfqh4¤ëNVY=qo‚qÔšÐði&€äU&Y$VÁªï>rg`a*‚SÊœ˜Ç]!c´‰ÝjöyôP I;`wÂD(ÉÃ?r¥±ÉÎú[̾ڳÝqB2Žu±é ??ÂFËÆE5Çòø ±Ì ™ÀOy4I@ønÊ1ØôùØÅc†¤j¹yÛ¤ÇÞ9?r=¯µX‰O^/©f‹ì&…J’ÆÓÚ;ü¶o‡[ëi5┚j?rU ÛÕ3á_î  õ£‚Z§‡L$=¶±*t‘g•øn¡.Hå?0«£:j³aAZå9J+ÄCÔë0¨0ÂB6ã^Ó÷ -Ñ•²ˆ42'ßâÔ%}ÄÍZõŽé’5>¤Æþ~wüP£*oD¨xê¤fßnÐÔ’`´Æk,E‰Sò×ùS_ÝN{ým¢¡Òݨú`פ©Ãý団ž´¢Ð2§£öXç‰äFͲ\)9¦y«—Þ¯/òrTN7­‚»NÇ6ñq%þؽ€n’P±ñ??áåˆáǼS´??êû»Žµäe”õ¡u…¢,‡¬ù…¢öå˜>Å©«vÞ( áì±+/! Ù—zÐ7XÝ4?nv7r¤gú¬5§ª€#‰D·„Oê71Á‡VóVÙŸŒY?rÀ‘•ÑU¢@=T.L£Té~|‰ÇÊM~1R§kàJȧ„zžÜbVš§ï9õ??'s 1ü`%ѹ˜y"SüFõ»$Ž“éN7ãR0lþrZ‡yòQ:³´M”§`ÁÍ{C gœõÄo¼.«[Á¤]X·ãíš>l½191ržI?0ÙA!›'­`ª…l µyï¿oD¨ž˜ïøŽë;ãÆKî??žú7·??èlµ€°N+2±’¤~ÆÚE\:vIo??‚+Äf$jûòç‘ÐIh=-Î+·¾>º6‰MûÃÑt1ió•õÐIrKøÂØð=Kл8(àe?n±Ù*–05¦Ù¤Uá|›Òêexj½òòj¼ÍglyÅ^¹>×+k¬p‘†ÂÑèI‹Z׿höЇºiÁjóÚ>ËMh–W#S??¸»±D0×~xýÑîƒ*³YÒý•¼o†Îúübšult¯´Õ|_m{o~rrt‡NÝ8V ñ‹ó8a°JÎi‚Þ=x6¤P}Ù„òRRñfõTý¹p<Ïö‡ÏØÚå|&ÃúNÇÔIš™óÃcú’ðcŠ‚Ç;6[q PšFAxÆ~ÆwB´Ñ’^º¸¦/äkp“Û ˜—B‚û?rJ{°ZÑêÞ­þC²þbÂHêÄÔÀ5éwÖ²Þ”Õ|kO€‡›`d¶Tá€F‰U¿šý…‡áŒš??÷f•×Ýß;n)üR,|:&KN7NTO8þç {SY<” lqkéÁñáßCÌ?0œdÅh¸Ì:¦5ÖæóßZwÁ[Sïn4zð·Øêo¿7òµö”öâ = ¸6$65ò©Ì€í«º5z\ š‹i 5«•Ò‘å ÉVm¶­P|íÇy!_eãü‚OYüõ–²1Z?0K?ruKœ<µf¢œã¦wˆ¹?n«UCñ¦Î‡Ê9?0†ç&§­¿½­)¹­”¦±ó–³L·ÆÿTƒÔeÄ”Gl:Ùñ’3ñ|?nDåÎ{6•„Ç“’áO?r=Ç}ª0*Áý´]‘î¸p!É”T·SLÜŒ¾ás¤á|:ÓÆ?rTÚ±ŠÒ·}ÈÂÓ©]3y$Ï„râÓiŽŽúT1c Ãb4Æ:ë#*“ˆ’v Ï'K|Z«#ˆ¼âæÓÑ”T}/6v¸0Õ‚òŸÆixEø0Îâ…†D ÞÔg.YWÙ±]àp²Ü…ùBiÇi ¶Y·]˜H??‹jË_ð`놨õ\j0^+g. Ï|€µ­HÍ:ñ»¬ì~g`Ä)5MôÈSµ€v̩êiߢÑÈll™Œj.v<“ Æt‘¶ÑõЙ5ÏK;‹ª”Ûcõ˜¢ÆO£??Sñ7d?0»oýó{ZŸ[¨îG*Ðjåí`ØuæsÐuä# ½ØÆÑÙÄR5¹íþ¥}´«&UÈii³Ä¥Ö½´?n,›FH­%Æ|N§¡õ0"jz[¿s7øQI² †¦B것¸ù˜\;Ôé"'1µ!ì‹ïÎÁ¢©1õ=›ŽÇl¡È¡šîâôÝ0cgªçu7oÓ=¨RSx·'nRkí¾«…2§:iu-ðýâä Eõ±\Ù°=³€,Ì„WÙ¢¯Óqžž $¨OÕÎâöV¾¥ðB ž&?nJ¬á :˜ÜFñ?0'¯ur›Â]‡”_ÜßiÙëLü5‰5ÆIÑÑþ¶?03ð\{5­û—îëìªû='ä³7N~P ´]PRªØDp½5ôŸáÔ³‘lxÏšªA·+Ž,ÖNjá[}Zë§‚_!òÓóaAÝ ûÙp’?r¬Ð'×NÌWò= í`Gé–_†‡Ê%§ÝÃ~“V¾2zEú?nå÷·ø}[ŒÛîû”3.¶ÝB9nÄÆ*VbN5YÕ™uÖÛiÏ9-hÃKvÜçŠBÔ0$%û¶öF€_9ªÚ²ºàøßþ÷Û__¿þõí¿9á³ê§ÞÄä€ñ{ŸÊ—îLt’Ä x'(žC",„´ªrI¸o/·¾âaèW„zjñ <ãM€@+G_&2vxYO³ÀI(àP ‹¾"Ãc†ÃGDãô ¼MEÀÚ™D4ɸ%¦F/0œq‘<ðPy.›¿?nì^:4~óÝd;E¹UÝ2ìUè[ZÏR±0Uú:ˆ^£ƒÈh§ªwò4¬Î‰ª`z—(G€–DyÕøœ*ò®êùAj¦ŽuÍwØw¶«?nÂ:B;͵Iu]…ân÷‘œ{~Ú÷—¶”S1øœhž*h»?n8kŸ-rЕ¶xë|rÖOóÿ¨§¸²ð¶§!^õ|y]=¹`jx²Þ*øšŠß3.¥âä\¸úþšWgÈõdÉ?r›ϬdÌ?rÏšš97<{npÝ»gÑ?rϤûeÙtÃ3ê~iV]ø_–]WNG9¡U–BË6V n¨ôf§Î¼’0¹È|¬NrëTŠD%öÈ<§³•á`§ÝÃfZ Éã«¶¦žÅ©¸i•’=Å¡„ð÷úbo»ëÞïwê'—=­":’R¤”…rXªŸèƒþÞIzôqØô'8IÊ%éAI•¤}˜ÌJˆ~4^£†¼÷¦çqc»üÕ5„ƒld^4he9<+°üîÓñt)³L_yàÞØ4ZØ£Ïp$FsŠÐoã\RÁápFçÄIH@ðFÍD{¼ó=´³¤«I6ƒ.c€'øDüö3”‘ÑQ†54–lºÇUõkWƒÀ²ÃÜ¡ö(Ÿã ê™{"”¼ø!¡ß–ºÛ­¬ ´Ä¢äŽ™5g~UÕ² ¨ïÎ R×á¸XóîîÞ“I·2N¯-l?nh½·rXt©IB@Š`=f(˜ŸW9‚[+s>^bþ8?r•‰„q]IõÛ«ÆIO.'q-ÍÝÍV£iÑ÷yB«êJ~9‹£æÒÔÎ@cÆðÈIif”Å/¤K_’Y‹,¤Â óÁ.4žŒ¬Ü“”ÕcQ,ƒs¶Q|N±VSƒÒ²Ó°æÈ½Ìו»–†ÎW›iõ7¨m5ÔÖÿ¤ìpƒãgóeù±’DM9¥¹¾¾jÅpjŒã@Wgwù?nÖök)ó?0tR{<±X@EÅRµøÔABŠ^¢lÛŪ)Fö '\~vŸ›»_Ð&œ(=Ò‰¯'ã:]tjÈ{L‚Êãª& u ?n˜–ß[d 7¡ˆÂE?ná·…pÑB¸xA‹†A"ü£¢(~߸¿¸!Täà;|}у_L ç`@w¦Ë{4¢åáç –K„Ë&†~Ù„_>ñu)–_Ê.鸴ãëK<´Ôãþ’ûI??4Mû*RpIˆ¦N÷•ˆÜC*¢%#÷’ލ=þÕ¥$<›Ë£ðûí=$%úÚŠ†ERriIXCWñ³*šd¡Û!tKËPÓs½¢¢ñBÑálÇLg6&úVȶ›[ÕôµJ]œ“X°Hº±/Ü®÷îö½Áv¾ö¾jÿj»ß`û_¿°&)÷×\„sIÚ0Xÿ»mY²gÿ?? ?n*·>•õþôdSöÜ0`íNcíCneP ]àòèÈ0g˜\Fä;ºÎŠN +#’¤‘DÎË®U?n$\cõ[ï%zôŽÞ\²·„rƬgœ}†¼DÃI™­ª°†&’¬É[žs6¿ÛbŸ¯)V‰i–z“XÇ[}Ñõ5-Çb#¼w˜6.Vå…X|”y94®óëyçúôÀÆ eb•4"×t6Uö©•„þvœⶸqÖÁ !þIíèaTà¨Ià—jÔØÉÛ·oÞÆ¶*Í–gˆ¾+1&Í÷ÞÞ#Jõóô÷‹ßÖ|Þ@J7#O1.ÛØïs…9íÇ\?rf\¿ñöÞD÷Ýül‰z+dFCüŒ­QMe[è›ïœñȱÏù¤í4J§?0ÆrÓûŠ)Ù¾-y^MðO)·Ó0Àu[GŸ“9¶ÀƒÏ7½èó!/Á<ª-Å&ÃþçÇ7ñóÏßÞ xCôù Åú| M’ÛÛèä/j??Js¸Îðg¸ToÚç“D3;ÈÖë$çІ¢jhhè¶Äõ¥’Ñ‹‹c‰ë·ÁD‹H¤QµOö䂈ùlËa7\n©^Â]D®ýý†õòõ¿þó¯ÅÕl¸äAqÕé"×5“Ó^ͰÓßï+§®xNŒæ°-7¿Á¥·Ðë¦ç‘ƒÞ\Æ?0ŠßïæRH“߉ǹ?0·ZÇÒ­Œ?r9½¾—ô½h£²‰¤éV#w5²gN{ß>éë·²‘vd-2õ ÿcväh5›1vÚ:ëÊWÊ!wЂr«ªÆøµ‡5èíï» ÑŠ€1„rÛC!à»p±~AYðL¨ñÚC¥åÍdAoiZ_?ròÕô½Ï[ß[èekG¬~SQ}5ϧxG‡Šƒ'[ŠÆ¶ÄRž¾ëƒª:ûr;2uëîpÍÈyö—˜:€^!ú2Ô6Ñog—ÂkḡävÙgÆ Ó¢ÙI-¸™,Xr·GÜÈ“ÉÍ^y]nP¯Î‡i§P®­@+fNûâÓ;O`¢Ë}1BóÖ"ØzôÚ²9=çôuºŸ¶äkvª!;ψK+ÊëFÂfÔ‚'ä8[”ÇGÔeZ´ÁEö±à“Ä3¼m“‚6¶¦nTsÀ@?r¾æ-ض€Çf¥ú5aýe¾:Ž+¯Í2u™õùâØÐEõÒö_§ìRqb=‚b3”HvÃ;ÍŸÜR4¨Òà=².Ã:rCUÞ5O’;§¶åëcÖ¾MVù\R_Å_=ìfëNþ¢:#¤rTjfý„˜ëžÌµé¹o–ÈË´ŠiÃNMŽÐp²Ô>mË$Ø??ÎãøO¶¥ux³§úƒa(·åÏ6Û-‰^*ó:Û%|7QGû¹fùÑ;ÅçHaÌ €IsGYƒ/œíž"~|7Éßæéx OÇåÌ F£µ*›¥C‡??ÇùEÜ«ÐW6"=8¸Ù’ÀHÀØ6½ôDÓiàã84b(«K¡'&oåÞAÛ«Gæ¦JÎ?rÞÆ°ø­Û ²› º”KÙê!WC8òÈE:jÑßm¢½~sòúýÇWgÍëë0þ‘(;(‡,ƒe¡2©3¢~‹«§D…Wm¨ÃSO.O›Ó™};oê¶‹Sø¹Óç°:]wåž™öôÅnçö —þíÕÚ«Ï[¹eÞŒ.  Ù]ZŒ§¹DSaÌvM¨,HÚ^ïFOÊv!x+Hw»îGþ«»ÞQ©³h©»f]ç¥_Ý¢ŸøZ¨VåtMÓ6‡‡4šÔ9xéÚi…sZÑ)QN8l”©³·’¨PÆOYIMüº?ni¾§àòÒ\èˆÏÀ=ÄïÕVެ¯·Þ :´Ôò×ó)ò¯Lèîd•|§Ô^ÄŠÚØâé2n‘å­Æ–¹¬x ®CÛD?0<ñ[ÙRÅFmU[O{Gµ¸;»ÅþœòeüŸg`웘«˜¾ù<ê=6_O;¢?0òœ[Q÷¶L³hËPbðBg?0?r™?n͆ÖSjÄyòaªšµ;uœqgõ{}s™*žåeCl>ÀŠ+ «¹ÝRuãâ??ï~Z0dbíè(¶G\‹c+ŸåÝíV^{“ôÅS(›ë‡Y¯.„çáqk|¥í…«X müê7 4rU†­Ú_[YÞûýd±ZÁ’-^÷[UØ›šç!Ö¦5^G¿<Žç¤LH“-Yt‹áeè¢ê7ýôkîø‚!.Ñ9ŸÁ(ÖVï7÷ ¶øÞèƒ œž{h¸v‘ÁyNû¿ž«Ô:À0¾·'$Ű´¨Âï âw?nÑA¸Çý½64e^_ÿÅQ„×k\™õ­˜Ë:7bûH߈yý¨ aM”¥¢Ã6*:lPQ”!Цœšj†SLM-Ѻ¦”ÚÌ_SHMý”QSÅ{RDµ>ö…Vþj2¨kkò§IŸ&{AÖïšä…#s8m ´‡Ô6v:Ʃ߾±yi`Òâî‹ãG$m¨ˆ‰ bl:ûŸ^ÀP„5á6þ_µ×oj-ûoãþ[xØíÛ¯ÖÝ•nsÄT6½(«¨©=7ŒV2 8F$©ª†mj4˜NVHWÖšgn:éš—q½‚?0ÞR^M‰×w Š3•é@˶I¡ˆf›:ÈM]{á?0v€Ú_Ä??;õÛ¶}¶0Q±H{AS:¯3\YœtßÊSÈq:»EbÞâKìI™¿iù²ÊÞ¢6ï ªÔûºØ«:s¼=£ªJ¨„Ž(¢M$¦Gm: †?n":ÈÈÛðÃÉØ”JAK¥E&†è?néÎ ÷P_–‹Áy1D2Â×ÁŸß½üᤎíTƒ†ˆz?n—çNFÖóñj¯Íõ½¸4ö#+m×µÎGN,ö%¨?r}ìÙp5:7?0©d›”Ѥøûõí+Ì?05¾?0—Áÿtx6Ëh*YX9}#:Zû£ÕðjF;2Þ €1»„GŒè4Æ0WçyÜo$/Ä C!¥¬\­*GTÆq7žèýXaBܸô£[ÏÈX[aœ.‚4¥»Æº&ÊL)·»¥Á#4X“1Ú„ãßÄÉ­­~Ì×Ñ|]˜ ëÃuyIYl³UÿšJ¿@¿tOüâ<ŒúÓtéE>µŽTA-£ÃÝ?0¤^B·#1¬1aÝ]*¿OTпŒ÷XE›ž•¼÷BrÃþÕké•¿?rB@uÐ#ýø??)yˆ-î•×ij2Æ\é`Õñ”CôÑÁAý&îнÇ70˜ÛØı½&õ?nØÀ\d¶Q¹1/E˜­2.“R½Í׫QƳyb;h{YÓuO?r•Á»´0t_àã^ýJë&¶ÆÑilJgi0|0&¢é‡Oqð tͯ18¹_#Oc½Tô\@©6ì„ [˜ÕU%À‘*Lg&´} ñðŽSW†ó¼iJ¼EÌ;ãÜQ¨€ÿåÈmé™_Qƒsšj™;iÜöOm3’T˨´(O›ÈoM¦úªÒk¡[ÂXÊÀD³íºy…í¶O°+œ/  ÉÓ\ss\‘-j;ûÔÕ;Wn îÔŠ'ÚÖÄ7]¨È ×çü!bÇ(InÒ»Œ?r0 €Û‘jZ d0Ù6½B6ª?rˆÊ8Hkú@¤^ :¡ÛÕó¨V bå~{Dféa(€ƒ0c;êê0§rF´ðg’†Æ¢>³,2HLãÖÓŠ*Q3w°Í# ŠtØð¸BNëk>*×Yüñ{bÉC—ÒÞ¿=;ËN ÈT\~¼Ý¦K™s +#J“ÍiN5(õ¬n.¨`hkB÷½Ò™,óÙÌ-òùÆcAµ£qÂ݆2C–1Ó´7úíàÑ£ÓG‡s¸’ Ê?nIê?n`olJÌ™48‹«†4¼Þj¡ÍCT7¦æpd'%d‡sÕBó»ļe¾Œªä«_ÿæeOZyŠÏ&¹²WY+sÄžîcu5YS+Æîñ³Ý⹑ÌÉøÓ?0KÔ±ñgUÎEê§%èÌáY!ü!ä£ö h½_”RÁ†_bº,s`RE”ǽíáòlTŠNŒQM‡FXfÚã¸Òj·˜‹‹[N¦D$ÏÏgÃeÌhkñX¥²b‘ö¨è\›ó-Ù6Lé Õãaº4|‹E˜MóÐ]í¢|ÅÆå5†•É-°:ÑÂt63ÃFÿH×þ4…š„g×F¥®à`â%WóN›Ðdï@ XðÝ•±ä+j ©™7x6¶ŽMŠù|Òþ¾"ÇŸ…~²_?rb¦rÌåoꑬ¤“7XH„-Gyâç,ÜN¢K·>ßtu8WÚi…Ýèp3ÅžH²"œa Ø)‹ÀžÌMc•”«¿i±Þ@?r ¼Dðè(Zß NNÖ‹QT×$ì´ç¤KÄf˜rBtó¥jO08Q^ljèSQ÷…êzzÑÞ íÎ]c]ôul—•?0´œê%Ƚ’?n¼ÖÒüñýë“w<:¢/ˆ?0Bpý‡Édòýk ÂWV¢ü0©¦#j…™¡ê•T¯{kgØäQ©uo˜‘w^øtódîx®q/ùnb(\Q#qñ×U}}‘P^¦“÷Qg…–ˆÒÃÆÅDôZéßaôìÝqÛÖ˜$z^£&€2цP~»Ç<'yÇâc4•ÓËlc¶VÀ†Íð‘ÑnÍÀ6ølg‚ Œ¹&˜ôÏm˜ŒzÝ ‰zödzÁtÌn?0=–Z†‚¦‘ŸÚêFæBmAvêSÓ>mÖRó0êáAm•Ì6uǯ*È& ãóVÖh¡ÂzŒ®¸=…ƒ§¦ó‘FuœO«íì?0t c;.Kwð¦³9ƒ]àI¡Òºâ¦cÏüŽM{܇åþûÝHÔj][KŠŽVÉa€1u…®?0Ñ*_€»Kð#~–vèÇýH«€ýjàpUp :X¶³~hwªûîo¤æ%ÿ[iƒ;%ÿNZaÞ`®7äŽæ}Ó¦ZØ>?0–óO×r÷çÖâv»P§³S<5©é#œÑj?rŒHè‚xÕÔ?rö:0¿c·{õ%ýn”˜ŒkèWãõ4¨îaJÏ6EÇQD!¾SæSK‘Oáü€ˆýw¶‡òÍ' 1 1ì¾$®«Ô€è³ZJâ}X¡¡(?rýA†EñÕš‹lR­wf»n·3Ër@™X 3][™aàÏ„Z¨Í"~<|Ùtj§b’é†ÃqÖMþºÝqO2wäµÌ¬P½?n”@;Møu_ÕCôö^D ¡ßBÜ™$óV:Þ8K1¬hëÑP·ªzï¬; hr‹ò¨†§uòÓ®¥z\O¨nõKÕ-€Bw΄'‰ýv¨;&b%&g}u¶|’nèlÍ[n£tø“XxÙö¶¹šËB+“£D’òü6£T¯W슂YÉÆÙøitA) °«(> aDìX-¨à¶g”.ä·Å7ß|µCƒó‘d:$˜TÐÉ6§¶„9ÆÿÓ™×1XϦUX­&*¤}‚¦; ÛžG]<óOˆ>{H;÷Ï¦Í =¨®Oõ$òHÉü¥y–Ü™†ä??ˆý½ÿÀ×ÐAÑ}Ô… èÞ ’I§¢NAMKõ ×ÌÛñ㛼¢ç>¼ûî§Á??¿Ý–·Ãgîxö Õ4ÆÞv•tË¹í²˜Ìþ´6Ò;M3[<±f¦øé(˜S×D!ðòl%m·éŸ!×%uÊ·°JÄ{˦þÆU Ÿ+í6™£sŠÚ `Ö;0Oð #ukZ¬çƒqenn,À€ÆÌÛK]Žœ¡oÞ{â…6¾Zr% ¨œf+õ€Ãû¬pHÀÇœYm¼ecñÚlL2¨u0PMº¬µ(㹃¢ÖãŽõvT?n¾œÍ×¥leÝ\70þP302qþúØVغ›º‹ÁÕ;¾+Ÿw€éEo~J% "yDƒ¼L‹sпYÊW!í-¨ñ#= ‹§v_ìE?r²ˆÿw̵›½„qߨžYØÝ?rZý‘ìVcµ 4$UVí¬¦¹áhjLÞ?r„øû½mëh‹J{º¨>??¶õ‡%ž…µ‘;8v÷Ž&ÈŠþT2Úebš2-øe™›ÏÛRÕoz±ý\é:ø+oeúªß>Ö³eÇù\ÊOÎ)=¬ÙÖÖü\J%ž”!çEäÊÙ®6?nÈq­#ÐËg§U7úHE7 "?n„Šâ…hP?0¦[Ó-’€x]œûùqwn6¢¾Ê̲:Œ*¦ÅlLLž9‹º??|ϺDìêu’j*ug‡&›rî‰Ê¶Åª?nÄC”wghŒ”ËÌä²!—ó×ÙøÞ|µ4HfWð.Iî4—XžÐˆóÌnì+º,ú_»j­vŠîKÏŽgò™ .POÿ)qêù?n\4Ô†§Š 'Ë¿é[1A§Œ|B|@[o ¨ºÈÍ £á%Í ‰¼ò1+ŸR€+ÜiYhßbC?r£³iÙYPì©‹èX ¹çr˜ûmÇ…å1[îïç6é Þ0¨Þ2[é²’?n'u3©??>Hžâgêk‡{¾uÐõ„þéžÐD‰Úçga¸iÓw‘€èîÈqûÏ-(ÝE=Oá3â;3µŽ<:8ðwáÐÛ³¿}ëéÇÁÞQ° «mø( á㪅CE?0„ø†¦â6ø­??ÃÇOB9PrûRæÞOñ·ŸødûÊ4Àžºñ*$c‹–;°VwKæ§™¨ÏÉQÓÌVª¶oê^g¶;/b ÷"âíDlVÚ9yóË$wFô S‡ØöijÐ2Nì[ø(˜còÜF{Ë|Y» àhÕpùž§%&<üVD%~*ÿ-Ì øL>ËFüµ^`à ×qⓃÆÔ—RÆQ¾*¥?r¿zfø{àf™ò«Ôbª‹œèÇÏ/*µš<5]èqO2?rèßKiñiy5öÊß=¦Âkÿ‡Î{5¥úµ_'²íRäq© ¸ük?nd‘:^édájá?0Õ°¶ òÛiUq ºX«Œýjãp» ¯?rŸê‰2Rï“ÿp†;c‰&ÙºohNæe]IWûyÙm"º“ØÛþš–Ù¼hJÚˆÅDRL}œ©¸5=é¿8Â??¹ ú5œÁ$UÙ]Q×Fs0!?nƒH“BoœŠ)ƒ1½ŠÝ^/‰ÿ¤U'8ïŒ|áýPq×ÜP³¥™t£M¨Iž8›ŽŒzŸ]÷4!*#îŽr¡Ìf&Ï>5Úüÿ?0?0ÿÿì½ÛžÛF’78×z?n4ü©X(VK–Ý´(jYnû×¶¬‘ä> UÃE`*€?0U%öp½çÝy”½Û»}”y’&‚?0 ¢h©ýëþÆêv‘ÌŒŒŒ??êÅWŠL~*) WAÔ¸ÜÈF­î(0hŒã´úÑÑ+‰MÙÚ¾Z7 i„ò±W™çÓök~$KÄ%bs‰b‰H:k%ÍÏÃ/!£ïí=SÛ\ƒ‚íŽ+׫ZÎñ‘<à uT”í°^Ò32)ü¥¾õÕ(4+Û0*pµ0‹æ‘Ñ ®ÄcêœBwM!™e驇õ¼oUè ™Ïl”¥¹§!RaªÍ¶‚˜(DÕ‚ üRNßÖ!¹“ÆÚÛ²Iý‡üñ‡þì.ÝB—îþ°šø| ‚yO—aQ„$}Œ¨àQ‘»•ôÛ¶×pEïë¸?r¶vK4¹mô'Vø·ÅîØwT1Í í©]99˜Çµ ‹3E†U=W†±»Õ0´˜Ç"ŸjºZ-ñp) ç%íð??Ïþ:ä®cdKá ï63çªMƒãvë&Èì²á·ßކW•\XëŠðrG}<ÓV šÂxå?nê:¨I²€`Ýz•µc­FúŒ‡šêh…‚¯‰fA”«·(èŽaÆÅ—ê² ¼uüJÅÔÿÊ®Ò>ÿôò›ƒ/îÜQ9¶Äƒ¦’fœ‡ÝØEê›)r"tÝ(åº;.Õ¡S¨Ýš{†WÄ_GyšäÊ=Rt¼G3û·š¼ÍÔû¢,Ö\ëPR,Ö«³Ù22¿-=ó1XíˆEosí­)I×9Ï*=ÁÇ%þ¼ÆŸÔ6jtA~ ¨Ý»:ˆ‹¦?0R?rò‘-5‡*g—6ªÈü8Ÿ%l35y÷¾zÄŒ²µ±tN󌛃Ÿ +#^³¯ÃÁClèñž§k{??[Ûÿ-×ßöðØó ÔX?rrʶaÁôGFu÷y¤ Þß 3Ó£ZOžéñвön_–"̯'e<Ñ\Äöƒ;oÏ,,½â{6ÍÂé,›múÃà„Ac¤V§¿úܶS™55{¬ŒRé™4ĺƒ2?ru£Kà®)?n> F{ø&Û_)wÖ6#î×—õ¾¨RX»ßQñ\ AŒƒØö50Öòì"ÜyÝ“ôɸéTÕTw+ˆÊü½>=Qw¡é¡.+Ýw┉¿þ¬°XáÁŠ™ÏêxÅ€†»ôúÂŽÁä1vž,¥éKY«ôctæ£ ²p?0¿ð3šÙ“SÇ%§Öäôßþurtð»éÁé§ÿÅnL~ÔÃGÐAG#^ÅÔF…bj¡h¤røŸÚùš["bÖS:ËKQ>U)Š“ë\»/(½xîúâù‹®ðö¹icXüª9uÏê8zR¤cÁª-µ]°"ů’0³´v¡6®7™ÝVÒ\T‘.K‹fmöÂÍMÀü2œ ¥žR•Ù|FÉÇåH¢ÁnÕqiá*!Ó^o¾Âº²[žVJÐfqÐl.ü ?n«ü[fÿ&S[ÜíL_HS!ÏuÅ»ô7ÙëòŸ³ÇZò¦gQìg×(ЊÌKCs(÷B–ëÈŽVtAwHµÜ¥ã®Ý¿fVA=ºï<—1y*zõ“$ÈÿJÿ©V²z¾./·#¯ïð·®à$OãTC³_–??•hg"U|`„XçˆÕä]• WéR[˜(À×Ã×ábÌýþ[‹œz¥+G¡ª,ë:µõñÑðžõiýeñU1¥Üª–·MÍ37±JÔ\ß×Ë%ŽÎœÜ–*¯£p?0„ m[:ÈÑ)«~P[eËZ±DA¿&_Òi’ou¥8°âÅ ?n¸'£á©DÇ=¡óñW™.;ªDÍš„ÑèMnQã)ó…ð+LcK>;¡z~k{Ö=Zôõ}=Ñ_ª¯L® ¹ÃØ­®h«›yè×uiVl„ûBSW6H%¯:êm± ÌÚ£—šœ2õ:¤"·ƒtGBi'Tu¢¤ˆ ª“{£¨š'##eÎj ¡1\rÔ…N•¦ga>óÓÐát×0Óª‰ˆ ‹ï´p¹Íùa0§x‹gU"ãÞ3U‰ù#xã•iš()ðéÓÇûÄ5F Îd5 nøë%ÞDJ*JUÛwçÕÅŽ­A–¼ ß`¿5:#4§ue\½Ó†ŒR¯²d¹|UDAXÛJçpïšB$IP?r°.NnÒ,¹¢Žº.ZäTV¨R”öÙÒ¶ã?nxµpGæmS¢ŽŠeH°j*ê™ØPû̲å|Œ½RÖQŽ,ÅlS3=· ãóFÛ‘ `æþ†Dý”Np NiËm@Òo¶?nÔü…½fr5Ë$$ZÀ#*òxz­ Þß2Gº|O“bQõدsæŠzZ…;¦I»¨„hù[ˆ%˜ßJÁÎGžîõ™7A¥§BUúv¾áªšœO;cãªð#êb¹FÕ>ÀLÏÖÑ2˜n‘:„ÇÓei \UÕŒp°MºÕè"²h÷ÃÓçÓO5Ê]Œ˜?0¥êŒR¯ÀcÝ¥åùUqáÓ2 dŒŽ#~¦´Û81°uPîóÛÊð\¼öš_~ªw“@J¿¼ <óÉA*ÍJEn±j°0õ={§öD‹þ`A·£úÆŽRÔ7=àJ¥oö«#»›»¦-?rg¬íww&G¶¿šº2v»?nШ¾cb¾™éÀ­“¡óeÌìÉ¿’âÏ??xûèà_Nïª;áz¿o?0­5b~Í0'­[¨ÔÒ陿Œ§àŒ€Õœq$®¹RÛì?0à#ÇC1—26Qª° EyÖ]ýK\¥?nÌ¥G3È(€kÊW’™¿´]¿÷íÊp??—Í–IiEfˆÒöõK¼¬ü”o™TG£îÖq]…ÞÄ&¼‡!‰«‘UÙÛÐêôÛseßHx]qelˆ¤¦Ðµ˜•Yª ²lÚI/¹ô?rp‹u\„Ù°>ð?0bñÇGäa.Š?0M­¬Ø7øPŸ f»Ò0ãíȈ¼Ù,Ÿ%ÉÒ©H¡´ÖÚØîV3Úžìűb>ùrz]` ›?0¡Ìß4ó72??“?0J%Vͽç¸÷tE! k£ –?0BŠ€R¬c¬þzbÄeÓËZA£2 Эí¾_ï•М0°´|=çyA¹7îph¶âˆPõ\²ô¬Kõ×§¿-‘ Me°‘+·n´G HÞÚQ\Ÿ¶Ô?n×H×wQºõõ²úŠêªbu;Œ”UŽ·Ó$N7à_J³i@†}/n”ÓÕ0ær4]FyLa6ذN`B(ƒqcö{¦Ž‚BÛc|cs›ë!ï}š àÂé® ÃެMCÙ¾!i—:C“qÊêö÷½±@¢í@ð¨8ò¬”Z‡eÇX©]/©çí ìŒnpttL[š6MïÆl ”½q™a³  ·Ñæ&Ý*¦;©‡n]'4xY°Ã?r´#@¨r߬“"L,袣™LF‡ˆ²-!65ïlɺÕÿ R»~Zöá!õ§„ß¼ƒ®B9.hpêÍC]Ú»ª¼v7JÚÒã’­˜±EÁ2Ü¢Ó£Á/ñt<ËØ^‰¨?0Dtç6¸êNÙžBí¡=tFÒýÐ_ú™Ìk°°Ù…Bš%çðÏNs¤Ž(9–Vu¼0GKí½HGZ‹¼(Ž”®AYhµ!p3ó¦;»ðÏw䮊tGnpvYÈ\Ö›˜`©¹Ÿ¸¸“œ¬ h¡.ˆê‚”Þ?05¶”£G??ÖŒ–yyû`Ã$1z Õî¸+ðŸâXûÓi“z¥WFKsð(fT¨£Ÿ·ìæ)Èïa‚]ô±‰ýÙƒd h|CX( YŠWnÏ"/ŽM€ë¹x¢HÜ=ËŠ7¢OR‹-¢§%oìÐô)]ä:ÆlŸ ½òr‰í~»ZAÑfž+OÈb¶¯*ÁI¨Ë.±g|?0cÌ“«É1¬]Nµ*¯)'^¶^?n„—½oÆÐÑb™«Ó»ää?0×OBt ¿LjFó¡¾e1eÏÃb¶°]ÁŸ4öÆ–ï²@>ÉU £ÀÂìׂк°O?r–TP“Ëàìð CÌó:YßÉB´?nJ·sõðµû§2ã» —³„ª=ƒ£w„/Â%µzÏ3£„øU¬µ}¬ÞÖªOªÐã=Ï+w7¯ÜÇ<½cÉþ’ÕÙÛ´%m>¹8m.dsIõ+"‡:ƒhR÷B]ô¿Å q |%D;´¥$8Wu#ôÂu=wIí{å`ÙÓäbg+Û/Þ?n0]Í i?rÓT<½ÌÃŽ&l˜?rõ5ÁRÃM‹ŠË›QÁ’b‘ì"är/B¤KÍÝ7¶THYp9©Zų‹¶–;]Sî™Ø?nÔ>mF±l¼}??R{xÅüŠß®}qÿž‰‡¿ûݶ€rÐtj“‹(T{ký2äyý‡‚9ÅÕöíOÑiŽo+ìÚ/Ââà±*n{Tgó¢Óžqü1i4*z7ˆ²pVèV}µø„Šu©øÅœÙÉçÄ^ûGa:›ŸC£µÑ²Í£ï¿Ÿ>þñÇ??~÷äEKÒ»hØéã-¸ÏªTC‹ñÀøÝ|D¸…{‰ú‚SFo—÷tbiÙ¨­5£œ~œ˜_¨3šÏ5ÿ|¢ð¨õ®øÎƒÅ±c;¶Ry-ñãúÛúm|–§_þÇ¿ÿŽõÿý??ÖÁçþÇ¿ÿ/Ǥª@b??T/z¯ÏB {įîÎP¸ óέ Ü¢«8—IàœðÀ·Y8Ûôrà«…ýð<±$T??8ô>8LÞiÞ (¹»I˜Õ¤‰ IH®-ãSõôNŠÞ9Á¬=‹‚ Œý²{þûÿø÷ÿ÷??þý¢¿õ®yc??\çᇷ[TYUfYú0³\êv 4­Ó O‹±°??Ê$xH?rB™Ìþ¸ãÙ<þüCçÈPñ'˜üU½Šñ4ó«7s»Ø^Ú¶¿f·ËÙ¬O^ä@— É›õ0 õÄ OéµÌñ)àí×—âkxb‡ØbÓb2:9•á¯åœÐ“BXr¶Ö-;gªò¨µÎA¨—…Œáô?nãÈý¼–KŸï„ETƒšðø›éÚg-¨Hå—>e?rB.-SåZ9úç??ÿùàѺX„´çÌÔíÐïý<šÁ??Úr5¶}ûέ†Qði»n-XòrI¿ôÿ7jˆ8l¸ùZ@ÃóOõ=|cM”íYŸj“Û¦'gmÐ UöȼÁ‘¯_ÉeðŒ ܵW§š[ÑZK3 ÎŒW?r¼¹p?0?ní>grv/¯õ‚-µÏ–s‡?nÞqµ4´t?rpÛÓ|¡‚4Ï/hßé%y•ÆrC^Ð}yUÏ\â72yó$ñx’ÑϰHìrJ‹÷Ç…//¢Ó…åQí>Kçò¥V•’Áø©”TÍ:H?nÉÆHAëUºç0ñvj??H³ð!9¾‰«K¤†ÿƽ(µ¨Èp£íº=/C÷°…+²0lU‘¤r» }*,¹X˜}Ù‡¾³¨[ÜutðKŠš´[ZÊÝEvcŒ4üUÖñ0?r¶8É´É{ ëò)éݾ èá§ÞXs<éi’ü`‹?0éx}h‰÷û•F¬ú€JymÖxN«?r éM/ Ó@‰@n”ÆEóQÊóš+Aiލ hËà¦pN‰£ÕSܶwöEyí¥!=w„t›="qæ}íÑÊÖ «'ÇÔ5Né·yÇ[ç†çF êÓ qŸ—’+Æ]??_!Òi . Ò[£ÆhY?r%ˆÞKp#š—8¸dÊØ©ˆ£¿m»ÚeLH†.¶6÷èõ™œ(T†ý©aÊ(ü§î 2NäÊÐ’ÖnŽWnÖ]>X¢¬KRê©Éœó–MçJ¼T–•p«Úä’| P–>AmÛ»•T«šýPyÝR~Š é´']„×U`Ú㓘&ÁÒÁ!¤=/™r‰¸±|‰ýä­ÆÍó,I[Á9nÏæbqj" MæZ<Ÿ.sý¹‰²BS¼Ì“ hO&Ö+”ynÄÚ“ LIç©gMê²ê©)8¯èÁŠŒú©€2oÝpHä»S1îF÷8.q:ú’×°¸{n†‰sL+i2YcÕbšÒWÕB­¤˜@@Úœ›cÃ’;Q‹ÄI0<Õ“¨6+£L5ê?n©X8âÆj(1/¦ß½øú»çYø(–xæžJ)Wöm|1=åÖ䉪Á'ÖA8¥BJ²vî¶)Ôl¥aÚ€,-¾ýFãiÒ¤Ç [/€fõŒ¸m"ÄŒ½Ý¡~ªã´B†Ð¬’WWóÔ’Ï3™ÅÎ/œa]ýý%õi9Ñÿ‰I+¢™$A-š‰¾»ê¹1ØíÛÜ5u+?nÔÆ™ûŒ'ƒèŒz'Žnî®???0k?n¼I¿ñÐêÊm)Oÿ¶>.‚×E?r\ÎÝ˱þÛ¸´rëýyÕ˜'¶¯”¬ªÛZ¥j¯g#W)ÖŠgÖWvEÝ—]ç ’fŠU™Û|úN(ŒLíZ¶¡uŠ£Ò®;WZwƆÊ#³×6еƵÎr¶?0?0v?0Wx®mˆ'£Æ[ˆ›ëw²Pœ\«ç]¶´Û‰(HJCÂMÚÐú³Ë¬Ã毩»GuŽå!‘éªêfÍCWµÐ?0’X˜õ›”c7+ÓœhS*i*`=»ZêöHiFB¨ïéÌQñ±»K]F¡”2(/éI¿©ù©»sqW;×v£bÃS‚âˆÎ"ØU=CÞ±Ê;6å^•;4æ"ú)rOL¹Ø“Tî=S.Ö¾ÊýÌ”‹.Ѩï‹l!ÕðFÒaæºëì…á€$P$Y>vlïT©íöŸÊ¤ÕÝÞ´j\שa\£ ©ÒBÓÖCJm©;Êoá~îEæPüÕ"Ò’mv§ùí£??QHâþþ»—Opñß(³$þ|R»¸NXÑó%Íà4É‹ºíîAˆ²x¾ž_{Ð[±Æsƒp9 gFݪvpk»BcCŠ(4")ˆ»Ä³E’9?n‹Ûôä‡h¤/Uߪ\ &uû_Š¡?0Ÿ Î6žú²Ñr‹F£Æye™Œ“>'ló.?rÇyNªIÅ‘ùÙ$Üô7s¬ƒZÃõXUÛþTþí—L8hX¸>П‰='‹3Ï¢黯7=ÎV­“/Õõ=(?0›ïXzh”Ôu4«h_ÐWœŸ·;??¦QGvz^×Þ¼ox?n^Íñ¦–—£V¾ØeFCñÏÐï-äy@ÛáGË%Ö!sõÒ)Áýã,Œù­Ziü÷¾þË7ßÜê?nê{,{K\?rôð„âÆ?nø²êë:æxĈ¹ uµÝÑm»ÖEµw1Qj¸ä¾e®…ÖbÒóÌÀ¤NÖÚ¡?n‹8UJöŽ“µ’¾»Öã+4½íª’x]:ºDûF®¥¾9 *—`˜NÇâØøËuH«C!ñ7ß 7Xo{2ª¯ºV—ý©Î׫FùC«ª‹é4f¾VßÍ›wÃɲFY³[fëÌckT~™Þßr‡M¥ˆ»ráÑùKò?rÉ(ä ÛQçfQŠ`‡òÌñ ™ú œËÇ‚â?nγdE‹˜bß…¸ZIÇ_¡#)çáWTE€=ópA˜Ïl9XY£ƒð*œ­‹ÐyãU>ÙÁßÝ.ùXßFl°µ«ª µUot\Ñ©:°›C7bË…1ú??øõ&6ãŽã«îóÜ=‚qJ/FbTÅQ‡*3U#{gu'}Õ‘¬µWeWÝÌg 5Ä£¾H|ÍÇêfb¿bØ:íü-}Ãܰ}ô~Aßh:ÓWúûÞó2é™ÖO»]ø÷S8¿ºRYm¨«Oçuõi^ph%O¿¡xôô¯þî©atf'ÆÛ14wǬ[%’&#êã–ª[ì€B~á Ñže“¦xF <Á„va»øQ¯™·gqåºI§”ááSß®m Î>5£(Ës"‚ܶð/g¯Ø«Ê®2° ±?rhš:˜sµtðø.ÅÅ6ýö®{éÑ>tseMšw9£yˆSR¥f "=ÖÒi¼–é3>Û¦¿ÆË†ä²O"O x­ÿÉíàîí@Å>ÎÙ6[yD€G•yâ°X™5) åo£KÉ Léµ$ © (mÔQ@¤†y\ëpÕoñò©ÇÑ<àFƇþÏÓã ßD5•:¿.'[›U9m‡¤`Ù_G²ÿ)ªçxÖwº’'*³šBd?nmè/ ­'³>•ŒnZ…¹#U0BÅòwúRL &â§ÜàÚ‚4™o²6ˆ(Y ¹Í&,7°å©?r/8áå"±O?r}è}#J?n9ïPm¿qßBD¹é9JÛçdö;3yDþ»8ÿz$–¦è¡ê’´'xKÇØXh{J6çòÉÙœ¡lÔU–ÏÚÆÜêüÝU:XQ>ýmçÿcŸØ‡¿Ì‰ åôcêY>òéÿXvC‹øÿ$§ø_OñzGÑÍÃ/¥ îfð?róÖ#7eIk¤§‘íü5Y»6ŠT©(R;_¹2Öü??ÅmþX(DG¿è¿Çq­›™¡ÒSfׂ¾0­Fú@ä©[o{„ïwh €¯OcÀ2þë^7®ØßK‘¡¡^íA°µÙtA⢢ÄÝÿQ-íg7n¤B3!íŽøhÛ"ŠÊÂ"jš‡Eÿk Þ÷-Ðlȇ/„Å®kŸÓ°[ËVc©“(Ý–*ñV„Ìû9G©BðʪÙÂàÌE¶B¹„˰K™??/eþu<þŠÓ!Ò&'£SÏ•å¬sN;}™äXhôþd§ŒnyÍÜûÔ]yƒ¹?n÷emÇÚ^Í=D‹Iyþظîiw4D‡0¨nƒõ šÍ¶fR)`\,T¨¹V@â¿GqãÝÖßjå(ƒÆ¶Z¯ß"óÏu0Õ´Ø&ˆýcó¦ÆC‡AmùaöX¬ÇÓÝßg”%µ.P¡·: ½¯[ë…·mÂjMœ…?rek±›?n×Á``»r!5õ¬FøTs|a»åÈ1LÜÖD»¶ýÈ0Hll#Û6^Õ­”x¯š˜»¾Ó;ÙÃÀˆÝ¬å¥¢wÑnq~ÄÕ+H‡ŠT¹‚ÿÓ.«YúE¨ÖMŒéiÒ#ý?0ñåZíqãäà¸ë¡îÚ:bçØ1)¡5ýc-ôu ƒk^í¥¼€Ã ÐÏ5±?0*H@@LÆÒô´¿ ~âdö (ïa©lo›‹…_rT jpÖµò‰ ªáº@s»L)²ëEbH‰ÚßÔyÈÜ5AÄǶ¨EDU}­ sj‡¡`²ïä–z  Qcm[Uªk»§?néxjÝðJ­¥-ù<±íNabëIÚT5.VvëV@?n”=Dò$·¯qbbŽù¤ÑϵGª§:¿FŠl%X掓´<Jªx’9”i¸ÚÀ ŠG‡Uæ ×5®Ø\5àš–W+µäØËaf;µŒÓƒS×ÖÛKƒÜU¯\Ʋ?rx§-—ṿTNÑiç#î´ÕÀ¬ðNýÔÂþ'??Ý5_'Và_B´ý»;K‹;øOÒRš YüÍnI;š£Mú %ØÁ²«‘)h+ŽérŸÝ’½ÉñÇ#ˆÉ_¨û^ bkü~ªð?0Ò¤bñi˜­rû´u™J=ZضڨEœlvB‘@?n0-—šÍþ8æ9;ä0øÁž9Ð2OQî)Â<]±ôBÑ??iµ£aö3¼\ª‰¤hµGLCGÏ:Ïü¸±Wû)¨%¡L»î¡ÂåÓ¤ºÓ‡5™t´èôÔ ×ÈôîÜÂÅ„¢è´j#‡ŠHÉ 9??Tf›;Œ—P¨ý­!=a9^Œ* +#3s1½Ô"†>ò§—Â?0‘÷@IqÁÔ©øÉI,g9†¾Ö&›Î@AÀ¸h;¾èð‘?0C-wñ]?0k?0MÕ’)UØogz¯ylWXØj²Ëw§k®‚ ‚—L*Љ 6‹Ò&u–ó•×ø_uhfUÔúmvšéÛ5`WÎFAwì¤`^£?0µöÏ,3A¶ÇI郎D~BËØ?n=Gk £Å©…ß¶ú$_gŠ÷i±??°Jñü6œàÞÎõÿè‹ZÓ®rw:Òž€·þtÛÉ|EkÈáí\da|DP/£o^q æÖt¨Ç7;(f«Ú)Ñäaé?r×h4HS½0lG<õ¯Ÿ|ÿäåÛì»—½ãQQÝ%´Û{E©©úP…„åFþ?numû:+äGíÔ ú›-üÕèbXt`×£ß.ù¦,zéçDhLÛfõp—½rÞ4hC‚ôÒ[û¬"¸ô•ãÉã·5jì¸oê&xkÎ;PsS–ÞôŽäÓô Šyª0µ‹vOuúO“· N¼F懓 Ý ÌJËáï_ú"oÏŠ_mªé{Rv¤KŒmÏ?rÅùÓxhÁ8ÌN;ÏeÒÓd¯ô‡¡¦DÍ.&[úbåö³e-¨ÇMoû`åˆé–Í,ú "ôA]“é—Å»^…Péz¨Ål8D‡ï½âr8BÊ»„uPÝ颊½ÖJgWfTh¹ÑaË ±ê1ìþdå¶ë«–úùŒý…WQ^äJº»??£ÏèGØ??ÐçÉ{cÌVÚVTobVzzÕˆI.{Eº<ùpRD“œÖJ¯ý?0ëDzÝ*eÄ)úŽ _»P2:¼ÐòìøT™±ùnw©üm#ªû•^Yf)ëXmœ¼{¿²hm|VMMô·âcù[2DÒ#×ë .V¦ºeÑš{÷Þ÷žÕ6ò(×üìÖì<7ÖÃ?0?rÆ#î÷ó FÃñ»{+Þã[®VÍXf+òñ—{rT\û]² ×mÙ©BiÖ^âŽý T +#g¼‘^yà¨W§n5³2ûÁažúñCëðÐz ¾aªÑ/1Å7±>JÑ/´ FñÜè{×A#ë´Ý„»¦6Ôš_ÎlÏâÙYu¦™ÖëÖZínÒõnêÔÈÆ%GµÍñ. Ý”w\²O®ÔÛ— ·èíB¿¢Å©*ŽŠ÷ËÜLoë%E]ÂæøÓÌr›K@5DuJG,Žþ‡øÓ/ÏèhÞYÛfT‰ûìÁ•“”‰Vü’c!Ÿ6$•êœÐ6t@Qô¦Ñ†rÔõ(É€LÄ@΃v±»üÎX•–€ÜR¶)`7䪬«YX3z?nÀ=k–-çú-âiýŠ??®Nrbý&‘¾n“I*.›µ¡ÙÕ A°RÎ,7CåÃRÃt^`Ê÷~º¢Ý7’x`„@u÷¶.X^g¸ÎX‘TÈihȵ‹„öBÒ:cŸ”µžî §–JA½mwRÓ6·[ÖÊÎqdé.ŒÍŽÒÚÁFßô= k öïd¹>l°¸¿WMwÚ‹(/ÕaLk[wËuË%b¬TO¡#ntà*uãw· n»;TFôa×gh-¿ÐùE3¿ZsA0MΓœQ ý¨½<Š…¼¯P÷Æ™)ÓÛ£œO×Q™?0ãŒkèGT‰~D²„ŒUÚ??C¥$Iv™\`(‡ËIi¶moÌÝ!r½†¨¶g- =1Tÿrð<9KŠü८Åè8Ùáî–ç îÃêDæˆÿBPày±?r}Ñ€¼]ƒûpÑJ´‰‘ ìã`Z&f¯^Ž&?0iº3Ä?nè7QeÜ?0Ê^ûT.Ñvãw ÍÞ¿0Nóà⺱bZëC7ª]gn‡%#ðtòGÀADCYîìþùÌ¥dHUB!ävS,k•5J`#ÁÕ6½˜ÎWF]r±0Ø2snqÜ*›?n·½ÃÖh@Q¦Oÿïçùh8’~ý“enD”£¦z{?0C'à3e{…oz9ÓD¼¶gòð??°—dmãIŠkg\}N_ÆÑ]ƒúÌH¤ß<Öf|³MiÆ—¥‡Ù?0Ïi;qÌ`3 bn\g„ äêc‚²‡ß4Å¿~©DÖ«Øv$c˜¶-Lbù2¢ÝÛÀµ»2¶a$Ì‚{a[ñw¶kUÚûöK^ó´·O/D›¯ODªª±2×íEÅ K2~±¯’öw„-T%Ð’á:€YöÇïx̸ü‘‘¾ijÿ1”ã(¡ú¹^wP~mÃoÖ±H¥ô!÷Ågˈ­š$³ìVY[«Ú̶'V¶',UÐPxåîáÞêîÑRð7†#—o´³ÒÜŠž½ E=<ýœbد/Å™­ûºLG5·ŠW‰´V?ne=JO?0J•/ªÕJºoØÂ,K²ƒƒWñlÎ.^ÅúêòULõË ,NCF7^€! 8Ì3ƸŸã”›š{KÈîµÉ*£Y"Tá².Öè»a1)Nôu¥vù*N⥼olQIC^ªüعü«*t¥TÔÐH9„v¨z8x¸4/yÁ¸[\„•öúêDzëMŸ÷áÜýK;/†MQÃ]+v_/tr¸ø öüɪy{û@¼zÊd´®é?n|š/ýܬ\ìX½sË@ÃÔÕK“dc›Ëë ð¶ üäh¨”…(ÛkJœ4Eé½v¶R~V3ƒ7ÏnI›!7fP©½ÄôÛáßM.€ZÇ yë÷ÿ¢ƒ/hœty¨j°FÝÃJÊuòùÉ|*¨ê'Ãëûv$B=ÅÔð@°§£š*n°?n:ÔíZó‚.`Û톃¤R­Â¡õçaf?n‰­½¼­Rh£Ç¶Êrwjö+83XÜý §*zÓÁ{iàñ¯_/á÷U4ÉRÆ%/®,pÙÕžm¾ÅÛœ˜¬Ý½Ú4µ?0¥¦Àlæ›N[-¨ ;H3÷ ôPÄ] ‰i'ð°º/¾¡\›« ©¶2°^¥úù¸^ÀÛ/©íš©ž’PEåGQµ3NÒ(?rœHñ ±Œ KŠg³l3??Ékj%Îê›À\M7ÅíÍ ‰L£xžˆ  R®i†Š#ƒÀØø°/é¼9‡ˆO.“Kërþì ±TR ½faˆHò…¿,_+Î2çuÛæ\—h7fqÜ0Av¶õ—äÂçövÇ-tnÕÎå‹cºn{Äßæ»,ñXèàúdd‰ó•ÅyË0ðÐ&Œ.´üË@µrú%%fË.M«ž»5r¹ä¶ñ?0ÚZ1™»Ë*€ŽÂ°~ßU–òEQÁºwÖæâv•¥ìŽ‚ë´§(?0º?n‹°íÂ?n ñz([Nóõœ…?0úùÆ!;&Ky¨URÆq·—¹‰ó3Ìë¼~y†I< ‘¸Ý“ä"?nK!èun»êŒ|mƒó^Ëç²²­Úºb%úkÒ5î7èv¶RÏÚzú¶©¥,MŒè&!7é°¥©ˆ½Ç­œ(:Ôee‡‹+‘?rF³%M,sZŽEË»¸6aÑ?nöšéØ7…ƒ=C›Y*~"?0”aÖ¨&QŠ|LVTª>EÞl~N9Ô®×èddJã³óM»}ËüˆŠa‹ÿPZdu¯‡~»:¢áxK&Éw^+›ré¯H/¥O”ÔߺG£¿y}ÄÇðSÊϧtg©îâ:Õ5M×á´µLí ·³r9¯m‰)??›.ëƒ7)S$a%3"Èò›lST,CÊ­YÁå0ïoã9Ó§•ÚA©n\$°òdíž·¸q W¨¹ªB¥Ô§…ñEÚ«¯POA"1‚I(XZýÖV¼â+”Ÿ?r‹K0šŽûµâKÓAÎ,ï¥GÂ}A³\RÄ›|¤fQ¾µ1ÄÉTe)4ÌXû5?n&¡ƒD£5-- p«pÛâ)v×¸ÇÆ²¢æ¢[Cƒ$Ùø­¿K•õ < ™P~—½§ôJóËÜѵONgåýO ÕÖE^ŒF?r´§voj½If¢´zÖ§ŸR_óxôº?0VýÞsë®h›§›"Lè7[ƒ‹Ö)bS×¾k‘¤ã?0kT&éÉ‹r°Éw™J'LÇ[ì±|¾N??/cèùüü€šxHíûÒ‚Êß?n.@qž†³h…gé×m¹U$Is]w¿,+ܾéÏpþ\çÍ[#E¶ªº“†!ù(wŽ ??7QVhéÆä¥‘е±wyiiBM&µgYpg]{Ÿu*ö­%Vר^JŠ ¨XCU;Éûp¨@W´O6œdëö¿¯Krh€…{?ryÚ‚$‹„â¿zhÁÝ8 52ýáë??µÌ±ú^ ÷9KÛ)ë0,hRmÛ@yGs¨hg‰Õ¾ƒ”^˜¸P–¬Sç¶ÕüÃpŸ?ng³¥C# Þ??>: ˜ÒE·Ë?0pBCJeœ˜Ð{ê-ð̉•×7¤©ä¡_vjvÄÝý’'λ+V¶¬šûižZï»7}[Lz\¡Tf›mý¨£öT”jÃñÕp”³í[®ƒ$(Ýá<·a«w›¹“hùózËQÜd`»yÑN.\!ï ¹ªõÚtÓ Kíaø6‘j†õ°4Á6,?r‹P˜Yîd¤ëa¾Ð’ŒÐ),á»”ã;‡º??M–ŽÚ0ö”Hü ,L¼"È×£œÄ+{ºÈôâþö¹Áçc³ýði¨Ms $ qàžÇ¢ë+ÍE]$}Í¿Œ—©È“o6díxv¡aÄ[0A‚¿ º$aº¼îÿø‡A°þÍ*o9IÍ_5LKß…9õÈWûaÉ¡ ³Ñ¬V›9uX¶ê‚€«ƒ¡Å[ñ÷û§tPÁV?0Ö+ I=u¥†U½©&–q½ÂËì{ûÓ•Êá’‡÷ÝfÕ)Üi,‡]¿Ë1ï.X-æíeE‹7RVçTy»t¸4ñÁ°ä1Eäè˜+ºóÀWéøN§nç¿%âÆ³lfÃòØÞ˜??¤ô‡þÃ;ìTËô]y£ëuBõ³­·¹äXflnÿƒ©ž6ÿ›+/ xÕ«íœéì¡Q??+¹·hüävÞ¦Q²*#]?rÍ?rK“á©§š¤eã¶¢FâàrG†Ñéy›!¦û4Ïâ¡1Øß6ueÂy¸òNãd|¡u»JˆD4q˜µ°iÞO/‹˜>ª½}t/8¸}4Ô,ümÿH÷q\ùà:ô3Ï”±"β0æþµ1}‘¬;P‡*BcV 8‹{û¦Ü‰N ªÃìöÔ´¶ö4ØUNo؃|}æØÿÕörkç2Ðv.ǧÑ …?ncxŸ±jÀ_2q¼Ú??}¸)·mÙx–æxƒßÉ©A³>9à{¬óÝ1›bä.¾(Ã?nJP±³ï@õ¶;O†&ä…ʼn¹¸°êÑë\ìPèý_ºçÍ›ÉÜ$ïŲ$á?r²Äì+•1t¬½ÄÝê¨dz^;È(†\ˆÁ”;%w‡œµi‰Pý5äÈ?nL9E^^F6Ú€|SŽC‰Û{OôN_7óYAº%ªª–wu´ÊùhÕ>…t_l¶/Dé6fQ]‰¶ì…–Æmn³¬ý,nŠM™ý?rwÚ ž¶Ã§£ƒP?rzÍl¤ö‚?0H®mt«Ãkk(Ù~æþ•íbÇoàt÷6lÛ¤ý.ª„!°Ö7ö[²-üº‰øªkW³zÐ)ÍöHÖÔ ¬Ó©¯v™ú†í¥ëwí:8ûX\þo!p¿V9®ö,(_F5½áv ­ò½{¨f5¨rò?04×TLCÀ±š;㤯Hýèáï†ã£SýˆÕƒÁ§t+‰‘{ ò×gy‘9ëtpé{Ç÷]Žª˜T[Ô£0ü¢šù×]à‡ªðÎÕ»ÿÈ™µ_nqcLQ}+-c׋?nÈ4$)ú„¡W_&³‹P¿¯F[Â+·7|sÑ¢Çó“0ž9xƒº?nWxƒêYb öoqÏaXˆiÂYêN½Éðè@lÒŸíkgO“t^Añ|Ó. ÌfÍxöÞ$¶`äô¡ùåùx`Ø3oócœ‡Ý]Úûö/š‹@/z`b+ЃÜg؃‡yŠF4ìîþ÷ÇÚÝë­Çt¹û7îoÑOÇû÷S#g?ny@3åõWá’›¬©¨\e7åSƒy¸Ù‡”Ä÷ã×Ѭv©7'uE´’6Ë=6f\žï™œrªzÛýË5ÜÇ h}[]nÎÛXkf´L{D7„GKõ‡Ìi\ÔìÃ}×´„kÅs=5ƒ¼>5—Ñ**¬ +#cY¼·»û_-¢ÂôZXµänmõnönØA íuªé ÷Ø)–?nÕ¸ÿ{#ãL.-‹0Ÿ/ª9ŒñG§ãG9l"v“ Ï8] š${›j,Ë{—ý?r5>ÌHÃh¯ˆn7Ú+bº3±|Ì-³ÍÙhŠ(,r¹¬ùJ‹°*¥µ jÑ%\¥…0дƒ0ŸeQZ æ?nã­{_UÈ÷«QÞ¶¼7??§Ó…'M¬§âþM>¼š4›?nN+IÃØÙ«ˆÀlͧ›Ÿž-cerU>Œ®ÛŸî½ðÑ}@Y½Ì8³_]Ý{u5â ¹óuH«Òì ï,³_©Àög¥S¼Éè`xÚ‰d[>»¬dvѽŒëö²wì;nÏÛwZŸèo,µXŸâ¨”uǬ}ƈ'Ç#s[˜£ì_½mÙ;kŸc ŒÃ«žwŠßP×ÎL•»¿m|ü1ZÁ¿\mNÖ‰wg×Ï»Û*¶w9Å©=Hø€~Ôo–ÑØ4¡ûW_,|£d»çÙöŠõ)*„[ý€µ¦8P*Î{íSœidT,gðÖÓBÅ ‡ôùÁ‰ÚªÍd¯ìÁÅÉÃ:¶‡”ð`½|È>?n»2®OëÞW.RQ°e³¡ ¼^2½á‹ÓXƒà›¦ì@q_ÝÚÓ$1Hf&_=”l»í§#˜œÒóPÓÕ´aººö§qB@b° cEÒ›—iÀxüéù÷êâÖbØÑ©`Ið“NóH¿Ð¡‘ÔûDR䇳̿\†ÔÕ·ö¶•ƒp‚nغ_-Àïâª0ù¢À/ËNu'2בSV£ðùUK{×A&óÁݬÎ+%o3UÖê\ÀcOî|]|·Œ€Ù_;©ÞV•¨L7??(ñÀ:>Þ³>­MêÂéÊ—ÙŘ|SÚ¿¨9\ŸçP°p;T†øð8·ý¸Ÿ"_<%)¿‰IW}7™ì8JX‡Ó/¸ùÈ´õt™­Âw¤éÄP5XnÝü¬É3d¾MWI½» ¿Ë­‚åW1?nÙŽ•?rX~ãþEWC?n¯=æ,72ñ\‘ísu8Pô‚Š€ æ,fEÛgÃ×zÚpä .ÐSZísúkfCÑ×.(­ðM,h=x]4ڻ躩×m­ááî^«Ã÷¿Ç­W¹­‚”f×út£Ó™??[èàÚD‚gb=>†¥~y1†•sMæP¯è;‰XŸÝ¿‡C…ú-ž®¹îVKçç³(¾ E͇8E¥ç˜Tè0Ìxô©žQȼ ÏRÞÚ£¦ýÉž–w(ƒõ›goÙÑž®KЀâF!I ÿ¶º…›1y<}S=©„#?n±ïï{?0»+æî.í¹CE3=)ç=wWwAt?n6A“¤ÇíVô´Ù`/-²?n¦Êôµ½ÊãD¹·]ÓX¨XHÅe„³Ñhæg0e³óõjåg×ö{ƒ©DT€G+3‹VU*“#“‹ÉläP’4s­üs=Åúi#|›'&Æ= “a"Å~£Ì“wSÂúDÖô¢ÑTÖ5b¢#в;š¾yÌ4±wÉ ÍÝMO_ó˜ô­+??倲F)ì7ÊS¿ç]Tש&Ð^­óhFGòøÜÞYÂ_QÒž÷²ŒhawÌ0??+ôáÈúoäǶ·£Øòl½j”Ò);Š ‚uÖ(A¿¡Áìªç½ù%ˆÞþöÀlöª,LI±³Ý#À92ŠÞ²Ãô÷þÎÝÒñ³;ØÏîܨ¿s›Ü½Åütº$N5ë/_ñAÁå?0ìaÐÆÖ],³óµÖ?0²Ü‘,Gp8Äb£k)ëý{µ›^´÷QÙ—ús« ÁûÕ»®ûfQ¢Š,Tè®öÝ«wÖä_ߟ޵^½õ^yô4˜!ä炚¯3nЬ œ··M““Ñé.¤PKï‹rtrjÜ4Û oµ|¨S??e¾cc¾`)Ó5"v̽°Å|é›~ÂÁCcêÁh«Ý“Rªa|=ÎuM™¼°TNËðò¨©jœS5s)~ ¬µbn§<ÎF@eÏnÄçUñ`¾•fIfÅ5ÙªÍa\¨ïKb6o€õâc³lüuÝ>Fêx¼[°§;^0«Š!{êÖÛ9ݲ‚¦ê¶ívÛ\D‡xÃ<¥©¼Œ˜±ûx4Ù×›‰ôdòO¿þûûü‡hÛ©OKãê¥äM¯??rGôïþ½{ø<þü³£ú'¾ž|~4ü§ãÏŽî ‡Ÿ}6ü§£ãáðÞÑ??YG¿D¬±…YÖÖñÿÄ¢…Åt§ª–ò-ek:Î׿>ZÑ*M²‚(±~Jg=kG`?0Ó%q·Ì_/INK¿“'9˶i9Þü«Xd¡¶ ÑŠ}¹ò%O¨¯;ôöûìùË¿NŸþ8}ùý ÌŽVTÖ·þôdúâÅ÷uµLI¾¼Å”?0Õ—º?? ¿.¢%Lò~¢Ï2k:⨨zêå_Ÿ=™>þöÉã??~÷ôžE×4Ï ®ÉKp]à…¡ÑÏÙ¦ÌTKpqæ·ôóñ2*3©Ã9ã»YR&®°38ˆÿ曞=ùƒÎÅI?rÝKè>ÎdB¸ð³ï¾÷ô·??}÷ìE §B‘1ØOøÁè .þƒ??›ù9)¬Ãâ/™ëbZô­hÞìÝÙU›ëtÐOtÈ­[Ï^>~ûòå3-§Î’U-•aáäÑÁ¿œ¾;yOëÔv v¶ôó\÷q0'9{ÎØÉ»íÛU\T-§±³¹³ ËÃ]mfѧ!‚qà:ÀMÐ"1³„ý‹NGï3TMcUa3US>n5sÖCui•¹üûVKÙÎêG3û‰Õ0Hz"?n£n.Œ’€ù‚³Õf6\RWuã—¨Úy¦?0lƒl2‡Rْ׫uÖT†V,?r.ú-‹iF–«kÇ­ø)|äÉpβ3yë8OÖùrÍ„Lã`ä>YLRE¨f¢QÊÐ 0š#'Ê?rL³d™€žÜ¯'|\ñIñx@gèŠÅx£s»j¸·G”Þ’ _§šf å(×®*¼÷í`u[ûňROWæÕ(??FÐøåk’1á}y2j`­º•Êþ\åç!µÔtG—šçµSÇ£JQ¡ÚÔ©â‰òÜ1»,S0\‡\Z< jUq*Þ6×E¥²‰Œ ¶aAÛ³¾[óÙ4eé¿H&?0ÇÙÙÆ¹ç•bäà‡˜>{òärêr>xS²ÆLî ÍåEÑÞúÏEÑY½Ä躥H0Vrâà§XñãÑ2–œÁãyY‹é–?n£©B¿2:;ãÀ_.mm,ÐÖ+1¥%“®¨?ná’D×ìâ’ç^U˜¡E¶ò¾éìç…uÏ??Εzk’v*£ÄÔGÇi'E=b€$¬^_#Ï•íÖ@;Ì0‰6Ì=s£3räáñà˜L5¬ßûõ<¤+żxE€ñî G뢴{ÎÈ‰Ž¡Ñd²bQ9„c!V;XäÖÖg¶îdéØ ÊÉëÉÝ7—±‡›»Z\Ó$^ŠÅÄ6r£—­â»K›åvÀj¼Íe/­Hu²¨mÎMÝeûòǯY3??F§‰DjB¾¾¦Î†¾k6¶¿<9¶»|À2)&^ßøYiÒ&'÷ᆂ>??[oŸ?nwV(š/³,ô‹:cźH˜!ëÙ:K“<<þþ»'O_Nýôò[ׄIÙóMgaFÅ~×$$Ê2bfPMÓ?r;Ù6aOÔé9·~;¶þûf1¥Ÿ†qoÑ[¦’­›~⨳÷WŽ£zø]–FÚ¾²y_µ˜lºKÇüR½ë#ÄXò©šmðIl%ËÀv{:£SD˜åÝ£?0éªrZÅöÆá…/3??mHy¹WZºOaê®}˜I:Ué¶whAâÑ&!3ÂÙ­G.V!aNŽ<ë3Ï::-w??n‡ãö¨ÅEï[¥ò›ÅS#MÓ`ÿ¬?nÎÎ9tºØ??~Ü×]×Mevµ{6‹u˜àÂ>ué§èUHJדшlAõKÍkêF{Úóò]%(£ÙÇWeê?r®$&³|bã$“…y®î†¹¡µDÇ=õvôý³§¸j)’ª´~ÉNå/Óxª2IJîAôtžø&hø©[o§¾{??ª‹(è$aËWW~–‹¼fg*ƉIõäùËécúóÝ7ß=~ôòÉô§§|ú㟟*û8Ä!ïqݰïË™Í|š°a¹b,%ð¬°5~ÖßUžäf¼Ÿ°{«qÓ |Í“«ÄÊ:€»cö&,kcVjÊ$,3ì?0bVgH)g›CRK7H׿SW+ß^UËo†Úùlj¬^¨>æýš…ëŸ_òþçÞpxÿ¾¼ÿ9ÞûõþçñþªÑetÆ??‰©.n~”_ç=·BúìA×ôË|£9zÖãŸ??yA70yâY??ýðô›'´NH‚õ2|š +#ßà­žÒ Ôïì;E€Ê®p0YúíMYL•àû-ˆï2¸îõêÜlÛ[)ìêìëd""•5ÐBSzIò©§ ON?0;[$P@'±~¹iÓ(7àÿÔòÓ‚K–²ìƧ³)àÁÊJ£t tp°ÎÃ̪9Ôf²”A Äx¡ƒ)ƒv‚`P?n2ÜZ‘-äó+U½{˶ºL“Ò7?r^Ã=*þ2HDg—{üÅu\øWjp>BÇsù}Vi²’_DD•~O@Ñ_Ö%…%Ĺ¡èËÃ¥º°[Ê× —lŒW߀ŒÇÃÁññàäïx`xý²Æ½qíI¿ùŠ3,²h–sîúç>—“šHºØ}üG-.~óícýå1^Yp:¥퇫$Ößÿàg¯¿þàŸÃêUgàŠ37ü×úûÙ:Z’|+NYøð2Ó$˜…:%Jcê7ý]â£t]ýÀÚ¾7ôöe´låWáM—ÊÜ;ËëiyA@Ü"~àÝrw^ÉÊ;ê??}ó¢–©/Á¦õþý½J"A¬–çAÊù/^|ý,C•࿺¢ÌÏq>¿R_§ÙÎtCIRþ†Dãïþ§²<,Ìùžuð;’*p ­šêL½¶}ê<±é>i‡À”#\Fp™•¿ì.#×Y0叿ËrÚÃL·äœµÍ –>DJÎ-#‚çY^¨ïÀ AòH®@¤¹Bä¯TÜhunåÙL˜¥Ù!2çÑü+õFoÙÖ¡Ž8PµûÅóžœögÊ̓p{Ë™¥ºÒßóÁRhBhãq%¾§5'ñ¬‡½pæËw±DÓiLä5»ÎÀÙÛ”Õ,.ú9ëMûâœC›7lEªV´ðßGóäŸñ­F¾+OÓ0‹è3Û.VƒÕÊ[~»¯ò™i±)˸&8G9åY.³HÂj©ÂhwnnH¥uÑN¥ ‘[Ê]…€6%†q‘f—YùW†bgÓ"ç±ýtß`n1g!¯VS¶nÍ6çî ŒÎ?nÊCgéª`é¯ÎŸöcó&¯w9Ïš»Ö×±TkW!=còP¦Ïæ2%?nR™´?nÚ)a+)??—IÙ:ÍEšÖJ·SSÄbj¥nfuH1Q^U³‘ÕòÕp²ö¡^9¤Þ’«1N­1„3ÑÏ,.aš*°ÁkÚ£ßÚnG×(ågsmÃ0¯a6ÞŠÇÒL…¹ÒêÃŒÖ ¼mÁ&eã†ïÔkó]£…Yg äÓžášÎD1›Òl¹]¬T\L¬½??±~|öò»Ÿ¾°KW®Q_ˬ5ð7†¸ºdJûã³o¾{JÈé˳G/k}ÿãã??Z??=U??üññß“&åÙ_­~üÓ–³ÚtÞÅK&¤côߊë®Î“‚¬DÃŒu¶£PQ o4†y Bz??Ì{Í‚±x/ؘ­‰¬\È'­†ò±«‰¹},sÚEÔaÀÝ›y]¸9߸l8SÔ¢!`-• ¢îѱ>Z1wF6 Z;Y~•+†oÊpWß@§ÏÎù2 Sá7›’9??™Ç-KÃvÈèéŠO eÓ¤·+)E“p6Òýü·ã쀘d["½fî5ìtöÕð‡Írw­/Lu‹3q0m›ÀÉvÖõ.À‰?0éMZŒ»j‡¡P0^FòÄ?0kc§þ@!Öi˜£MÞ:Kõ³„ÁÜ_EËk2ßc•L©!ëØ’_òxd°4ä{”¦†,\ç!n9Û­EíÄ1Qm’NYý‹¿Ÿ¾ IâÉKj'MŸ??ùéI4_??‡É±¬­¹)úïž‘Üò’^=~¶ÅOßéqÉHþ*Ñ3VOñZÜU9x>„“²ƒÏ5?ràŽ;?r…AQ´ñÈz09:øÝéÝÿ¢ŸW·±±Ë°Cí2¬¹ @Ÿ§ÄñÝB× sP’ÃÍp;Ï„¸«ÌÂhY‰%”KWǃ#ë°6÷\)˜öèlªáÅY¥¼eV*>?rp@ßjŒ–µ›Ê ㈺iKxZëÕÒj•7O¶™xsÍ@3[yäN¬1ƒ?r”ýXÒ§9××X®¯½¸äà£6iŠeSyøQ9 ë¿Â}€5¿Xieq›zÑ6Q*æ•yZq³³…§€’V¤r½È†òšÐÇžÜÎO¹]J½k”9 å¢nú¼A¨´`rL}uk‡4€?n4„”aX^Íýk˜EçJ^§Ç4¶ŽM*“r’Oשí5ªÑϾ´Ÿš£=mbÚB?r.ͤG²ù·Ûùã,uýŒüÀƲö\eû»#[2Iª<³‚‰¬vLJãuî§À&rwÑêZ*­öÊû¥uéGõõÛ + ¦Bmaéhpl^PágGÄuÄÑYë¤÷?r~¥shþÄYª-I™KQbÔ{T¬u}½èÌHc!ZÚoF+‰ŸDé©èÒýÌf%. 2tÄzpÓ¬Gƒ³ÏÏ‚ âRãG¬+zPƒÀ+¸º.Xåã žwÖƒæövh?r÷Cy%Aô³û‹ÚÅ•u·5OhYÝ??jw}¬3õŠYË$‹rs–r~‘ 4ëŸA]X0TF"ìï]C#Å<Û{ÂÊP7FR´2þ!jêe.0C¯Ý?0X¢xº²) o^è‚8“¿Œ6áÎ"nwˆj6à§‚Ýã'¦eWç‹>æ|@3Rœmû ‡f±?0>„ÓHëX0óèóÌHN(’Úþ2Ò]€µBP«(þÒ?n2Ú}À~RRšÛ7Úk –dÁÚ$ fÇŸ@ìòEö5÷Ê?r·7ûâ–礛՘œ(X†ªÒšzæS»‚ZÃ??¨uªnœ+"Ùˆ«ß>fÊèJ^úqe 6;®É@C!õ0œ9¶RK·„2LYS¸ó(+¹-:yùεKóð†¯­ÑhNÿF¶ÛÃÏñwòyG´"–Fgj,ùÁêæŒÀ>~>øbp2ø§›Ú Ê£Ôùñ…²ÂÛØCür»#–?n¡¸??8gÏD(Œ"Û6Œÿq> ?nÔ„¸ïî$†?00/ES›_^§ú9)·²c²T*'¡d ›Ûp;wGåk$ð5ZãYúŽEïýV°4²ðƒØåÂJO+ÊóuhßXFGÀ$ô—^='îþϵ0K³p><¨Rÿ0õ•°Ý—åð¨üÛ»÷ŠÅ -m«ù%=$îªM>ß½_É×°òŸfJ'[¥úù ý­ïvOvŽßqáü…hdûB¨¶öÜ›¾Å’fS†•³L’‹ÜZF¡z›ß½¾dCô•O°~¬w:??§©¹>S›RÒŽ±Ò¨h+ήñŽ`‘\*Ûñt÷”ç”<÷ð-¦ÊQ0 £Ò³e˜Ã‡aÿ:¬â^ý²ÞÕ úe0èƒÛ9OÖR¶ªrxÖP)B'÷G¤O`Á§‰©6(f%3_dîqoG7ܵ‰ûF\_×F(Û+ãô*$Oº¡Ë¢i?0S;p¶Î²0æ ï¸x¶oüá‘håD¨?rX7ÆKçŸHŸßx6sµg–íî¡ô¬Ú1ÂãLÆkæ|oÔæªÔv0„ó£z+W÷Q£Þ9†"BüÛ)ö‰Ã=LXš&‘¥v‹ƒ6×5^?r ?0‚V')mLë—z:ð<]Œ©€A±ÂÄl‰Š,=5œ=l•¤Ño??ŒØho@7ü£žóÊg»¯í*˜²ãö®¸öµ·í?ný½çjÅN.ÈoLh vªeqSoëü`¨â?0zcw÷±tÝÚxG‰ònPÞ Ê©ÝVèÔYG1ËÖqMGq¯SGÁ/›‘¼Ç[ÒLIÈŽ£ žJí Û8ID[Ì0 ¶ëÜm,‚³x;£>Éïµúl/%R÷„2{iC+`ƇGŸ‚aÖ/bEnï8÷µ€CCöpüÓiÎç>Øo‹#>çˆ)´÷5‹dÉ«›PsN`mçd¾›cÖJ߃p= …Ôª`­*{¥³u^„™ØØueuìAí-?0"Ô=¦1Ȧ÷üº=õ° |mzNÏÈú—HÕ>£“H”5Cð—xVI ÍØmW­™`Ñ}6_`r!¸½ñÜv~)܈à8ºñP„ §+tÑþÒPû±Ç~R/D3ûá÷¶Wzîw¹~wø§j|„"§ÚΩÞ?0âpcÚáu°5–ïHnuÊ6R¸é]•Ñ\ÕѸSÓÅf(ÙH?0?rÒ$uT1½¶za•Š_ðI•;.î©=Í÷2´Ó#MMH©?r’”ü´QmK,”®ìX;Åæ©kÓ€z–Jãí>s×ËÑ7 " ÑLÈ{\Qt¢ q£$üLªXÙÁ2¾žÕ/Ú,Y¥N0Ê™ås€‹W=ëŒxí`SƧÌeKœÉÉÜN*Î&?rïeo;”é1X^cÝ—1ÒÏ¿¶ï"=ñ&Rÿ–Rsß{ʨ Öâ&æ•ìãH·™çÚJæ>Ì'³S†åj'']•ÕUˆÃ‚ñͦx¨ùñý üÿ!š%ìûç +#ôÿƒ¬ûŸŸÿ??ÇÇ÷ïÿêÿç¿øÚ%2üú˜ýe]^|à*¢'PûÆàòã÷ðJûÝ>ì¨^Í+'?rfß hÛØü+à£!ó”¥f˜ÊæÚ;}-ˆ´ˆ/Þþ8C™1Jjï³,–5‚êl¡50Êl…85n5·$\q7ÃKá±pú¢ó0'‘t2Ý«sÝ)GÃ$ÉçªzJ©§Õ³cá±,òÍ´H¦Ùù™óöŒ$˜«|vpDÊ|Þ×î‡gÛ”C¡/@ÚvˆË—,+·Y2!Ø^YŸRÁÏjÄ*ùkv «ýuèÖ”…êE³>LìÛGÃ+u˜¿òjfQ]^Ã¥& ¢Ûß x??ªw"¹ÐCàúÀ°åyô6du÷•¿Œ&QmÊêÞ™/\K^ºô£ü¾pݦFQÏ2aTPï¾Ñ>î°˜¨Ð6ØBçyúãëÌ¿l¡â‰9¬lÞmgàª@S¯Ž-úTóo¦ðø)ŠËf¿ fJ¿­K×б 0c\5Œ5©ƒ8¼tìçø=Œ²i.Ñó ôß³Éèþ©wŠ™¿mé?0ÂižzøA¸ "ŸÊàÉ÷ì,¹rLaª½âÑ¤Š 5èããûfTÊ ½ƒppC^¨Ñ•´@ÒB'ËsÝÎÕõ¶Îh¹s{q"¨èî>üd! ‰¬,ê<|¸^Ù³OŸ<¢CÊË$ʃ93àŽ0ªÈ}R”0(ƒZhl??{ú,ö7:]†›p9>vws즰 G͸RÛøKå1bï8‰ëËçƒW Oñû÷ä —³ûþ½[:±÷ŸÔ’%ì??Ó÷ L)±©ù¦°´éÂórå-ÞXËŒú 0‡‡F¼5®­-¬$úîÌÔüV '¦˜úK*V[DÆ 3è„ÐþÑÎ¥IÛ‹U­d½ºm¼éѾ”ž[.Züéàè†O­Þu{¢×íIߺíY³û¯×Y«ùMý[¾ºZ-ÙMÝØ¦Wh¶ÆZ\Û??½ü†Äž¯Þz€"5¨cÛÚDáåï“«±}da߆òÙ"dq>V7УÃÃËËËÁåÉ É·t9$$öÃç„. gpÀ‹±MeoÛÖ"ŒÎÿ*9+P8×¢ª>CÞõØFz¬¢Ø‹Ä_Fq8¶WQ,CÛøÏIÆ‰Š´´u‡®„‘N뵚À¼ôÛ3*–ĉ/Ó1äDݽ”䘼¸^Òeˆ@¸€‚p??øŒò¾{ÿàõÕ‡ç Íá˯v±?nñ²ö’ÀËh—uާÕ œõh, ÈÈ` ‡@›S"¶ôbV°Í,.ˆžŒæ€[ÃÒÃ×q=ñ¤L<²Û-î<(¨Õq9$T(¸Æ¬†+ûáíœ:?0¹"ê'š] ­§Òåˆ$]M1—ñ—/>žæÑžuíñ¶\0Ë3,*z—æ†í‰ Nqë#ŸÿWAœ³àõÿ{tr|OžÿOîü#žÿ=ÿ«[8þ‘ù1±Bþ¥]¸wŒV¬ ˆRÜg…y¾­žmî=ÕvÊ~Ü/?0¾7£†éW±ÙÏèzYD3??/æ‡ÇÓdP@Ÿ”¨`ŸA¬k@K…N× øˆþíÊüý£/PÕ×w9Ú£è‹'¥sÒg/Ÿë/ÿŒé¯ÏˤÏÿ¤¿¼üËKýå‘þøúé‹oC8ÊÚþ$j¹4NxÎèl¯Â!Š¿ñÑôäóßÁ1h[•¢ ½ Ý£²Tu´žFiî•6E»ÝŠæ›Œ ÙÕÏföíúìÖ­ˆ¢{ŠíI0¦ÿ?r??;¶Uê}¤ÎçGCzìpfoµ7ÈÂÀ8z| *“œ­ª_qPûqž¥µ_QZ‡ ‹­F½¦?rZã4Ë´”ö&ƒJ‰„´8ð€XYsMË#`ŠHg!4:¶ÝʋҎ êÈŽœ³kFÖÌ¢Ž+¦Å•Ñ;ß?r¿önt²ƒ‰¿CE†Á¤v‡Â- ÜF"Sé;ê=¬†»8 Þ®îë]\M€:Øý&ؽ?n쾡û[]_ß•A†gU£¤N“˜¿)[UP橊)JÉÉg'Þ–xÙt†éË6ò…í&tà {EÕÖ= §$(>ĨK¥b:ê6üýñ¥??wèÍ’5KGµÒlIϨ3ìýéÖFòý±)K3•;×R®ºªés–¿²]´ìJÒo޽#Ùzêýd€¥,ò”Ö¯Ó7äõ3%ÔÎËšþ‰aðFÛ4ŒÙf+ó>ø¼7t%Èý>xæóʯó7A¼íÿ?rÙŽ$‰ê²‘)Y›z—?n³¼Sd µª?ngrÅxªî¯t*;‰ä«XÇy²ŒfTk`òHI,"ŒîÍÏÐΰê:¶hi(×ß®šóœ~oópöu6v {d}q„¹‡¿q­„ö7QO Õôû’¼ŽåÞ½ ÁÇ>xÈâ$ða¯„ÈáŽ+2ó277gOìµM÷© Ì??飺Óm6Ø©ˆANrsäÐk‚Z?0äõ©µ:«cªà(cŠÄ÷&VÃŒj©ý¹ÚuvÆþ?0e«p³Ž–€!??oÔŠ*…•:²**a[ y7ìŽ:çÂÃáŽ*ç;ÐˆŽ¡.îDSë¬ æf?rävÀƒ,ðR‰=Õý:˜³”wƒjZ5}}Dy¦¼‰j±† …‰M$ßö"ÉX-ÛEÀ¼Òˆnn i.)Êo„ŠF´…ŠÒ*T%7z!ä?0¸„Y†xòE®•fõ?r´¹ý{ÖEõêJÊ:¬ˆV# }øÂ™ÚdšjÔê)±ÂêQ—”<RDʰæG¥9l5œåéí1ÇÙˆXSýf\;9¥tàéCÖ€|ž‰+T]ï@yõ…òŸSÂ{‰FÄ쀜‚ÏûÒeSÕuÃþžãù ?0vÁÜßöö|3GÁÍ}s9&žw-²\¬Õ>ESžæ-?rWRfôoHýÇ€þaüLm™:Ù#:˾™"Ì~áS?nŠÂñçû¾†™kþÙÕê:[¥³#*ùüùv†S]žUË1ͦO??õ3·]âxØU†r¸”¨‰ØÀ‘C ‘Èyi¨8g4Ãåýp¼Z@Òžøª×<˜æFÑÙ`û¯y§.Ãܯ?r‰§‚Â/†’ ,|{ØÂU’¦ªÝàØShÿ£YHÛqȃ<L×Áv›h™¯ æ_ÐaŽ`†ÜûìˆR]£{£Îæn”•0–žšÐâ®H¸hºªY¯æ!%)–èéaðøˆwÓzÍSóÉë'MLàž*q7 Þ¹5Ä?0Ìh‡¤}ÏÒ¡Ûiñ|Áï"Ô|e°}Ø:ÛA¦7#fê5pqêÊŠ.jpùóö Oþéø.‰˜Õf¥¢ê.“ø\µ#kòîý©\â‘GõˆÓ)˜^&–wRC—ók>Ðx³i?0%,O^?r”æ?0Ù^‹Aèz½ïùq 7N_?rZ $?rnQ PêäŠOllRЭi„å=Ú#ûÔíj¨€O®Ç8ß½¦L‹ŽçAÙê©‚ëâI‰9ö凉6u…ªú,ÑJ8V®2‘]P¬iÍûÀ´äF¦³Æ?noÝ)yhº?r¢o¾D1V|©Ç¥[}èÉ»žLœ¸þ¢ hð7±Iec¶ŽcÜõ¿God7»ÞÖ¡ÝÐ4-’¼ÀYŽÙ` ^Ñéôà¾< íu6.$FÆ»§„Ûm&ÌxÖ‡†‰SŸ‘ž¥~â6@é˜WäIéyO×Ðw씞a÷~î.‡-,)wbxä·Ý4e¥3%~ɯ2Öñ?rbè`IZ×Á!óµóŠÜèð_ìqbihº/„_cRÑæð\¡ö í<:w9†©v ~*Ͼ^zo–z `칚¯¦8&ÀYXY´Xö³’Ô²w&íô´Ó)ão¢]£ëÚ¯©Y Ãø§éŠmØ¡&ØN0zãíÜ#Bp\~ž”ŸŸs??™F›Ṵ̀5¶Vù¶µçá2Ž«êËàÉï}ýͨǯ“ömÀ²jâ˜Á¦‚a?0Í>lOqu·HÒŽWu‘Ϊ˜çjè&cg¥ÉrI=‚`žÁ–!ÒI¶:ÊЗöü20^®1^Y=*0??u–$LæÁ©†d?0IË Ï#õŽuxLÖ³¿ÿþ»§@ÚËÍ$¾¶'~^À½ÝÊ\æÏž??ÅwÍ›î'Ϻ„íà:KB—ƒ§Ž¬ÛÙO‹Ê±ZxåÁ9‡ô½c7ÖîìA”ÓÂ?r/hÖ]£qô¹iƒl¬xöþÿßj×ó¡,e®ÅÙ®¥»jÝ $>uîñB[€_r‹5jJ2/çê)Ïû)Ì?rZˆì?n\¯œÃWÔÈÓê–-ÍP|[¿mNØÓ[ýÚ¡,¸®–_9‹x!\¥úÞH˜5¢IÏ#y½9˜-%;àkMÎ’¢lsÐv?0 GýGü!“…äg¢&ƒsÀ•, Hœ{G¿»¿ãU}Hìz[ÖCÑ~ƒä½EÞž=  \î×;Dæ)xN¸÷]¸»¥ÿsœiòIøoôûU¼Ëù 4m˜P÷ªKîObßè^þ·Ó _ñ’è?0¹YJêq×`fxHÇ<`CÆà\’–XÖéï%ÉÜÔR“×ÊäâKØPP*:äG>êHž’§ísH:»6‘ö÷…À-U±Š2§&•…åp(Ósö,DŸ".ûÍEvÝ·ü·úu™ß‘ÎyïÿŽÎÓ÷l¡®Y.§É…´sѨNbš‡_@ôìð.¹ŠN4Þ²q·)u<Ìõë„£#¢XÌvãÆLbÔÌZøW~ÊNœ¨%2!pô7@)E{éw*ÚÖ7¨žœ)çô|ÐçP OÍ’mb]kÆYsÜz¤XhßQx5¿äÓ/UZ±Íö1"­_ö?rÔ£YÙÙ¶MÇ™.`R·ÂŒŠ•Úø'çj n‰ÜšP¢¢»·ÊÅ.ßTôž?r§/D°?nz•Ó¯A”Q³.5Ô¹;2±ðIÍä PJâ€kDLóheøyl»§£[½0Åö›?0‡àa`‚$‚CÌ^ZÉ:Û*–·@‡®û6Þ…aZf[-RYfžPœrjIx<Úí]‹zÁ|Ò—cÛ?0ªº‘\ô,käô¿mPW>”Áß-–¿Ûù!”dl¢lV׽˟«ˆê(ÛÑÓìr™ý(ÕÔÛ0K žY¶¨¨gjjW)ýô¨-äËJ±zçÝû;V”[Él¶N# +#V{ÏŒðÉ*ÌÎÃxv­Ä‰òu)Sv[6…­$¦•÷ë§<´T1¬Ï°°|¢rV+tð*~??U?0þE“*?n"í¯É:¼|¤~÷ñ`iÞRñd Fls;X¬Õ3ù`tô“§G¼é`¡ 4*\{*ÅÞN öº<åå­©šéEú¦4JÉ÷8-iןœl𦱘{>fvÅ$"Rk˜”ëÌ\wcø›Š±ì«l½ &CM-®ÓPñˆÒt«YoÖ÷ÿXýM2£‘M­i ÓF¥n¡¸“põªzØpÃŒ@B•ÑÐD—’ŽåÀ:ÞÙ5Bå@øÙŠ¥.Ü©„ÝE³¶á[y„º§|ž6îC3©´Dï† ,úNË÷5ŽÝ¿ÔÄ2Û‰ÉåõáKK.Šþ%ÕSrŽ{ £‘Ø:ÖŸZyÜiK{p&%\èŽËÙÇdNYÖ컟ט<Ù&ÈL=ÇxHÑh$Œì\iØ&µ*‰Ñ4(À)'g³@´·”*D€h.·¯}0‹Õ&ä#Ý9kãÍd¦ãÁnFIÂ[´ýÎðâØxšKŵüLÚçÏ{F1Ó, Zð}X“žq,°rõßt±)à^¶Z4dן$#è5 ëíVî£fïR5nÏ…>Gìx€2ü„ W{ÃÑ^‚{2Óg”}%?0UHõ$c0v%W¥Á£¸ñ¦mã鱄’]?nÏŽz é°Á=®²¡ub?reûÆûJX3˜ÔUç$K8W–¿»µãÂ’ñîåÐ7(ÁTóX‰oªùø /Š+u‚" 3‹ènëÅóÔ_ ¼æ»ëV©þÛ¯Î.aK°¹àXJ÷a‡Sµƒ=˜rWÈ%ÅeG{ª®i :M.éiÞÖFT£C‹b¢˜Ë‚A踷ͽù¨ËI°yÑJõú‹'a{ ´Š‘`w^Ÿ?nŽcD[üDY1ܵ>{%£1)=µ¤Õ}½/÷Z"¬GA^«[o‡{ÜŠóµ¨~?rÜ¥´h "U™–èÃj?rê`ŠÄ?rªÇ~Ào IeÕ)&c5Ú¢ˆuw’²_½‚~˜ÿ°È¢\€ü²þ??†Çmÿã{ŸÿêÿãÑÿÇë<‰ÛN=útH¯ϳ æ)âÁ’wE??ŽÞ†»L¨?0CˈѨРËH?0€××?0^d›Êñ^½þAÀѾŒUÉc•[m2Û=„¡Œ¸CúÀß$KdHÓ-ÝyÉñ,û¼ÊçwT! )XGp]ça†Ø>?nÓZDV+}–" _eåH(¼½ÕÃ&QAZóÐÇR8ÂØ??ƒT³Ý¥¶±¬Ýw!%.ÉÀ×êfuê´õ!Íyh#uKÖG g/ÓÈbѤá3¸bŒ•;zy (>Û-üÜ/ŠÌ¡$¼¥˜Ò–+S?0ZîÃÞc Ð Tç虃à5(‚0Ÿñ˜ OUö'jÃ܌ȴ^ÅŸXß>ùþ’nç¯âÛùTÛ¾ü³H?nI??l)Rl%U*iáUÆ…®[}‰b6EPûõ‘«R7®Û"|?rÊÕò.v6àB)ø§˜r€½iã~zúÝKnÜGm*ˆø¹m>ßs°ÎýõyئïQ1öÿùÇ§÷÷éèþf,T+H™}ÓN•4õì[B $œ†îÚ{ºè ®÷¥èb==˘#¦*¬Ž#;VËÒâ—G0‡¤_³ÏBuçWѸ<ÅC=™WŠRC ^\®œƒö,M§ë°¸ÍCb¦AN_7Ôï’*šWвdþ³Èê%ä,IŠ)×ÒOOLn˜Ã Ôø$­lBû}™øT Ž½D®a¦Á’°Á’§ŽEˆÉàî¬$¡¡S>œÜåHË2h¼¨ÐSÀäºÒZ…1aØ]W•…Js™×'„ìo½UuRC•”º»JÚýï?r‡‡+z‘d£r®xGù:晴rD•˜Üÿè*ôoõ:Ç?rPzÔè-#R.¯9Ê=Ý ÛÍC.šûRÔ­·&®Z´ŒR¹e9+NòéZ{霶Âp–%aF²Ð…c¯Óá…’a u…¶öÄIMo+á(FIÞ5Í+”q¤%J¼‰‚¥ LŒä³u~mHNæs­[98•æ®X ƒ)ÚNw¿ºÚ(UvkEškAe;¾?r_SØ??~ó?rÙP??± ‰$xú­EgèR äAôä±×» ˆ”CÄŸ¹¶r½?n1ýÌèP_RšèrÅAxE'µ^Œºé‚F(È,ke÷žõ:ÜÚjæ“wÖ)¦µà \†Eèòºiqóà ](ø8ˆÄt?nÎ. ûT, ÓNóñˆ¡¢´¬-Fö§-9®u×åi Áh ¶#fw T 7‚zÅHó³j^……¯î??ð,ÞQu៿iÕ‹DY­PNîAKº\ŸG1\ +m¾¿¼Î#Þ?r4­Š´IDxÐ… ¢K¶¿M°ä¿‹@ÜEjiQχ(¿PNò§JÄÄÐço¿þÚB:šª—‹íîD2ÏÂüÒ8ොˈý¦K!ŸÀøÜn#0z–"γ¦Ö¸Ô”8= γÔX´Í¸‰!+Š¢\i±Í*Lœ°ç–oï:ð‘3¶I¨}b­jœ«‡WÕèº}È@h??2@‰¨8²ã©1ø„¢kð‘Ç#¦f¶>`ÄùN±käkxÛêLÁ˜TªÄÐAƒiêèiãlèÕ™» D#Éiã‹»ñ±¢€H×í‘ñÄcÏN+úXHZ$©‚½ÚNYý–ݳ®j“Ÿ1í%11)¶§«á–‰-¦Ÿ¼ŒÏêØt¢•cµêa¸ø›L¼'=1¸ªqªeOŽîxÏÒ­Fèm”:e‡x[ÚM7­º,dý­?r¬Bë^‚hGÿâÓõìZʼ4zðèÖš%‚±Í—þy®ÆÊVKÁÆs?nSN ýÙ‰IdkÚd§­}Ôé„°´;^žd¨¦ƒr$žÎêåu÷g7+þˆÍÂHíÙ¬£o°?nØ® ÏN½UærÙZJ*ë&ÛJ°N»¶ä…Í})zségå(.ÙxÊú þó$œ²­1ºCñ[ú…Içv˜ÿ?0z_©ÂD:gzsÖÈ¥úÁ¬•¿õ´úÙùÔ=8¶fúhZçY²Naf~ G88UÍl““%ºô«p¶.BçknÊ–óR121™™˜ùRþߘyc~|0÷fDû²ï~rú—«,½ßZrÿ:žw­QäUK”»É?n8Jâ(_„¥•¹ålåB׸bY{ÁöbÕ?røÀõËrÖ¾âREåM2’èæ+pˆ~O\ºJK¦Sû›u”GHW—Å•¬ ª¿MÀk¡àÊ]ãj…¤g”žƒpù·ïÂëY|B°´@G}‚E-H« ´ì ’zÇcE»Û6-”?rì=>Ó‚h5°#°Â¥Þ¸V~*-q¤PÛ4Ê)©;q÷~˜.Ö12ó$ˆ,ý|IâãÕÖOrˆ~âö(€`/&õa'?r9ÀÝg?ryÓô‰õäÇoêKmi—Á@J~tâj£4 :@à¨/·q¶Žñ"ïKk¶À³Õb¬þI\ wú™¿ÒB(’µ Šo ­À”‡5ÄŠ*c©-w‚ž*6Æ÷Wû¯ôïÿ?0?0ÿÿìýë~Û8²/ ×Up¸ßì!;´"ÉŽ“ö´²Æ;ÝÞ“ÄÛ™Ãr{ô£%*æX§²cg&ïs>\Á¾Àu%Oý–?0–@‹Îqz­d¦-,?n@¡P(?n–ÿ™ÿ¾ÊýO×7-ßÿôð×îÿõíþgé6Íù)[|ÏÏáõµx»<+<æ!ó§[HÓ$>ƒ­r,£ÞÆ©³Ý¿ìF¸Ú8òèugÿÏG”´ »È‹£ð–/ØŸžΦ9'Ò£pO3w5={öâ`÷§Þ«ÃçÅíL»Øÿq¿÷ü»U „±÷”î\|öª?0æ$Óôãk²çè'>E??ÏnÎÒ‰z„dëŸ_ÏÔðc¾²ÉBBÉ7¯õãÛKì­<ÉtV¡2Lq“ËUšM'Zÿµ÷r¿÷âÕñöO»/a'©ŽfüæÆ—$’IQž4?0®Ie|ìlLó⻹ihè(¤½á0È©z첋n4´)?0ø¼`æëråÝÕŠI1µõÏ“þ1µ}J ìdàëzÆëùÞZQ%??7ß„EŒü3ßóÃ[üµgÍÌ—0cº.ÛEêÞÁ®zO²ŒßÃ&­Æê×Üv$—3ZõF‡ð#»šæ3ùNŒ=ÈšE¬Î?rÑh¨nÑ,¬Æç•¼¢Šx&Üeþp8ž%äâLj÷ܽC-£Rav <ýñßéÊݧGÅñFÝø÷ýGey¯µd<óæ¯coNÏôß·KÌZϵÆd).1lZg3·ùøÌ–æóp·œ7%)ìu í-wOßy'’½q?ràÇô:D‚j sw•¨¾Ùñ‚b[œ»:9pËGýSG²ˆ Y„d¸.¡ï*A°.}QPx§’”ÊÐÈá&þð?nÌeä@øð’’Ê’’k^ë}‚bf¢)¬Ê¨†¶_«0·Ê¡ô‚ø’Öµ³‹@¦8”5Ï{0[Ðc|–c(ßr²ª±ÇY¢oóÅ "RæïèµÝêlxßñnX—’I 4Fmß‹C±ÛäS¢/òf^—©:¡Ü§&“}Rm±æÚ_äx wŸP•m–@Kä¿~'˜tˆ–†ÖØü‰þ<ÃŒVK cÒWº~vfœŒ ²kŒÕ¸Ñ»qÜX.ÉM(3¦„[5nìôAkêòÀ&iþ»¬ˆŸ•Y‡ G€6ÓÉpª¸¾Û„ûZ%À ˆ;Oô)h/O‡Qbu$]™ÖR5äé A¥üP6&h×MIHé nEöÏÞ}Æ6œèF?n–Û„6ÚMp°r3—[f2,ní2zÑ“¢¨Æ¢Ùñ™u¼Óºxüþ”ÔsI@©¤ ¾ÕͯÕÁfžÄiÀ@ºqÌ=ݦw ?ròžÎÓ¹vËž #Ó?nåf¸ÌuÆâ?n ôZ§‹V—¼˜ªÈKQ"B=•à”m6tòî¤.óª;Æsæ^ªômÜ«H0Ì+nCÿä +#b‚C´Ð¿qŸ3%²wûì"?nm™¿+)˜˜u ªlˆPÿ-våãœúÖëÖ:eÐÚ!Ì‹ñ Xï<Ú|\µX‰À}Ë¡ýÄñý\hòDÏ@õöF JϦã3¬:.slÅ8Ê6ßfé<Ñ¥Šå~¶Ê¦ ò?0눂hbYî¹Bν{ÙrkŽ\mdPr-FGažT͇˜ºµƒ‘BÏŠ¡,{ÉrÕ?nÜ…‚P¬eôV°ôR)ôÌå X•ç-V‘•¶vNƒ¡§¼Á³ò‡ü|ú–ÌîÄ?rãÜñE›ÊÖø•M??Ü@¡N×ò·œæSúš§¶ôôߢ2Ýâ7òÈ^tµ øt:îv‘Ú˜Š R•ËŸi9eëq‹åÓBÉésÒJ?ræÇº—uOt2þµy„ØM wáÐEÙÒ™ô3ßša¨ž~™¨x~Õܪ}…YgR±ˆVÅŸä*¦âUémºhn7QÉøäèøpwûÅ©˜‘MMù7?rkÚ¯C'ºgû‡/¶è|Xšú=„ýò·<¿`"Sˆiî×?rÙ”~êÝçîpû6ãqù6Þ4ïñ2òjÃŒ.±§®9 U×ùãÙ:ö«§¯_ã‡Æh¿oã+ÿ´0»™ì<³ÂÝZ3£Ýˆ:«Û™&^tq Á'Àr¤ÄnQquTécñ3/ˆ´Û8Dt#_áóy:®<Ýr³ÝYh"‹ «›NÖ·?n“ Œ_¥±1µƒšRˆ ‚Äçeâ,†ò³Éì^|æ„àOåj÷âj¬GºDlAã–&ª“‚Lfô{ä‚:‡°õFñ?r 9‚¤”Ü?ršÇãÙ(éeñ\alž¿sÃ¥s¿q?r.3µË©€èEB¹[àŠ¦Ñølúfgïè`ÿhïxoÿåV<Ÿ«ÛÁ{³´_°QÛ‡ÞÉ<]/`Jý6¾ªhãYzÝ#þ=âÉ –õ†a2m7œå5Zøª¢…ߦƒù¹‚È’ü­æD/D…!ymzvI|î½soªúÈù§kÀ´6úÕ=G(ugH¿z»¥þ‚aQq`xáo??Z¨pQ^Ád¿ó& X´.€`Š„ýÖ'ƒ4ö*ßòn½Bï¾å˜uk aÙp(šyóï{ùÔ£(Nœg—¸£3Í·p±¯=šÇ£5/ܸVòê‚Ù?rfW˧¤‘(0C´ì ž̨§ù˜þ÷·þ(*Іè°Û¯AeH¨`V( Àxtv9&KÔIñDØçô„ß_.[­øûâeš¥t¸;­°i<’ë2Ÿ::ÍçÀgÒÅ·hùË|–´éLAæÌ[õ”šÜõm–dh7÷Ç8S·&gÕ%¾ÍÜdR‘䯻ÒM›U×|:KDºlÒy:%hÑâ)RIÓCÄKýÐO³¾ÎÅé¢Ë×n펳Ý*;‚\yúóiænƒ×8Ÿ¨VÞ®ïg4®h€æ|¢«Ÿ‘Œ×ñT#dý ý{1Q¿ø¼Ô43Ýzú €D°i=J^Ê4ˆç²íd—f‰’?nk?0uÕkÕ÷ªôù ëW3æ ¾¹•Ñk%ÀMgÎbowú`êNŸfNJªð÷³D±À¶6Võ÷ÙLK$ü¢¿ø—Æ{ñ¾ÔcÉÊÍ4¥zâ Å/~d8d&“¹ÊÆÏ‘N6ÜÑ#‘¿çxF¸¯,U{l6Î÷ö|É?nœXÚ:øÉµÙä°öK8£Xi~¢ÍÕ¾ƒLšeÁ9ŠÐ?0NSAtxÛÇÿÇêÇ_óyÕx%?0 ZÞ2¹ºÓÕ?r"¯:6÷“€ŠTó`Þ¾wGxßy·—ÕÈ,š??sM³WÄd¤üR»' :Ó?n`RðHBÒ.u2¸ÊC›Ók1h À²d­öZÕk°À‡?nès íРš@Ñ!Ÿ ”=Ò[‡Y ö3ì¶z‘ÚkR@!þ,[½æª?nƒ´??ƃð6ËÞÑÌ%=|¶¶°öv[¾hì÷ŠQ…¼ÜG¶uVÒce©KØ,KIÈÎo"Ú0ÆÂÍB"ËÐ zȈ–få„{ŠˆþO…,å̃Õ.ç@wg$³sN?r¼Åg&ÊÒ>?r¹QEÇ–JäÞuICÎöNš$+ª‰œó–‹E<‚«ÐXÓìnoŽÔƒ1ƒ8í~@4Íì@&Sê*ËúÙ¢¼Í€-ÉT5ø¹ái¹aÆñß—’Ò ’Æ(DP¨Ê‘ÖÉE¥.”Yu'¦Z+Ê«#ß”­ 8kç‹ /qi°eqº¸Í^T±Rñ!=뽸@sìÝH\„)\WXú@›³D 3öï¢%ò—&‰kÞ?n‰/…™Üêò¨­Ë¼ûŒ$4µæ`¬ã0°†Åùò­ô)ùSÀ½Q?nQÄœõ*ÝTê×…·ÜÁ¿GŸ?nAǺz„½"œ¹ÐÒ0¢¾ãÙFÜÜhQ¹ûX9f6–Ö*0`BÒ‹Q/¬ô Ëœ`Ä‚[©ÿdÓ]Q4Q-¤P$¥á|Z{Ñ$æ)U›2@Ò矑Œœ.ªUs6ú˜þ¢(·Ý'/›ÎÏ6?0}ÒþL&àNë<9vÇt ¶kL¦bKôX[r…¿RueìÒ{¯‡?nõza(ŽEåŠ §4înÀêÝ>s•ßh|ê¸Ô©›$ÊÃÅæÉLw1¡7rKø=þyÿåÁöñϘ Ž O´K㣔ûjÅþ£µvé1ÄÈNZ°ä2æmt³®µÎ-x™Ç\„5uwϱw½å©Ð]8àyêÀX¸z²ä3&3‘c¡pêš?rˆm»˜ÝHñ,1c{|Dï¦ê⦧81øÄþ¸$rÇé€v_1b'f½Õªø^T!˜¢zEB8ŒX¼U?0ç¾?rl‘ ÁÙÐènlôeÅ?nmËRLؼœaW?rÓ€È> 5.¥Ý ŽY7¸ÏòàÝ@F´" ]]½ÎŠ.×ñ^J2ƒ¾œlÑxTEF¥8fެ¯®ÇH²Ì/îÈûî»åh â8‘DÞÈ3£öa´4W†NBú—ºU¢;^ÜíG¼½ÄcÛ ‘ÚK ¶fîp Ä;nPTOˆ‹kΉ™ë€\PÑtn*Ï?r-ªBÕ PõT­{×^?0JÏ?0 §§ÙW‡€,7ñÈ?0½îqPê)|P_-Y+PU6ÃÆ'3Å8l15BÃ}‹ÿv9š«+åiú¢÷n¶ÚmyÿçFëWÿí[ü·|JŽ—óÒ?r ü’ÎâÁ?0ljU!æµSmïàjc[ˆ /“ùÛiv±HØ”›6¡î¸ïþVŽ{±ýtÿ(Zy1éNœŒÑTà ¹‚Lœ zðùL¯Y¢Eêz…‹Û/*ͯúç—gŒõèªÿóår”.¤Ô­IzÂÞ¹Šï÷öþ´ÉsKžÌo춉mõrJ—\‹Å0°­‹A^<íõ/Œ7^rôkBóÖ¿ðÌÛd`½¼ÎfÖ[j¿PÇxÅk(œ€° ´ådu'áÇCù æý Oåo ¡ BÊÉã,y³£ï‹B?0?n`ÐÚÅViÝél ßÞ—??^mbrÝR^þanèÏèö*2Ó_$«MwÕí%0y£«o­·éÄ~­·q¿Gm×ÛXNÚ´’/›×+"…ßK>(~,šTuA[9äuA¤ë€?0ÀÕ£ø˜#“úˆG·ß‹Àkùût‚•ÀD$RKtÑK…Îã–Yú¸i>n– +#??¢™è#~ÊÐ`ø¢~…'ª¥×·"ìWDË EmG½éEÅ·³N³?r}«­HÝt¥öó>N¸2žòÇìRÝAÅ{Ôå¥ÄʉËUI_ÛÓ7ç?nu ×üáõ%¾ÁEÌÍy†ðèÅKZåÈ?n€YÉLªˆò1T‹‡¬Øe„R`þÒ‹lXש r·EJ%wÁJ—³^![‰ÆÐ?rF_Nh4€íexiQƪO:«!¶x6ÙáKÜG­CêÊ«*«³¸·?r^n³ !Ljáþhš'A•³È»qoœ_ž-+ôy\Çm!£îÔæòÛ›´Ö¢œbæ¯Ó½•™Ñm–WcÓßOTÕµm,˜oi2¹¦aM«­*?n)ôĶ-8keöŽÊÞ1Ù;5³›Å\Ö5­1Á ˜E^Ç??Më Á¢}¢á0øè9Ú½ˆ–s¨Y9á;FcUƒJ—^–AžÊ6äh[x !SÊαô×èÙ—.ÑzÀ£‘"ìn޲Œ™Ð(s žlC’PGllhüIºãèîTuZœq¸E¼^mÿxâŽKüšëD‡¢Ã/~{S*þÕÂü,5Å:v6§Fêoä ),y'„FŠZa¿caK‚Jm·µ®WÛÄ_95 ÚA©é6t+¼g)Ñ?r‘1”F^kÑÌa½i òýjL¥lgThn0­T&E-Ôô¾Opæÿ~Ï7%Iu‘÷T‘÷‘Ó†°ÿ7 ‚ÌÚm«æJÁÞ Š?r^(\Ëóã½§ÛGǽ½gŠwßöÿ©?nùyÿ€¶Õ:†ŸéóýýƒÊÅŽ,G6¦"Õž¡w|üü0ýÎcBëöÆñVÀ.³«ô*ñ”õy6JEáaå©v2(Kß—Ë)å ÒÌGI2 ÚÍŽ¡B O—I¹ŸÝ K°Á—t] ›£m»Q2#¬å†ô?n­Så>ß(x΀ÓIýÝœ;OŠÏwKì§ÃýW¨”Ìì¾U®†8tß2'g´#þìï½”´ôŸµ-å0Þ9ܧ±¼ûâÇÝãŸ÷¾B[~”"Ù»L¼X‚‰mx½bÓÛå[â7Ëo´ß#@¬bK›ú[UC[æt‡˜êDh«¿p•ÅmäÂèë)Ö8žõú£4™ð⸟δȒL/(*ùeœP®ÓúÇbqŸ–kƒŠÐq:=Sºå‹ÐÇÐÞ¥ÁdfãœÂ‹âÌÞÚay¿å¶±³’z½´On³­g ÁÜü€é§z+1tö¦PzÅõ/‚78„k_/î˳ ò”·"{ÇìZ?nÞ€éæ¶"ÜVó-i¶ÏjæµI‘í/ qêœúû•ëF5 _Ô›±Œ;/¯ 0¹ ¿úRšIòÖÓ£¡°öb)´%o°wz ]S{ «§d®’Z,É©F–Yº,ŸK×Q!õ½Fdô%„¿ƒ^v– yÞÆÙ hÿÎx¢¶l¼bÃåßVª¨’ºn¹°‡sè=AçV™ZBºaIY ãÚ|óÿöoÉÿö–¼ŽÏÙùÿËùÿ·éeSÞÿ¾ùèÑ7ÿÿÿ ÷¿#°íæ¿ÃF陸^\ð~N3.HªpÔç²ë]s¾}øÓþËN[Α¹€¶ŒƒTj:épù gÅ%õÅ·äZÞxn?n??ûmüùgj…qÔ9xúVudôFygCŸ¢=ïÅ£ÒEÒ DÉD Æí¦Ó1ÃN'2B¶Îâûb:QÉø(’û??.ª €SÖtÂm³ü?rŒB_¹«ÝŠ·Àåˆ?nŸÇ£¹Õ6xg0…ÁöfÈ´B†7ÞÍç´ø"îqÑ­ž{?nYI?rW“ççqÇ~½„äu2éÒRWlýìf6¯‰EÁºñh.®‡GîRŸ.'ùål¦NZ«EiA5þ©éP,âž ´1Ia”(uÌÚ8!œ<ì4ÝÁSlz[l+£QQ¡µ ²z£a³Qœ?n34l«»Wžø‹.¥f)´§wôÂFá¨,Œ‡¤ÐU&¨ˆdѬqè¢SæÓ©7šN^ÛþßRuªŸ‚S„*”~ÃL¡%Ù~BQ­ ­µÕ}šÎóâ>ŒBNÁ••+^õºs£ƒÿ™#QW#tÊ Sµ3Ä,­n{X©°âo¤¸ÉÄSa“ˆØù,®îsá÷6éë$Ÿ;b¶ù÷}?0« ¶y™òx˜ôÎ67?nb êd«³qJ’õ²õÔðªn¿þ4½ïº,f]4«Õb£ ÀéqÃpwS†5óŽû*vCËmÄ¥É>3pÎ^mC€é(@Öˆ&Z?0Q«šËÄñsK¨o~Dgû[fžŠh¦w4?0=fô¨j@Ï3z¥ô8¸%€êl¼·§Md©T”sâãëq2F(‚¦ÄE+ùû®¼¬Y‰\€ø¦½_yúqó«Ðد¤7J®’ÅéÍã›Y­mßñR/*ØË“~!{Œ ±Wð«ÔÜœØy¸Y £͹ù¸4&°[‘*í¥ý½{TȢܣB^=4$1î"É“c¤&Ãץޙ‰ÛZœÒi].*}Âøéš‘än»îâ)’6Ín47`1¬£ªfëZÏe E>?rÒ®Ìë:» ¾kîíD®–î¿æãÒlC??…†æÿÿt`9dœ£/&êðƒ"ºI¹”5{¼åsRtåyºê+·GèdÄ3RùÔ¥Û£t’3Úee§L[6×å« ;M,Ýƹ%*yèÒjÙQR_C½›µ½.£h¿Fuzâ¡©e–Ž+Küc€Äéߡꞧso÷åñî¡Á ;î??ÛE(Žzûq£?rµŸá¬*T.ÇznÓÉì’¦ÐÁtò[8iÿøwhOÑÉqÅtÄnÑ¥I–U;ßÚ[³v:?0ÂoFÇ!û_Flƒ€ð‹ØÿøŸŒÿñ¨½ùeíßú??§Àg¢ß¿HÿS¯·Iûïúzç[ÿû¯ Ö2ÉgdŸä×…K%¨eÃmF³ÎtÌoÔ–s‡IŠ—6æB ÊAOèw”¼ŒÇ \á’*{/y”ýyïe„¨Žîð,ñåü<Ï®þùî.¹Oõv¶Ÿ??ß>Bh¨£îlº(š«c» ðT–Ä#ÏE…Ý‹=í|:ÅY#8>}H|—ìmܚОÓc’>>ÒÉÙåëÈÓAvi;qBP‘ÊEÙ2íô¤€ðÀpxV &ìËÑ‹WšA%§Z9>*òFUÐ?r(¹ç´ºN¦q†eãUB?r—a"×·~J0†CËäÔÖXÒÜø;»??¾ú ªS)ÓÕÕ•61ØÞËgûÂuïÃ…ÿf¢.„âg`Pa(ĵ  svüu(ÁêRê&´b?rÉlÉ„2_.è­mNæ£1Ãã^²ŒöFc(vÑâ}2Ç» O¾§2 EÖ£Uè…îK"S°³Cá&ACp´j &·‹ ¼8V(¿ôüËd÷ðpÿpKß6ù[®ëo'¯wD?rÇ$ã¶tÞuÊëUÞöÈh~YhЭwÌj>xVˆ¡";k5œ‚Ó…Ü Âå«<¦ù"ˆ~UG’"ô*Ð9ÇûV¯aýÅ6úԜ٠Á[éû(Å21[@ï%˜ñ…?rAoâût–LŸñRúš‘4¿2$èÀb?r„~-A`F[|ÇKé«FyK ˜^¬Ô7ƒ¯‰n¸ÝÓ³Ú= ‰ÖÝ3ÑÝ?nÕ÷ù ›ÏóEug=`Ãp` ¨H)A¥9zÞÀ¨wgË)šUóÍ„•˜z“·qO¬ŒQær«$5µ;JJó½ îÄøq:bp"‹&S?0úï”n²©¯¸o7—NŽÎØÿ)0Îj™©ŒE!ªLä‰*'m¢S<ó¤Ù†£ò B§¼Ðè ð¦âØ?rÒ_[ÓêógS`3A«}ñÁ ÐÊ€i??ÍËTþ‘z4…ƒ`GYtÊaé2ªÊßfêw¯&ß&µð)IgÜ­)íASìõÏnµ^Ìr{Zª%¼ã,·>yP;kÓÑàwA#Ðá,ÉÆiž+×·éh¤úîa¿ñˆN@M/áég‰h~øk'Ï#Ïž²)JŸLý%]ámqç&É}»ƒ)á蜊 üm§Œ‚0ÛíOF7Ýl6D“RïuŽ4??Kã«-ž‘_ß ‹?rÕRÞù +#feº¤€‚EvýàÞNßuøVë"^W('ã¼äd.@)A€ŠÖyš%ƒd‚KDƒKu~5ÓTG ñ‰^5ñƒÒÒLx¬óNÊãðÔʣߊÐý'þ??¶ZksÒôÚdF§®Nyzc4²Ð8® `‘<ŸÎtºzlTJ#-~OªÏ–uƒVTûÞÈÚÑݸ WH7åð*lºâÀ1W`‘,ßÊν ä»ûQ±Å^wèÕ7=¨J›’À°éÆéÇ$z-ØKðu?nJcïþ)†,£Ñcõ;_æ±QŠQ¤<Ô·Hƒ †Âéa*¯”ë×S\ò x™õ^:+Õç©r€Ý;PU1Pç“¢.çÓ|^ÔÅž«4é¿é}yèÔ†ìSª%H¬tiQ @'áv"ÕX£û¬ 8*hâö±FAÍŠ2.’á4Q•WU?0dy¦??·îR™“ïN½ žL'áR1Òcy%Ö,ÁÊ??80™‘¢ʱ{2´f:×9ƒ²™E¨'BÜ»ò£ ÄjUl_·¼.›¡šýË,£ª?r36‡=LêÅÈæu‘W|3“’‘Á뎹¤??RÙ)÷hÚ§¹ßåo$€É£[ûà³-1J¼ÎÙ$í†ôú7•h`ËQ°T‚??¹™ÂE²Ù“üðÃÚšã×qa·nPl16ûS5ÊÝ0Ü/õA~ưší˜û¯:±¼â¦©4óÈȧ˜=¼Bžt}??RJ\Þ…Rî¸íFçéê_³µúË/Z»2wÒÒ³\­è2—Còc5”ùEå|* ê4T}¸è´!&¿ßÞËÝM}¢ÿ/Wv‹??Ö‹ßrcm©°‡qX´Á’½R-ºK3%ª9GcÐY¨M†¥š?0–WÀÞ~…%ùâ6åd€=ux4œ#¸®¦ƒÁ¥yÔÛ;ÚÙ;Ô÷ñ’1¦oÔglQߊ鼜Fy&çhG’ëË!a¼û?n”~€]LŠÂ¢ÐvÎn#qª®:~iívÇûÎ8ÐñMïÃIùª÷Q¾t /ò??éz›N_L”¥tv½¾Ýx÷Þtˆ¿ÚÐÐÓÙÍ< QЃ{9JƈVJý¤ŸüŽ*™I“*¸F\k½PN0ó$ÓÊdFDW1†ÂUÇèðo‚fP™‡e¯ öféjœÐ‘+ð{ß{zxá÷¸†¯¸%oƒ€TtÇ\¥’W\D­¬ŠrŠ`91ůs–ýsbLµÔ™>zô¨ˡχ½lªo£¸;û/ŸÿÕf/½×ÅÐVŽY~¤0 ‡µ¼Y`$St@­¨…޾òU£ÂP.á&Á¢àl„ÓÄÊD«sÖ÷šÞ†[¼‡T¥WÈ51Ä3þ®&Zmé͇VãiUýå™UOYDÅiX®Š¡Yˆ½øšì¬Šj÷hX®ÙšzZ#ÝsßFf??ÁèÜ2ê*›rA\ ] õæMÄ Æ…ë³ËB¬ªnÓ 2J\º8Ü}¼õ¸]¯±èŽä'e8\J5¡Ë/g‹åÏ ,âÐé‹LD]EO”Ôþ‡§µj%ùíæ£æf³#ðoü ÂÕ%?n‘+Çä§êmÚÄRv˜¾ÞâûruõÁÂs¯Ø± –(d‡˜ÐºJ|h©q+ßÓÑSBeÕ¦üuXˆT³±*Ù?n0~üXÇ&9H“â$fTƒqq\øÔáv9WÐÕ}/?0ÅkÅ;îàíÈLjgPOóÙŽëyêžò¶±\#XÉ<£…&Cž#4"½ùô0Œds™n»»Òç®Ô u??,h ž WÌ:j»¬ÐJ‡ƒò Â{kÁpP┡’åsé&’ƧD´iàÙtfã¥ïPIô„eÝy5sªý,[¤•g‰…* QïDTdí?rή¥²;¥—ò.ìð²\,©üK_®ÜÉz@J=·œ0œ˜w!oVÊ)gìeˆÞx\¬BÚþvR¿LVÇì·.dm‡NO–oKPŸ¶Ü˜­K–wÇ.æK%´7€ZŽØ/ÏÃï™»ßö1ïÏrÛ;ï¬h.ëxðIÛ8_ƒqfVN˜c»úª 4W+\†??Ól5În×Ã]î ·/¡á* üÀBĈè?0’V—c¸`AªÓ UDXƒó|¶¨†lv¤óª„JDó ìæ€¢‹ƒßæÒYbeoÿH…¨ðâœR¤—)%5Õ¢þµê¡¹ûãöγŠ»ª»ó©¾šyp™€­Ê'8äíßb˺#£:ÃÕ)èŽsDKò˾ù³éh´:4°¸ˆ|ÐQ7bɲ_€Nó5ö#ð&ò¢›‘Ù"²xàu¥<Ð&SƒBp2œjH´Ðœú¼‘ [Ädì??¾÷Rvº©®èvQ7>ç#ÆCe³J¡øçí×0#xFévŠô p²z„ðÇ'âãr$­Mj£4¹ŽÔP‘q?rå¥EòÒ>7©3Z—óWݶêÒëŸ_@ˆ´·tØ[µ–Lt?rÁ\ôDŠ‘4„¥+éóßËÝrZãN±lpc˜¬h+æ&½ÿœôºéôºÀiîÜ3??yWÊ1pv9TqžË½‚{õ)9ð!…[UœO%Ö¿± %EŒc"KúW°­ï7­"$Ý ’EÞ°þµdu¯»ª<Ç!ød;ýŸƒåüóïÿIï4ëq'óºo/níAm”‡.*BOšeêñ¤³EqOÍííN¬"Õqâ¦%S!i—eªÝ,2 D}ÀÝá?0·/˜¿{Ìx.•P$qXµIØaK1oÁS"²7|ꃸ¢òÊ)^X·>¹=Œ?r2ö1£ëöhNÖ•¬KЈ$éи¸Lª£â‰Ò슞ù/ÖŽv·ŸþLbîçããƒí¦Û‚(õOKé‡ŒŠ³þ9?n\×^!ŠÚðx ‡±âÁÕRck•*?rÎЉ¯ ¦v4ãIè¶q²¨}Ûë´ZÞþO·É"¾ötÿåñáþó-Rï¯×â×I—Dl«±³}¼K— +#qµÞ7vÿr¼Õx¾ÿtûxoÿå–‡¥??VùmZæwÞ[Vc mìoy~YØšX 5Éþó?0ÚZüßydmµ­öÚËçGTÜúûÆÑîáŸv·¼WÛj.±¸j¼:z© ·¶ä—÷÷÷vš¯^4÷iŽn5¨‚Ïö~*%¶?rßüWEÐŒj;‰°pg˜½šý OdTÛÈ–ƒãLáÀ­*–GÛ%ý’Ñox‹ù !åLNöÑù4xw¦Å¬??aC üÒ‚¹=^¥fʬ¹¶ö„ÕÙJž×S3r50ñY|áIøËíÛÿ…üòû¿ø´Ùj/íÿ>Üø¶ÿûkÜÿÍW,XjbZÜñ./m¦]&Œùxi¸#¹nÓëQvu[øÖÜÞ=šgI<ÞÎú*fê Éû·yæ$9Yd“êá­æf§CO|ÕEÕ(ØÜ7T«'§Âù†=S€ò{µ7U†Ô¦rµ,õ¦ãÀqðÎF«¸µÀd0„5g—´@à SAl`d*]”º‰©,]‘kD¡| Ûhi¢ìû]V[Lx[ÝßǤø.zþ¶NǸoMkÞp®Çz’u¿&¥??ž¡'­³\pÕ|«¶°UÊòö;Ó¹d$§¢#Ub¤?n’Sd??E›‹´érÚíV°¯`<?nC6–›jI½äqÙüéå«Þ³ýÃÛÇv÷ú³ø{)¨ñgue??Øþ‹ÌÎ?r¥ àùåYù'‹×ÞµÖ¾??ýŽðš¥4E?0–Ñk®H)Tx²/äŽüÐuåêÚ«âÇ;×$ ¸Ö6Ä™¾¦GUÛR³­M-r{™²\QáÐ_ 2-•§:5'n7DÊx8ÆGVÙ&Ãò8Ûƒ'.hêâOh6Åì¥Ï8b>ã­œÞ|æ;^Ë?0—é?0óW)íu‘&'??p—Y¾£Q…QP\@mbÕ{æÛ«èt?nv…m8Õ#G4Öžh€JˆJÜ3&4<>B‘³vd €Æ¼…)|ÉèîS> ‚« JÖ}‚‚="ô=r†Õ·Ø-ªºR©†IRaÁJ=s$îDe#ìÃâ¼ó’¡bBqåü°ní gX6Kh@.\¨6yÜ‘2ÂP:œyC·?rj88Q¦åÖðÔ»Ä0¼„Ü?r² ôò„!º¶ÀÒJ¾ÖŒÿ…ÖÿS’òò?n ¯sÿÏúÆú7ÿï¯ÓÿƒI>JÏ|Þþ_öÿßxÔi}ëÿ¯aÿQ¶n2ð¿Ø;n<%ÞÈÒ×çs/致ÐnykøyäÄ—#‚N_dñ1OÚp¸¥Î¦Ïì4Ï’äA«IÛHë(í=ô•Í2ß5æ²I_ƒú_K:~ÿgé8Æq / ÿ;õM9þ;ôóÇÿ·û¿ÔE] CÛàå$,üf°æÐ )·òÆ)µ;Õ™lfƒBCënÏçYzv9OT1¡0¬ë'ü”??ðÝXê·ü©(€>OM0»ÔȲbÈqJp\áþdñ ƒWŽZ`ádáiiϻ픇HöÍ»JÁ“ÞYîÃ(LìÉÅ©tTúCr£šÄ•—‹R¦ày\Ø‹?n?0­‹v¿ø\Ĥ0Ãy áCÓ/‘f£æ³bß?0g?0 ?r²h“/Xô9-ƒ(Iôc3hÝxËÛ›G-0™Ft¾õr¶¥B¤#\_©°P6 .ƩՀh„ eæêõÞf*¾n¯çWYæö tw…aÏ•®Pî/ßE­VöÅד‹ßæÿ9fÿ/²þ{¸¾!×Ö}•ùÿÛü_k¸8âÄ(y€{<"Nqä8d‹¤n».ã8¿P›Ût¿€úÒ*¢âMghX‚Ga"'Qä?n½'OD———ËSžvW¬r›ƒíñúýóþ¦ò©/Š.¤CQkð„L®]äjüªÇÿåp˜Àä èÿ-­ÿ76Ö¿æøÿ¦ÿŸ¥“8ï§©ñæÈ.ûs£è+ö¨^À•KŒ—.m„I½¿…WgY|£†£Ðàõ0,»QdÉ8NᲫðXÍø41>oÍÆäÐêÿz~Ú®Eš÷Dç5%†Žëìs\Ëòr:÷’Éôòõ¹ªjîhRº÷Ñ1^Š“èÉ€^N}‰h¡A±H””-*RqO?nÆZÒBbÔÎÉ üÅ»_`còÇ®øÊM¯*hÿD•¹EØíõÔyr]Ñs̘M¡ÍåƒÉÊŽÀS¼7¦vÀ®mnpIB5s7ûñ¨¯"«QžÐÁ‡Rá7˜­’õfFQvu‘š«?n1è.g”˜èfó¬ª>^iŽ"N·lªAÇ©W.uRnÀÛ–=ÀÈ|ñâjK÷òF¢,¦¸¤›éUÄÝG–J/*DC ÀæÑÝ7¿õ~¸—??qŽ!³ÌhŠºæ Qî¶¢N¥e#µi¯WKð4þUçü|!ýÿa»åÐÿ76¿ÍÿÿÊú¿TT68ƒÎ§ÓQιúç4ó°±_™”ù‹²F–aq4g^]D ½ŸaÔˆg03þÇ(>KŒ»çË#?ráñ9>i„´sqžöÖ}_äR—Â-(ß;؈èÏfäýL¿¸Ý%é_ô0wA·YHA•‰Äßµzhð.¹TƒPô­ÊGm¦÷–š 0‚$7 Ki‘#rÂjkžXF÷Ê´TR¸ºä)H­o=3tB"?nsˆÅ§Òa"ä×8Åu ¾í'[§wº•ZÖ©¦JÖ©Ñø#(öº…q[aöUZá?0úö–§®Ïmwèáàø›ôxü—czì<ÆgúGÏëëô|tø'zÜxDDÖS_Ý ˜—õß+”eVˆDÅSºdñhA„¯^)§*{ï¥Â±A09/µ²uM't?0ô¬G`ï£ES„T±Cƒð‡ÀÖÚ¢ª½Ú=ü+½ ÓáîÑÁ>eŸîïÈQiÜ"‹æ—ûê®}ã.«PÃ_”Ò­T´Nô<ÛÞ{n%¯Å_vö_lSL²ª-ßyq`%>T4??{u´»c¥¢/þºŒá‘J><<Ú=¶R«âdê÷º´íWÇ??ÛõhéäßÉU/:KõÝþ£½t¢«Ñ÷øE5Èñöñ+ô4×÷¾ ž¯pîÊ$‰€ÁH˺)má¦{3"- ¬ˆYMYU¸4ˆ¬Ñ-]„°=ëD#uC•Ù`ÕßÃ~X\„%.IÏÊå…ãïÕ×q2??Ÿ,]ŸÆpÐǵu:ÞlÉi«˜+ºf?n¨jáyžÄÎó³znêB4²²Rë1Üè½ì6Ÿe2M_ìHìZ–šÛJ5QÍ7ÜO.Ø•Šèþc‘V¦<¬QBì(!ËõááÝ1¢Î¤Hþ(´™ i=JÍõ¬y.2ÍQ}#E^è1ëRaraXš‡­É(¯Ã¤zj­³N1#À2œâŠjLPE•ËüD.FªG WR˜7z¦SÓLWÍaoT¦.æ—DàaY]‹á‚g±w¨™²("4Éjã⬾ WÛ(†§hV§ïÙâCñó/õ$??j uÄ)\Œþâaš9 7Íà'xóBYä9Ÿ,H–¹¾4|ůó{¦¾r^[¹zã:w#ÅÁ›ÐÎ;²˜ñ-kДuCôÞÄAèŽÊ˜ÅÝ65+ý­Í3¦IÓA×j÷f:P–|L¥dDÝšQQ(0‹U‰qF‚u#8/Z¢©¦ÇÍÃÑ2ßH°[0¹žs ÖjOò·IÆè³l–1â,» f0ãÅó2f¤2n<ß {¶À90g ¼µi.p™ãyÍÜ[gµè„mØ¥´&ïY .+»uôÞ¸-9‚ST\¦¡HdÇÇ7j;˦³„ /*DßVh–™Òð¦‹9‰^ÓÉE˜c6gÉ0½Vw—çÓlÞU>ÂxŒÉ?0<…'ÍP£x|6ˆ½ë-ïÚêí>+»Æa¾ &<`F~æ†~V±Ãi®Ê?ra¹Ì˜ýÿ¡YQ¦ìCÝÈdPWg?nèW'„|ÞVc/º(??×áÌt7ÙÊìõnüE×jL”#ŸSÂÅyí5xIn¾©£6??~:ðu%4éDý\|x£ÓÞ¯±~ù•P§à‰³")£„ZK¶bÝbÊ?n ïÓôO“~ä±VRio#ø¿ùYýó+µýjl_XÁçîùpß±š”jËU\èÂ-(Â-]Wõ›á¯:͘/OŠGRˆüU e«ù-G¿™§L6`¹R??= Zu9 \;Ôg­=‹’3N,Ý)‹Š³'ç²_s4…?n*¾7™o Êô¡€+á?r·”:ÃŽQýµž8®ÂÇuòÏûUùçý:ù³AUþlP+\™¿ý者¿«Õz•Ôǵ¨ïWæï׫ýmL1Õ3sÏÆñJåfqÖ~(tï"¿ò”2HÄÀË]¸ÃBF?nké"¤º¬ùùnt·É¼)Hg4C¾W㓵Щ‘U¢ÔÃã»Õ¤%ªüUF/+õqT!hž÷ïDó÷‚ddÿ’ß¼/(&ÌQQ‚ 8܉âÇ‚bdÿŠ_6gƒ¨(AR|7¾x$)þH¶`|™à?n`.(–\ñîNo?n‚ß}½Œî$÷]¤ÑËaw7†r/þH†`|±``.dˆþÝ(–â®ÿ‘3¾¾ ˜£¢ÉÂw–Ó­eº³–Ò6ÖÌ%£UbdJs»yÉu¯ Ê«‚ˆõÛȦ¯xySüÆ‘Ñ+ù1 W¯Ò†X7\Ê¢fúØ?0n¸T/õõø©ëk6Ð_wœ_ãâ«ó;ýñß]ßâAA“o¿øúÔþZ?nª7/µ¿—Qb¡è𾤌ôÒö ÁMýrj©³L¤®#õr&R7<­ÅU®(¸”7®BbW“ÜYFæËñ .Dþož^½maÉÕº¾wí©=”{y¡iàI]¨ +#¿œ=S?0Êí},ÿni÷ÖB-ZúðÇCÓ/¤-¸;ÏèGEp„a:BPm½˜†ÐÃ2ÊL`4U¬¤°?nØØ»î€å1ì?0ߨÏ¢?nÏØZö6óÊ]ë5¨-5cëÐ.ˆ=s·³ò®šÓ8Rm1{Å7‰p ~R£ˆª›BÔ0ms»íeë·¶úŸŸð+>q›ë±*›ÓÛÂkB}µœ&&“ž$PŠC74£ÔEe?0iÊ€FgRršÖ„›ÚÞmšN&†¥M'„•³9Ïäš[ík›SµgŽÛÒ>Áo?n1Îüá%ùk(,‹ µ×m¤ pI)¨ö½?rHAst9sxX/JÑfWõXŒðLC«tý\|˜ÏG”ºøôÀ°íª¯¡ùíÓ™p³Zò‹ë´G 52ĹÄÙžhMœÁáÆR8«î¼Ø>P`z,ÓEß’߈! Î±WSBpæÇÕÂVµHä®/P|JÁ+å,qP=ñZ-Q3K¢f,Q³B¢ªzt[EMÔ²e–È–…,34~å'fjýPþ¨e‹{ø€ðåQ)yV’ëz—±Î'×U?ržN‡ò å¯å>ˆðË/ÿÍS«[ßµšh"ù½úÔŸu"‹Æ*‚-‹ÜU±§%q-_×Þ‰˜a¸¥Ù§º7œfý¤è…kÕr’¸Žtã/úëjçék¾ÀÁx??Ó¹àpçsˆ[Å’¢½³©Sò??:Ð[hb-wW“ÕûÁ`½ïU)dÁüz®¿E®½üYž!…p/PP‘åHúïwM.+šŠ„‘/†ZáLìæ®ºë`þçó¹ê/SA[j`Ü``îð‹B¦å¸ëˆ¿ž0D->4”ð1¥iâÕ=ˆóËÙ(‰S[¼/e≽æj2?0œ?nQ‹¢¡d'7Á u?nx‚38¦˜šÓðöV4}ïͧSo4¼æ–cuå:?nÀÅ¡D€þ.$¸+ˆf€¢Q#2„þ†!Y%º yÜiˆúŠ+O?0Æ},ßÈØm-a­P8òW}ûœªŠ‘oôÏÿz#tû_f|ªÈUk,r§J'sMI3+îunšû?0è1¼û†w?ryH™AWŸï?ršüÿÒ¸eMB±P/]mq‘Öý½ÈSþJ°>ÌSeFV7²êï½×Ùôr–óü­Öß Ò«ñt€6€öÑÞ qöoÓâý·aR—0²ïbö¡³*Œï’l?ntpNþÎ Ú›ÞšêL{X¼sÉÈfZMw袞÷:ü.À¹ù´¤h¿d87G¨TÐRóڿ̲d‚p~Ãq¦¨…ºRU­îTÅBŒOÒSÄûy9ôBÛ¶à7p RÈ2.RœÔÖZJºJ{CC È@Б%®ŸªìËyD?rUÝý;¨' ˜e®ßòW[¹D–{×`rUA1xW4xAQª4÷AÞÔ?0G59<ÅæÐ£&]¯[ÅÌ48†~8]Y©Â fŠ¡¬Ç[“ÌæždÚ›_uŽ¡¿âiÆ–Õw_R›ø°©¤,ë¬üÌbO_n¿ØýkEu·–ÅÞð’ÊóY™IU÷c¹I‘Ya•æj«ß²­X%Ù(*mÅêë?n[q© Á‹•ÐâD¶°«´[mÅV­Ô£´kÂMm??ÒVÌ8j«Mª+í¼Ò"£RýÈð??[S¸BQœ34fY:ÍÒùÞââ·B0¾M áõ'ý\|˜M³"OŸ^d3QABÄ%:N5!>3ê³;QwyQìsŽ]ôÆÇŽ\¦¾Ûbúñ„àWסb`[ÝÏe?0f±È­;…~Äî„¢ð²ÄÐiíú¥RfèÏ+„†,MˆjxÆZ#‡N¼UrØÕÓÏRvpLÅ??hª´œô ¯Z™ W?n CGí…›§þϼY‹<ÊÂŒÈâÜT+-ô„|øÕ™|^ÚcºMO»&Ö3ï÷g¤å˜Õ?rj_:šiçh”¬³î…oÓÉärá—–( ÁS²Oh¤¥pŒ€óèÌaùe0›æE”V¥¥%TN’a;ÍT¨:ÊÃãй>2áWSD^E|g¨¥òÑ ´~õ¾+ê ý>xL¯ô?rÏ©#/·1¯)Ô®Ú‰Æwj/Z ¸¢÷’ÉRïéï²ãôaËàD„H°2jçèã×µN'ƒé[ú¦Á±ô~ð?0 öFE‡­wtÞ‰>óÌ"6®Yƒ–R‡1˜ié+U&e3D|‡r³œ\žÇ§Þ??»žŠÄ<¢Ç?nAî‹€ æ_mß)híÁ¨]÷r¹Õ¡]PL¤1^í­üI&LíªµIé7]‘÷ÛÂå“,\ÜÛ)oõAx§õNIÏ(*á{Ú˜äÂ*WB­”¡¼ÚÔµÛþ–·©¨sxÄO¤ÂÑÑ ýT”:z¦¿‘ŠGGÏô—žÁÇô‚Ÿ÷ÿ…o_øvÿƒb°/rÿS»³¹ÑZºÿiã×pÿß·ø¯YÒøÈÀ­Q)€kãùÎÏú¸X Wë눫ùˆ$àîÑÓíƒ]}¡8n¡ÆÕ­¹î'¸FÜü •0\DwUÎ*æúˆE:/^¦gñÈ[¤«D;w[çÆ-ñVn€Û!ÚHº^GÊÿ¥r:*…fqã2›R££ÁL0F;2|ñZï‚æñ»_&¿´Ö×OÖŽ=Fînyóó4÷èÿ³Œ=Ýèb×Î._{ÿÊæ¸‰Ä Yv9Cÿ¢Äm?0ý6÷ÆTôZ– GI_}&1X£é=eÑá“Â{o§ÙEœM/'êø?nÜÿfݹ7ŸS-ÉUÚOro’$/IUÚÚÚ»ñ†7Õ›~ÃÖ{N“$Üþ¸9jÜ@À ÕwåHî2ètt"£20ní¬)µ#?n;˜¶#½#VãS?0œÏ¡n‰¾u®iV< œ3¿ =Áø¬*‰ÕñJ¢s¡=Ü–Q1°ï‡Xí¨g}ŸƒsÍ…êAÝÌ/Ï‚"ÜÐx‹ü3µÿ<>iÓÞrä‰Ú¯j'v¨óÓÁ$öCv 83~úyåN§¨V÷SU«©7´ƒð¤õAõ«®Ï­àjáÖ>IÞÚú¸újs¼)´Ècòéd8Ï !Ô?0fˆ@ûW)¿MöÖɱ$4Ý¥M 9X×”r?nK?re±tkIܳÀu€53l¦9µ??žiNád,?ncË ì3Éz²¢¨ JQ€ÊWÒ{­uåöÑ×UQ?0¹u·;K¯nÔŸ+jVüyX£êy¢³8ónæD™·Ñx??¯DƒñƱ‚†qø“È¢2,/& K_=G,£‡È?0FŸ±³'o*³óÀUéINá-Ö™!yk8ŸŒ¸®ªgF¢g@» Dá/`îrÄÙ‚G¬†5s£1ꟻݺs9£ŒVÐ|EF“3ºîß É±î|o*a›yxvÍçݵ6ŠÂÒ?nw:˜NJQêeãŸEp0Ë{å”}‡…$g½”3yÊá½³.F¼¼°e?rj `í¬k—IøÙ‡„WÚù_%Ž»vÑh7hS÷º× ˆþaBFÇ<¥‚??D·ô+‹‹‡{$ûkø(s½™¾ì?rຊ3:< l܉IèøõªVkLДÿβ$¾p:k}|_ó*â_·§e;©ì¶è?0Ó‡Òçîv€J–'¸@¶–²gîôjU0تãFn{¹Ä"ÕýOا_MR?0í(PqÕëj¾àî/ÄìÅAæÑ-ìÊ&N #¬ª‰Å\Umõ•W¡¥òrƶMßzþÝZ­éV•¨]Ü×¶}±¢ôw¦Ø*Õíb{¹ghÄ2~¼±-ž™_ÅüE_äš­Ðê5eTš™N·ÕÓvb!8½-OP‹ÑºŽ9 ÓŸ{X™Ó–ŒEr/×WôVÎ}Œ+kŸVÕ¨³é,h "LçÀèßMѽ·v³–†0'M'(Lö4ãu×Iœi``74.ƒÒññÌÿåºÕòc…þLAi–äùÝaSƒ/Ü<ñùø¡>/ܾšý??¹þR÷????lm>Zºÿíá¯àþçoöÊ<*ì¹u+šÂq4ÏïÞ>cà÷F¡Þì©dÖk8W:•ð‹uíóäÚf­ò$y??ž%=:¼‚Q˜_|+]m†Nü-JŸ(w+õ8Çc¦3ÿ}åv<ÖÇ ÙÈE¼tˆ=”çΘ’xà»L¸pÙ:ŒÀC,9ai©ÈÃ'ÀÊÜÒ?nëÎÏj¦Ë†!äàŸàU%4Çt2»œûB@©6¡RÔïr¸f‹!”»"???r·ùbÙKRæhK¯JÝyaàÓ!(uݤ¶¼'÷ò0ñëË–ãþçV×%ÉE¶I±%)ZÊ\}u%­Ñdt&­Ü‰øÔ¾­ù2ï¶ÖS¥©r³;´#÷_gÞ¡¨½<ÙÂ8ðçhe$[-}™ŸŸ‘¾å0ÿÏœ2(?r@A+7÷²›•Ù½FrLö¹5U[¶Kï/söüdòJIç2QL¿b«õ¥;i¦ù }Îy#[d6ĉÌ+zGöÒ®"ÑtÓ¤j!¸Øßš ¢²c3qBÆP4Œí/—xýù(¾^IñõI{ë@ëì Êžÿ”¤ö+IµØEÙžú BWM¤² ³ód\1‰á??ñ7üæöŸÝÿK߆Œ%àg_ÿ­ot:ÒÿkcýÛú﫬ÿxa—ßäô<Äo·$ÓdJË£á”öc‚õ¨XbQ¢|RUJ‘ñ‹2žàmÿX¸˜<ù<‹Œ3{Ø`9&P£+;Þr Ÿ£Ñ³„LWYA½ÓZ&@Üë# z›Ö +#)99%: Û˜øÉ;K<Æé‘Êt/—WK«2ô?nµ¦rT?rý ÊäêZšVbtj8÷NÓñ?r??N'ô'¾Zã=׋lgPxäÃÇÈó{Úw)¥˜ðœß&X·¶liÓÇ!ôNãýÐE6ü]bF«*^•t§.]Ù‹gÉüm’LèHØÚ½éÄró‰ò‹^4?rWòU‡ñ½h%Rߺû«¥.öæl??ß!ÛæÃ‡ë‹Œ{wȸÑù~ãûÍGï¹'Ê„ßS?n¸‰d%„Â7×ljiî^¿.ú¼ñeXë-$Š“9Ç-ªe<\¨M æâ\²ÍÄÉ/mñh¤\i†0üzÀ«pÛxH>ùø@\&Ç:øpƒ‡ˆÉ¹Y/g{Ódý/®ÿ¥Ãx0Ȱ1àgÖÿÚõ%ýDÎ7ýï+èõÌÿ‘w©}z#Ø]ãéŠ>Y¸žoy??í¬uÖžŽâKZoÂC\Çb‚»d‡ú•ŒÓÉ0žxO›Þ‹Ë„Ì4Yã|>Ÿå[¼&¹yy×þ³›A6]°â´ØÇýót’(+YNŸ¯ýbKÃÎÁKn(ÑÈœL®Rª“Z»û‡Çí½Üïí=ÛÞÙ9ôCošélzWC_¤k?0=ä“j¬Uô„ÃL@ç›×;Ü6†å¬³iž^—s«$gnžHx9¼œÌÓ1:ä—3d¢öߟᰰ?nöp“Ï“1‚,Š&©ÜëÑä¦âŸøEa3fï?0íRýÓ¯+‡¿ÉÅ_dý¿±ÙÚ”òŸþýªäÿ7ùïØæ$¥í7/çéÈÈn~ʧ8z´ÈžÎÀ}I¾íL?0Ëø8÷ô“[ö¿Zìk>΃¢tÚ½ì£Nå#BT³7Ä^W®$“úøô©‡-3??âìï½<Þ= ŠLaY p´¯Lþ½Y DÝeÃ\R£– €w¹<]Å&š?n?0¢Üd>Žó‹Z¹hf#ÛÙ¯öéÎóçÕ??Ía:ô&‹³›À×ãS[Zó¬ø˜DŽ.'ûG~qì¡ï‡ÄyÒK²l2íb7ƒ•p[¬;'a'Kuæ]U?r·(¿ôSÑþAÈó6-0 uÃ+ºþì&K†n6üy~S?n‘L¦¦0£+„´_ ðí’“‚ |¼!åb#j??$TÙ´??Êuzé,(Hq‘QvO¼³!´Ýj‚R„Kád`'FúÆ’;ªIຟ›éPaŸOUz‰&Ý2€BÑ‘·X7ª÷pu(G~b?0oÀ‚cµbàXžÚð‘x;9tuºêoõê:Ù»ç[·Õ¤vÉJÙÃìØ‚Ÿ¶¤3i|4Y °F2ÑR¶ë¿:~¶öØ·(šõ?n&ãµ7Ÿ‚1lŒxuQiÒ­Ôaä» ¸¾á5'ñÓQêp[^™EÔœŸ?r@ %ST§Ê,×ÚeJ,@–ÏRŠòŽæhåW©A(A‹KûI?r2Ä!ôbÓáï,æ¨æÞÁÕæ¶~L)u9Ä5êâŸ:,èïÃb±’ TVÿ‡×ÅÙØ`ÞÔÑ$?r6Vµ›ßjªÿ=@lTÎvk –jãe2Ç a»¡š h1 ÍO9ƒ‚Ë#ƒ;ò–d R-?rÒU¡h]ó9qòƧ=&ÛCYP­¨'¢a–$<©ù¬lô‚dQ¡¬ršþõÿ¢ýg7Ÿ]ÿôpã¡Ôÿ>|øëÓÿ¿éÿ¬ìóÛlÏ!t„¶oö]ŠÿÁ_;^·jã±QìÚ¯¯è??åM© œÖe?nñ®w7(íZ=ðDåÀ?0~ì;\BK^B‘W¨iTDbT¶•Fek`¬ÔŠ£…ûºøbv?rr£TK'ÒY™S¿U§€AŒ<ÌÍ€²ºÿÀß÷¦jHáÇ÷ª–ÝÐ<¢zÁÏûÐçÉeé„Aå\qy•B.À8M¢pEoò;H±!ðnÁ˜#²{wïtLKès=*A‰ ªú›2¢ÃDª@Ô˜ësÆïyÿļÏ{˜qe—«zŠî[àÜ´¬MÝ^Âf­Va¯Ç¥J?0ï½—}@Zz¯âÞ?n%¬6S žEÈò‡äêPÛ‡YL6se7?rôEÚ;qF6]ÆaŸÌê>ñ÷2$ñšÂ¦ùšÅ*«þ S‡ùGëýgÏ¥…âq 8£Ø×‚ÅP ˆœí?r úÔU‰ÿJUI'=LV²½éE\ÕÞpB㺠ôãm´ù¯ÕH›µ[ °CŠ‹€ ^¯wÜðUÍZ…??ï“ìê¥7~Ó²J}ø˜Á$ÛIV~å`øÔC T“ó_€¹)kúÿêùVY……?0ßMnW!%±ã[é›Nt|Fž…¡pûYoïåî±4ž]mói??ÎçlÙ>mº8 &lÀ—YZÁoPÈb\Ê©LÆð?0Û~ k9‹YŽ[ƒ;¶ÃæRCl~`CHtæ1B¬ ¤Ä{õ›ÍdþDíÆì)IãtW#®hQ§h¢R §‚©—MoMÆ8®¿Ê{)E$q®ù"tÂ5q2g0:€ƒ8ÿÝkGQâÂ6Ûµ(}lü÷•;ÁÙÿ}ÔêlüçŠÿùÍþ4Å3£ †Â{ݹÆìžz??¾zöl÷°·ÿ§ÝÃgÏ÷ÿL_ÚívãÅö_zÛ;ÛÇô ×Àôžï¾üéD>ÝþºC!ù÷Ž÷ö_ vçq Þ8»GGàqƒDü«—G»OA‹¹Zfÿév¾JAq*'??šAâ×ßêåŒ)ç{®Ä?nåæ6mRîôÔ2rpŨ?n{O·$ÁU›âÕ]ûäËļz¾ÿò§Å&õ3ìzÛŸwþ¼¸³øüÒ¹Å^M‚ÙýÖ;/„ZtÇà@ áýŒNÉM*·Ý.‡«vâÁÞVü ¦Ÿ§Ã®Qî¶`Š’,Kj€>OâZ—ñß6µ¾Ù¾Êùÿÿæ)Vw¼óëÙ¾ÞËøæòW‚,xÄ ^ì¯H¾LòdþŽr;ŽsMT¾<\{JÙÖ(Äø4{p6šž=Äñz»½A‡¾æçÓ2ÿºùyâMÕ✄¿A?r¦™0KÆÉèÆ{›ŒT¬FœQ%ró©§–×Þ|JÙâ9æE‚žâI¬$€<²eç[ÞáËnÖ¦“Ñ?rM—óéZÒïG”Ÿ¤"JæD™Y‡C-¦)P¯mûÓËIÓ•–Š…AÜHòö&fYõé¾÷(Ä…‚íGÚWÿñ°Ûï7˜Ê8x©G6û3S‚ ½œãhí´üA¯(@›×¥¬?rúGE5µÙ…" z¨Ò^ìîì½zQ$·#¯¥“ÿøjûðxïùnñ¡yëúÃÏ{??ý\$®«èµ\—??fGÉk0BÙ±ç÷ˆ—–öÅ…‡ãø‚Tñ䵊fï•«vvFø{??¦óÒU›¬fA·B.ÎÀ™Š X÷È" IC!‰YUrQư²åÑÙýsj} py$ øþƒ×ZqÎ[ĸ¢l/Q=–¿…±~S¼†?rwsšK„§óxÄAð_îĖ%µµUjRG£"W¹ý¾b!ÊPÁõÒ+8Líps(®-ðÇp±ãäP©Omû_d×w[¢nÜB²ƒ‹lGBÚÕ`Gñ«e’äIS@±Y€çš›òàdÁYÍc‡§·öšD³(M÷ŸÑCû#ôA1‚Mú8 ‹ak·ãë"q£e§æH[k#MT<@yÌvF1.ÚY}‡ÊõãYÜOç7‹pÛôÀ‹`ÀôPÁ·$¦·D¨I§‰ g•ŒÈ4¦ƒ­#ÏÅvÃ>]¢"\ˆÏ??t%ý’5ÍÊp‘LüD3F‰Ö†Ý€¸w‡j‡6,/ƒŒŒDº²ò‹–²&5Ü’–ªÑø Û¹º ½[€4ªEq=¹!„ñFäm„·ØR#ª”5… #h$‚ì/uY˼ùd×Upò‡2²(ÏÁcŠÊ·Tœ4Ò-kŒ/ oɽƅª+%Aæ¡UfÚY<€‹õ¢Ñˆšý›þ( ‚ÖõîSR®Ûíp™ÿí“åzV?r¥[ú¿ s¶ènõÒËd ¶˜\o-ï=OÕœ-ïž³Ò«yñFˆJB¿??º™ÑÖ‡q~¡ß"pv¹œ\Xfñõ;5Ŷ•ŠÒ»ÂšÖ'—#½?rp¢,Àèƒ=Ð3³ °.Hà›‚ÐDñÙ(¹;&AQoÅokÞ,FøšInbúX­×-²á|U±G³„¶i’øÊ¾ë«Ôêaè*Õˆ»Œòåêè£.¦ßbàÓ(ˆGó¤t¥õίäçšÌf£›4H?r™rõÚ@?r<$ÓÃ-)X¤Á³(Kg¸MÎöƒU'IªaW{ÁRÑ]FY«¾RìµH£$üÖFO}lTbcä®h;À˜Á­×­Å²áÝt:î¶#ˆ×îFiPÎc̨ö +# 2żã/àp‹yðåž×&£0ƒ‘ 1Z¶??ZÃè]ØŸØ_é/(ôî«÷ÝùMÎkä+¥a¯‰a‘Ã¥Ùômî±ÁœkVUà¸1LßÒ:Â+N Ö 7ž%¢kƒ¨ÀrrsJ,,ƒ¡Lƒ2?rvr}ª—ì­?nhD|^€CŽ:²HR‘É÷þãþŸÿñ??ÿ×ÿøŸÿ¯ruêªQÎ=JÏò,¥¬ã<+ZüŽmóîLå˶VÐ?n*á ÷î,D??wVÓ+'KÄÿ×7æÔ8’ÛCiHQ('ç-ùy27HõP 6#/¥ÿ«Á](¬™2m–ó9eC?n À€qd=¬g&»5oyêá.e³åß(}=!¸Ù4÷J2\}ÀªŠ±¨ø2¨¦5KB÷V€È‹Ñb¡³»Àö ˆ¥ªÜÅD#%RÛò³ý­œ;¬êxÎ.ƒ¿¯‚bÁƒ>ý{ÈQT…$œì„å´[‡‚ÁGžýþw¬ýWL!-gg“VˆRÎX­^гnÚpѵU8ÛíÂû§Êh?rÏ1r '§I­«Ýâ64yÐ÷ø!”í¬ÂÞ ??eQëúáú#S7^Ø+eJei”9 v£Ý‘ó7²?0]û¡cÕ’–¦†ÍÚãÿ±ÿ½â®È?0eP?n”º:Y-eݬ›õñRÖGµ²>re}VŽŒï#j°ÚÍÑÞ A—r¢Mnkñǵ‹°¥Tû¶ÒªÔÝ»TÐ¥z÷??¸Ã.z…'‚–j¬N†Íâëï‘ëÂÚ†€œ¡rvî0Ûm=?nÛÏ:å04E©|=+ÇßãÛÇ_[ð2¢Œ¥6/!Å]Õ¦®Â»^F¸ üõš,Óº_/ýkf èXe]ëš§#Ço–­§K[]£Y\ò¬‚¹vÀˆ×ׄkèÕ¼à÷Æ9ƒa‰CˆÃ¸%ÙÚ‰/@c±f‰z?r#À%Ê8Ägy0¸&­\=Ü„‹5蛟ù'ZΫwjkìKU·µ„©YõRãWÕ¾Rm«.NLvEhäƒ8Ós¶ÓžR,8º`•±„Á”D2¸mLÃòö$€<ÂpˆL°Ö2‰ZJ‡5{ôÙˆÜK[/_½ÐNf½§û‡‡»OÕ™€Ÿ“ùщ…†÷7OO¨?0³FRø?0‘Ll¤»OŸº]84õv5Ɖ K¦!Y[ú®k\ôê«ÍŽv5óói67u5_+pÏ|Zd7y‹JÙ9P’ÉRn¹¦ÎòAze×!K’A/ŸŽ¦câJl¨_ΓÁ¤ù4 ¬–4cõÙ[•Ê7²lJe=òÄúÀ(·è¿û¢šk¥ž¼¹+ïÙšj9-ny)-™¼Ë[†#ì+›ƒæ¿X {dt–±8éÒ?r Ábq©sðG4Ö}¶d®‡•Ð ½†Ü¹µ­à•J²ëòaË!ƒ¿GDÐ…´Žs·º×Kw·ô_è)Ä´! ZI”ïvš#jhåܼЀB¥AáÞú•SË\JÁúÒÎ5þ•H1BÀ(~ËCG_v-ôz»7Pì$³©\˜Å7—[ßä}øfw¾Â@â^,w*z t¸@ïïNËÙÛ8?0.ЀØ-¦m*qf¹QÐrm?0âCªŸf7λòôþæ#X Ü}$‰‘#CðqwA¤–D*n­i?0?0š‡"¿Óc‡™ò`÷åösDYw£MFŒøI=¼íúWâëÎäs ôã¢ÙYýPnhq†€[¯•†Ýkӥ܌á[Uùï»å–^¯Ì PIÞí–Y¦šKD7ˆŠ×¨,ÕqL*ø<Á2EW=`2"¯º[j×ZX®®ë?rÞŠ±{ó)‡îÍçº7«†îÍçº7_kèÞügºÿŸ?0ìýíbÛ8’/Œïg_—{r–LKŠ$;NâŒ{68ÝùOÞ&IÏlÇ«CI”ͶDªIJ¶“Î÷ÿ<ø\ÉS??!,RT^Îì™ÝI·m’(?n P(?n›¡{ó7º7ßpèÊñ5±Ei‘P¦;44o;óŽmC¿m4[Á»Öci°CÃB˜”b³ZxžÚ¤žÀ[Ÿ÷¦}ðNmðF±&Ÿ†¾Y›Îʸ‡ocØ6°y RZßérzPIßñZ &óVë´q‰_öd][Ô÷ g¢'¡¸OÿööÚÖ‹­{¤íö3îjH’^?ræv›ºíVdwî¿ÌvX–‡K¹Ñ:<°E‘ûCú‰ÚÏKˆv¢WnÏúÚu†¾µÇïWC„ú²ÖrXE„?0¤ûM{·h~ƒ÷ ^$‡g•6b÷÷4$ê³pª?r[=ª·šìœ†…‚78$r Gçá}órxPiTX7†E{ Úeu©¬!N??—¹Ð÷íw窱¢`kIàÝ??¬„aÿ~e(<8<¸ß< ZY*m¾Žò{¬u”·ûëTñ¯ÛjÛ?r'Ò¦+&œƒs3Ží]m«e•ÎOQºf¥’x´à WÞµqŸ0t²Ê¤¸JÔ•ÖAé#«éÀhÜ•s%DQ äž#g&⤟…?0jêœäÑd~]PrDöoÙQ4ÕúþJäã?n¾Y¥ÉaÐ/rfE­4ÞÏ}¨_0hÚíÌî-,˜Z“FœÃßéLÔ,˜ä”?nTÎh‹ë2Yzý-¦M¢íN=I™m1ÖÄ4·}„–ha4ðv?nR¿šÕ­Ø<äiò6¼ûêÖ¶Xߘ­£ð‡›\ŒA?rý¡i×DO_Ú·R?nœ>½¥-ü è^±??xâW aic¢#XY?0¼®Ê~àÐþþ†~0*OËbszY¡·™©&<“H’¥J„ºTÖ(·K{ø0ÝI­&v¾·Ûl??,2ä±ùpPýpW}ˆ}¦³Ï0Eþö l :È® ÃÈüž‘T“9ɯºƒB{Í›1ömŒeÓhûZ­éØÁd•¦¤òn”u)EˆE“dˆâb¤U°rÚÎküjÉraÙD£5?nEâwÒüͨáŽóÙvÙ„’‚JÓÉzYã´¶=ì^É`’ÛYUÓîÅt1g¹`PQ«’ £}¿òqX÷qŸO.V¾àë Ïh›´A[¢xtÔÑ!KeƒY‡bú9¤çûÅ3¼mûö;Òñm8,Þïã­|ãï‡æýóüŽE-Szh¨ý<ÄE¡¡òÀP†tëçþ·úÔîƒPgQ¬©5<åt~æº ŠñMòñ›òö^QÚ°àñÐæSdþÞÚ=À´¸Iûú½ªÝ¶P©éÞ•ÿ‡¥ÿ‰4ñsXù¹G??÷‹Ÿø)ºà°ô³O??hÔ»EW¼Ç?r??vÿåé=üçÞ¡Ë€”Ðé±ûqði“îíu+77qÕóy>.ÊÓ:à–~¯†ôç5çÑÌ£rrÑ!0­DÛML(+)ºÕj÷+m¶#‡‹æþNåln±ÏhÿÿƒÍăŠT³‘m½™Yd€˜?0^’•îÙËÕÌd“(rM3 »ÝfºÝT\ÜñÞÚm"ü~ ÿÍ¿?r·°¡/ôi >6³ó©â6QŒ½ûµã¸Ÿ'mW‘›vâºÖe©ÌYÚ8‹;šaê¶;IÓ-6´!ÊgHïû®@Y!0¶º’f¬é\OÝ%?np?01×D»+¥æoØîµÒ÷i]«µ7ñ—5s;7Û{D{¯øÂžat±Œã£Ïé2°pêÖâlï»`ʈšø¾–½bž-²·ðƒóîØÅ?r|ë‚-lTP-°¦a£À2˜?0´S)…e„)RV„õÂÚ+n–Õ3ôs¾6{¿ 6¢‚gUü¬±÷æIr¹Z–©ò{¨ÄfÆ<2DS¯(·ž˜)µâm³P¨®v¹£Põ;Ʋž÷\¨ž)²kºª:MZ¸¿ +#÷ß9þ÷*æ»/÷¿áú¸_]ÿîÞýÇúÿïòþ·$ÓO$–LŒräê&+P¿ëÛàtxBNP'o‹´`•_déZ'ýå)àGôímº. ÆI¦Sé±ø˜_Œ&óHwAg‡Ï£"]'ýüî1™ª!îéO2eEÈx÷è͈n#yõúç·ÊÏ6÷Ü ˜ü¾8~_,÷'éA??ç¿'ËUöûÕ"p{ÙrÐï®oVŽoó4 Rg{çxŒóäÜ1oêo^gçaŒ¿æËíÛ—tnñ<+öß+Güì(íÏ?në±Bn'?0S‘ žíT”N©øc'dyBSëtsžeSY‚Ô.Ìf'@^Fä&kÕ)6‹ÐõMvàµ\ e‘@_DýŽo¼ÊÑ?rÐ-øu…i¬x^ªâq¶ÈÕ¹†\¾Ð0‰ gƒÌô.jèéë$™Ÿ\‡“U®ÒP4J÷UEùE?r¨‡‹IqR˜fǪãû¸?r1_rÑbvÔ*+®QÕ“zQÎaóe/[Ô/‰fÔ¼³a@‡ëÞQÕ&}­ƒJ×dÜ(L°œßƒqJÞôošÄ˜ü‰Dx?r"bµò†cQ½ÂY 'ª‘¡ÎSP2Cþ…ªÇÎ{Þa£v¬º]3"˜tD¢¹Ïj&ýRu?r¢XUA¢„·ÝN° Vj#0ã:}³êî‘á¹Å@rfóUvÓ®sõ>îùvLø† h©H8-]L§à¼ŠlÐreªƒ_$Š[´M¤Ù©»^ºg…JšÓúÞK‹É ç⦃ÓîàŒÈ¸Ú„°Æ(°a…J“‰ õœr›;sŒ!Æ–æ&¿Yšt½tŽyvóx¨ƒÀ‚¾;L9f3hÜájî©)`=SË o½ÜH?rHÛ‹yÎ;¯‰]*7áøˆ>yôÁ/˜»,ãºM–ؘåî‰óÑj–FÆ®Iù[«Ê?ràþº  >¡ª»ÄÝKL©ö>¡Ð ý¦JècÃ+ÊÃ6„Óðþ']ZÿÎ>tˆ6ýãûÖ2S÷ý4Òú‡û>51uqµPÊÕÍM??Ç ‡IÌÝ,œY@šÝ7­SÞù™%XËCF¨Õ(’ð9H'Ñ:<¢jºnéÊÄY¬JCoæB¹0S ÞÆ??uÂ#çVª6›)£O¸T&ú˜©ô¦¾ºú„Á8ç®ð>5‘??Zß¶€¬Âàëú¥œZT.ú??™*Fô1ã+ú2ϧò?0‹õÔ®UÞ.›¾£·b>¥OßtxlxØ¥²§á<ÌÃc5ŒxΟ•æüÙÝÝhÆËm“Ò»JIóöJLAµÖª{qrå‘ZëSGIgøâ¹·~éÞZÜšvoýtëÅ­·Z½BgŸ\P_òŠâ:N??988°zÌÇMÉè¼GŽ[´j—›®ûñSÙÌ ÓÜï˜rhäæs–ÓçqѵuáE¾Oîÿ¸Šìo¹þ_OpaÓòæo»þìïíß­®ÿïõïþcýÿ÷¸þ§Õ"u§,Ôï$!âD¿ÌiÅLeI[AºÏ.æáõæ…%óÍÂ=åú·Eó€±;¨õU)?râ¯ÍÑd†xôò2¡vœ“??é8/=~õ¶CÌ!é??¿¼>=þéäñŸ”–ROâõë ?rÙ†;õæ‹==~úãN&ŒIH¯š“q†&Á§"u‘ç:õ§G9=}úâõ a.^^¿yõÃIñöâçw~¦*JDœX­šr¿Zd^ìa÷íòº…Ô­–*M0wG*'ÓàF"Ÿ_s%"ð½W-0Zª)êX‹>O¯ÊÕbŽû]o2;§ƒº¾}ƒ“îOY–ÁšÂÏ8Á¸oY4¶ ‹$êµÓ`½-™x³?n·"?0¿a#Ä<¡$ýD´º“å²k¯×°Àê]pk2£dý„Ì·ownwæqgÙI]ÉŽ*7†uÜh¤t/“ÌÜá(η±aÑÌÆ šõæQ|Ù°ˆÉ£ƒþ~sÑ…N“µ®©å4ɱòÙF°2Wm€?n6šÍ¶¶}2NòFŽbÍ< G¿f5¤›8…£ešLp;ßîY>)ÒV¨˜Õƒã æ‹3>~²ΗW$SXóð6óàGË+_ÀNa'UàƒþA0¥HàýFà}<6?0SŠe™4yÁ¼9Ox•6SŠ/Å#¢o¨s¢Qä:]3åÓpâË ¡2C:î­þpz´ùÕ»Õÿøiº1¦´!™nL»Å¯ÃÏD²¹ª…«‘! æt3v~ã©Ø@„±ƒlž4Ü•°Z"ôK¯PLJü›Sñze“ësz´.{œ«Y€>{š^¿”¹Wøn¨ûàæg夌°…ëpîiôONH7£L.ÂÉ%ÕjíÕLZ‰5U“?n®gAâ"‰G+ÈuxtÚ¦‚;0?nÁÀòÚÎ’ö±=ÀŸ~U±áje@k%´êB*|ÅfrwŠffÓf÷W’ܱÍý?0Œ­ÛuruX Àɪéj:«%´g¦ªÿÅô)‰)/MÆU)ë(šqe-'q­£t•u³d–_Ø^µç»‰˜x¢Y±D¯ÙgR·Gya¶ '­“nœÑ$M€ÛyÎÔi>5?0“97É*u¿þY‘ôÓ“'N†3b!eš®RÔuÕÊ4M6]û-Þj†$’áÙŠê»@ø*ŠžPU=*þí;‰•187wÅ€âý,ˆq;æÈ·zóË-‡¬/ *OMLý¬ËùÇáDî¨t¯û+m?r:Q¦»ÍçS~¼ï·wZûÖ[lEN^|ë¹Q2^ͨ¤âÁïTÒ³n:íf?0‘Òéˆk`®Ò?rÌUZ©SÁB‡X穪–"M¸ˆîY¼w(]ƒmþcñŽƒkÔoâAj?r(ÝÔstq¾²??`hÚ‰qÔ>…ãÝšòÉÇn÷–è¹uí‚í”QGñÖ·n³ê9¬T}ˆojæ£d|Â¥¸#ÈàÕð?0rÕ`ßß ªÅ¥¾ìƹF¦ú¢æ€Àhk¶›ÌºùUòÐÁðŒpÌi•¡cßšB_¦ßðÙz›ÑÛ®œäJů}übžÊi†» ó=¿ª>»elR?rŠþË-Lu™SF¢ƒR•öÅó" núó¤Ó–€F…Ú*S&´S¢³R¥(uEÉrHæÑ9»àðoo¨ßÊFÞ,ÊñšwŠcÚfÌ\û ‚Ä5ØŠ(XM£Ä¹?nÖ!êªV??z9€ÂZ[¦“²nçy~Ñ¥<]e<Ô2’Òsbto8s~Œ~ v_Esv}‡D!8~+Û™Á¦j¢§¡»L.–WF”GÓ刜™³´†zåòzD™ºøä–,ФÌ]|¿GDÿÇä¥ðtÿlúú?rš‘i£’kŒˆì¡’6‹U–³@Ð!A‡.Š¢ODM÷¢KÔ´U;ß[×1¹©91trQ8âÙáÇ>€~ÕQœ€ŒÓ³j§˜CA¼Æ†h´ôüÍùhJÑÞ×ܼ)“¬ËØ&ðN¯ËYæh‰ksg„]þ±Ê)[ì­6UÑŠí‚àµ9ÖÒ?09‡¾ûËâ¥5˸5ŠVn‡ðµÂÅ e‚³‚‰n@Æý`ž;³Kþ;å¿Ú¹Qž??· È.’«Q±'ÿ!ëøÈÐ&'Ýp6Cëuè°P6ùmô?r~ÍXØÏ“j¡_…KÜf” xPØh¾È¶Xq²‹”Åš(ßàm\w^MÇžKP®_¿à/Q´¥†ÒÀ:¥[F˜8KèÆiDnÜÓB4…‹PE‰8:;=*]SCÓ?rƲdŸ_D¿^Îq²ü-ÍòÕúêšoo-a@Ì “Ù$?0 à§C÷ê,??ýÕ+X”9²ùaö©iì5f^Íaø¡î–Ï‘l½÷?nOÏuþä0(·yˆ­¾0~1ž»—·)Ì «ˆ–1a^Zrc8˜×úåèìRÔE'(??¶f,ÚK h?0éB¬÷2¡¤ãI6Ňc³½P7ŸYCÖ®ÒêVæáuÄ^\dv+™o8Ù«]‰gM%MÇç£yæ‰1i­1…uc-¢ÜÊ%e4R4¡fGÎGÓ­Kƒ}âW0è]ØãbVKÚJ =€C´´†…´W7‘šRWo½È?n²|LÄŠu x‡õXm®ZT˜øua ò0ËËœÀ9/œšhh£66%CÙ!Òü©R=³,fÊGØ]GËÌí¨ý _fæ}妼Ի·få`Œ%ó‰·±›0Îf„F!žg™¥|hgP]ïc8•¤˜ÇV#²¶_Ö)'?0Ø{M„)ˆ9?r”úiC„à9eS¾mŠôËðm6°ÒÀ¹Z›mW\Afö†–47¨ì5?niGÇA©R¤d–ë¡,'X“æÓoR"z ã#s©»0?ri-c°Ü «?r™ªÝë:¢>'g|¤³ÈÎQ?rcƒWii¹¢™Z5žGc@ËMS3ª-›^GðœDzžî:—‹KYmP¡L&¢r¢N¢¢ußáјv;z€ØõŠÃpª™Àuìß?0†kuút± ϱ8𤡽â +#{¨zqx,–äùù>.t‚îÂYFKµ'aÆV)ºˆËU@Á2ߤÎf Á­¯~ë^ä:®s[Å*ÓÛ´ÑÌ9•+Dí‘x?rIæüÆ”&«Tt¢Ww‰DþÐ3‰\ZŠC@£§äãµ–îÞt$9Œ/”'‡¶¨M‡9[ 㤫2êáÀŸxCÍË’4½á¡ÝNÚ¡Í[‹,ÝÛi÷vêþgÖ‰{·îÊÜ©U ‰Áþ?r«Z;tê®Õ—VIš\ÅXˆ–†æ,¢¹$??zßßß??í/ø§—¼£œ¶^cŠ4P,iHM.<÷??¼þï§¿­éÌP÷ÁÙïê÷ÇagÿÓ¥ÿ??´6ûÛˆÎCèùÒo°˜F1;£©C NžqÆ>Îo+úLÞê§·²3c$}è$1ö³Õ.A™çû\ïkÀžì<þá?r›O ˆ–6Á  /Ï ·üç;ß<­B¬‡gBH‹V¶¦bm2Ä,_šÍµ|_&—!$M§åO?r?03Rp®ßƒ ¿[ÊÉBšët;²KÈì²)C&leL›³mÉõ¤f=Ë—`®~6*ºqäÂ÷©väzJÏ{¢C3ˆQXdÙ3×*’Ï…<­!(§ä:"ò2ïfõdä­•åQ¨ñ d]‰7 š+¿¨‚Ü:{2´MXÃ;ÑbŸ•–êíáÀÿ5Yå¶^ƒeŒU[ɸaËÛ??H¦0Ä1Uuv,Èöëq1??˜pÑΙ[/™(½fñ  ß}ùÃdDSé¸Ä.‡µ^hð6¬kyN孿ŲØC÷üº.6Fô” 6.ÃêË%ä ?? ©Q»Ç6’wôÁ`±Iç­y…yöŠ<·G•õëöàØ[ÍIvÞtõò Øù}ÒO3‡ÓW)Ÿ$„–Æ- õ¹Ðéòv¶Ò”ïˆj¦0†`à*¾uŸ‰hÒU´OG5bG×Âo݃’¾tÌáÀ.šCˬ–Ñ-¸f, zhvxO+2ùÂê:/ïàÅÄÉä2?nMjMerçåÁö•š¨x×x.ÆaŠÍÁ.…\-Hd³œ¾”¨fãíMaöRµ—¿½JzXÚû¾:XijÃy÷aÝšê ->¹và9®(J®žôÕ½¢rd¾O6U؇WÐ450ë™É2Œ)ÑÝb(¾…ý/Æjw­LÇyJÞ|‘zÆB¯ß:~‚+Ú¤GÃ2&%¸Q¾®C1½G¸oLBÈB ›Œ\³è»\S/?riî¼<þWü¯†A²6(¼ªÈ¡…aÒZ’Œ9žÏÑ'.dåìß—¦I M$Ü&t¸ix}˜ÓÏXÚp,\„qœª&[ßšªµv2§Ïð¹ÁT`Þ"ÍÒ†e·¹¡´£ˆçÆÿ,†èi¦`‹ŠÃÑGA2êh;Å[ÚÇL\&IƉ@=Q¡nÑì?nRSÍ?n¥©£ç×ÍPÐ#¥–y%ÕZæÂ´¡Š<¢èÐVr›Ê(!åàF¡µUcâØ%3ª=T‡¨?nSœ¤m\4 ´£ðþû*^e,q<ÒŒo‚?nµpÃYü] ¸‘Âk!Î…˜}z2O²Ðûò0 í¥LǃBœšÀѯ˜¯¾®0“e´YsH­ÚÛÜÛT9 6¹—©¦®6#£ @4{—ô޳V‘jÝÎ6øix«¸ÈEÅCÞ×Ykór„HÊ;¢¬IL„ƒ [€ =gQ;0‘ÀÀy_G1îŒ jÌ'#hˆ¥vÔÐÉbJÙÀIÍV)“ Æß2ã3¥Sh Ny-&âRDC½ÿOlßó Ÿß®ÿ·rŸ¸ÙÊx9cÉ íóÆÚ7ÞíU¢ùÂ0Ö*Ó¼‚?rxËÙòáÎ:V‘´G½i¯YþÍ+ÕÕlMÿÕ5ú–\Þ ØaD¬¯ÁQ«)ÜÎN«Nh H«qB8JèþÞ®Æxõ*gNvÎ¼ÒæþÍ£9Îøø¶¯´zn›B–}ˆY<•EÉÓÆlzcѥήçDUF–ÑHuQµ‘™aÙ?rí2Ji=œfØNùÆK‚v^è‹Õ¬9”_³‰Ï«x^²GDåƺð¬ß7µ5f”fóÜñ¨#‘cr/™à­2½ƒí™¥ºmÂ)ÜN7¨pÅì X÷³5dÀ9¡ª›¢º›¢ÀA8±ª“YH©KlvèΔªbõŸ. õG]PËò#݆öòé³G??½zqBèÖÝuöœ_’¶l©î›C1!Y0 ÏWA:-áÖùœ×op¥à+²@p±Úñ 'œƒYìýÉè¼Üñ2¬ºV©1©?0ô4ž-S—ÀUÜ.zVáŠ<7Ïè#œñT üò|uO>"ê[.0ª š»—/yžP%É1nIvœ%8ëã??nDÉ]"fÝq.Åö××”W;xì8ôQ0™%”Yþ¡Nc–Ø9TSÓ‚•ë±;YzvcÒ ¦Ð¨§ÉÑ?rz©K5Öt!·`Àvó”Æì`^6îní’sj½mÇÃá…‹Ô¦·`n¼lk?r??Ø­?nÑì¬.Lïè9 £·ûùMJ4m«ä¸bLáÆô9 ü/“NØDi¾"nåÉrŽÓ…T1Z©ËO©‰—«ÜQLÆlƒ$ßÔ Þ°èέìŽ2ñ§Ž_lØs:8:óšmÝ€¢>ñ_ܰÆÍÝÚØ×¤eM˜ÍD]ÛÍeí¦2{pq]PÄ×˾ÂPfŒdVÐõõ62ÃýSÙîf2‹ÊF™PoåÞKváZ³V+Œ«9;¢'šªn]Ñ”G“^¯÷>¶¢%­F ›”Ô(êØO:Sú<ÃÞâM¡r̃™ù ³ÉEî9·½‹Ã>üL9.H0'<9Â?n¸]Ž"m4ÜàÀñ€<¿a·ä¿ÍC(ÊxiPM'øÏÕ’I´ƒ†ÿšò_íÑä?nÎ[}—[Ñÿ†ó/L?rƒomSÉ. üòJg\ê/iñåÀ…àr–Uþ„uí¬Lð°àl¬r[ jÙlt©ŽôϼË6ЋôbPª ÃæƒÏ6?r-C‘¸€ªÝ³¦ lLç;ÅNƒ¤ÍÞÔns2R­Yš›ú’¢Ii[žejyÆA}Ü6Ò7䢸p8«%¢%ñ“\ºµk+‹¹ÿIË+[Oðÿ ­—ÿ†ËåÏ_Œš¥§q\”ËNÅÙl†Ko¿rZë"ÈàV˜·]vuÔG4Œ¿gÍr4?rKEnEš×aÍ[Ъôr‘ «h  úêÕ®tÛü^?rfɉ‰!­uŽËòÌÁQ‘FmbÊGKÇ’7Y [?r?r8¤Y°ÍênA>}÷šx ù˜#óÝ9)w%/óvf8,V?r­¦"9-¨M<½­½~€’ìñòö®a%¶QnÉe³¯úç¶u"0.Ýí¢£¶4Ýjè¿*¯9Kkßj??pØx`QÃØ‚ld¼Z*êׯKËÐYp㹓q©%\ÿ já°j‰'W'™??ÝùøÉÑÍb"æÛŽR€5Ñpª³i »UqmŠí8°²è<Ó…%”ùSƒW+‡^Þ\O̯½·Ï~|wòæ…u¸,7`4?0pÐØ<·Óÿ¹±wog{J¦ìOƒ>{É7|+†¹[}´0š­]0>Î%¢røsœÄ_T£Š&??r‚k³Íé½q˜ç0yÀš¯NŸI敦2’8I<änAz±‰°u…ïN?n¦kØZx'7SR‰UGG‹Ü|XüÆ©ƒ}I…ùÍYP§£?nd¬Ñ„kê*Ý Ú“n¨8b¬›Æ¥mãÙ¦æ=¼qCdl-b(ÇK˜@æl5oªø_ʈƒu1Þ>±Ð"¤ŸŸßé*iâí J]fÉC2ĺš¯Î±}??ƒ˜¦I ã”æZHŽ“,´öK¦áÒÚ!c˜F™Là7O¢PW·âÆ{¬ÃŽIwÇ?0‹²M}mœs¬Ó!Q*²ñ¬ER±?n¶¹´Í Õ«ÇºMŒ7»»*;O@Ž;KÈÐïÚÐî"Ê2ÁWÃ{ìÔŽ:Yº¸ðèL€mc!ÛùP|™m¾Š¯¶÷U?r³]ïO.­»`.P«ø4>ߟ—èÌÅL*ˆH‘#_ýé¨`Øp£¸VÊ…2…Â7D¡e5%}á¡ÏÝEAÀÌä?nH()8ÛÃlªžE1Ù·ó+¿nG.Yò®D·Ú¦ç°¬˜°¬»íUË힪ø-» ZO?rûµ¹NKRà"Êñ©ŒÁLŸO.Ÿ‰éÜõ-±ÛâË©aTØÜ5öXíð–Žc§‘üÂzÊ,æ`-RAUy×éå«wØO‰nJôÈaTG:e_?0>e“‡17Çü†Ìç3£S@7þ.›DE?0vÓËQC_„¬³½¦Ç¸e øœ½‚Ò›™Þ÷PÌÇ^ãxš&ÑÔ!!W¥ nÔ‘w,°8Ú€ÂÐY®ÝºÚª$\”-Ò¾ðíÔLQµ)š®BPjªêÚLƒ‘æo‚¬Èˆwó^T·¾¦d>[ôÄU]YæY†Ùp7htreColH»Áè-ý@??‡ô³ÆýÊèú³fþËš€`ybTƘ›w´Ã¥7®þë`¨®‰©¸) ¨-mÈål€?nð¶ËG—³‡:€íUúÆëœ¾£Ž™öÖ3ñq[:dK|¸i° +#®;ý;½^l®¾yòï/ž^ýÉà1¸tš¨o{Y‚ð–»j‡úûøÉÆ'è¯áøÉ£¿lºŽ°¡j‚ò0ÓÖ¨žóš÷>ù’8ÞJ¯Îòö6m²³/çd¾š†öå„ô•--$òô¾%åì2¦# p~ç[s±UÞ"?rõE??ænsýŽÞ;7³›Ñ¿uŠ+}§Óê#\¦a“T=Ó£–°L€Yذ„6ÏÏ„2k„dgW¦¬@Èåy‚Ià×µ¤šjxŒÓ©Š¬5¡µdAì—bc кXŸ”pVXà ݆ÊÊ…víhUöÛUÉNŸ"ìÇÒ”F\R®O[ɛ֒÷)cƱâ#9¼ïÞpWJ—ð£°=÷Øuï?rú—{ÄÏ6fÝ ¶?0Ðí¦Ë.!zèDðÀƒGÔŸ_áVƒaNIjØwÊÁ¥œ;³$¹3R÷s\ð@¸úž‡œþ‰ô‘4ç7¨Ï›bIäZHÞ*,ßP?0Lo5* ‹7kz?03³åè×ܶ-êïÙ|ÉWíàj0±q„óxs‰ÕAw~&À¬d{PYE0Ô&M9ª}ÊWãÚHëñ!"c'¸üÙ¯/¡º4„LÔBwâ6—ye¼\Ö9úXͦlB?nxLÓ¥Dxj¶ë–4¥aS׳??Á’ä]ûå8o€ØÓ×VmžÐêzéÈ,›ãRŠ-õÍÚêK œEdÛ­Rk}kñ¶Êdµ•Á©¨`ºøRê‘4M "X´ÌT#ƒ–YåC}Ñí¨¾H9|Q}9>Š”á?n8„Å›çU@†öi2áºæÏ•ŒùÐ.ïÛú¯:r`Ä÷KÜ£¶Â>‘Œ(ØšÍÄžE??¨mdpfžÍÜ¿Ã:¹ÿi¶ÎÐêÿÃõ…Ž¥[VHòâ"‡©µO!™äÚÚ”ñŠE”v=›.¦)%™ÝKp&¼5 ÍëØó”¦ëµ–‚‘Xõªa-bKÁ‡Ž­›/=“Xè§ôd¢¨›Y`+†¼ …!bgu¾³‡u—Š¢Z\+Z5EsN}¥¦G¯þl£¿hBÿSú ýE§ølÐcm"Ñ7¤š1U‡žrŽÓr ô¥cRLdj˜†@£È>??—ÙuŠÌ^'?0N©w°µB0xŒ¦×xÂ呲tr1"ëÞܭܰÌc#ŒLx?0hͰ–9íÑM¹(F‡>ÏÂåWäžA6”I4€øφglÛ¸NÛpYXN !§G«ûY¸ºW—tØè÷ï œ~Ó_ß|ýþøvÅÚÛÇ…Eç??ÐRß÷·ù¯Ïú5´vÒÅ(?r T’;‚ r¤¶lÈ ›{„n;hZnµR‹5ŠŽ‚‰…Т«t65•Äw>¿Þ‹u{½?rd{½?rè7ª÷b-ëmÊ‘õ†Ìù5Ãm³j5 [¡[¬£°ZœÃù£Xiå$]+c>Iæ Ñmž{ób9ø/`Œ}laä;Ç¥+H%6·0¥Ó?nê¶3Ü(‘欢"e5Ì>Ø+0ëZA?røP4€ØÀÌ(W×%¾½ ÄU½E˜îx…€PEXT¸‡]}v`ª,¥N¢|áB?rš9Þ"Š;EŒÇE#õ[WÞ5´ã­Úû]Ü?nø…Ÿ³=ûvª´¡#—¼Ã‹¨òÒBÞÜ5@eéé{gØ??¸×~?nE²1Š£ÅjÝâ|q•p¹~] þ Jaw^Óf…«š)›pô4SšáýÖu¯R›˜E—ç÷ªGL툉И†iX¸fæ×yÅ“>q§sƒøò»_õΛ–~Ê}Ù+êAyFèñŒÐ+ëàÛï"¨ƒ­Ž{<¤ùAiä¾åŸd.x&Z!5 ‹»¾ÓÏî@¸x°ãàž[§ÎƒîÚWëDê[};ƒþð€Ä?0þˆ Z2p]&ü×SE°ùôæù³ÏÞÑQš§ÏžŸTVÕâT‹[™qcT®\Tጒd.¶pwñC3ô‘\°¤FüÀ·`L7+gÕB7QÒmƒ»^1h8ï·Ò‡=DkÒ‹'Ô?0†ûtfc|EÐe í “%§ ÔTM뽿°mš5kii,¦övÕ‹jhÑD‹pi4,bDцèÂqGÞçkM>£A°KÜÚU˜‡ào=’?0Çh8¥G¾ô0ž|~GŠŽÕ­VjÑÊŸ:Lù\tT’%T(Ž‘KI2E}púC¹›Ò7Æ–0¥ª;í³²^Ku°oB›qpÞ_ÓŸ^¼µWþHƒ×?rI9z¬Î3œq&Uw.ufTñNLç°‹Í@*½ûbJ xSgëú_=/—?0ÛÑŠÌÍýêAò›ªoÁ:ë I2ºþ  ëÜ/®??XZpj¯âÚ»Š'¹}ÜSGòÌ33–…žÍ"s°Kx¯t5É,î “a`¹æÍÒaLáÖÜiT²œÒ‹âQ&Õg"¹Yô̘±ú«Jó‹Àä·îhFRRÕÌ½ÂØ?ncŽÅì®òY÷>¾ ÂBvì]ÊEw©—Ó韭;ËèCóU¦/yo·À1UŸMä_?rI8!FŠâ8¡Ì†Ý?nµä^)À‘=Z-‚ìÒ­õ@??M.ÉÔ›_ô0ÚâÄ#ÉÕOì;âÖÐ@Ô½%g$t!ëÚcRsœ?0w[%yhëi•ê)ïÚ¾Çh}&¼ÖeþuiTü«Ã^åFŽÓïÎT³b‘Á‚Å9ýøéì}\Z¦µ´ùîùm¯»¼OÓÆQoöé}Œ²0‰àì|y.#n^¥U-·ní:éå}bŒv}Bn#Ú|ypc†r!??Œà+žlÊ="=hzìZG¥V±˜G¬?0ðÛ:Õò²Cêó 8+ g}†Å¦÷A«É6 òªdqD@“ Ëk‚ñÂ\U m¬"ðŒÎ§_Ð[—$”3ÑwµGmN®<{ù®ãØYÎÚ/zcLÚ›Ödÿùí›må ú,àäâ??ÀÑ©;dlÒzËmÉñýÁƒõäb5î:Í)ßÅ´†êDä—$?0ÅIµÏ?rÂoŸ(ºXå¸afüj–Õgâ$Πå0Œa<‡ª³¬á#ï|ÀTnµHËph½í@;gÁLO#/ž¼|« ‘~‡€¬m“¬êéÙÊ9V%±úeUÓðQô1]BºÂ°Ä£»[«Fdf…9kAÕ¡E„«[‹dÙ™Ûp¼ÛÜKòöÉëi3»º…ÝHfv7°šË:æb>—׌žyÇ/æ52·òÚ:s a&—^²Iãè¦:ÖåtœóÒŠº —uà¨LXÊnPÄ™'¯uvɪÚI9í{$?rO¤* Ó&FetaÚ¤Þ#°ÇjrI>qY¶¢þ U$ØÝLÌ­RKì&ËÃ…çæ´ Fò¤sî,£iùîÍr8?r9a[耢 _ÖÔþôìùó m­‰y‰N‡D>¢ôUyiôŠHžáÔX©‹ ¦8Ú=²·¹¢èzòHèýÇ?nã2åK׎=÷Õ‹^9??¼9yô§“7nÇ÷åÅUÍMñù±Âë;+÷»ÜV ??ºÆšÑjÑån¢V”›\¶®l$ÐN:] |o79!eŸC†´·„•¯€Þ½Iƒ®«‰ÝZ“oTCµá%zpÑC©k¾þåÝOÞUÄÆ4€²siÿQ.©U[5¸ôòIè–¨ÉF¹d"d‰åà;ßQùweùB}k/àº|<Iùb*4"_‚ðBW€D,”ƒ×àïÑSGÒïïÎ%Á\ÒÅIª¬û„[®n¬ñL7×Ùôû¢ruQd}j)ÓKKO •1êÑÅbï¾M³ƒ2€ši:hǽŸ…ŸÎ©¸_ÜÄfE —·ë¨öîo]hðT@“Ó1ùZpèøµkò2ê???0uã9¡8¼Î=ϸgsⳑQFâ)Z‡~á9i´3^6tËò!æíÉ(—'yc+FJû¾'ä#‚,NÙ`ëLW)šSwbç ¦Ââ(ìFíلˬ‹•i™ª3—GÍé¼Rås9Ek%åžõâò‚!ô8»T¹£¡Èo`zÊ¢nÆ¢¨>×L¢bjSi’L<0‘\ŒH3 Ú4†×js3¯ì¡Šc¡›5v:qÔÊô{â(…º.l”M«˜½Å X±hmÛ¬3ÿp^Øà™Îê˜ÂØz7avêhÈæoÎù¦Áô(Ž»¤e@ê‚D¸_þ”…Ðêˉ&iÂ騳4DGk^Š+¾2_œ H¬•Q¦CŽzZÏßzC:¿[ÙâV¦û"¸8QÍ-;6œ 9“«Ý›&íø??}nEw+«Gdå1$ÂXœJñ±|ÆÞmÎô"??>Êür`Ûî¿©›ºÃAvƇAzža`¨uœ¯ÝœÛ&¤¬adܱ7û³¼$éf?0˜zmÀmæf¡eö¦–İ’Š•w.‰ºhthC4¿‰½šûÒ@¶Mø‰ÔÊθÄs\pÇN'(Is¨"Ãøs¤£¸ß\ö}u£ò‰ìŹºq´X)2uàÍôK µhè—ž˜Ëž î­_º·Ý[Ðh&Ø™œ÷Zz’Ú§È-£Q:bâ86õÝÄLÙ_¨!ëÕ§•«|ÓÅ Ì„‘1 -âF?nÀ7èåÛg½l5¦-¤h‹ü2P ÄW_Þq*Pücª©ÓAÂf?n„ÔAŒ&!“¾™F!ëFdЍ}»Gn–×ݨøsAû??‰Õoèôí??YD/Tù½;‡ñãèMC•äÛdHRv-"È&QÔ^„¨é«·ªzÍ7GÒç-â²X=ôN^??{}²íÚÈõ)Ød7ß]•hW'0ù-õªùvàЯۗSï¯Û:Ýɹ0FS…v6·²X²WW…c-–£"F­IòţǯÞnôï$£.³ŽÒ$†ÝsUøü§¯Þ<¦èw¯ë=¬?rožGE(?n#s®?nÿƒà&‹`BH¯VÎbaVÜø1B4?r2?rxûú,!|í&¿ Œû½}åô’»ž²TЧ¢šëZé.‹eïÏ«pV®{ËUîyî,I0Vp¢_j­×Š3¸þí¡`Û]ë«-¾#Å}?rÄP½Dc8‡:‚ºÊÛš +#d`e®¶ýͨùU†Š×Hyk›Ãx:Y®F|'?n™zÕî½êëØg,8ìÃâvŠ0;8]òøõϸ¹$œ¨û.ªÖF?0×ßµÚUÚ»„,BêŸS"b\Å.ïæ‰‘þ(ÏÓh¼Êõ´QçV)ï8#Ù¯Ø%?0\×rm;"’uÀä·²WG)šÍ¢IÆ9Ç@,Ø;g®O’4ÌZ‹~º ðÚ~<‚#.ñ¢º¹ ìÙÓ_Foér“wnÝSÄb2„Øì¦Sp|ų»÷a,°Š‹õoº÷J˜C¢¤ú¸›ëg ­=.¡¬rxíjüèõmº.ÇI¦è±ø¨BúêÏoó4 Ò‰šôpIjUb©DB2€‡ÖîèeI„þ¬b~¸‰Âù82óh¼·á5M2;D|??o¼š™uDÞqHxéì$6ÿðÓO4Ú²O/¹½ÜùŸNÿzðÔÇ!^þ¾àïßïÜõUêþSN¸ õ} /šF ÓQ™§¥ÌÅ÷›Í÷ØÛ<¸ß/ÈÈUÊM‡òÒÏ´CåÐ=ç™Ï3%ÙŽúÓO]ú3ÜüqøÏ‘õ‡WQ–€Ú™–„·ó©5×”æ¾%<î8;›së§£[/Žn½…Ü2úWºÓ(á{¾€%°ª–@ ¨¦ &ñòÌ4D}Õ:Έÿ×Äœ/ð9‰;\ðìõˆ7NW1Œ4Þ??0û¼éÏwñ<2°âc~ïƒÊ?r¡Å¯=[Ý1•ËìN’ÓÏDè¹Ud»ï¯û}þð×-Aþ££:º—}€%7ìéï‡ÃÃÞ去”ÿ`LùïÑÏ}wϘûˆ˜çþ3¬ªÈËÆT|{þÜÕ8°wÀ”S–¢(1ðcµý1Ô!XŒ§R/FË$+n'Gü4‹ùˆ„ºH¦üùs¿xŒ¸vü²$9Dïô¶©'LÖ»:@´Y•bç|bz°¢á§ï1€ŠFWØ´„‚䮘ˆáX°v8E³THßn.]*W´0×Is,w;3˜$8÷WBµ+ejC]W ¾ý÷éç€tY] ]RK6t½áûëAåì¦Òˆ"ÝH†÷]z ÜÎ8ïwöè/º ;fç[Sw*·§Õ.Ùƒ»Ìy°ž2ª_ÔÈæè™)øð–q1Ö!m¥Î®;Yìß³MuÅ•úGH?0wDu×ÍC`¥ßºæŠûT¹`ÅÆÚ—UÐ9‰2¿ƒ™‚¶y5R_ô,¦doJÙÁDšý¼¿¾??p?r¬à./xuJMãûvSës¢²nÏŸ£~”õ ~=æ;ØtûWßpÿfüš}f,ÉйÆîŸQkÍ~`êØA&??õ¼×,ô‰–z¤ -2<@:O£µ×¸¿Ä—_5#ï.ý"¹¹…'ú½ÁÞÝ”É 1“ÃG“¹ ¸rê»ï3…Ã@((½?0!ŒM0FBÂÈ7¢Aw¼)WIØûLF‰Jí(¼gÍÍïv?nLæï&›ß$0ªÍÔÑtUÚëð`÷ko°ÃúNÐÇLÓÝ,\õ„ÒÔä÷whòZ6þ™þ¹å6·ž5BSó¶^Ñ’L¼â¬Ú™=÷8YJŠ??£¡ið¤4ë™ÐpÏ?n­ÿEKo£ÿûGfï{¤®#yÖ²¸£ß`‘pÌN8”^gTMúc¾°\×'?0Ìg=Ó‰l\Áë¨/öÁ²Õ2L½?rù|,Èïm(&Ò:Š¢Ž"¤ê  H9VÙ š˜cMV%#Þ¾ýÑt{vñ5 œäÅŠ^òHlßñ|E?0â?n}3¸²gþLñ¢Œ ˜MOÝõÒ=3e "øX_sþ˜å®}§+V–½·£goŸ<{ãeØÙÑ|¶Ü¨†Cãbzc’Ê6ß±b0 <ƒo8`P1ãûM¥F‘—s‚«QlVœv7ËÎ%`«Ég„ÕiO)>à~_¹ŸXpÿ³¬ Yª-¡ê/ä3xÂßD6P|¼Qî¡??t€ª£š—´æ2TQÕ±Ø`º‰ª7§¡§ñ’ù+ùc6M‰ŸÏÀV?n[¸k†ÕæÖ‹AÌ^RŒZSMÒ–*˜VÐ @ƈÜþn(¹‡${4Â~2ƒ( Ãiù[’VN?0,˜Œ±ëZ~ÑËцLDYíÆ³Kø]€ÍD²)Þð²ˆä<3ÆÖöHÊ’PDI ¡U7?0-¼–vŒ2?0úTƒÃýû¡l3äØ&Ù/7ü ã0?rò$=!·Ùm›ø»û# Í9Öæý»ÅÆX)0#6Ëèw·ë¸þç4%á• TAF+ƒ$S..X`ÃwŽLFù?nÓ¡gŽ<®à†³úÍ7QoÞÿñN‰©¤>bi^lÁõ»µ«q.I¡©óµ]+ÖÔìÙ¨ZB“ÜAÁFL‰B??o‚°rËyBaùâ‰B7[ž}ækïI-CÌ(ÜR ࣎Y?0™E¡á¶Ñ±ÍâBîv2†š‹,PŽeÃË&0Êú®å·p»µ^¿1PÁhûYì“íŠrL1@ã·ž·À ´??êK«xÅ—?0X'ņÚµfÿ¯8ž¶¼ùïÿÕý¿{Ãìÿý]îÿ%™~JCýÄN?n›·›¬nCצD>OÜq^ÿ2$µã—×'£Ç??<þ]µ)½È‰ êu.xå/¢©®N4%i¨??§‘]î²é€|Ž??>yû¶£ßž7is…o‰öŽÉ¼>½ãt9ÊB#¦g¯éöÞw¯$º: Žs°áè雓“ž½|"ñIB7¸ëo¬Gï&KÚbö’ñ¯á$÷åJØpžù¾#eO„¨ãïêQøepžÌ¼§{¼ˆ†ŒÑ¬ro0ìûÍgÉÙÿÊNM×lʱ??ÕíHu)±êo™½ê3ió(c§J?nÊÖ¤ßRtL•É¥)ʨªÑ’”RÃ??y›! Ž ùÑ‘[·ŒÔ¤n2Š–ëC†pKµŽ„î©ÿܳZØ)‚$óYÄV‡|YB;n›1±D-ml§Z\ÅäÈ/‘&—„08ã!rB:Ø-²”(²Õ˜³Õ|4Žâ)›Û—Ûí¶#Çoø.F0võ¾Ì#k¯×¢íD]etÏÖU´è´h=ìEë{͈¤ú<Âåk Gâg¯×‡*⡎x@Â?r3:Yn‡0l??°ÎÚ_9.¸ŠV:œ?? ·>Ií‘ÞÝm6’o¢1޾µp¼—­iü˜›ŽQWìèœ8?nÕÂY~F¬‰¦[k5gïkÓäÉ瓌±¼{còá›??Ç¢[Z¯E^IvJó,‘d ·3Fº’×2&ˆoø’Ð9Öô?rñ7ÓY@ŪŠzÿV†¹å<¡’Á­<î`xOÉù{TèmžùÆ»Y.© $Ôÿåkî??•2¢EŠÅt5p!¨ux¯Gnèé¬K-í)Å^ëój§—æ&Š0WÕ gvlLº%îžP1Žù•ÝøG¦m¼Q­‘îÌÐ0áç§Q¦îêÃNþ<{­<°ID„æcËè¨ÄÄk© ô—jCõVKœ†õ>VXy䬙›zˆb [5<ÿSÇ;5ÚëÛÁî:×¥nsmºLxM¬'ˆ+qíTáù|5ŽïJ´ãŽC¸ÄÓ¦jÖí×PËÞï»þ®w#âJÏÑ\hj†œ!è¶d5…ª‰\ÖÜ̯Lñ°•dI PÒP˜N·g1-h¦÷¨_“?0·;˜öæFj¾î÷ôL4$??[€ÕY$p¼à?nH5ÐÀª¼˜,U«Tê(q¦Ž£êbËVþßÒ½À§¡õ‰Ï€fv55óGtŒBÔPHåÁرÑè”u²4„UÞ #d¥tã™’‘V[Ó}gvøË`M‚H]óýo˜šïÜQóóÇñŠÃÙæ\ºïŠöÀ¦C‰ÅárÓ7;]Ãv<(êOà\9D÷™Aõ£=•R´pW]§}D5¶{iP??b¼Ú®,šêfç!ë§gµúh®Ë‚ÕÚçgÒ °úœ²’!2x@]—®á€™»·Ö—i@Ìà‘ÿÙÛ$Éá èµ»·UõÈJÁ–è€R›(2…ÃÑ]‚ÄâÁÁ¾…R’§ò¹’þ‹h‰d¬U\£‹bˆ«æ!ú+2Chîä:·*²ÄÜ<šüÞEÙ(Ä6Ÿ™ûPàæŒ˜Ê&2ý†CR‰胤Úy ,Xº‹¥fË‚Q €èBf½Z—©”díÝ\ ÒCÿô~¿ƒÆ>kÒ.œcÑNB9\%ÝêRįë-¹ýÂi`¹£Ë’’€I”¯š½g… C4™ÎÒZ?0î§uIåó.…!—,̈[ÂÓŽtÒ÷Ѳ[HûКîtÿHOm|}9©®9tWä2ĵ÷15Ÿæ—g§ùúÌ„¤Ü} HL˜åóõ‘3ød-î +¤.ƒ^m’Z×`E{$6F§{J¥’-£·Å&¬šíÏó9ôµÊë•(JG#Ò:–æw¶Âع1sAju÷¡£•oT£'fM~<:ôCÉabžù~ʼnÞo`;LËIj½ëvhn‹ßR$w²ºä÷£•°-?n’ì?0&Ø"mod\4q± s,D«i6Z†)láñ<šÒSwP®¡‹c?rzö³kD…׉ ¦wûô/˜b À¬Ž\¿‚€ :í ]…û%ù¸Ó°Lލú-pÀ"xßß‚fÿ3‚ª.iôóϛ,õÞ_ºÜ +#k)@ *žã®$]Tï—‘ˆùCw,"²~WµmÜ¢«u;?rUÜÊÔ}ß;íwœ}÷??\ÞŒ²P¶`!5û­3µ¥LJ2:Ž‘¿’±{õ7ì«vT_+Øf÷vƒf½d®ÒÝ}a\Úd·*£ðÚ{ýîñ«—/[ײ A“‰’ÅÌSßËÍq¿6²Ù[g«]Œû’Âl%gŽº5Aª˜”v7?n7,]®&îð·.ÞHÕ“7o^½9rŒ ?0¬ÆúQtƒLÒcÚ˜´´¾A$9í Plƒhu‹¸_ž?r‚¬BB<5ó$Ã??t‚uMµ$ÌHšoH; ' TÎ4šÍÂ7/±Giwéì??Ø  vw'²?0&¹Õc(KDqI†½ÁáýÞÝÃÞÀßu;[LàX1´Î?rZ†jH.ŒLz“¹¸8)«¾x¶}mò?r¶Ùtc/¢†~-KÏl€P¹¶½ Y?n?n‘è[ÈçÛ©¸Ÿ¡•8â½wëN¸ºdäÃj«µ.¼fÖùÜŽµÃïK•+???rDá)îé&é®·"MCF;Ë—SoKz€¶¦Ú¯Œ’Sd«2#…n»-£Ù)*¹$1º¥Šð%½\­wú¡:÷× +#™~®î!¶ÇÁ—{ñ¼­ìºN†0¹÷”† ñÑrìÙÅ·äälÚJ’e¡e,ÍzìK0\¨ï4Ý"nèÝh`K¯‡£6â$]6]û úš¬ëé’3 ÿU’^:“ \î´ ìz„@æn›”ÌZÝ$fÐԦ߈á–*<™Š'Ò&«ê{ÞuÁKÛ·NS[O¢¬«ælJ>?n¦Á’P™ñf0U½öLBiÏ@7ÖuIu§ýlÊk¡¯\Ö‰••tÀGFàB~S“¸,{µ®Úpã>%ÃwÐLÙÄO¿ûå…:×?n fGT‹Ê눔O°f޶:¿ pb|¨¿"~üúwTÇAi­ 3d&õ«yÜ·$Å€Œ‰z˵¡åbéùÅ€©åYŠX8Þ—b –xäÍsF+øC|n hÒ4ì0ÃB,2Ò°z®Ol(Û›ÏõÆE¿:­‘è÷†wzûzÃ}·SI»7ìíêÓ´Ñ£6qˆ¯XíÄMêÙ.6:D:›>Z~Êë#më¸^%ž¾|WIî÷lN$ ÀÆ%[È~>0TxÅá0ti¿lf‘õ´÷^xèšsÍ»—®úŸ3O’ËÕrsF–­¡º˜òb! VÎôpÛçŠòuV‰¹F_N•´»µj:í”÷Ñ}Ž3ÝW8??O2ßÊDn€V&¼w :"×@¥V}Ùaá;M+/2‘åÈ.©pä¡ FæB6Ró“•ó"X‡Å ±Ð 2:•ß ïò£èÈZ–k2P¬D¤ *Î'XÄXu=?r—gÒÂ>O* €Š9G­rýZŸ%dPÞF™„O’hD”I•ôõ–ÉRì¢qOán¨@L’êt¶…2p«,[ÏecY=tP絸4š·rÑM—8ှÍn1ܯ{tš€5j¡’;<ÃòhOöÏš¢ë?0@8vYØxÂÆ•ƆU†]H%I´ûš×ò2[ÞŽ~ÿþ}¼V(ü®·5iòn(ø²¤ÈŽC¿‡¶¸ ŒÉÙÅ©&ÄýxŽbJ-©Ê”€oö¹??uBÏÿTÂ7Þ/œ)Ù%?0Me„»â³0Œ·ãÔØЯý HKt {™/¦;戎^<ýÒ Ÿˆ Ì@ ÷zoÏ ½1@)¥ hRUœóÁ)’Ï´z«Öc¼ô!b¡Î½u0_…#5Jw‘ÿ;¼‚ mú´<›C5ñ©$æÂ1Ñéµè fŸÕN|ðF€À] )×ÃÎf'ÇúMäÜýÐ >g\‹Ö£&?r93WzPÖAU†3nÕö7@çƒÍ‰¤|X>œdZ?nXGy!Ä\#¼ß·ð©Ê„sD1—¢Ã9âESR>þ ¾ã?0Ÿm|H%GÉl“h¤›=¬reç¥v3&ú]õ°.5êØ¬D6ëJ‹tXl¤øPvQ??¯ èÑìÜÐ Ççvl+ui"”&É¢’ø¡$D œ9lOQ7DÒE£U??FD®;¨þùU‡ïßбt<_Âl‚dçWê¶d`š~œ¹=`s¾C•qÓ"îÖ]aùü$½wÆ4Ëy€è «þ-÷p³$Wá7_jæ‹`>3扃þÃûï>üØÿ´ø8øÄßî߆ŸnUÍÊÛ>aë~¹g.©æù„Ó¥b ž¤eŸ¦~vcî¶ réNVKoà£óÒ>©èßÛ—åµÆ_Çõþßÿçÿÿïv?n"_D+ˆ ˜¨ÉŸè™fAýf˜2QUwŠêîœ|¸áà€8êfçÜE (3Ç:}JÈPDL|WP<ø‘ÁêÏÿÏÏ?r—À®Žpó53‹ÉX­ãÿ~ÿo×Öê;DP¾÷Oÿø÷÷ÿ›Õÿ÷7Žÿ·??ìï‹øw‡ûWñÿþÿo3¿* ùÍ2Ìtæ·ôw¾$]‘ÿÃ==ã)ð"²¿?rbikˆÏvû6N\øu‡òhWwB€z£àötŒB\pNUÞ-d¡¸Äì‚ÔupÌ?nbˆJoîÅ×õÖï†9:%Šá–o8a6É€ˆSŒÆ;,á;úô8‰sZËdÍ—×(}Lò$½©~~‡f0ß*ÿ·Ò;Õ é¾]lO¥]i8ÕàËf‡¦)dãÉ¿ŸÔÅk¬¿ñí/Oßn»íMFe|úrtòâ&þçwùᇪÀ³Wüò$É)…Ÿ_=¡ÀÄäùŠúê4ÉaBÈDXE¸$_­’_`\œáD¸Š)#??®a,)•ç‹C/-o~Þ+ è‰èAÃáYêëÕâ$Yš»­ÇtÅ`Ä>“Ïp/R0/FÞò2w:D9ÇÛÁ£}åz.[ÕÑa˜·2òõpM¾"›on¡Ò".ˆH¦¬†,ñ9ˆ½Ì?n/XD»dÁ(^ìiUÔXâEo,=«$8nBØy's\‡K”@ôP×÷º] ìBÿH»·l(ñåMh ¥ð:ßT„ßM JL²Fºm‹<é8ß ö$‚šëDzܘÒJRw¡SâFÚõž“DôŒîÇŒ??âX‚ÖGí'D/£¾®Ã¹§;÷““~þÑ^²€áŽWêÙ˧¯ìM§kÞþÔâ?nš©%ºX¬‰Ï,ý\kK„¥µãíÚ¯‚}MÄL‘…{©ø¾®èå3pÜúT/ò%€œ*$X'³Ë¹G¦2wí4Ûj??—¦»leLj–®³ Ë5W0µèg;;ë,ó˜—v¸a—Ф®÷ï³Û>5Ï{–•ï½Þí÷>ÙÛcV>F{,7Ï­ºéŽÿx£H„¥»nαÖx_q•’âóXocɵ5??ÌÁæ\ý)Õ“W¯ïß ¿qõSì›=kUäY3ÆÉ |í ‰t§$¨ûn wNO3ô°…&ÓcBÖqpϱ 8—ÖáWZ‚-2Ÿ[À%º€‘Zlø9o÷¢;NºªEʯ???rÒiØo??;j??díaâL/¡:˜‚Õa¼Z„)4Oô _VÇŒæÓølÃÜQcΡ·w¾Ç “»0eÅTn2›™TËh‘¢û«÷%1©>ô¬ÉßT|טŸ­ O?n”œ36^=x‘%Õ•&’Šb?raèÝÄ5L²?r³[­ƒ&¶4¡ï`[WŸñV¯þî§;ËtöŠ}y]~µ —Ó(Ý?0¨7+Ïo?0øÕ‚È8îηÝT<àö¤ü6i²LZ©°åÎŒÇÑê/ãþÒæ>£úâ&Xö‚q .GÅ ŠO—€øƒeh*¢Þ-ˆ˜Lo•Âô'«ÉYöäaŠ ] ú•¤bÛ§öôéË2x±Åu&wI ¤ÔmÈ׿ðr@:ú¹@ǵo44‘QÏl]³ŸoÌ›‡jähqis©ÚL³Alm% ®~Wn©.™£ke¤ÂõºÕçZò個lÇÙ„_%é8ˆ‘¨¶5¯±P}HyhâCgB<:¶™o7ªq§WAprq Ì\'ösÕ?nº‡•›r" ßoß5›´®ju;ÏlÖê…÷ªª™¾+èt±òÌo:8Òï}E€ÐUÞ‡É(i"ÊËÄY°Q^äYµ?nÄ^ÙWf4ÃxÌ£u(Îóéxî‹8![’Ñæ¸ äƒÕdsg{0 do Ù½íô/qÒˆ«ŽÌF|Äú˜þ<>V¿Û¥ykµDÕø„Ьš¨Šê‰éIÔ-¼Ö>ß)¾µ,s/™{†½;‡ˆÐùjÏûn=H,;1:l-š?nè¥#RÎ=½•Ýšrظ?r‡éh%1¶OpÒÝI®4ß7Agw;À^æ²EËþ—þçòéì$µŽ?nèN&XÚ³ÖQøšZ‹’…ä7v»:Rò0%\¤N]«t>áËbD*N?0Ú‡[p–šôL‹—½º?0{(ÍòùZƒÊeÀy Í©^äÏ ‹þ*K/ý½8*îªe°cþ[Ú}ïòíð½9{xã¹:›µM&‚f€QÃ×)–j¨–rÞ¿ÞÊýð^r±Áû¯º?nE?r:êMà14à&Ãâø¢u½¡òz)²º·]ª¸Äã ¢¼Áä Ý‘‘=¸¦Œ‚œx•ŒVËÞ¡7›Õ°‹ðWƒ•¡0ž3¾ ;HÓôÍm>P±Õßàí¢¬À쎈œ˜Åçs£Ú–€¥ZÎÚrKÁ*^`²œ"Ü-gRŠ"•´»$ލQq’ÑŠ6X6dà:ƒ{cb+QmG­Òmz¹hn½,ÓíQßÇý=iP³µ_¢uVgÕl’êÇž:ºæ™öÄ;–.gžBHêý4¥”âÄâbiz|¼[cЉ o³,M’\µU†ÿ®£Å®‰ÞÌ« îX€Øú€*[ž8µ3-m¨ÓÓ1ggV"lïÉÕ1?0ÌwCðÚrV8ƒŸ^[ǰ¸â’lß\“]R®‰¬l?0¡ä­7÷_ôÙ´Ò.Îý˜ãcV!—xezÎ C£y Òft`^?rJŒ?? dz¿ã"¬Ô\ˆîâ-‚k¯O3`îw°]èó%¤36‰ÒgýN1b»if—ñÎÝ[ýƒi÷VÈ¿ü:Úü’3õ-‰ƒåâ´wi§)‘JùEcê4¸iL»HV[Ð’>“‡ÉYH:ÏT&ûâ8)¾Íbû›8«@eTÛ ß“;43Õªîé_WýïüñGê¾Åîž@¦Goáÿn°¡]åwÜÌ–Ä,Ou1ÏtÓ¤¢F—‡]ugã°­jžU¯ªw³p—G6¼q‡ +#ßrúsÑæT†nŽþk?rìâäÙÉúÜ}.ÃRIÎ*¬0là _j2Ƭ‚t×—$ëó_DÓioÅ B 3Ž-Å„sD…Kc.·-ö‹Ì«ó)·ÈÕöŸ( ÌÇäèÛÁÞ]¿µ#œÝGã~Y´Œï['Ÿæh÷_Q–‘v‘m6¢Íá?rÖX:þ™õ;Ê[*óöµoõ”2°µ“„Ì•Í(«P \q4«pÁ@>Tc *k1°ÔWôÌ??¬q³ß½×bqz•?nóH4ãŒÇÇêÐfE WV…ór†«J 2da‹f7¦?n¥níâ;..9rîµ`ŠP?ra°¢K¿iE€÷¥QÖp'’®Ç+çØ,.øt}tÅ‚>êíbíÜ#§^ÅT$Aý_M|ŸÕrx)“AŠÒ†§®L¤4ù1XÊo\u‰Üýòoý>¹tð¶w¿wHó~y~{‰~[??߀Ú€°LÑ9‰µT´üv9Ÿ–Ò”÷bÐGkÝŵt³`Õ@–Ø(è¤m(¯fä²$(Æ7Ïâ­ãh§ªÊ?0çÄþeî–µ²ñBƒ`í0«\y6–÷ - Z am)5’Je-‰(È_m¤àjì.”d³+ì_(Æz[ 6¼8uUôâÑԭЧ¢%oc/* *ÂUDȬ/çøRžÑxÃÕb±bo+•  ?rƒË‹??ü1ÜÞç;žZT?0‹;¢_ê|²ÿËᤆoN~ôåf]ˆõ:ÞyVÛ¢Až‡‹%\ä¸FNœqo°*ƒ–ðå4!j{4hæáˆvA±Î6’ŠX¨°žá —ÍÈrƒñ»öÖê†=ˆÀv“Ål Ëf¶3¢?nÎwì$Ê·\²ÙìÝ^ºëZ™x¾m,Zô ÑÓôîµÌliði½a!ÝI²g‡n¦Îv•X:ï^£,/wr«j?nÕ¶š‰*Ë’庻×U¾¢®P6¸¢BÃ5Î[%߯É*E¸ÐYª‚Jôf£1¹éÑèßä´¦¢KíÙ†þ"bÌ`A.µª82ëÈÖ ï8fÞ‘jlœ,­`~ÚÅ€‰ž%™MîLG‰ßô1”i'C”nIÆöXsò,'mä[@æ-%@"oKÍrê3[?0Ð=š“‰_[Êæ­ÃméhÂmé‰[?0`:ÜJþvê³›Åvö´²ÞøÛjpÌ+èM²YC,©«,”õ¨ÄX-€p]í`“ÝÀ»aZl¢ñ4ÎZa0S¶‰¦“@z_Âcÿ}ž±á¡‡´>3Àd|°Nxpâ??NÇý·:ÿw1¢8?0ø·=ÿ7âüßáÁàïêüß??Îÿ€*¤“»Éh‰U¶û* Ú°¹Æ²’ì¡Äà‘/UÂ7$uÍ1#T÷ñc¼)ƒïN™À³/)“2™¬‹ÍàAs/][ê˜7%’šê(!Ñœ¶5ÍâÜ9Á3üTéCWÁ:¤¿‹¦»/Ôð3$]èÐo—`4 Âý ´ÁË•ÂMYç<ª%æbƒ”óN- Ê4Yf?ræi|Jxj”Ò¢:ÖìïÛ8”—’z:«wZÊpš•Q;𨷒څóPïˆ Ñ+6u­i,Ó‚Oø¥€%9NBeµm”›yýM›Y^¿gh˜»lžêjéšÚ×±5eóp3ü°ÝhÚObÚÓeª0ÓëYŽÞT¬í¼ÖSÜZ*ö°$ô߯??êdÝ%¥ú"FrQ7Pùö¶¨‡œÔ^>ìJײaKÊbœb}«^Ã:Cûæ›Ü4bÃôOÌ“bOÙì±íäå-bõKÎßqd|~ÛÇK¬©w ËÙiQ¯¯¶C±‹0ì/ê¡wòòÕÉËwMÖ6!yýƒ÷B™çnŸdËK¦3ÜVÊ- íTÝ ~~ó¼ƒwsý>¿›-?r/˜ea<Á³u§??Ði2>Þd1ågLò›sL«ñárX!ñÔ<ÎNrÿâã)V¾~ö\ëá›Oí/??<³¿Ðåñö6 ›“ÐÊ,ùî'}¤JÛb`FØØdü½Âx¹‚zˆdØà:lâë°mM¿}}òxôöÑÓ“"Pýè‡V.ä÷á¨$Xºúž bÛëw/GïÞÚ±«ÜÿèþW‰ÝÙÙÇûAÿ¢V•ãKΠí…1qIbv~ýæÝ/ty>X„%šÕ"Ó,Z4 D³wîD}¶ÎÃâÏÓ$΋ÇW:4¾i³ ØÐÕNÛSM\ ôÏÐ@ Ô`Š ‚Õ<÷ ‹J‹eÓ5 Mbq‹’ϤVuÏ¥¸‡WžûæÇ¨‡xC\ÜïÓB`zÅhGx)>­q̽®R”ùík‚ò5áH¡7a4Ãî4Ô0u6’ý‘ Ò@Š4“]¦Z;fè~ózbøï^O`s{Á:bó·Ÿ&>œâα0Î(¸ÝGË«?níjöÕûàÌæ(â#Ü¡ÿ•!’2‚5ãƒ3ß×̤ŠÐǽÊeF\\gô€V"ÖèÕUÌÃ?0a>åJœ)^­Û*<‹«•Å;ÔSsÖÞ+5DNeh&îž4+Ì&fk§Ò3xFf?0áÝU™Y±)}_êïÐªÅæèÆfévÐÿ÷n]÷ø‡±ï"_§1©L,Ÿ?r ¶¿?nyÑ|ãȸ"øm{Ø\áÝ¢ÃãÚ?0°Ø&Óÿ”cêÚÂUvc]Ö¬V1Ö@µIi°¨ý¾8Ãm•ù˜>DJ8¢üÆkèY†Ú…-Fñj>gK‘èDL¥'”{/n#"!‡]ÅY0Ó×ÛY¹Tò$^k‘Ùqý*šßÔã1/÷¤ßË0ìE‹©H?nTRIXi?0©"ª?r¤¶¡í0ë`‚¦h×"°£Y×È‹ög¨¾‹åƒ_~OÔ¯Ìm¸-R¶)Ù$H§ÞÌ—eó⡹l¬5üúº…ûb!Ê-ŠØÁ¯Î°\ž@õû±UºÍþu(ÁÆ9›µc,.aÌTÿÝu€·žf€ôë=ëÜalÝh+ƒœùíiµOœ<÷–«œ/!5å*ÄáT+¢ÁÉmB3S¢HË%3?r7§$cã†YB¿XÁ??v¿Î#d`hè@'ÆÉ¦$÷L†kõªì•ïÜF ¬Ø^oÒg”÷ÑZœFÿË»p˜¨ËmûÑŸ©³N“åXkmµ½k Ë‘PÀ /­±€ Ë,ŽŠ ÀE©0•i’,Êlf>í­:¿ØÙÿôÁLìÖ¿øÓ“ѽ~ßV fiŽV ÚÝUÝÞ?0Þ½+!m\†TfæY­¤¨ê`Ñèãpü±*V vûpúÈ´²ÌO¿{ùuîú?r›ù"æ#”>Â_@‚Hq¥¿ø7ë©xz­^evÑWާµ“@ãA1Ó¿ì˜æt9WU'YÂ=lÌÍýΖ  ÄRë/¬…ü“þö#ߪmŽ +dœìE3 +#ºmZ+rSçÖŽé·2#Mt‘æ¦A¥¬W°Ä¨ä–D¿÷šfÝ(…Ÿ ˨!¨ƒA%Ý:äÚ´5 A•½ììX̨Ÿ=/}^„åhÆ´Œ2]çh‹ÿ•,½KæÀ×DBÖîêË£¥H»«™0wXyå„¡Råyéý%çkƤÓ:ZKq>ZKGcŽ*JTÇ„Îydi6?0«B®$øäN ‚^ÀÚX €À À'ôéAèóøqNqay.£&PÕ_>Èðb)@ð½¾íô{Ôø­ªCjy’ DÜCÎq«7œ9??F??8o½ Ûê*Ç¥*]BÓ¥œ(S>­6˜¹Ûö8Ø“(; "µ-âyëÀZeì©¿Zx§k+Œ¤¶‡•¯C¿DtfæßL ¥f¬2Ö??€È†HÍùi®E¿Äe÷XQ-Ë0'‹1Ébt£k°5JhÑ»‰«:À±©Ð%kE??À±U65Èc*( ¨ÞôК7>Ì??²I’K¡Òá­pªWØ[&f<޲6Á À_€xzÚèÈì›9µkuê³±sFPÚ¦é4ºŽ²Ñ¸Ös¹+yÙfŸ‚cÎ)úÈsY?0ÄÆR3ƒ_Ÿ“¨T½5”`Îl¦$5å?nx cÚFi_v›=‰˜ñS.}<ƒm¥Mz_Ý3%??¯kQHuP·¨&3MíÉ4Jµ°ú´^l¨Ð]ErŸ»)‘§®Ú"‘ò¹e"Ï–B™åÌdÝt«R‚è¾-§ív' ùlÂ$qº›}zdkÅÎÕýì8ìQK½x¶áER!ººìºQCö??³/Pؤý‰©]úÈŒî!ŸK¶~ª7ލ¥d.˜×i¨y^»Õ«Y…Çoµ h63Mˆ;«ÑgÜE%Ͱ+”Zn6”óˆî8à3OÄ@}úÒ„!w[€Ó–“Jº!dËyÅœ¾Ø«³ÝOš´{¸ËÍ"ZÚM’Õ|ê˜-#³M„¡[©}k†ÀßPP©½‘?n¶5™¿åÞ6Å”yño£ó8 úÛg??ÂÒÄÅNúƒ¾$ fcÉojf}õYxM–Kl,!„¥Ýv8Ø*x©n§è_Ð$l{k´»äyÅê2jËmF‚?0üÜÞÐ~M¤é‰ƒo4e®?r‘}OדH¦?r[ÜÛ³_)eÚ$N»ô‰fº«£ƒxñ§AË”µIQË߸¦Ž¢ª»÷!›tëöþâÕK¼ýÅê0í¤¥¯Èv`ç¹Sß~‰×?nNÂ()à¦á¼È,ž—ÉR×ŲJ‚˾)2i"¨‘˜ðœ—,p´ÌxmݰSò}ÿÊ ¸ŒÝ\m¸z¬A]÷¨ÛE³›Íœ´–m[÷o]ö Lí]Á8¼ti>2ƒYOn?n??]}³Ýbã׸Ž+‘kÓpÓ™ŠÝµu\9ó¥={nS*v¼†%ÖÞfêõ<Ò¯R¿Œn=fÚç^>þ_¯Þn9{x©íØ—–]Gû¡÷Þ=úñmÙ¸³Vè«w· nUpÐ1‡áųÓÓDß¡‰ÄÓË3$Ûã¦HÆMœ7ÀË÷®ú›Wï½;=è“H¨|ܧ‡•Ã{ýOÖYY¶!¯]t´àúåigK\D˜SúUŠ#lŸÖ7DÃQƒµ·š´{DWÔ³.˜’m&R≄—¬þæ‘’å‚££^.Á÷e=õ™ƒÞŒôýh¡H¥v»YÊ>õú3#޸޳_½’Ç/?r¦l¨h®z8þØÿtôqðéH]þ7JÒˆÎË´Ë®âRhIÇnûðÕxá ;?r±ŽÎÂNæYŽ®`F]Æ(.2¢'kÊ,,Ÿ£|ŽuMÕßÍÕFÊ]ú¨`’ÁNX…oêØåAàv*Ÿ»q‚ˆá±LX»ú£Â°ËÄ‹hŽÆA‡¥Õâ™Eñ?rn“tu#*ÇVDi‹`©ˆé­ú5”Î(•&ÒT°ùìˆ+3yiR ¦AeŒ›¾‘:äáq¹ ê{Ûfé_—Âú(«k?ný?rôÔ$V¾ÊQøuøïr<ݪ9>‰Gs=Û™>Ѻܒ,)+14Gé*&‘ã(ÄmIJë–Ð@U¸9½ŒúòAŸ}@+¢;˜)dŸÚfÄðÎKVù±¿'Ž&!ß2 ÄÇôóÙâ˜í ¶3Çöj×{˜‚1ÌË4¥éŠôUöìÇ\åS [“]„†ó@H]ж$-ßOätÕû¼É?r‡Å¢â·g~=‡h&Â$¡??†dZàǤÏo곆ÿ_Ù<ÇåÖ1Ðõ=Dëˆr1#P}G{P÷ņjõ¥~D|fŸ”Ù¬‹¢ò„ï»u~J‚_®rt*y,æ¶Ü°ï¢÷Õu¾ì*mé|;Ÿá[M7rßÐù•0Ëé^~£~aL“wo›U¼9Æ"›E·(WÚYS‡:Ðë ÍÂé(ÐËù‘È(ZP’þÍ[0K®kJ2?rˆÄ/kCÊY(iÚ+®"D>ïòsõd¨Êƒƒ¡wûš/2þK5ï"fÜ©{ÚëõÎ\…Ϩð9eÊîÀ؆#9£ì·%¿}¶Ë.!W>¢\²:zn—k^¾ Ì×!™NSØ<U|š¶æ¹ïÞ3ÄBêr1eFÈS??5ÅÔ€?nQóÑñg‡wG‡ÇÏYó1®ý£ÃOŸ÷ÿpØöÙéìÌ“X[0ž??;¼£>?r„??|vØÿôÙ3U_cc\•}3ô¶/| G§#û#ùŒÇ'‡ŸýáJfýÕ0•'ts.ãöhÒŸ•œÊÿCrlöÂ1¶‘/¿ß(ôÆt˜»¿•¤ú$Õ+ úÙQú·\Øn?0ÇRàu⪸°ã&À¤dÈÉ IÔ¤h§ž×LKX½Y#&A¿Å—߈è]Y,#}WkùÊ_È7W|õÝ??ïãö 2R?0Z±È¼…kÁ»Ùïô¨æÃü'¹MB–Æë€¤LMOf;¡À9p›Çô³#9_­#þ¸ûfÔò+—Ò~¸&²Æeô,¤4àxf_ÃéÛº‹›Ëɧüí ›ösp—ê-ØÿßÛ®Õ?r{,dW,}óÇ#UZn*H¥í=)'ÐRg§’ù0¼KT-b1Eˆón2FYZt€d4ÒêÅt,Ö©°1\H_ù9Î?0_•A*²ß—¿=O)°Ii¼cÆÁêµV@Ú 1²~Rû¿± "õ@šÔ0©š õcYȽîMŽ‚°ìp³¢Nˆ~"ãT&DáÆ’Ó†® D ßP‚ý£Øf',à˜>¸MÚ0Èfü„c?nîÀ&ð¼f]#dÛmǰíQOçLáZ²NY¡¶pÁô„(†¨.mÂj@äFè›y?0­‡›w?nü>a¹Þ. bÿ¯Ý$RĦTéoµe„”™FÄî)÷ôÒKîo€‰Þ—Ñ1äyT?r_??}Nz,Ú åm˜7~x_]Æ^;°ßG•/ÐÖÉ£ŒšÁß› °COSz–R ¼¬þ‰ÅaI?04‹ÅŒ˜ªª IT‚8ÍÙÉú£“9ÒJ y·œ^Ÿ–g_DÍ'ñÈåE~ZÑ0VôprgcZ¶&Ý`Ô±bìBçM¡»ïxÅlÓvÅJXì\îÂ7W +#b™OwíU¨à??ßâsz÷¥¦bæz`~ûï9ß¿ÿ¹?nžÃ1’?0È <ÿŒ:ú¨–ù´óìBèLWiåÕÕ±¹D±¥¤f2°¦Y& .š{CñÄ•'`L£XÂå{Z£þDXrµD–8·â“8HÑLá‡qC@-3Ä"ÐÃå¬.u q;»¡a‰YWâͦl¶ o;;°§™C§ã€$jB¯®RjFA?r$oÄã]Ö‡)¶¢Á×£\)¢§ö8Tª 408‡Ýå5»N]|LjÅŽ¾i.O™/W['&°º°Is9.ÝÿM˜Éëþñ3ü_ÁL’pNÎ?0è¨÷î}² ~Š FB*??o(±-Öa¥×ØîÖ!€>M”A¸íëôÊÃz/dvõETN1ÀÝz¢—²åÞŒCŬºÎóyoµ0oË_¡kÅö¯ÒP¼TêK&Èw<»¿™ŸÐ³k!ÙijRQw{#1à ֓^lùQ÷‰3:?0ÆÞV?n7Ð#€~Q% æz I`ª%ù·€5ÊŸpº ‘%I W…lȆH æBc8‚ÀáŠ9à2š®³[Û˜Y¾—ÎC"õó1†<ÞrY­¨€äEë©7Fhæ´ö¿?r±?nÓf‰©QJE—xB;¸jVÖyÖc/˜ú(±i­7Wrµ #‘C6ÊñÁÑŠwjt¡Bzœxœ!?nçξޥ§ùå¬ñOpß5Ît"ý_"r󬔟XÖÙÎë€çvsÖYL¯E¦Qö+YøM>¼œ]?rßÄ›?0.ˆD›H6 âÁgâ)?n¦Ç;åR;³ŽÑŽ‹þ:ûÇ4—4£ì„ä¦êýÎJI££ÀiD¬ÃÝ%õ/Æ* ®ÅóÝ Þ¿=>TNÜ£ö|Þ!ÞY©rŒ~$Ç(z©Écôduˆ~ â@,¿›OÊ]ñr(Ç,ýí¾7GE‡±àýùû¦—þŸüÏ‹ãIv‡ôÏãüÏO(f½ŸÿùÙÓgÿŒùŸÿ'ÿ3Ü‹lXŸ¹›¹åÙœÀÕ3—Lwnbñ©‘ÝS8;,í\Î??¼xñÃ_ÿ:øÓ7??üðÍË4”Úy1??¾Z=Åwrj*¯x7hÒÿUµ™¬Ù%¥|ù~øþÕ‹§„Ì’­ßYC‡(ýÍ‹¼à ”q°›’ùf1«óyó½zšå` n\².·Úí뜛Õ?ræí©ÊñɨƒGHO)õý4ò‚nKç|Y×s5ÄßÑOÊê·=¡³¤?0üWŸpþ??@ßœù†À¢~Û©?0ñÒÎ(od€â£Ð’:‘^âr€v¶%=èÆúf¶()ŠyDQ6Ë+®x9\æ«™ù‚?n© ¸mÝR€Ò‘E+‹˜81€^F…2˜dru5Þ}ßuïù…M+¬°DÈš‚¦«÷é݌֔ܗùèjP!Žf³ÏÅ(`é¢ÚKL dŸèÂÕ+ãUÀFt6F½sD™®“ŽÝOu©»à½œò¢1ð??ô_°/”a‰OËÝ2,٠ĘJ}}D'…›—ýÃÓMS^•ÝMPé€WðÃï}høZnôAÇ#÷ØêOOO~ª'ÔëÙdŒ*úY ÇaÜ ü$X^³é<ó 8^s¾&Τ’Ò™½‰"”ñrMÅÃx×¹nGãæp§M #¥1Á)ã^â‡z‰³ˆÞÙç’öGÇËjx]§MciVMÑ‚mÔþdj·éQJéÔéÉ×|}¸Å·x]vÉÛä@)áìó³”9³ÕOº×Œ´Wäð^¾éñ?07ã›)AvÒ°Æìsü§Ý¿Œ*·S9O{ä쬣÷ê#ƒê²´¶§^†&ÞêýVºß±y¤J&D!]{zUw¨k Ê ;_{Ío&·$SlÜ¥¿šì>ž—Ë鰡ºtäŽg½˜–Xò89ç$|àX”&ýå]è_ØðH8Jr??´Ñ¨ÆÏÑ7*Ü[.WQ¢‡yÓ´Üg:âü8‹ñÆ÷È Mɼ¹+œÂùÚ2…VÊט­-kR´mVÄ „%©¤*Ùñ†¿*d$†"¶ê<1‡¿ä°«ÛL&ÓõBÏB˜Ù´;†îä™1>Nø»îìTî^¼Ãâ†þ\O¦köáá DÊÿÐo5¶7ímƒ“ºý`yípý É`G[0©ò^4ú)«t6¬»ÏÇ“ü:ž\¡qu+ÀOã” þ€VàsVò†!>ÓU`€*''xÏ>â¿_ñþú%ÿá¯×“«\C˜×EÓÑ2??ø4tíøï_¾>î¿ìŸ=ùÿÅI«ôõÈ/õ¦õ/Ë$êž|ùÑWgO’è—¯£_’¨[ué×E+ßìÿ×YÊ„%üý‘ø5–v—ÞÞ+[“™MITT·8e.žÈ[b|`¢tPW·–BB¯?rÙ,)ý;%¡ÀE}Û f{b”›è š?näÔSý xaø†N??޽^¦®+G}É‹¢ÐN¨Âz²—Ê]€T|†ryì  rip*§ýý#ØÕRAú»<–’áE¥•dv÷zô—ŽêÈÙ+ÓWAÆ@Ä©0Sÿi ??º²V:MÊs·f»ï%ô·D„Ý4àuª·±ù^¨˜¿WõªheZolµÃ­?0Ê`Xš>¼œvÛZ¡u1í}ýë¯ÔZ™¥??yœÄvá¡ËéÿKQB2µ­U…¾‹x+˜ ÆÄÎ ?rŨ‘ÙHi6é0Î¥$²»Ùn^Ú²zÁ§.¡µ´á`o…j??¡ê vß½Ì\3À9>k)#lŽŸ°ÔMüÀèˆE²TÈM^æ€C÷wú¸e°1HܺB8Ú·kŠÆÂÁ—é  #6jK3¸+e’éeC??îI}/‰¥3.?0È탧1Áаùÿ½Ò$øÀ]¿}õ_itöÜÜ21h¥2•™6V[æ°ÅþsÿÑtÿq›ßõý¥ÿèeœÞ§à=J…Šl}¿ù¥ñFY¡objH6ˆeèÓ>0”s|³ÝUàøíf)»¥¼±¥•BJ„êwêɋ틦µãë¯qñþ¬øàF)ÑÜDü±>ø­¥ÿøÑã_7Ÿöà7ìÓü…ÕŠµ•~ý•Ú‰<…nF¶sÑ›Œ*ÝÎÚ)ƒMöÌ£4Ì<çÍúŽ>÷mrâÞw^»7¥ ˆûœȵí&éºðˆöl÷T„|h¹‚ô¶?nWª1AÆœ?rEV¶`ª„ÜínNûÑ´YH=DïôÕ±)enF<ûÇi$ú"ú!*#f3¥á??‰á¤ª¶»?r×`¸ßÞT$ü¨|Ïܬ5Ф¤m¦îO~^ Êyñ†©µn„BnÌå¸÷¶Åâl;†b+~uÒTiw oä…ØÌ?0#ëA¾ÁvœÆ]ÑîßDn¯ô…‹!@:êƒ)êÊThMðê¢éÁ1@ň;âÁ?rŠWÖóa’H#_Ñi’S>8E~ýyüEãZ Q“ö„èÐR,ôw›ð’–˜˜8b^6ey_Ùý¬c%5?n?rMX”1:b›ÙkéÔ2+[%Ù¼òŠ Ê”B|V:?nÄX97²ÞpÓ€j…åáqµçë«,5edÚÛP0M7½W—?r*ÉöE[†rã;P¼m#h¿LNûcms¤y¯}í‡f„lf(H\d??y§Z­÷Lx¯”sïÒÒaN½Ei{W]^?r?r`à›¥W’z–ÏrEÖF &ÄèD¾áÈ’Ö¯üÚD‰¥ìr•[Hß??ýV:³t}äÓ.ažÆ³E‘­“Ëö<¿žȚЭ³Ìy<FTwÖýÒ6LöÝ©vGäÁ0ËzÖƒY±:ïbbQû»??zWoé'‡¸œÓ·’ø%ìwú}K??oÙJï=Zµ™À6øá;†WÚ~)á¤ôEI„ŽÕe‘ bââí~Wì¾/ÎçM.*‘‡o'rAô1‚-??ÓaÝq×Q ™$w¼öpìdÈ$å'5³Tù5%Ø•þ>X¬ê-?nÊ×xå~LÖ¿ªãËi¥}å«ëd5¤KhÆ…=Üÿ¼±Ó'‡€wÏž½‡a¿‡߃½‡õ¡õð¨û°‚:Ë35S‘§¢e]ðy2ùþweê=Ø©0È 01^¢ªÎ9ü¨òÌvŸÏ­ç€‰¸fIžµû#=˜€*ú"ÅÚ ©¥Õñª1vtV¾Ûºµ‹{ÄŤÂY™>î¬Þ³_eål>‡3–ù–h5g9ª6½w¸éùÆ—è´ùgô`8¯llsËKL›ù¾*†ó‚î¸×í÷¸ ˜3+ÄÏ?0ŠJ“ï?rÆø[ÌÌ@ D–1vãM TÙIs£ÉõÐ&À7ÝB~‘A¾¤^„Ö¥Bçž‹«péî+Ðcã9q4Á‹Ê2ñàw“Å`^¾ Qïëþÿš&“È9ÅI·–‡ƒÚy;dy–’ª=p7À͘¨ûz†vŠa.›†3,sûu1£Ó¼€Í5‘ÑœØäP語ƒDš‡l–{pŒl=G?nmÃØ§s¡7fÏøç`V¾UQ/ԇƵzöX‰Á)z­3†ËMG•Eú/_î¬#ªôaõž’o½!%7¦$`©„‰Qþ¢`O&ÃkÐ{^„M·.¼MÁú+7…òS'-j€\q÷ôýßÎûǤÌã?n o¬€úRP5nvÑ‘ˆô:ð»yÌÏxXÕç³YmÀÌpƒ*óŒ9Eå¿8tìað"Ö6,½VÅÑ|õpc…Ê¢˜Ïˤ-;HÎ=u¢<„Õ»,WëµÌȇ†ùFZ¸º)òµ™êBŒüR¾5®Í]­cçóeˆÃ§ mäÞ?n£%Ån£¢ÞÒ´’ â?r׋b MÔFd ñWÔƒn©FŽ’€??kxlŒèº÷Û4ö]•2ãG“e??L8/G·2÷v‰+äFÖaåFIÃ_7€Û¿9ÞÖ˜˜y’Ãk].áFL®ý/2ßgÞrqgÎÞÖ݉žD:±ƒï[#¬^ì™ËùÂZ@X€ÞVWN!ŒóQÀ»¬l…ætòrHµï2.[~3Z¬Á\¦??ErÍqÅÒ5ÏÃM]›šû;4<ÈmÇ‚ü8[•<šŸR9„THãð©Ø’cÛHY´ö®TƒÑ–RÕ³,Mù|°Ã™¹æµ©#Œ<"›8âˆÕµÙ•²Œjr-rÏè/^Ãp±Ã98Àýªºœ¹3põ»rÅ͆?0¶?0 È˘æ—?01ËóÆ”¶ªÕüŠm†7½¾i[Q8í8óÛGеâZàM<æÑD‰Š^pš/ûʵ¢H’{†a œþ¨'#Žøÿ¿ã½MqÛ0VØ6a¥ýá5Ú‡¬ˆ>Æ"6”?0]ˆ‹Ž¼–-Y ¾Î¨§QÐæ?n¤@€©C5íp¤ŽÆ˜å2‚ðíåAÈ’Üœà½AÏW?n€ZÌOHF½ª“- ›‡×*ƒWM/?n.fsºå Ç6d–K?nÙø¡€eÿ¯Ž~kwcXæ~>«ª ݲhžýÈ_´È˜á÷M$1Zxô´XbÍ,æU[X±@ŒAzÔ èO/p?n¸‚ʯººÇÝ&ž¹{|×}jV™9‘ÕÏ?0´:È£&Š&³›ŠØ’õf1)óÞ*.ÎáÁ§šÈ88ý@¥@ÄA%CÆœîì,ãŠmÚ2ÖÒtˆ·]]}?r&Ræ§O—@¦VŽ$±¸‘%Õãú‰ç^YÐ9itRMI™³Â¬gæQVc•Ñû:‰½¥o!«Ö`Ïéx´3šSï!‹>ó!QO´yõlƽ$N:Ã+«Êµèõ6h¬ºÊ"‡\bí{>VïZÐ╈3î·Eži»X…çJÅèÖyÝBŸxÅXÆPJäÖ.‚ƒF•À/¯?0Ÿ?nàÃ{C¤ô×{žß.èÜÇëLI¦“nÚ¨Ñ*$Ö½#²„"¹;“????lþìGG­¦ºµÕö{ÿP‚Ù?rɃ§ó?nv8\æC²\ôâô—‚È$ÐÔ'±¿?rЮ"”Q0¹¬aFrS¶@f(ðLé¥=Ò¼+×@ËC •6{ˆ^k¨`{­±H©}#~vÓ¨jZ?rÉëÙ9F„*™A\r‘óy#Mªpãw|–t bÙë‚øm¯€žH%ñå§»zâ¼Å\Ë6Qÿ¤i$5‹?0Q lj€“ Õ‡#êL r¢m,•)Sù¦4åøL~`?ršU»¨ì­ßÖɶœG8²ÁœC1#»†—±”sa1ìsRKÄcËÿdL³lÊq4-4‹xZ¹„ ·Æ§o¯ÆlG»Ù/ãzËû{¦‰˜§ŠZ[àØÞz0Ô¤aíflG¡gOÇ…]ß&Ty3ùÛ%°€Žé&Óæ/­IEW0¬øÈkÚ G¾$’ÜWúô•=©ÇÛâìŽí8œïb)@ý8zßÂÒP²©U*ðºwÚÐc ÓX€øX“¬N[SŠ'æµ#TÙœ66‹²vÖŽ˜?r$Š_³=<'nà 2&dpd¬³Ÿ²¬›Ì]¿ ?0}°WÇ sl†Æ•e½zO¥î3ß›Ý`b˜Ç ?07•BçzŒ®Û9øŸ ýç>$[kc¤¸6¾Àƒ¡àG|f×4zš9Y>5ãh°i¾¦ûMIÿø,LþÃ'•ýŸH‡KÊ)-%Cgqþ|j—)lW7µI„N†ÅçÖõFÑ%ïz³8½„9ìÝÑÅ.î(ǺqˆáÅ1¿~ÊŸ)='A8k‘4ãú¡_wdÃï:=‡ •Ã:í¾YÎé?r{á*ÿS£Ü<±=—ÞëEô1~$Ñ'ŸD½‚Y¬£Äª&Zí7ï;ô´G¸ÑiÞ8ý»Ld0T¶óÎ10&à‚÷›%S`>”dÁ>â¸sêÀ:àw84Šó7u®²êW¼É«RÞ6}ÝÝÓV#deÃS£¢ßX=}Ò½ Dkú„5gß.M¦ÓÅ@î58¥Ò³äDãT,k1ªÒl63W!}’­h?0f·V©æª(1oÂŒm€…ÆûÛ®ð %b?r•¬yRDT.Fþ&¾ÌÁÛïmê¯oz kÓ¸S*§ŠÈVölët!ÙÔ@•unÖ½]PvÛÈ›;ìµÊ·aï_ˆý­àÆ[T…~ p§¡‹”¶¾Ã¤ª<“¨àöÞ¼|Ã:E4"mh!σ1?r/çN#7Ÿ›Ø‹ÔR,†êü n4š údFoxoˆSPl˜àbz|œn¨«çlŸ¾ZE¦US®ïĺ Gî·Ü hœbF_F,u«:IçB©Mßµ&¯{plÎ|«ÙIM‘{ˆž®Íå´ñIôôàÐ3Ò°Õ,vÂdýV5í üJíÂX ?n­ýT)4jÙjØ‚zø#ðLµuÀG7l¤2âeÏÃpÓø”cö€ÓÖbo5X"#Ê}±‹ä c?0_=Ndt?rK ÇEx3NV–EM^“˜´šã‘†r)ëS¤”:Ò‰,bhgÙjðck’q•x„’¢¾«Û‘‹žÑ?ræ`3+s[Þ!5w¿¹©ˆe £ím äýéW$L¼?r2#^†Ü^¥3RÁ%aÞ„…«õ%ƒ +#¶°}h??†eú;ì2ךq‚*œ¯$Ù¾#ò„³ƒ‡A?0©è`€†ü¤ÓØ Ý&]4¦p\‰/5F!`ªèfR$Ñ[ôœí^DŠà£ñ{”ŠyÍÚ”®“úMˆ<­¸SÇH¡{I úûV›1êM«Æ›Z ®>¤Ð/{Ýu$Æ“ÁC¾+¡!¤×Šëë“”Á‹á¢|XZÀRHXq*h×Í&½?rMD¸5ƒ¶iQ·ŸÐ„?0Ggs.'Ü&Á>ޏ^)u`ܬt* m(ˆl|=edö76Çrvð ­˜3̾$~§Ðx2'Rúøq4+3xÜSê¥aXGtQ:Š­€SUdYNéäŒFçQo§@we>N?nj Uqù%¡}´/#cµ"¨3‡ÀÓIí$Œ¢7´‹92z`8Y?rÕ7?rf{hYsUãð|”u‡´5–àûš>¦{;‘±/g·lÃgS1šà«7¿ã„=±OûöQ–` fË(§=/RUsexŒ„#‰b™kqºC/¶Yxaldñå0[ÙÏ0_«PehK…èÙ,þÑa`+š»bXÛ;ÂÙö‰m/z{ÁÛ†ÐÝ®èÅ¹ã®‡ŽºágäDOîOï!8ÑÄþAÔ^3ˆí¬ÖeÝ0YެºfÂÔ$- m%?n׉†â· ªMâ0k.Y|T°1}4Õ??<÷¶<Û¦ÀU³ ªKYPõ0ÁÇ—­äê|œÂl™BøÔsJ„‘•Ò3±Ì׊c?r«*/ks”¸¹ó°ÒåÍÑh½­)ú˜ã'¨¾FA|Oؤ[ãòòrSìlR‘³<Äw,ÇË3ãE+‚8«¶Ó]ód´/LZB`K9o¨4íuîMúÕÉ×q?n«˜TºLÏv5Þ Vv®9Rkl¥|ßš2Dê¹óðÙCjšÖ%Û^ì?rM:JºKþ+½Ô{$*¸žäU& p”&š.jäWõú÷av]i'sSHÑúyl#@èH)\Ð2¤½€¯§O©ðhèrc6yŠ3Q EŒpŠpëÀ?nvˆØŠ¹Î ˆ·mo0ŽÄT?0ûÎ#ÛÁàC/¥Ÿ¥&ÄÒ—ó‡2òÁƒ_ï"pnaÍ™,ŒKjö!×~TÛŶ©ÌÐ’™º…€©Á÷)lV%}+.x=â¸ËE"–sÓiA?0˜2Y*²—ØdˆKeFW¶5£¬Ër¡ˆ9ŽzQû©ÑmÝ+@Q…Ѝ(}€~RÿÓU rˆ6‡aÛãàr3Í6IyÉ`J:3ØéL ¹% ă‚ u±óS¸;ÔÞX&{«cƒÝ¦ti«Ý%ÐèäÝ{aÛ??;|úÙsu/Ç'Rw¬só©‡ê3…¨:Ó‚·'û¾ÌV߯Åê°·2?nÝ”¦;ŠªÌœq‚Ÿä{“do6²?nEË{V&°ÃÊ´F?rµ:'1JÚG°öžÜäÍè¶û‚½:r§YÕrEÀr;åÚ27CA`‹òýNFQ°k•TˆJzc??Ê$„•$æì% ž6Ìû§þšB…@G'qî.}:íÿ´Þ«üˆ¼Rô@I¸gy¡ãÅ`ÉÕx§KmJ¶œTìø´2Ô’-O<3±y^"|¤XN–­ôæà0"3jߦUŒª$‰¨©ìýç!{ÿ®ÿNÿS¨äEÊî ¦ Ýï Œ8½Ò#*‘ý!©Ø£†iÝŒ¹VC6íŽ9ýƒºˆ^ƒ&‡•tƒ=÷Cz¢)š Ìäâød@oà«~´ÔÁY¸˜cÚVí\µ$iùq'ï..qü¾ð$;aƒ—"¥j?r‹¶¼¯=ä??è6ˆNv›ãGú»ñˆ?0yâ ÷¬£¦ù\QÍ'ãÌJIm:)Ë›¯¢£pà¡:Øè×GëŽé2öê&ìøšh?0óBÌ[o#_mG„±›jÖ3&ÈæGÕ°(8fÄ¥G4œã™’³Å_ò|×1$'È$›.‹ª$2®.ç·üDßd$3¦’3ævÅÌ?r!ƒ ^ÓY†l,™ÇÛó©Jm¢«^v(«SG™kij¯W‰À*e«±•1ݽ7°åˆòJª¶O46:ÏGÃE•#¸y)’O— a´¢)½]L·¨!›X:':Hã5‚Èì©“0&êÞÃðŸA&½éð?n ª–žÉ0N¶ÌŒŸ1‹-Ù?rö‚C€*¤‘nåÃL ͨÀ§N§{†Éûë·ßþðý/¢^c5/Ù¶i¨®³¼LÆCq»Ò£ª/œ¡7DdUˆ Ý9k£6’ç‹?0ã®Ã­¹×ýA„vË˃ö§×Ím7sµh…ÍdS×hôKOÙfUâ#¯Ã§rš×.I\Ý 6GZG*˜³{Œ`]… g‹fLÞòª"tE€¼ôâñÄ8î?nÒÍ„ºª&4Ѓ› ¿ _ÿTXÞÕ½ÞÛ†??¥åÆRUÊ?0 Ç3‰sz“¨©¥Wq8N7µ[We«aï>`@“éýÃòŒŠjOMV/ –Ë Ñ:y–Ó:áÈ%G¾üTl©Ü“µÈl†Ö³°.öáªV„ÂF?0†j`˳@­‡™Ã~gå¶A¾!ùæ--Kl1¹Î(Ü\VXb·‰=²Ø@ëâ©ÉÐ<ÛÒýehÙ¼iV°áJüõEIf­ûM¾Ð7¯§ùõjÑï¿b•o­×¹¢«ùª=^ÛEÀûýÞ j¸AãìQ±4{¬–yIQ±ÔÜ”$ñý;o³$E=}÷þÌINj ò]Œˆ5uK¹Ahà°ÁRîwÔØfâxa„A9É]žìÜ¡( 0Ü}Ñÿ3-Lô'î‚ÚÏ } ün˳®~£õ)Y" Ýß~a³,ØŠ¨Z0oìQ\`µ¦¸&"uçíY±PÀI¢í ÂË4;ÏšVñp4Éâð®ùûLˆíõ†YB¢ET5æJz{.©òÁÅqôU'Q¼;¼Ž6Ìã ‡òñiàbhdz° ‡úXTö‚™—Ëé0"ìÇùŒ³o—tQ¡­§³y7á7Ë6þ-#x„7GXFaÓÙõDã!q¥Y_+RXŒðô[ËÈè°×fÒKJ??l@Ž´ÉVñž%¦ÌXµ„šøñ5ëä…¯G½»!§[~§8ÄøsBµb!•N^Htâá’†‰ÀôþÒ€Ñÿö[RáœçÖ¹§äk¸›g§†Ì4‰[a½™Ýð9‹0??æ8!Ä?rãÐFBp‘œ¾î³gê„Ù‹dÄ‚÷•9rcSÉÀ=*ç*èhŒ¢ðra,ì8œ@´½i“Œ±êFNÀUÚh5‹<"€$5º‡-‰…"º+âÊúÏU£2¾„eÜB ð;Q–¡/’‹#ý!r¼¹KéD¬ì´‚жð2Tw†á0wH‹nMñ°YÅ÷ž€¸xv±²aU’‰bÖØˆÆšlŒëhˆþð‹§ÇÓwï‰.¾W럖ºÀs*ÐçŸÒWÓQ!H~E1YtYO¯— AóEÍ™…£ér@¢R>‰DMù7>ù72gá[6«+÷£˜ÑOr(š“¤|ÔV¾Uƒ¥BôÎ%•ÌSj?rAµ%uO??>h\·z’ uÌ—ËÎK7—ýv¦Ô‘Åñ~]ôº…Jhý`³BÚ_bW¼¯¹¥=??@bw­AìEKbX×åÚr<¥º§W†¥ûLSÉe‚zø¶±n»r¢>ë¥ÑÕ1°uûiïPY鯋ëò÷»@TcMþkì”Ó«£3¨åmüOb›FMÝðY?nb½§qCòîœ?r¢¼lÜxy{ |ö¤Ð–üò*>cÌtCóÙ¼çê&V;£•ñæVÆÛxÿ(Ö­Ù-N8m{Â1®6µ±F=Dàe\yXÍø âßI€°[ÜæÐ»H°û«bægÄO6ìž<]&+æì*•?r‘lÙ­á­zgJ”z¨û"˜Ç+K–?r×uÜʤª)êÉjãØa’t?nÑŸuÐKjAç™M#<éaÎMfábpK½”ë½`N¯ù-‘jëCŸ~*ZuTQ¢²rÞÉK]'äÈm}-óaÉÇÙAÜÖbÈ«N”\ä¹ 1'(ƒ7ÅüŽÊV=”H IÒïü™þ|‹ÑC)‚Y¯ƒ»Ê¸»D_C!#*˜å£YFc¾¨ÇûŸé’QBäPEØÄ¼w %êžÆ.??¬Ø v¡KÁû…r;o3&À:r$<ä3%ÎnË+'œã†YÓ5oƒŸIrfFmQvj¡8™l®Æí€‚ð¯Û!~í>CgÖnVmmù©lû¾tŠÀgOY;0²!)jöHàIÄú¿Ü‘š¡KÚcp2v4Q»u1$pÚ$;ÉxîŒ,çÛƒéÛã²;PGvžhðüôÍϯ¾ÿæ½y·P‘±¦!»ô9¬Z¥0œ ÍÒPø¡4(q` Ì“`rQD±xã ‹©t6cu´C@??³ãÐÉ&]Y=\yƒ«œÊlމËZZZ³®Ø4ÔÙ„Ô^½cš…•ߦ¢]NT°öñ%YÕ´4"Ö¤Vó²ŠP=BæˆÕ]$@ߨcÀœ]J-fö®¤AÉ2©øå¦ãæT]¤#ž%írEFlÎÚªæ‰q»9;3ô68"óŒ ”o¿¾{ÿkÔ#?r½ãTð&“Ý‹UÄæÀÃöÁ¿ÑÔ̈Õg.pšos?rP›¥_?naÄj°`FRwĘ™YÖ]‘C$ÓÈm!o;;¼+âºÙ…²B÷â¸#??Ò©¨€íðÆÝSùiFOÝ“~XpdÝ^´™œãÒËY=C™u‘½°ÌØ[0|8ªg:5´ÄÉzLŽüûpªÝÛçd©lÓŽ’èo+}7Qº ±n³•NÂÛ‰¤¡ÂZUãJhíA§Öî=†ñä›bÄò¸¾ +#ö5Ä?nx-ÉFàxù¡ßÌ®gy6JíU±Àû0§=œ"ç≩Œy=£é'… ŠœPä~j9SÈi}ÝC á ÕZýêoí&²n9CúÕB€ÈáH¹9IMq½cÖ!ÝÞViƒÔ‡‡‰äqýHæHJ„ׇÕOί?0×wôuà3'j?r³kv˜:ÔäMëí/kQö²œ³Eųˆß»;®µíK²óÆ­ÇË¥±ÓÛ¢íf…u è(å~ +H9—±ˆ”ÖœzTÔÔ.½øùç¿þÜÄr—_… ¬£S?nÕÿ¥xTýR°üâéÓ)Ù“ÕýO6«“@O‡”¶ & ¥’96çúÝ÷??¾|÷ýËW??}óê; ‡jS\Q¼øc.¾E »2ôQ;„ñûã $ÈN®p×F|›å¼RLŠ©!`–E»5à“3Dr&êHÎ3ŒœYRY³\;¨tæiûçòÜÏ^g%¯C”φñ?n[~QôX³rÌRþÛ·ƒo¾ýöÅ_½|ÿãŸ^üÇ÷??þY.ª Î1¬.W’¨5¨Do¾·p‚ áõäœ|-†GŠu¤„´­ZLB,ŠÈ®â„D(“‹¼Òã·l’8-Ο??£z½·ç§ý£ÏÎÖ²–a5šLôumäÓpq1]6.¦WK•à??Þ&ƒí‘å;•ib6(¢¿uoqÞ^±–ðÉsPô«¿‘bɱF9X9»¼oÉÑr[ÉIÁQ]hïÏ€Ps±‰z+tÒ¯ö9ç‘Ú¦b[w­Nµ;·¬s??F6ƒ-QÇ­žmô÷.ó‰¼jé×¹;Q….­B,à0uu(jëë”6n(jýô¸mç·^µŠ€]†B¼§Ô¾#h먽àÇdìcxGcÉ vpc×/??ܸ(Ú6=¶ùŽÏýÒÓ??ßâÔ4RÍ&{–³lÛøÊ`%z‰ó8a¾Ìgt®ÍÌþjÁÈ)A³Æ$ ·3P?0í6MÀ3m¬eÆž?0  —,•èICúhA{úÌZåOrÏÛ?rŸu»ù„ú9<¯Ð«ÞŠÜƒ—vqÉr"ç ®b:•h¨£Lì:è÷¿Ó ó×)ë̘™;§È ^§Rx¸o(H+¤Æ‘E°; Œîù#þR.?nþ¬ÞTةݮIÏp€‰ôEÚ¯Š[®¸ê¾ívPÖ¬E¡ «xa²+ú¶š×NÞØb?03Ïú”p¡kÐÿ0w´÷ƒ!ªéæ½â#!vâF¾ Ús}ͰÏó¨ñŠÎpé¨/©›;á…D°]6Ø GMq÷ ²ïöŠ ›FÏ·E¥×ÁarÛO”Ò0LIQõv«ëÒÓóa9¸¿wU9µvRØ­ <ëdHfL°Ùyj>'\ì°äÀ&Jv¬Wn~“O6½¸4_àð¶_àœ¶ßœžÙÏG•]G¶ùÂäîèwœ„S‡í²ïµ—ò1ð™òè2 ÛõöoKÙ°vEÀcx\qm_Ù+?rˆÓKlGP€Q „sD è;¾ÂÞwΞ‰XUÓµ³2‚]‚¾n5£^Ôz ~‘Þ¢77?rcQ1ç²’Ó@A xï²…<(årñ/ñ½׳.9áÃFSnÙ&7Á'Z‹Ч\ÏÕ%ßb%øš¯#ã¦ú¯~áÐR??GEý,ýßù¤õë¿ÔÍ=mßÌS-«+t3•z‰#]ý™Í??­˜Fxéc§¯vÂ7Û¹”®&s®¥{´ÿÕJõiJ3yz:ä}¨åM24“‚»f(Í·êø©o¦ôã*ºžÑýKÁâÓ%¡ï¸Ñ—òKãû6m<Ý??:¬ÓHpz??Üë1tµu)&ݧä.£¦é¤ùx2|•}¾€Ì …]ÞÉSÏò??¸âhéIíÈòy´l‡šÁ£²Cg^©jC1…]¯Àe«p¿NÒ!Ã¥äáÀ?0Ï à%9„C„.Ù?0@ŒäØŠj¯âò" áðôS<àS ^T«|¾†Þ᥷÷¾V?nQÄNœ÷ \ËEø|¡Ã´•P÷J@i/z(*ÁÌbe׃G( Œ‰Pt ”>ÆMþÎ £ý#YÃÝ?0±Ì'Mú%®ñ Å2‚®Œas79Áç*ìªZ<°¬×ëp³ïµëél7F’Æi'ŸpÁ„¦àå??þoW*ÅþY‹±vº¾ì…½ÑB½`J!Xá„ô°—-/¤™É??ÿÐ=¦}xÜmRh…º;›ƒò§ï J9Ô«ë6ëŽ!ë¹îxq[Iô¾×¤Óçßn.½dã„atq3zeb?0ñ¨ÚÿJEµXa™®v€>Óìi¶H*¯s`;0ºòÓF2°;Âòe¢ï± žXN*º–e²àH”æ¥ÉN1ƒ›èˆ†ò˜þ=?rHk›€ôE³`›ßJ7í²ìåeX†–ãØ²sϯ!ome¼ ˆwkh¿5WXÜÎ&GöÛbÌoÍ·`ðöi˜v$9¹‹cüQ uÿ½¤nm+YΑ%,˜Õk?nùó‹??{2@Gê7ó|ÿðÑŒ™8!$šˆ$AÍ€§ìðÛ0ãÊâ{Ùσ„CñKJ"€v'öPmRŽ 9«Ö¥S`Bpuöò¢^¨7Š£^ÑQg¤O.ÒâAÙ˜Ç1v®A@âÿ6xñ¯^ -†£n€å?rW°$»Y¨Ÿ³Rºˆ‚÷X·šîûW’"9@Ó$b9´¯‡úà\pçóYïɸsI±T{Bžq)ã-±9ÿö•ŸXt ¢¹ßÉö:|$‘ê"eÖNÂë“}[ÂŽý?rN`Q½ÁoÀßYÚ¬û¯ Ñ–%ÄY–|ÒÿLS § ÚêÒ]žäZyzÖ¹Oº³Ö’°ûRL-‡èKd94¤Ã—¾™]Ã¥Òf )óŒ ƒîÚ€³,]Š/4·}·µ]å =Ã?n ›ò¾Ø«æç¤¦éÇ_fq?n±›»Œy5x´µ5Ø¥3#³°cÃ,L×®n0Ó‹,[—[à’–ox+ŽüýGÐe­2–ÆBÍDc®Q»pü [¢Ü~&.³ä¾˜djC¶M?r0¦¼çx÷iÊ‚Éòˆ¬6¬6ò< ¾¢?0•FjÝIôf”<€î|ý„ÏW`ÄBªÂ ©z1½±Köp‡r®àÓŸeò¥›ÝµX5±+Q$QÔ‡‰âƒ‚ð@ˆ‰‹Á'J…ÇeŒDûâ4AWø–Œ–-ú”XîøÌ#"žnÍEŽ WæwÄø€Î+ÐØÿ¡z~é àùœ¸HkéItné²T§è©±™7=/›&çÃ(Á0-ýï £z?nšÓØæÓÛê XŸ¤ÀÙee45R l:òC.)˜6~ûX°¥L—ÕýQCwÒ?0³¼/ä æ÷‰Î5`\„ÿý›ŸìGSrǟ̯óU˜GVÀœ|…¿¿>Ê~ý¥€ºÃ–OÌ›¾¯ÍœOéY©îDÏ’ð¥º1Ž(ç©Â??•66msÎó¹¯,Å,'VyŠÊ­Á4ŒQQ?0QÊɯ›÷?0Œ)¡ßîª+›:À[švù_1ý– q”çÚÞ"Ìó¯'…Ùƒç˜M"ˆzå»LRh|e9¡²¨³Ä®›#--¶ÇjùqAYKŽ#‹7L‡õ¶9½—å-k¦º@æösÞ!BwR= iër?r2µn*»8Â;Ÿ?0#Ã?0ß oƒi^áTÒ4R?r¯ëT{¡(Á?0?nXe_éˆ~!Q4Î??kU’ì gmÌb´Qž€Ð5C ½ `”ÆúÝû4r&n¸CW×À±è>PeJö­Æ ¿3f;µdDì¦í#8´BÜz+Dš2Ì·nÒs ò0ý‡o‹ìF ²Ï1Y²×e™8æÜ¢ü]=“C)Ù ï|<Ëãý¨3??9;ª×–9[ôÕIôìðóç|‘Ïðëùáokâ& ¶[&<‘Žcôµb%|í°äúŠÇ•ÜT2‰KÍ~'±y·2˜Óð®¶—B¢Aw×è¦Ú%×’q›²\¤›=\=XäU}ýë¯äýXªí¼iü”ÑH;7N~œœí Ý%€?0?n¯»|*ÍØä¦0YÂò±5#G‡´†ô§s¶Ù¯ÚBòggÎf·0V_Ô3±Þë’I: Þ$Ä‚bŠËC«žc{÷0¬<·ÖÔêÿ6Ži¸¹ðJÚ¥¹?rlR(UeVí†C¸»“±ØM‡%Æ^æU-Þ`“„Ör T3ä@Ž‹QÉþO¹?0ÉÀztÛîŸ%UÀ7±ÆÓY½9á¤Ê§Þð4¾;,7x"ÅWW|®«íÏqwYiÝñåf¿z'=2ÛÞ,¶gTg×øV(›Êñ]¨‹ /?0-*zíjÌÆ‰¬—t|vª®NÁ¾#yæª|ùªhû,Œ—7jxƒÇ›Kñ\f¾ï[È%VUu‹{Vfy¿Á³,¯Fq{þpôÉŠ‘~{]3SþQ¥Î*TÕµ‰š[ÁTk8|/ŠT~ÏëH(Ð\ Ò¶ ÑùúBÔ ÿ¾ë˜ºÝÇ©ªm"Uwß”vd¥Ì'xØ(!î!@×dæñÜ Ñ$Ëü‰±PÁOÌ€ ‚Þµ|páÞömK’F²ï¤àæ5?0°FNAhÑãÖžá§çM??«ôMŸÈµÍç“èHÇà">‘GIñ=³Ž2!«Ý£žŠtZ:g5IÀ²Ð£m[áÝŠ­´xÊ]Jƒ|aY­DÒÑä7å?r[‹†ºÐ~ž"mw—as??hÎ:#’L?0Õ~*”ŽKÛ1ç€ÀG\oK©Rét€h.ß TmR™ìž%@ ;å©)”,„‡@I&Ì“†Eäë¦×‹c»G²Cqtu8ãþ½—»læ;Ž…&~¶ +#דÆèi‹fÜ,Ô>êŒÊÉjL´E=œ;ià°ž²Iø7½;ª=ÿh_ðŒ-ya­?0™nô”-hÎÍ> ¨ãPËTäY“-ÃjNôÓþN'kÅÚü4<Á­×ÄXR—<^TŽÓfí)VÔÏg¥‚d$›Ò Ìíp&ÛiD1sA‹~ùñ’4>í (2—"s¯ÈÞÖY(fƒO¯Íx??{vÐ*¡>0hƒ:™ó»aäY tKaŸ¸?0²bãÅü€Ï ¡’À€éS]A·¸Š—p*åüAü·F+ZõbN"oTIt/0SÜ hÁ€“6žA™Ö¨$”ˆX<Ñw3HXñÍŒg‡ÏÜÃû;9\çãº#M+¼Ç¦ÝÖŠÔ?n‚ÕóG\‰<&ãeÐ+ÁÄ4«|ŒÀõXk ª99ݱø¼â*J¿¬¸;…3#£.Òv¸ko‡h×)Ÿ^³ó×'õ˜#__Œ»Þ[D9UoÂZzC º!¨Â<‘Õ¤Q[y/ÝÒ¿cŽbùòÞ¦Ž <Éá*é*ád.?rQåo9‰å@½Åyº+&Ð?rÞwÕ/Ò@è³q;¯C¼i1š?nùsyg®bûÆr´’€ “ŠmlȬ—€?r€ºß¶à?0G ¥¦ÜÝ??Ë󇛚(B>æ°#P_!åËøýÅ¡`ÊÒÕÔ}ÂúSùÐÝ2#?0ûd5.;èN«úZ½®=O/³°åÖñ9#˜Îy€‡­üFêÒ餪Àæ™Zc#ç:V§o{wî8ûÓúX|ðÐKÓO¬áQ®ªÅÍM ´õÇvïoï«°??N ½BQêQ]kîùã©Féá*|{)î¨Êw-К·¦/¼´®/˜°V>|䆗­•??6Ôò?n—[2"h»>ÞæM†"‰ŠÖY¿61Åš}ÂáZ¤ãxB’l°‰âá!’5°òÍÏÎé[õe$±«ó”´– ž«îó¢Òm…[`d<ÌtÌV ¾±ñì5]Ãì:bê¬=´(GMØå#;»Ÿ”«&uNßnÜ?n]­~D}}¬ÐòÔ=áL~´LF³²ÌG«?rw@ú‘µp•ä>ðWév«9n<$[£1wMø*Œ‹~ÈÀÉÍÍ=ÙÉá8ŠcmŒ:À¥"œ41¤˜EüÖ'_[œ«¥?0À>P‹¡‰Þo[ßø<«TÂ&ÖÉF3Ò1÷·VQïÌn+„¦Ãâ?râ¡ÝÛ)þ³Œ½^Ñ·Büp':¯³× P??,¤Ù!o VçÜ?nÖ©¥@¨Š±®o«0hËœÀê é'¯¬ø·QÊ“`(|Î?r^Ô `㲌¶çzcšîÁ±äæ ïA; Öh¡\ì¼ñáë©9&vÅ]±$‚3‚*s0»M5ÞâÃAÛU˜Å¸Æ‰‘ÒU‰Ëì¤ÔHmÌ)U?rça¶|‡&G~ÿ`Êá(È1mm€D+ZÄŽ.gç;~øFCü[ôƒ%9ãF?0?0¤è®q¦6éQû©|3èÜžÖJM‹±óU¢_–çE­R¾6¿<ºN?n«Çh®áõд2‘A†È¼®J;}K˜ H`³<­ã€‘žm°c£ ÍG™‰’ešg“a„ÑÓt©Ì1g¾-^@xÏ©'Ó˜5¬Ü×Ú±ù@‹o=ö¦ZœÃ7ó6=Jž'¶º³œ2¯ŸàÖª#rëÛ®ØZã!ÃeΜ¡³CO˜Ó (ì Åz€ªäÄùü,Ýr¶b"›Ú4Û¶Ó¸DɺË3Íù&d÷MH¢´Ž¶•L€e:{ïŽY›SÆ%U„NS?r«¶šp;» ßQ7zÇ„AT¨gé>Ìn+D,Ã6\=`¦ƒû«Ñè6!С B‹‚QêÕZ^rƒ×V‡FüÓµ®ÄÑljtó??¸é]ðˆ§ÍHòÝA656vÿý³”Áw;ƒUÛ?ròf¬£6ި̵oO©w?0Ëaš,}·Üvd-ïÝÿtž¢¯‘No+³è€ É 1¹Ó(N?r/ñT»#*˜nC=2àg½dûÁ¾·ž\Iüu["”4„íü;üfüAžµÞòƒÄþV«êlÏûÛ£çT²!ï{ÁÝèÚm¶O­w„†tr,CýF±5o¶ÖmG!è½Ù‡ªÍDÎb6Oë¹MííQ>òY‹ÕX^<®ûì±ÊD7ie€]°GÍÆSߦµòÚôÒÇÃlj›ÚÚI/h[”U18æ=—·½E¦õ½buMl ÒB÷h]:ðÚ#-ìXI…í¡ŸÕ—«¶ßÅÄâMæqŸ!ãǰ¦Ã:œoÖ†÷¾;—Üÿƒù¢îýe~“SÂO³kJ²ƒßOˆîš1_/ªK!»4¤jòù}r"Uõ;!ÄÞÝ4?nô€WŸ"Ýeè]ç>`ÍÕg (sî~4àìêÕ%Š à0†yÔ{LÝ3M­Æ®pgål.sÆ“ðábMjx<œ]בâ‚Lvlbx4+4–Ãi©'ãëZ…¥¶e€Åî¦è5æ„äsPœƒ—À9Ï^ÑïY9,ß0ßÙVí~Fâö 1¡’ƒï$ÍçûõÍ~œ„-nïiTÎn-³+”äfI¸qØñ0¾vä„NKh9åDÚxPzV­å ±˜zèy)VÎJ¬õ¨m^…û‘3²2DíãÞνA{¡‚>?nD%p}7®í5ÍOž_õÝ#™4¬¹ê€6¾hé+Ô®ZÝIx#Ñ[}P<¥ÕBIšÇe.Xöîºa;°tDõ-d9`m S;Œ`_'*à°;OµYÈ—êü-ȇî>P÷KuÊKè¢I ÇG™ž7Œ §ÑÇwˆî+…öÝ鑲íB,¶)6ÊE1€‚6‹ôçb’y³y¸ù²#·Wfp]ù×*w¥À sŪ€ŠƒônKê{5i”-œñZÃ%Í/òy‹þz‡œ·hŸuï»æùçá$ºÁà (<œÞy½¤çÎFžï û1ÇK+z@=çÖm;í{gr¨…Œc¼ÆdÆæe9[š/%í¹J«ð)?nž¡£³Éň_ήóXÆÂfÒêõ±)¬=Ï礘)ŒÈatw<~O-šlõ{Vv\—\®ñÇ—ÓŠ¾‰ª“Z~QÌç½ÄVýäw>bØßYNhM“^Z—aì-jÏ>v“¾o ‚aý‡fZèš&?0µ¸ä…Ú¦®ò)oʳ~ÐóR`ì¿üÄßz̓P,ŸöN?nZÐÔqæÅ”ºõ8+6=¦;#®fvAlFÒ‡Ùº´ªv¤=Bº³42è)†È¦2RB`ØÅÔxž® Òª–§š‹ö%QË¥‘D€‰¼qÒçÃ)hS˜&¥Mü/qp•¿áäÀ´????Œý…M3:p¨ËV”߆¾¡yÒê<á¦Â¬zÐÃE?nÜSƒs(j¹´ØU_{¶” *°ç0üÀF\îÍ”æ 0­t…êÒ¸l8" y)Ø©ç¡Ôº'Ô³ð!zã"„ï…¼òvôí%ñLÔ¼8-“l@ÛèT\ç^ Î\€$ÙΟ[£QÞšKIK² áÛC&¸š aÎ%øüˆ¹Ž·Jó½Gp»ºÓ׳sIÒ$„/?? r£ŠEôíz]x¸°UKw“µÊÄ$žFÑ/Øó«€n5´JnËÿÁH‘>°—°@~V-_bfî);ƒ©:*Y!~i Ïd,м!¡4$•RÏXQ>Á8½=³vzØÕdW+hÄè©%‚âŠ9•p(ór’¤¬¾18íi´ ͼ??ûÇ»O¿)"ÅfºJ¼,þ€/„6Äÿ‡e¯aI°áTDÁ˜ØÉü~Ň5Êõ΢urqld¿2{iãc/ù¯q(©Å·Û9#`W*ˆ3??ÙLQO …7Äî»ÙîåÁ|6ïÝšƒ!g¹’¼˜h;älÜ&+ÉV@°eí“ÉiXj9õÂg>*¨ñ”*æ:?0Oœ/‰!7GÀ+sp;$ñîáÁ±m.€Å¶WÜ|! ·I9vq¯‡rûÂã%¤è G·/$5I¢åkÂeÊ@²É’Ä®½Š$Iϸ¦S¿ØsU(ÄHú\(:v¯»ØedÓH­bÿ"Ño¿_g·¼ÒKº—gï§q˜ta«{³—RéecÏ~™4Ÿ–-š¾õ#Ññó¤{|MµeÁ„EKSö:Ù´tAåJ¸…0á ͇ Á ŒEG g¡¨&˜ Ì¥ Tæ©Ä²zùˆ?0ÖC ÎÐr8Z,¦±??Á(cÙ»©çJÜ£¹M0Æ4¦zlÑž5Jœ$a1UXJ%<˜îþfYòp¯¥Ÿ×ß|}?n…™>F’A¯•ý õl#g2=}÷^4wÑé»÷g—xœ‹µÇÔÍÀMÒ6ëpR<Œ·oò–o<ž‡³ÉsÈBJ²L¾¾ ¨´1|?rA§1`š–—¥Ì:ŽÊ­û‹“XipPô“o‡÷µˆFX¤+AB”??jWè³IéV]ò-g¾ºÍ¡kޤk¡ B#8+#bÕ2ž¾k»DÀ°Z^é·õPÙ–?re/·G|™ùòåä¢@:Å—ßÿ·nt¦m¸¤Mš7B²ÅÒP™JǦ~ +#?nÊêÉd®_×ò£ã1í_|CŠü«n dƒ•¹ª´ÈVuA*x?0„©Ø.m㾊c{AØ[š…€&ÁÖ„)“4ÉêQÖò’Á¥M’<®è÷_\>?n-CY0:Ù¼W”„×› f-ãÒ¡Ï(T¾Z -Þ[<ƒ–æ ø1(E‡ÁÕ¨3Z+œ„@Ý…`AÜ9ØÁ“”­Ò·ÅO®PÝÇlnUí_òa0×l¤{=ilaʪŠZ;Z>)‘5Í»shK {šdL‚1:d¡žëQÊM&]®ôJvŒÛ‰´`Û{ò)/ç?n½{óòUË–J&zãÎæ92«?rn3Eß±KÚàfWBÔºÕÂvnR?r²E½ùÞèÚ7 âJñ?rC²‘Í.ˆDžJÛ)µw–˜Ü Z¹'•ݨÙLÉÌÅ:²†AÐSc!øðxl¼w>??ô–«£ªë<Ÿ£??áÁU??piSù¿ÿñÏÄ] .Õ¤˜ Ûòä-îÿÂcsEˆÆu>¤CjRÑ?n¿Â«ë¡„¼àT?n†é¤ 5úETͦ9«öÏ fyçdSû&º@™Û’*q;ñ^÷£—X™5¥é¹q©ÕZ%ýq¬ÛöÈwÔMzÈeÝp“¨¸™ùÓ£ãU³Û²&»$Çë^ÔÄÀ"a¹J†Ü:ín¦AVõÄF’†æ½8/¨Q.°ÿ¨: ^0ëçZRb™Hú¸Ìu°˜a`[Óá›f-æóYYÓÌ0ƒ)ƃæsF•ÊÛ &WÆsBFÐr ó@ó H퉥A›??½ìc¹½™-"ÌûþV?0<ŒÎM#‚~Ùp´ŒÀ„6&ÑÕaÓäõ"ËQ»„„n<¹X”Cæyž4½™]“4TÅ€¬4˜Ú'!ÌMÿwßÿøjðÝ÷/_ýôÍ«ïÒõ2Õ‡½þVÞGíð†þLØÔͬ4ÍÃ[†=d ïî•“Aݶ¾>ÐAΆ:uu‡àæhÐ\‚`…¨rV0D?n7†1ê~LðŽ·†S‹1Cø'?rÒ?0ó(×í¡|k*.ެ½§¢ ôX7ÙBJOqGö9EÌ ¢]BR4Ü£NÚ’ùDLB?r°Q—^˜ª"®‚…©ŽäGUç¤Ðf®è·À2_?0ÒzÀ>oþ(boq¥IÅæSÛ˜îòÍ`D0ë\b)PjÏ??£@žAËnY »øÅ<:ÏéÌBX;2ÆÎh»*–Æ…¦’Q?n,˜êÛ²>X>kN{??Ýѧ Ú9Òa_—Ïblk¨þô/ƒ{ñ³±%áWÈ‘  Ì:TÍBw“E¨Èh*A.Ëå…ÜbÝT:«¼û¿ð™N}uk%¼²‡;‚Jþ×wïmxFuÜKk¦.GïÓEùA3ìüø> i6iéEY.æõ×t¾DOÀÎ@«ç¥“ìaÒéØ$ã+cæeShn‰*T ÜPà`¤fæoH|S¿!®‰ø"9”]Á*úòD«ÅLëIJØ(‚‡ÂŒºÞœp2F{³ë 8³ä Ù±²EÕ¢–@Õ³YDàÔNiÜÛy»ãÊàjeç#þ=æB•ï<â&ÅnÍ`ÜïGAUÕ±%àŒ#¡ßÎ ºƒkg‰L¼_]NùóvxÍŸ¯gÔ.}OÂD–•Ü¡®VHEO?0>(§UÇÊ{ïôºRÝ[OFÝO Ç´¯¹v…$çCpL$ð> oïúwï(üyw›ÃDKÙÓ% Õ–bFO?05Ù’ÚñØ`P¥Ã®QÑ ýU3 ½Ñ±T· =ðB?nÚ-Ë|…’l 9]¾ˆå꘸J¼ˆpašet/ÚäÓ£é‘}ñ4x¾™ŒêrQO®@¼Ž{㊯7²¡ÒˆópÛ»Ks;j79G6“ò‹ÕnY眶,¾š`ó\-m¯³@?0ÓRÎ)±ÇÙ£{mœ$îܶŠmìy N·†|KÀÚYÁ¦MŠ???rÅÕÇNºæ€Ó½Û¨&>N"Fslõæ³ÌäŸ/“¹|rÑ$N-Ø?n·å\ž`…q!îSx²‚>™‡ k)<ÞþȦ[ ãéJ>—á zÍäLÁ?n‹^IÑ«pÑ¥]Eâˆ©Ú ¼)ž+Á4ÒBya,×Î6‡v¥µ‡M¾?0…/??Á‹u1š ü*-†.Ò=®¢,ê¢iœP]7¼®ór½é×f„¸ÅjSÅé}kÈî{Ðö€ó˜rœ_ž|º:OäüÛ}šÃ1”½%àâÆsÈNŽLfÑ«¨Ï΋ÑrFTjŸUêdÙ™€¢ñÞŠN^â3@¨Â £y¡Ï^ÚŽvxåßkf°÷>/£ÔÏr6YüöóÀÁÓe‚&‹8ùíçêšs’ã¼ ,ì-úìÂEo¥èíï8ÿ£åç/‰H%Ðúßos¢ñžÌ?n°Úizh0&4æôÙkú´ëÀÛcˆ]‚fTKÕ¤z’ë]v”;ZÊ,Þ-üRxÆÂ<㎜ÝÙ&½Ÿã°†ÃUøôQuÆÚ­3ª¤Uÿì,IÂó†­  TÔ¤º:88hµ–$ÁY?r;©óµúMv±Ëœ˜UÏãµR;xu1)Æ÷`¾iÊæ ­ÂïC5o‡Wù ‰z_ˆÜ"é6PM‘Á²lðÕ[þíR=»Î¯«YáßjG¯áv¾¨¨Ò¼Š”g˜·8]‘WZß=“ëÉ`Ùbmõ zDCýS~~5õ>=|šF/ÿå__þ'Öu<®ö›Z±VJÃÃc?0e³rŒQKÒp=㱃áèf1)óÞJ¹w¤?r è¦Ë‹š?r;=•VFüèu(Tk`\ši?0ŽyÆÆ§‹âŠüm‹³8í*y_½™ç/ÊrVšaÞúÏc»méøîù‰BH'c7¨?nj›5ý=CÏ<¾c‰3QQc“d³Áú}0ߎF]íz9§“2o¿Bìïç[úaÅPv IUtw¾ˆ°ÒGbs-Aã¡ëÚórGQã§1¾Æ¦-“?r?nu–dôŸa±Ü1MÙmôŸ}—M*t;4c;»ÊÙ¨“Ýe?nÎÑ,ŽÓ†ŸÊö…‡u½ÊøS5Á½¦à_íÇlŸg“Ò‹ØрӘ¨?0G#¢e® j5a£ v†'Ë—ÚP5ØàÒso3Zõ}—ë˜ô뜌{â ÅÅEËNÔðÈyÝŽy…ú’¿ÊJÕ¤&ÜˆŽµ™NÀS^P¼Ûå‹g€ÊyƒP¾•¾I­ï‡?n-f£kš©?rµä­_©¤-I5¼ÂxÊÔiÊÔ“vUÒ>Ô~>qùá8§ùÊó.h¼ãW?rüOuM6a̰*Œ/£#˜…aHÚí`?0Ѽ¬CLWr†©Ä7> €š·K=|î ¶³Â2ð'ÝpEq+B›úbQ5óî7)cß7#[mɬi„*Ý&ÖLÓã³æû5ÁËwÙzÏ…rv¼D'?nŒ~e4r0œ ©¾1øúUtx÷üçuZª  7ât7¨+v³KØy¼ˆ»&Ð?n‘褣…4t¨@‡Ö¨4»%¨(ùUÌJœ òê«*˜Ñq í}jl2žäÓL¹&_Oà†=?næ±'ñv‚lÇ~°?nš©J¾j»Y??Ø*Vl“•ÓP:µXZÔ£;jLf$¦ÓàÛý²ÐiÙ -š¤˜(ÃPÇ–B§$#ÕSû ?rKðQ! C±"W8%àÇ.FZ(x É—[>Ñ΂îyáÃ0Hˆñ· dçºÚdžLì8I‚‡¥IØôô1àXŧ?n7),í¹L„Ô±’eà•/ÃþO7¯3Îë§Ø?nvÍáyåq^EUÜ ¥˜ CdW¸]Ãô,H(¿zÓd²©´‘v]ù&µ6[Úp^IÔ{^÷ˆE1¢A›°~i`žAØñ ŒŒL‘‹QÒgû'†:®NèüOÚ{ƒ,ƒ­Ö†óˆýo–TõÍ-t• zµdò·MîV#yøÄ„m€¥üÌ»Q™KïÇýᇗ¯¾ye• P7ͨ9ïR¢:þ^Ó=Úu&›ÒÆú?rçÑ?nµ¹3+}AkK9@TÛ 5¥æÛKáz@¥ð±­Ø«¾,ÄÍŘSéËÚÜ\ì’f‚Šá,å¯g[Ê.ª¼”²øºµ, Qv²­¿’uÆ.aÄEÔ.dÄŠMÆÄBn]Ï(Ièn_‹¯9†zeœ‹Â†ââÓÛ–ÌÐ?nw©P¦³°7:JéL÷»Å4/‰ì)˜f»s9¼83Ck>z•¬a#„­äzt2=:Þ»ÍjDzë8ôžþüóõaÀ×ß“ÌnÆèIÔ{z|t¸fPQõ•ë3 Is‘l…s$@À?0œ¨»†…C Çà“•âî§Ñ>üñ"¡Û]™´K€™ ½*è&™óÝmVÖyÆPSº»Ã¢7??a;CLò†Š§Ç]¶’nZÆQ:qÁU7°ÒŠyo‘+/sB“ms4Réêq –7ÇxsɆަ‰i‚ËNºUÑlL>ï¬8œ7%"…}A^ã4wB'<ƒwsïS±‚bÃ?0‡pܹ0;0=u7Öl„4½‘©dÎ[”áâsÜN®¯YÊ×'zãJTŽ‹"NéQ}ˆGTɾ6:t“d<ÒG$C‚•$c:åyÉ>ퟅ„ýŽ Ì Šiü̃??)Ìdå"Ù»ÆËSóv°§1f˜%ü ɲù×é†âÁŽì÷PÅà½HãÜ·ntÝÚc.tØ€¤Z#åÃn“¥•gP^æî+Øta‘]áà&±dú$W÷ôB‰#¥æjqBærjðŽZ•lÎÊ„ë>>â[ï¶äÆÄhÆow†‚ŠX-¾?nè·L䦵ܽf»ˆ«hÛa–?0Ý>j6¯r ,ë¤Óäîl¬[ލ??è gøÁ³Ãß7KÁÙó?0äƒg-—‡^¥ÑÎ}/¹_5Çfß6½3†=tÆ…“Œª©VN–Å-æ;[hçJ2ìù°„Ã6\ÐÀBN£7px#û& »ÑÉ?0q66N7¶Qœœü& ‘u 葹??}Hàµð@Ö‚½«ÊŽéÉOJGÂ4 ýGÕZxò¤šþŠðD???nrÒ"O»¼µ9¡—=Õ¿eT¯CpÐ…?0 ÜM@Ѝlƒ†/ +#?naÁ¹?rì «ý#Çi».¼~+zãàýM8äÁ??ƒü{ðǰï(ƒâÃËB1§ÇâE=žWJ*PÆxõ6ñÌ+‚ˆÁ4ÌÚÔT*€jº?nø¨?0Ò*¶èþ6€U9rô[á›j\RÝΆ=XÕ8JDñÔÁe¹3.?n>TªÎ_À3}oZòÞcÜ]LÆb•Õ®ÏÁ8Jü•ÍOͧ—-K4/Kì Wh"€eF¤¡ç%R«­ÌE¾Xé(óŠä\R‰#»PA½ªó¡v[´›E”¾+Ü,fu>ïa´H½´›°š|;C„«(äXöÌk½µ1.ÎÒBˆÛÓF8xPhJN©K ßáÿ¬Šújm`j0˜»7ÂWV<1Ž-Ûrèø8Åô euq1H>Æœ&þ>‹µÈbð5Ý£²še7€raHÚ¿Ò7²¸´<ï7R‡ŸËHmÖ¡læ^?nBÑäÁOßüüêûo~ˆï+>›Õ´¡ëaÊ—¿íÝ+ãÞÁǧ¿üòÉYB_’8elÌuºïð’{[¥ôÂô࢜-æV,U»û¨Ìݧi?0 {¸ÌÐ8dÜöìX½n8ÚæR©§ÎsÍÑ>h®nó(¡pp’·k\ÍBÎÛ?0qœH\4¼B24¬óÀõPoy“OÉò|OpVÿO‚1`q¾l/šÝJÑ,\VPŠ ~²Îec'ÙÆ9߈bpb°°"ÂAiAAã±9¤?rKi¯èA>0™@úJa|ÅèÆ÷h¡Çú1Ên*¬.³Ñ?n(Í ËL›éÚ*o‰_`€{yÂåø^ø¾ùgº>‰è?0Åg»¯vÅj>œcÅ0î,ÒÃxÛ¸¡Ööa[ÎÎÆª[b‰±_—³Ù•=òÖqžKB*™Þ ^¸í?0wPæoâp*,„™ZÎÓû쨰^Öøðpcqö ¼œVÝ)`É=›¶E+va-Ð ?r½á{?rV˜ë ƒ“ù² ÅZ±–+çóÆôŸFw™¯âöW +ݺ-e0ÒR‡XL¸•ÔÁ^–bQ?r…8 wñ% ??ãîZ„¬8;Á†Ixšàd  EQ“_£Zì)8éä·ä‹vQ‰¤>Ë1ý?roª°lGƒ”±ÆQu*<ÝaûÍîNz‘jÊw‹í@QNaôp9 ^'ê('÷#÷WÎñqíûûÎÒ8ÍBÒ>\£Ã‹Á¸ÔyW9p*Íd°¼ãréßñˆïØ?n|H°±´V\ÝøŒ•aØ6PK0nP×’4úxhpØ)ÜÂÙö6G&„L~l2C0œDæG;^ÿ×>T4n{ÔNè.gëuQîzbŠ©€ý±B-Jq8»˜÷Á1ÈÇ:¨2AæY?rªYF&aóK"ÿ¼$ÉE¹± –Éæ—œm6áÄHï¢>-šÒIBÇHÕãQ_ŽOÜšuÜT¸èF|{ÙóVÙ¶¡¥ZÛhHÞˆ§ú[TÞJ¡D¶¥H}÷q`Ú¯ÃÖà?r_Ñ_‹ÕÖ¼Fb×`34ªqºa¹4Vfð¢è¾WfgaÑF5$ ËÅ‚§€y¸‰ž%,öÚ½ÕëEc??*´ÖÆÈMÎDD?n €Û±i&Òi€T¼R·ƒaÆEöLŽíÜ_ä·ƒæVËÌ"6“pŒ›•¨nÂõÝðw`_ëöë9¥—ŽÈò´…÷É1õÛ(ó¢2l†aó†Uôv¹q Y1C´ÿÍÿùïßÿÈmª¼is†@źÄÏs¸È‹ÁøªG}ÑéhÇWƒÆò¦2z•¢Ñ.&<¡cVöèÓ¾fóðøì·Yà£íç¢6Z`—$f¸…+ûÆTȉ<ûðU¶aq®Ž]*ÝQhÑU7LÍ$øãbÎÁ§õÉ×[=‘Ëlõm\À)9pàØ¡•¨é4ê黯ö_Þaá#3Y›æáj›zÒØE÷ºÊ½Õ±™^pì?n†–6qL´µ¼¨ž-`¸AfIýˆ8ûô—åF²ÇÖÃjbð?r €!ÑÊÓ•˜›¢ é|1Û¢ñˆ@I"ñŒ¡=dœ#ú¨+/„ªdMRó¡2CÊ uc5myÍ(Ö YE [¯?n`”ò{=øÆŒN݊гŒç‰ÂUù$šÖLÖ±¥mÇ´&’ÓÝÏ]WÊpPU¬WfjÖ}‰_œ‚ošNΖÿ?0ì}ùsã6²ÿïóWpY5µb†Vlç¨÷ÕDÉ»²y÷ÞWy]*J$mÆÉ!)Ùã¼üï_|Ðl„Z0Çš¼Ù­–D?r Ñh4ºÝY£ÍëÞšâj³;p:/ŠMño¾Æø“û&1Ž3t"Fbú–×=É´?n‚&™µj†çhæLœìÆœ1“ç@ŽÿÇS Ç5={øBžBÓv^LJ‘ÿ¤ûmçÌ×ÓÑÏw8MÍû›26 bt+è¸1&ÖÎ ˜L$å.`oÕÿg¼|ó¢LÉQ×5….Ë®C7 È!Ø”Žív ÀxùR—qã‘§©½+ê“Có1€Ù È—Xv¿•ø•£90‘Ë–¥:\” 7ŸjªXQçÐ޲SKè€ &U29kH8ÓA-f÷Ž&+ušª¡„ÊrLîHK6|.8ÇŸáãv›çÅÃ<<{9ý2??ã,^mŒŽ9g­Þ1"Ó=7©„¸O°Ò\NçJÎ\æ¬æIÍÖÅÊ–ªÉ ¿Zh^4›€YŽy.ŠÇçD4GkÞ*X´‹ÍÎ9‘äP¤é?nÉ€ôv›‘#¤=(>Ñ ›|Ñß|£]Ï'/ÛHCÑ_P_žÈ?r´÷¡Ì-OJ«»Œ)¡GܸVhÇÌZñ¬î¬«”*éügÍx„2gëj¥]¨G¡»¡Ui‚Ÿ%êãÄYÛ þ%Õ÷&žç1Ðph$£E?0S½ÎR9¥ëžÝd9Šë\pi•¾¯I:u¢!>X@}´²ðuIñd¾/EçÈÁͺ¿Þ»”ƒ<5-™@{aïh,æ(® 5òÀ¹#ë]úêêÄzENMR¤/€ÑßaºqðËßꘘ“¬iÊjúíÿüò·þ­b¡V’Ë´ÊZrîHvY@M†näÎ}³hQh¬­×:`“ö?rÑKu@þ×_þñ·FÛãV) ÏMÖ«:Õß8½iK Þ-©ž %ñ\T>&q€L8:˜šŽ5Øè¨2Á™u)¿ö_c,W”CX2ú­0SúØ…èÚU¹šÉÖ±ÛJÇ£Eìô^Œ‚3§1‡’ÃéôS$žÔ•_a.È7C5è¾™6tQ¶¶ïj(Pq€ 4ÂrÁ‡TÜm¶ÙˆÇy†dË+1†1éøÀ¤n“&õs)úGuÅúœM¬IÒËš7 ¾ö.×ÄŽz¢t·JJ,Rî÷,€³·Âl£äº(òމò¼ã8&ï®gœekÈ9s°M‰WÓEœ“ÖLnþW®×Àä´oxÑU“µÈLk#éÅK ã ëoÜxõ_ÎC²^N¸Ò£@GCWxO{m][­wrôsÁöî“hÖz$¡Ñø?0# tƬ?0?r)Í‘ŸîÇÓ<].ICehD‰ å^û<‹0ŠkDpý(Z}ZÃÈ„ð²8@["Kª.¨Ü oÅÈØYüóᅢq‰MÐ…4€ñjíÖÏzQ·aQ·aQ·¨;Þr¼4Œ’žÌv+›FågœÈZ`d™ßþù¿÷ïÿý-ð6^`葤8DÛ®æÐ?r¿ºÝ–w­s‚"?nבþŒ.áö¡â¹$ºùŒ áÏŒ ͦx1œ±–†ÑÙh´KŠŽ£ ziàêÁ8éHMÜeoÛI]¯çŽ¥†Jh»úáÇ똚™Ù¹£ð²Žyó»í"ú¶?rëù·ß~ûÛÿúåwÛ VÞk¡¤!C)Ë.(Xý¬GØ@®öë‹ëHé ??ÿ\…·eñFp3Â>®÷ÊHN…EC¹Ô!¶ëZOZÉ…bþ?r´ÔË(˜S[úR9£Šr0ú·Ì±$ [9§íŒÉ4‹ªû:Q=I1¦¤{¥n²¼Ð´¿ŸáÙµw¬ñ¥÷ GžöÈ0TÞîJ}‰ž¸Ì(pb$ëÝ­˜“œ¦Ù /wò)ÁŠ(@xf@o¡g‡ß"Îýô¶ç¨ó}¬#y5paàû?nl3»),%c¦BÓ8@y\reþ$öØT'5LÏ3­ŸˆÁÙ™å¹ä+·7U Bh}Õr«©íé¶!ú ó?r¡EgE÷].GÒöPƒnËîè´pÈ›êØjºíϳ¦ï".9§UÌæ3ä¡4NèÕË ë–œ.+ƒ®‚ºNýUÿ}ÊßG#e¸ÁzÐéXÑ5ÑÚ0©o°ptýBŠèš·ªò¼Íº–w—?? /<‹JbåW%‡ìÜ?rcuÚ3…ÉFtK‚‡öDG@ŸÇïA˜~‚«r=ZƒÇOK˜‘Y°Á ³*%:‚}ú0+tá"ÆßKÖ11ø˜Ò[Š”‡G›Û'¨«¶§àLîœÒ‹Šž¨H8ô÷‡6X%e°„ޝè`?r:nhuY«^v÷YVL\/µ Ž¢|W¡`Ç<B?r¾øNPc=(¦š¬ƒ@ò̺s•/ÚÇ^÷Š!<ÅrØ3Àä ¤ŒÒÁ×= òéÚ8‡ i³7Û¬ì?nØÚúÜå”ÞÝ‚½þKÙ’%l»­ëªQŸuÒ´™‚1q˜o|§ÅÅ??¦ù{®« \ˆn»×1ÕÓx?róo~4–0IrGË(Úã‹Á{øÇÊRº±N0‹Í21}559ž¶k•M%æT˜'m·Ðvfó¼³áLÊXää墳¨ètqv›^" :PúáÈ©£F4äOeî`(`c6Oñ§½rF"vQƒ™£w™Þàã€n¢ +#Jó4þø>-Ü0·kç|ßýçI§«dRðë8Ô–òÀÏ˹]9È@8ÜÞw“ÎÕÔ«}”–â fJÏ iÕØ›Âsh¡éšy‚ؒö®À®ÚžiÚ`â@)¿{|“mª]FmÃÊh´0À‰…MhL?nN$¹x¯3ÏÁ‰¼æUôØÌàCÛ ¶ßš-0äeµVª˜Oë9ÍŠ¥Èaìý/-SŽzc|¼y$OÐ.†19=?? ó§'†;b˜È{Èh¦qÃÍ,F0?nÿrÏôE¹Pçψéþ ˆœòñ²yÙ2/aµ¦£+Tc{ÛUì¤jKºjS¬`a–eÎ8¶4??Îd€Þ»¿¬T«‚?r²;ZT#+˜ }“—©JI.¢+'™ÜÓytû‰¼`DÓ…KǼ4Éݦ!?rAuµ‰U—i·2àhžN¿þ(—#/úRúß¶yÚ(_÷ÄéMF%l´.+9?0ÎÜ’  ÀÆ¡#0ŠâÈ‹@ñÏ߿›a'™ÒÏæºÖʹxÈ‚€¡::’×pÀ]{­è~þÃlzžÿL~øñ쇽Ö<>Ç©ÅèNŒÞDã¤c{-`D› 2£khÇ·°/#7ÏçÚÏìec§¹jÿÖ[%å ÕGAù:$7JÝ­GËWÚÜÃò•` Ú'1ÜÿŠ‘²‹5jþŒãå"gê QOƒ=26Gù3tbZ7Ä®®#ç´IUüþ€Þô«<§”ž‚8`z¸é¥“kŽê…®ŸHÏŸK/A!.ãƒí†z9Ç4îu…–ðº°G­W ¥ûx!™8 餱jîçukxïô{«@Ú?0àŠÌ?n\’%Ó(JDÈ6jDnȬ«¤öS?r Φþ™^w ‚OƒKg[†VüŒ™Š¶,#.6õûµ¯á ´JØe•—Ì#‰ÒLGâ€,Ñ3»«YéÈ]ÓkéÎá1"Þƒ9A-g–ʉë1/îÝ«v“Z??ò2pܲÍÏöÑü ¢±À¢ÖÏÔù?rÆlýܶõ«°%v9\IX<$[ÙÜÏJ®„ruñe˜,‚ˆë[¼4â<ä‰:|Þ¤‡Ï„œ{˜<¡dûè>£©ÚŸÊ·ŠSù1¦Ù}ŠÙF[m%Be&á›hû},£XlžäðJz”V›=(†íë‡SØ<;Bf—tMÍÝCê’›7ÓzÛM¨„w³G´œ64 åÕÉÓÀÕR@š4<„’?0Ñæ.4 …?0MÐrsn´Rö'¯n“"“/ɼ%¾úôÃØ¸—ÎmU—_ìk¶ŸÁWŒ4!Ùf¡7T6@~3mªûUµ-¥sPtå*]^C*oÅ"¥¶¢p¡ ƒE^í2^ŸÅ”»öo%CŒ’L¥K›r1/Î=zæ.²¢6Ù–Œð/—º˜êÀB£/ô ºæoÝñ/ê.-° u&*Ð2%Ó&ßÄÖ¿ÖáiÕ?0-DJ_ÞF›n˜Þ%úJÒ¹ïÌî’òÎOýÔH0÷‘Æø(e@O´ø?0ø\dÃωd}{/å"Öã#ßZè7ŒÙ¸•ÂQ‰ÒVG%*óy™¿º€l8o_}c!#<¬é0u@¡Áê±^ƆûÛb9®&ÂFï,@’T£H:·yî%&qΈ|­5és®è6©À»Àö†2‘K“ëÞ.j6ÇN“[ÜŽش梵õzÅÆ>jn[7°jœ…ƒ\¹ÐrûàRtu§]ßM™þm6ï]?r P7Ü zè V>±¯pßý¼Û¢ h™¢†úQåÐÜêŽí½îëâšâ#H‹ŠËÜjZÉݯ¤TÙÕ1ZŒ _q&‚ï ‡šD¶I ׈ª;?nƒy!×X¹5€c®á ]¹m^ìkâ™þ²º³ôTw¬<·nyA×MOË›CO–SÜ2ºƒ æM;·«F×iÖ´Ñ~?nzTÄhJÿ ^¾írŒª™š5Ù1-+©¹C7“U¶¢v–óîÍßH<Ä9NHìŽE÷ÿÌž¹Fâò\ç;87¶eÝû(ú=e}°á¢í?0Ðõƒ9@¡¦ÚWw@tÃÕïë‡ížQJ¡œ7a¼Ë¯ÕeÓ-VùˆŠT³}8ÁËdY5fÅ’ Ñh—ŽÇQ\Á¨i€#Õ“Úû.#‘iÓ¹:ÓˆEì¡ÄAø‰ ë3‚%'Wv]›†c@õPtçÛ»ÔôQȧE—mÚÉñÈà˜ò³Aˆ·3ËSDªZén˜Ýà—XÑíŠds]An¬åÃÀ|l+ãó³p|~ÏÇhaOÄôvpÿ’¹“ˆùOÛ­‡\ˆª–ö¬aØ¿yŸ§Q·ùµqΚìÑ0 _¶Ÿöúš’€ 9ï×j*BìÓ°1h †¼ê†y½{(=6³3¨Û{ø@/û(4¹Kâud¯§÷ y&Í`??9ј“µ*÷@õhP¾62kÿð<=ñòôäÿž§äé/¤“ÛÐB“£ÙÀÀ¹§×E¾-WÎAN?ràמìñjÅ‚Ì nÙK´~¹o^•p,Ó©™ôiþvÀ¨eçT³·™ÀZÂ?r¯Pá0Ò`=Œ%íÓ]íFEÇ(*ñªjðaÇÃKÊ7zòð¾ªg¬ABFp‚¬$£/Ò×ÐévppÇ!²%íªMÇh´ï=Ïœ£úHQ3j¦\sh‰ì6aŽÐ»~ìíè‡UL-i(aZA F-xSy.PÌF+‘Ž è‘íÖ%ýY-à??ÉbÆìyùêVµ¦MÛ ªçSì ×£wH6 ã4ŒºÇ7Éo{”ÓÞ¸Ú]Yi5‚Ð?r‘5®š‚|¶ü–BX߯+²Ì?n¨4¨ †ÖyQbYPÂ)õ–LA—¦Æ?rѳÑq9‹:.@—Õ\~²‘IFR”€û%’„j…ž^v¾%wܮ΃¹ dnûº²ÎÛ|hGÐG-âDOeÑ`—•ÿñ_Æ÷œ}À™—ÛÊ£?n÷@®é¿˜®Ó?nw/ä‹FÂ`ªÇ‡ªW묌(˜]û§Qf,h@™µ¼"•œéj%ÍÄŸØf|äæߊ»ÐéS;ïNOH?nä{'ïÂf/Em|·ÔS6m¡fj5¿i§T2µK4S\)ÍK£wZrºì9ért N«¤ß¦ÚŒ|šO@»üZÞÁ 7Ó‘]cÕÏ­ýrÇÇ4#á«ó[(|É¥ž´ g²?rÀ½$£Ã½Ð™c7 ¼&wË)n¦u²í +¼q?r «ER§Hb)r?0š æÖŸü&?núê<¦a>Ò…§»Ú[C>ÍúN±?0)Ÿayª„ǘ—Q·MÞñžÉ;0$À¡F±4Šý®G£/—ȼ㠲 ‹IÜS'q"Ö}OsVÕÕ…ÒxÅ„ÅELh[ÐëqôÏ=ŘM@È2¶ºt/^£W\Õ‰Tá®?0ÔÖ¹U/á6°’ÚPw1¸ÇÚN0¼Õ¥" ;»8EWÈ>lÅzAÚÂÑŠô\ÌçΛŠü‚ïiû)]¢vé•Þã%ŒI`³Û›Uñ=›MÁ÷I±Žõ߀| ??Ú÷ë÷-æÈ}gd&Á×Ütê×P²¬ÿº?r›Ýß©á?0³w²á€Hÿ¹æƒñÆ?0Ùáõ4½~‘£D9¿#>{¨Ÿ²Òõ~²Ú¹&)Ê6¨ºÛ¬CÇv“Avâ(??¼½ÃŸ÷§¶–÷gkÙìþm-›Ýß·­e³¨îb|Œw&¡s!k’¸H®Œk}6D‡ÐP,©éB*d)2$£LÀÉPú"ÃIË”ºH=´:’F“šPá%Q’Å’ZS$Jƒ$÷—épPËjºN7]{ýžºc!`ÇåôÛŸüæ'÷Q{ GÒIls§ˆf¢êYGÇ«pu«6úEê¹–¨ŠT÷eÈv}OJenIW8hi[¨VOnð䔃 hšÜì>´i’'N¨ÿ÷d˜,/t[õ”Þb%]]\›÷—hA~ÏlçÂf9á”Âê——îSW¬JiÕ)ª˜’îª4sª†–:ü÷&]u¤Ôn“•ZI[^@Œ*/GõBUJ6ãûqº1LØc v5‡äi[/ÿ6,Æ“ûî¿/8>?0ÊÉVÛF²Ú6«m#Ym×jÛ| «m#Zm¯Õö£ÕÇïeµÅ2xØUX¦­e£J­ß'Xy—éCÍsº¦}FýE“Â’¦˜+C‹ÎIàj‚®‚¸È\‹ïÌÈ÷d† õ¡eÓ9õ¦j¼9úµòT´5ðYºD³µÓ[§64[ä YPØ;molÙ|=]6•˾×ì F??ox‘_GïgF~†ä¿M9]—²u=”_Èê{Y…/¯x??¿¯Î??Ý/ó//óŠgZÀOpê?0!;>ÏpÛ¸MÚ§ÿÐké'w订a$ÒU4yщ·é„ጼ[vˆÆ˜'¤çø<#¤sßå21Õ,CÓÙ&ÓÌî>–zEË }£æ??ºÈ|t‘yï.2 T,qCœGŽ_ìô1±\:â D«EËrGH?r~tÃѼd³›ËÚ©ñ’‘L¾xÔ'ÜôàÁÖðŠnttáùèÂóÑ…gÔ¬Žg/|GŠŸÞå§™nv¢×OóÑëgŒ×]mü?n[+ÂJ·çuó«àïýîcÑè0L8ë>˜ú'¤$Øt}Pèûù7}¡ Ä´¾áѽIÜ`'ƒéü.&ãº>¦=ng½ƒo£.Å£¶boßS³‘“Ö=9ÉéÔaŸîQ: .óŒ/ï+m­=áoÒ7 ÑI`Àñ¿è4(ÄÌWøÙtvV©ƒüNsÎîdÀ“ã —c08ÏÛŸâÉWq\†§Û¢CUCŒ6>ñvØÅ¡UN«Úœ¬QZBvß[Ž,4:?ns]kr|Ë!páH‹Q€³Ï YîÑC´¦©—W€@šã  û¾ ¶k r`?nŠÕ4‘REûÙ+£vE»PM-ꦺi²¶Ep`³RúT^^†åŒüfý~,ú°õBVb¾€b€;Uу•étŠ)¶ùS`Á·šÃp+n\ûÈcKãKZ- ?0R"je»vAÃ\̆ÁìJØ+þß:{GË{‡‘·íÅì%£÷æ}W×®‡M¨î+£Ýüà0ðgu³(RGÇ?0¦×°„B;¾#–@˜uØ_ÒÜàÚ:Ñš¬  ím×(Ùñ"¾ø2ê W`¸r‹&þ:~d„ ·(ð×s&ÉÜL¨ç??uhpÚýÕÝdÚhÖÙÐqÙK­IŽ!Ÿ~Jþ"yyðøÉÌYi¶bÉOPça‘³ ó¹D߀H`Az²"{«š ”3S_é© â‚×”$i¼l5¨MYr!^fª›=8·ˆc»^ãÓN¥B¼‡Ÿ?ntDiVøIí²Ñz„* Ár©Iø½8ʰ¤¦tº§‡U¯§ZhNñ /›™(Ѭœ†'¢„Tp¬ü@—ðƒzï(ŒÛ5Šbdm7i«¦ËÒ‰.7½ËÞªUH"û€êŒÚW?0':"Šêb÷ -Ñî{¯o GQY!¤U8r ¬JÈXoß«ÊDý•9¦ãÚtøfz)ë¾Hkš£J•7¦òJ×crBy_ÑåDÒpü")?0íØ0dÓ ºOÁ±÷Èlz|„žž¸@l×ÝÛ£ùúÖ:«)5>^‰þj¾Þ$åM¦\S<ð­k5A›·büÆ‚+Ð~0’ÅÛ8O§ËˆPI¢¢ZL=škB‘—ƒ¸S°ÌVɶ͂u¶ËÖï«í:¥æf²X(‹‡%Ì~) ] ñ,éIiîiÛ.ШvŠuvÛ¾6Vd3À ];”Îm¤„dâ`Ç~g@ÆüªÚÔ[µ~Q/YuÛd¤YÛe$ ìEcÄtt¤k†*’.úøD¹ŸzýÙ[ÂeSÝe%ÛK1C´!âöD©=ýR F¼A2š[{ǤŽËý£û Î??kìi¶Ëš"/Vz®\ºóøã¹$ùÂGÜ8¶±3›A—c&ôo©IC1Ì…mUäÂØ¤Ýœš÷IÙ`Cå¥Ùï4ëÙ‚eÃ?n·$8Êáy!yÛ¬æ {»ö,ô¡{k¼Âw’´l};Tœš'½åâûÀ½¦¬ë­¾ß'x?n¾.Î/??G§øÍmÒÞÒ›/..ƒOôû'}²9mW¹Ý¬n·ª[t?rgµªšTƒËËu¶"¿''ŸiT<¤‘JlÓM¢TÏKI(÷I\zÔѤSMÚ.¨Ê,?0Ì 8Ò×™¢Ï½ÞSà²>Ÿ “?rU–êÌA9ï«häCGP±fT}È’®iì/(I"°Ù&ë.Ö ]‡6Î@óèeØ›¬KÒ¤KÑîi ƒäïœ÷ÊÐ¥!è~Xdσ£îH7jú¬‡ª¡¼xL íˆrŠ>ùC÷Ó¼}:Åïg /%šòñr¢JGv§ÍÍ6G7T/‚ÿþçØ™Ó&yÞU5V=Ü+è-î\,ÃØº¾QTËmaÏÈåË%›Æðõœü'XݳF_¹Ö˜ä1jÆißl]3.ã~0¦ÓΨÙZ?0Þ aˆZœ×Ý„¶Ôcä`9jWhåäo†Vps¹íލTß@qŒö??ý4˜ô(Ç(Ê0¨é¦…¿§³W”[›%ø5¡é°Àpµü^U"*^NÛÛDMÍ$:r#pS”= 1—gs•Sã?r0*YÑ­O04¤?r4_òнÙ~QY„*Ë_ßþò°†=’òi—Á;Û09MSJ«<Á‹å0¼3R_£Œ +n2½Î"=˜DW³Ï>»–¨˜•ZÛå—ŸcR…hšf«*Í&aÒ®Š‚½f8 =‹Q•Ùý‚l´Ckó,ày‹;~?n7·KªaÛauÓ Û •1еkãsÛWï‹2·ùµ…†‡¹ „ 1 þ Àìo9³SÏ?nvÄÄ‘wœvù@ø¼²pwmÝgÚB5šóE¦­¥MjÅϱÛÇijXR—ÙÎ’\›nCm‡Ã¹Åœÿôw¡¶Sl[â…¨­ûrø|'??§ÑÞVm^‹/9Åé(O¢¾»^ÄEò’_Ò¢_R–Ö£/»sá•ßúÈËñ¶9r”#^0¼Xµ•/V1N½[õîV¹¼%œòª•dq€°éC¡v³3E0(0W=ˆk‘YQ¡lÓ ¨#HŽXñnùhii.%c?r´›?nøÃqn´[å7àFÄ1T%brÃD¨¬??õÁ·‹ïüeÙúnË¥ÌÁÞ’²èpA"/'ƒ±‡àc"¦/OÂ4秸 Hcãœ5E\Î^¦Ê¬)Yïs~%³þAùïÛJ“wÊWIBD?rUÜ–]r—YÌ;6kÙ´=ð¹lô§?rõ#˜£²iž xfÚEŒ”é¯þé7¿û÷ú¯ð…xÅ;­:¨@w¤p?ni??^È•Kç^ÉŒÎá‹Ä}PŠOŠ¢a|äø_È[³ÜU/rá&¬¥äï5É¡ga xNŸT›Ú¸Ý.§E­UíÄ‚g«b??MV.ôaê?0÷CÞ/¦ž+ò™ƒ†ÌcsŒ›‡g/§_æg½Ñìèïp­ӵËydÅŒh}0 ÇfS7-cß4XÖþçNå:YfkÑrRä‚W%ÿvë&Ú_ÊPãjÚlT¡þØÁ_ÍÕ@<וeLMW·ÙJÒq?rô*Ì[Ü-?0ÕQcú[Öù‡1ãßòÃÃøVÚxåGб‹;9ðd¥8O‡`›pg[vqC›4Cû¡/8I[è,hF_ÛvÉR‹ ;q/GaŲª»ÐãCï>DÜ $«î1À.÷(vÕóüœ«üÚº1€ÊN_ji2RxW9#š‚P@ã@ÒU }2¼±YØ'¬èýÌŒƒwÆ€??ƶú«<ñìçb-Ÿ¶Yv7±Wüvx¹ì}S@fá±ùzÛÞN¢wÏìX.×wR&[œUµ¨n[žy‚ðUpyþù??ŒL;~3*)lB€Ãa…Mš~¾m»l$zpHå. åŒ&Ð>9¤„å?r•õ»–»Xäq€˜¬Âýxú-®Ü6Ûöq|q‘b 9{¶ ‰$Ö>?n”%S•C?rEWëªuc“úÄ-s™¼hoY äjtXØXQÒ"=R[&u{[‘6`áiבÌFŠ’ 5*·—(W5Ã@(ë¿^B*D¬`”s.â´ë,ƒ'Ü“FêY©·_ø/YúÇ%_®® Š,û??17>,Û®bí³]”CZöd1 {®?nO¨¾Õ=óʾƒB®ghYÝ y«ú@‚£Ñ¢¤K)û¸¦W:Z¤ƒÀPp Êì˜Ó·sˆ•$q#ÆÌùU&¶÷N`dɃ?0}îOH^ Tüԋɱ‡¬½ ½ãgLècêDlñ ö§¹Ì9ž±ç¡®›3Ì8¡A׌0tÙ*]Ô\«ñ’·?râ¡8Ñp×Ì“+¾fÿèöF,×RšË÷½€:zd$)ÃYÕrÅ`œ«E›ä<:t![ÒŸͨ?rççÛ«Î¥"WžÀ»‘!8Ô*Y—À«ùöÚ¸ïè`ª¨ÐÜA¥Ù-ŸØóa¼1&?r§Úm‘â½y=$°Kðï~œ:®?0qPém».R ÁLMúnëÄñi1œQ÷÷ˆ}²X΀6cIƒÅ?nÕiºÝÔí ã?0þ» 8îÎûx½Y4IW5í|ÆŠÇ‚FÑ4+Éø¹íò³P¤??„»Wë-N¬c¦¶gUÿçq°dÝ´X¼G,ÌÚÍcQO¿S~/ºXkì$ÿ–ç¿Gÿ¼¾Û= Âù~±ø—úÍ·¿øýQ=g[Ò,÷þ÷eú¿ÁËéeNš??ªlHÍÏ‚3H9˜Aÿ²)Zò úu“5OŠªÂ1£DÝ7Á+åìxƒ RT-ùÅz_;B[ghKpõÇÆË·‰íëÏ,I¢²^lË<Ì.»™¥1å¶<·s¼ŠTTæîŒ¸¶A·I?rÎtOY! w—Gb??0R»GjT¾è/Óubè }Xoö-h¾þYßSÔ‹y€¸¿¡ƒ‹ÝΣNêaã"ÜÛþR*áPý‡Fîn3§äìßÃbGÿ¸ol!k™(»f™Ò(D˜OÝkqmGJ!œëEAv·¢öK:],éŽc¯“,–EÙãÀL4ŠžåÝK$€àé˜Ç¬i&ah ÃC×Có£Ö­”)ä‘ ‡¿B©¶ÒžÁšåÒºA??¾Ap‰Õ\ÖãÉý'¤•öÒ£_Î Vè,•{º¿ ¾ºKÞN¿¯ot??ÕG¢ž,è×èv7&¶ç’…4r}¹ßz??Há=y™¾z™F“¯??Ãn‚³þY9Þ’þ]%Þ,Žï-xÏ›‹ñC•!óV2dÎI ¬$o:ŠF°Qÿ??jÆë†V”»d]¤ä Œš$ˈ»‚왬û¼è¨YýÇù%Žæ2)ƒ??ÖšW$Á7t??zwþ wÚë‹ÒþÁd¾ ‰¿ìÐN{'oãq°mÙ#ñâ!;@¾¯V“ñÊú´¬éCÇÔ¦ ­EœòM‘‘[¿‹S`ê×T‡²¢§œ»+sÿ_ÛûóŸ¶`Ò5A<ààÝk?0ÁgKÇXkÁ‹Jr¢æñIsD?0ŽÎ„1]ŒwL—ÇJ?rEÏ£(zW¤ºEÁ–T)8xI¡5¥¨šöt}+‡¶BË‚[øÃŸŽŒÝÕ6ãbk%Ýá3^äb`-¾#Õ??xøuðÙùì' æÌ‚pþ°€Ô¹® {ìæ=j‹üƒY&(ï"™Ÿæ”õ Ií·¥õ£°ë%~˜ù2¿ —eã 5‚N)œ¨Oé0†û/ù æ—)YW}t‰¾–Pù‚š[?rô¾_ºÜ…¹ik‡>:Ä|_6qjXr?n—Ð+?r .:$öe’¢;wæ2ÙmƒI×·4Ã]Ú)ZPZ êµN¶e»­k}{=PçY?0zÙR¬tH W/<‚$Ñ8F…F"ç–[N8a/Û]M±>®®gvì|˜7TL¹†BèsêÁÕÅì:’JR‚¸à¾û ­Ø<¯3pò)“Æ8#Xa^âkù£(F»äÊd€@µv›¬­Ö;xS)<·êYUúï·šyAg˜ÜƤ“Ú,÷É®܉[TXé®~ú8ÂÞËuRÞ2h±-DuÆ–qB>VG“êb™iÄÜø/"Sò„nÄH×€ù›Ã#Ãp ¥¢Ðñ¡¥ÙZšíŒÐjÿŸ½çìn×u??ëWðèn‰g÷$S®w‹2ã7n;[¦¬ž,ѶnÔF¤œrrÿûH‰¦•äe^¯É± À&°-k<›ÙÃÞxÜ[H=áhIÎ[JùÒj<\Ø‹¯£‡#a5€Ó7Ÿ—wÔIaËÞJsÊZO7‰ã 4”Q«–ñèæÁâ’gæ®´± Za°].Ö;@F??³JBÑy£¼¶@â—s<ßûëRoà.D%¡Üÿû¥J(Ÿ•Oé—dx+Ò-™c¾âLŸUÍ8MÓ,áGÿò‹˜”?rH­%* l‰‘~HçÃ(èä'ã¢x¾^ui‘¼<3É0ð3*ï|…2‚À¾V4ÒÕן?nã4‘$m³¹Q¶'éÙf8Iì\­Ò$@$5¬§8]ê*8(ºÖ õ}hÊ…Ty‡eÆ­“Sl 3Øv¾âuî©Óxuú´iB‰*å¾ÒðcW¥†Î¡äE#¯Rý‰Q´`IE»??«ìP§ÊÉï?0{OrÐ?rùªR«‚ë-¤GÉ DàܯȞóâÁx¸ý”§ +¥Ý–Zú»+Q؃½•åH—ì&y¤Ù<‡qh*Jñ±ŽŠ˜«‹´å^jó;ö)úÎËKóÊRRP¦v®¢B[Š3÷Q¬èÑ;2ÿ’+?0D9zÕ?n¤G§óÙøæÿÿþmxˆ7­‹‹’Ûÿ?0?rø;ítðÝ<;ihoøkvZgß4Ov«uÒ8i}Óh6Ûgg߯Fd8x äÿjþÿ…¸1žáñšˆÇ½gÛ^gðRÛ&~ˆƒ<’¤Ðn6#Qïãi>§©@ 3BWKLñntœv”ËÄ\¸Q†Ër—¼°£pä…r†Ž[À¸?n£€¡ï¶àNlÇì‡/ÀWòc%Ãqâ¶p§*¼òìMå×i(—¿‰œ½ · î9¹£Ái¦ÉÈVI»”1œ¤`‰Âߪàð-ž E{~?nN—¢%Aæ좰"sq†*C2ø îì¢L‘Gó]§'QUá˜R~§WÂqªSNsŠô%£-„ü‚ÃP½:ÁqdG›‚—š Óa|ŠO9A19jЀdÅ˃ïäT´”cè}CM—î–‚„¶šmû‘Ï÷…÷ÈØoª?nØúÝ’À¤7˜-$8ÿ£U?0íS -ÿ˜[öà5x??𾕍_—ÍFC‚ m8û-÷mE»¹“:!« ¼x2j’…Ý¿‡öpYøWëb1šMsvÆã€FD•š47§hÇM¤¼,$9»rËÁã:Êýr9 ]…«eÜU‰ù•²<pÀ;°ÈÇRèrh°.f X15*’æcĽGÒ‹ÖÁ=îÆë5£<·WyüÑåb…C÷ Àe£Ïø4A·yèÏc_¯N%šˆv¤ü5–?ráMÚZx#?0IÄ’[ä4Æ»¾Gí*9«ˆ.±*SØýU5´´|‹Â$ !Î {FE àsA4<°³¡C{âÁËØ[¡žªf©GSê O“¾ó¡M#=ƈ“?nÑ{yÊD¹¿ç¹òÛ›ó`”‡uåó>6bÁó{¶ªd÷̵?njdWdÏ -D—Ð0,{1xOº2À¦5-³é”X—ÓÞrðŽXýÞðœü¶è!e1V„‘åÂZXK³"Ø¿]ìeÌGs‹X‹w—Køl§RÄPpö†Ã Ù½_{£ñÞâ-±¨@Gì}ަöåÂ:ô6š"?ný(—ô0X‹…Æ‹nɇไHô ’ŠéÝl±,Ȫ»­©¥Ó4'È2FóÓÞcd6^ü›†ªRTºv#Èy×ûÕ²ÏÓå¸0ͨ,Só‰Ë²0W4½:†Ó4ôc¦$©2 ù]ãúMíü4ޤíw~±üòÆ^ü2--ݸ[¶V´ Kîvx=ì öYÚ’´SÙ×7§U‰™ÌJ9ÞJôÓ1—/äˆÎÃÞç‡É/Å=’‡<5qí K(Œ²îÂ/fE©-{V*ŸO°ùâr9m‚IîGÒ+aBž²‡Q&½·£©U‹ûvYmyÔïùìb`Þ…Ï磥z„ß}¤ÌùëÃæ]«RkNÆ·,Ý"~=_hDqfjcª }¾Æ†Cï‚~ °aœ÷ÞCæÿÂ$›??ÌÁ‘*®Á+G6 ¸Ÿ÷6}ÙÃ?nUÿI÷'çQ€X,}7r{©jéd_2VQ¼å”fåÖgÁSÐ3šlÊí0ååë":šÿzú|aÍ{Ý`EÄuŸð9¢wÕ°µ<ÅõÖ¯›fET!¸\‰)“ö^ÎÚ q†R…@µJºF9éñ°€—ô«–ø¯dŸ¬ñðPœìRýð¨Ì£ŸAì}íÅò¢‚}é® :LTñCI‡q dlýët¬9×S{ñ”’‰5YB§j\Ê«??'4\ÆÜ ^×^«¾Yð´2ooçønåPü†ÑŸ-ì……é}dææD\‹F“ªÚ’¡ÛLK£®ý’ºj#] ?0EÓ¥êÍó…mM³!4) 5·ãªj³Lg·bî~·9¤=FŒ<L–£ &½??Gâ??B8Ÿ„gö:‚øº8hÚ·‰µìA½Ú‡î1øÿá¯hû%2+ÒxsHŒ<óºfCÿ×=D±”cþ»I õzC;š}0#§ñu@ÓûDÜ÷ÇÖï}x¿ãM@W?nšñ-p½/‘8Å J¯0°ÓŒmüÃAÝ?0™ÐDžOæÖ[ûòBtY·œ'ìu½~}}]ÛÜ:Ô#»úz&tS_e~à±Üu¼ñù± +#8Cÿ준w{~ñº`Øf+,/õW`}¬+³gÎÞ¿ÄÜ(<þH̺ÏXFY=¢×??‹Ýö¬»Ê6ßCᎢÃN)F§z äÝr9‡\µp¢Gäs«ÑxMÌÙ{³š;›à¤<{?n×\Ï…šMGv?09É@æŽBŸzž_T¦m‚mÿñ‚;[ûPÃ(ÞVKD??ïq!…X÷ù­b@UcqXBatPëĉn‹ô,rìD$ôÍyäóBl'ºlŠ£™§á¨0POÑPzò]J [Š1§¿z…º'¾èNcÀâœäh2½%˜¢jË÷G\Çhy?0;Q5tŽÕ‘œ¥ˆEàcøße¹;@&!øÞ:©¸>ÂŽ#,ÜD!² Õ£^–ìI|Í“?n•ОÀÂÕ5¢á}€l>†Ü *r| ý@È@Ý…»(¦£$SÁ%<")ì<ðž>Dí¢X…áQ:??cC(‡p²}ÓKS±Ûý£?nwv”cËv9=(sÀŶçÁžs?r\À4ìýjƒõb~1›ÛcU2ͼÙBKcñÙ˜ÅÉ>h»,p÷?nx±›…À¯Ð>ƒ*üšBÅ -p.4€¯=s6QD]ó}>?r›Ïƒê•§¹M麀}¶7Øíq[YK*7NȬ0qÌ÷.ÙïÍ Î+Çõ;.ÖF{ýzT¡Ñnƒ+IJԥzDÔÚ><¼Ÿõ¼À CîAö`¦N ¯²P?r 'ÃͯNæùqæiÞäó=]¤ªâkgÏtMWa™«X6a;žgãœ;C™+qáŒišÊq#[?0ì 'I‚⊜6"× É?rPoMRŽwð%äoNJÖ>g‚jø!?0At.HmÈC†€¸ ?0Ù‚7ƒ_J¸¿B²![ê»ð?06gç¯?râ8.xƒØYàpJœ05v¾GcT’0éÀnŒ5”'rƒ<|´H ã|m@¼¤ê¯Ãe8x¨¸I(’ G&1Õ‰¦º-Øòl•²EWþ‰œû¸û,7U×õcüÑŽûTR‡^>%@ð.!Häîß Ùa.hãÉv×½9>»;Æ>{*Ê'áN?n8x’Õ]  dF ¦vº?0H*¨ðÔ½Ü1Þ…ŸGnPÜÍ îºr»‰_jCæ:dÇ~ĸŸ^J\gÕÝE^?r?0êÒÓ$fx7½AiÛ4鱨ÆH;L“lÕÅǨ??ŒW¨_‰° Ó¬M$ÚR??~Ó „‹Ç)~›˜>Ï?n“3ÖM®\vv,f"ñû'žv=¢@ØöFÄ?nÞ?0'9œoµ‘ð$›0èÂO(Û$7]øÉ`„ÈÏmì±l ‘òK×î›?nÛ™Œœ´†7KÐÑØÜ€Jä[G¢À8Œåå„1GBùç[?n»âk»Yp“ø7¡“„yR¯h-ÙÆ<†ᇹöüþºâ„$?0oðŒ`ïÒN†\<~ž]|3ìpE.¢À/>4”—?n©ãù"ØnÓ…¦RþI»¦?nCã  ø?n—?0‡ÐxÕ#€±BÐÞ#mhë?0ÿ*„‚Ô€'pÀ QúpÔ+ …£r8kDʇJå?nnu(ìï;ãï*gÆyÖ\ÕpÀÂg Bô/dŸƒÚgbØô†æëå,v›È7‘æx%i¼ÂrÍX ŸØù’бKSŽóä¦1˜ ჱúy‡RÊ-¯TuF¥à_”øÝèÍçckÑ›ÚËß—ÂhU??²m±Æã~MçôSZ¹¯ªÙ÷C0 B#Kï{Ø’?rãlÐûñb¶£i?nuêý0ïX_ÐM??;¾_3º óîI UâfËï—g>œôv„ t¿LÅ"Ðû_ã?0¼ ¥5ߨ=4ÑŸd«ùI,ìwëGà€Ž~̱¢óÀôó7eWÓ¾Âæ—‘f"?0­é³³êv*32"223ò/2¢ñHo+2àFMõͬ‰G–“Cõ(AÉS%?rÌ?0h™}•ÙñÖåfƒ"Œ…h'åbÀWžúÙ :ÅMX€Fqœ»¢‚µGŲ*l/ŒÂêë‚JæpómÊk³Ž_ÑŠ<õ£ê€q#J8—›Ž„qc‰…â,/mÆé¢Èpjf€¢¥fu/ômˆ¿/¼¦I¤L¢B5Rᦧ'.n¿â÷Û!]<2VSÙÚL??/?rÏëá»"ßngê¶6^}DzËÿeüøÁ'oW»W&'JÓ_>]HJÔR¡‘EXK ¬¯}CC:äºë¢µÆL®7Q…|n{§c³YË?r(´w7¸)!;£Àõ{¿>»¢]p[ ¼)6{åúÒÀ+ý+0ôÛ‘’¡AOH9ñõ}²nön™G’EônÕýd–kC‹Riöe¢´ ¹ èo??aôz1qñZ#âÝ~ÓÍÊRW4Ы?0á¼ò}óýkeXwþͳ§ª?0¸ì~¶È9IV„¸ðÿÜ쮟nY›ËºY¸uÀÁ˜mÅ@va0êUígc. æ`|øMóÞ¿¦Ù_£QÝW-CbÌÛ|þâß?0—SûÐn>,«M «3ÛDšIúWyöæÕŸ“{ ¨R$>HünòE¡û‹¤b2#•öäé9 sY· j½À¥õ_ìje·mpÙ†ÌftAñüJ¦O?n}ƒùV¯nl¬:6܆©ÐjÕpß¡%´Ð­3i,§ÿÌÔ ©Y’-fÕ?n <Oi'™Ueå`îß¾£íƒNf~°‡“x?0Åö<ü kÎïÛø…:??ÎÎGÑŒÞm(8íÇ??'p0•ë¡&ˆKñU…Í8‘4ç%ˆ–Ny®’ ÃÜE£ùž;‹Œ¬™,~cå›äù`f>ÎQ/ÿñãÇ¿Wÿ|“6B¢†n‹U-ÔÖlu??Îyq?0Èßñ{9äxyœ&\)˜K9>Ê5<Ä*Žú¼{+FŽþV›2.ÕìW0¦<]yñ6ñªÜŠ`Lé«AÜÙÿôæÙýW=°ì??­íÕ¶ZÞÀMâ³yê'P7¼µ«Š6´‘²öO2 hp$§wê7€£Ÿn(|Dη§'ï'z_õDÃêëÿ¦‡³ †äšfÅ}*úž¢2UÙcƒ_G¶ˆ‰šµê×óß7¯_ÀÖ{sI‘0…)ST*KÉóä¾§fò"ZTIáÕÖJ½2_'ûŒR{4©q½OW+ožh—/›$¾£./ò"‰‡†ZQÂD§75Œ·qZ¯©æ…z7â«‚¯‘:Ó.Ñï§Ò’HU9ñ¨Êé«G&xúo W'IœVyAîç¢àRÜOPñ¡ïÈ>1tÛ'sõ_î¸/~“L`Á»ÎĸgWÁAýƒwÜGGÜ`O_¿~ùú³›+ÔÕ^DÀUÃÀæÿÿöèn nÙRþ,{Ö¬)ª½òx%k¥KùeÞðþ?nÆT´‰Ê*XM(Q´+¢?rv2R†!Z'ôÎRíÇhÃSûÁ+þtSZËýEößWI²™­r:iâd³Ÿº‘šIA.H']Îkᜪmí`6 h îrUaÕík•a*\;7JRp8£)ƒ,íSK4÷ôº>ß¿£ïŸ‡‘èÙëÆ*»ÉBíkÀ??êÿù.÷œüUà²ÿáz`›"ë"š.q0“/Tƒ~„p6úZ³|æèïÜ="VÎ2½¨”8lîZÊ?n%i]CÎGeºéÈÉ’ÊkÏBáíV%¶µÆÏbýº·9YKÞ½ñ´WÉ?0eÐ…YNYðÀeÅû^·ý¸{9U¨û˜ú”?0$˜©ÊSëOP€“´jðådÇ®Žå•'P­ÚZÁj2f°Ì Lä¸SIyÊÞ­£?rï%m]qøƒ‚<£ÈëEW‡[ýhª*). ´"‘ÎP•tc/ÏdånèM‹T%[~Ž€¯)y+ëÈîEÒP„[?0()¥p#__¡Öº„†÷"¢ß¤Kàç›òF8޽@µ=QÏÙR@ \9¹!Tþ={Ÿ²"Yìž¾|6xy®˜­óqš”;bÒéÚ3”žÞ7´ìxKHBÌÊ´¤v“qœ®ïP,2àÂÈp :å&ÍJ@ž5ú9£Ë*çMÇ„HZäÚ€ÇË+)ÑÞ˜.bìHnÊ¡®Ñ úM_¬ÑUk9ìÊóm%H€(:Œ\ošÜGvLMÉ Z¡Ûû{§[è›}»?n¹Œi!áã[«#û‹Ušd¸äÈqÓ „›aãÁé%¯dÛq›%×>F$fy6«?nìcÍÉ‚XƒÉ*ɶ·;IM౉Wíâ¹M=ÄîÞ —iÚgxf$K§í}’y£5º“Çú8ÊÇ©¾·Î‹„2!nKˆÕ08À:* SpOªÏ??Ê\ȵ <à<à„G4†Ö® =Pç(¹XѼy`Ðo>³˜qÏåúã,8*Þ~ü·¸ñìïS‡ŽBZL×i6XèsÛÉGO\huq·¹áî??ØÞ›ßû¼ò »ƒš»Ã•^R+0Uô??¿m±ü¼æEãN¯Ýí$ë^¿±¥Y¸ï,á:Žz$??Uš™‹EÛ{Ðq€_•ò?0_FwIȦ±˜-eØ! Qœûû³ï¿u;ºOÝÔŽ!°¼¤.)$®jо,3–—z÷¾¼¼yÑj5£|ô{ÀËh=t£mÊ:‘iƒGª…ãz›QkmPäâ?0Q7‚¶\êž›@´ÊîñÑI‘‰ÊQ–]·$’ÊžÍ[e??Ã8zaav÷q®˜!¨ÔÞñÖj`Ü6)pt”ÛÞWN>Ó§šA¿'ýˤ›EÜ%j¹aaûNp#»‰#2ÑH =D¹ÀB³­l4rà¤KÊ¡,n[·n‹ëñõY}–!ê7ÄÂbé†å#)òy"åÊ/É)ÕÁÀBÀv“2u§o±~zUäïpøðªÐvïÒàäæ7™®!ÆÉgX…ø›ÜlÇÑ¿_×´'š7ƒn??|ßMUòÆ:DOΤMˆ«Ówó•{^meLTYÁ_ªl'VH˜Ÿ9¹Fޝô<ÄMÿô¶ðÑgµ·g=už÷÷*ÝÝì£Gs­~Kˆâr²—S^H<ýÑ–c8xÆfòQ×3÷Fǰ“„!ZÀôDVݘ.<®·R?nµXGD¹­á–¬µTó˜ëK De„MPíQœ¨?ré&üH^úg6îm|>uAŒã‰ïÛ&$âÖR'ÿì?nÉ<« õö:R 7-ß¹ja8´ÙÔWCnú²=¹üÈ#‹Ó2‘ƒ??µ j¨”h¼ÀŒ Ú??yCÿˆ7HãílÔPKùaF1éa`8jŸýŒJkÏöÍìXIÙ^'„B_Ê«ŽlQË??'1Ʋ¹H?n¼-­.Or†F™Û¿™ÏUUà†Üìº\zÖwùÙAtœ”}¿°ÒôíÒíÀ¨åvÉ©|!ÙnK”w…pÔÖÒ2ÝœÊês†í¶€@žØ“ºŽ0ð¬ G?nÝ‹››åö?rIHܦ= ñ(ÙÐyɵ–Óçø™WÔ!e o¹8jÔ??ôâô]ˆ7‡¡§¥¨JX–ãTÑÛj§”$)×Hn¥ñ#/tþ9q… ÂM{ñõôÑD'ZN·¸¡×t`©ÉueZwÔý—FÕzàKw^1/%×7‡L®¥Ý)?0ѸýM+üæ×’§í *rC,õp·ÒîVq˜$W¢Åä«Ôš)…öÔ´­+”> ¾§:TÇ¥„™Á‡‹Žå0Ünb¼!ShN­êŸLp1 !ÐÖ·­<ΜǩÒ#LA†bÿ­â[(#ñµ5.?nŠæeCÍ£Å"*{VylÉK[n V5h9®5Ñw®$F±ð+aèÉgPb4*È}ïè¼¥©AXàÜã@Óý½¢k«.¨Ã[²RÄq¾Üû2¸±Jﻫ´"@hÂüÚOëóò’o½¶˜PeºÚµ è° `?0~”ˆ ZÃ,yϵ Û·† ¸£»JÏy–8ÛÝëBtyÉ×X{xh‘ RY¬¥«”âûAv$ŸLY»&ÄH£›,ö[®Ô¢x¶Fa¹½9ÜT_#à-“N®ëiiyÀ1mºlpt³þlU½Ì’Ä ©nºs8ÙmkëßCÝVƒmV{f4Öûa$›§¥m ÝÚæ`³%aËAUÀsxŠ­[Vj,QšÜE8Iø)ìËì|öG‡¥˜¾ÂF†½Ê?0ÖúÔUšOH–Ú?r_k-BëŸ IÍéTÅ,ð T_"êä ;zëUÚJ^àé…9ÀüËÑþ†ï¶ªË̸¯ c‹DÁ·ÑVÉð“Ömc.+—ï¢"޶‘‚¿??OÇPüYØ“pWÕ¹?rÔ-5¸?nqc ”MÇoa›^¥k³ŽZrß§™ÉáѽS ²IjƒÈ†’4m¢Óæ5Eû‹&¥v­ðw<[¬•0f‚È+¿+S'Þ6³*=éÖLÂÂN(g\ïçWR9dX]žu_ñH“*Ö£.L…fq0͸¿r‘Jⵋ`¦ê¼mµ\ìði¹ÉPôp--Þ¶D«U›aÝÇ»qÎõ¸uFé«à¨£Ëˆ¦·uyve??ÉÔ0ðä#wñVrŠâGv6Q7¦æ4T&×›U'ñt|2:½°ñC8Àgm!ú#7fàÃ}þfÈvÅð3½ŸG£ì%²'Ö§?nE÷ì({ïTÁCÒÚÃnÚÇÞÝV³YÓ£äÛ|ÙqÈòX¬Ã™—?0q{ŒÝfeÑv‘-Ð/#cL>Ò!&ï›N??Ûä¤NËÍð>­©§Ôy°º¥_&_EÖHI²EÇ,fz]¯ò€½ÌVâá z×*CC!õô,ûéÅEs??“ìiæ¹*Þ¤®5××(¼/ó­{áZãiÆ®5º—ÜŒªoQË$`r²;w£äS??JS÷¬­ò”FÚ÷pu¥>ŒÏëj_bŶÛúU†Ê}„É*ÀŽëãÑuÒ‹Rûz}4ž Wf BDhäÑÑÊ* ŒåêGç …ÑÅÁÓc]À’óŠtÇEGÖŸ®²©Ê_¸Ìí]h¦‹dV¥ñ€k¯þp6l‹­öµ>£µ€J!±#6ãmð½[pZš¿ÿ£[0=ùj¬ÚüwæŠðìáݯ~yÿáW9ý´þEÅß]ózÇËܧÔTètí›*TZå–¾f×cä½{Û_µ\õ–•ÇŒ°L ùÝ{ëã’¾îª/tÛÉRõ:»PsÏ݋л†ÃKFÂürtj5WỚiƒ`zw¬?n[.ŠñŒOߘùÇÃ{KM°?nƒÐjŸ˜Þû«A-©†Œ ž5ðLw€ü `pˆ¥¿?0 ¨_Ÿ³°=#(]Š ìáfºÖ$Ô,}ê6l„U»ú(MÀÞÁtPQR}eˆjnlɬN˜dÛuRDUÂ&(TjŸ“³º·. ÒÉ<ˆTúÀ_ZÔš#½@¥^k?np?r¦?0« ’?n7:ð\ F!›GzŸnPÖÌ1w|m¢ðÛìï<Óõ4’àÂÖö½Û??e¡ÛyhˆÂsW_'Ô??™´Aƒ …º7µ÷2köù»Ëw5_“jrêVÀ??ưRä\ô±#ÌW#ñý@E—öË…ý¿¬ãï<÷ŸDþD“¸­>ÛF:Hêfçõníã?nºc©‘/͸6wDm:7õ9¹•ŒÓ¢€ŸGšà?rA÷S/¹±—/Ë?rq#._p;çh¼ê‡eÌà¨vL’øâá…Aă ‰Ô**k6"ŒçB€6 d_?ruùàfÀîzoQ UC÷;’»=A™âmöâå›§cí5?nu+=ÕƒÓ?nñnze’x¥ŽÓ?n=Œ>»È‹nÖé —ãZXi ÀLÏÕÙ ‹uý è~è¯^}Û̃—{‰R$ÕœA,ôY oø‘!·BÕ|PÍ]Í9)Àî=;oÙ=üq9>Ž=åÁêñ;µ#¶î›?0Û<0À<zËm¦G7©½åÆMÕ*°šsT¢¤‚ŠJvøYÍÇÖoŒàâ´Þ.&»Í¢ÒfJDYž}XçÛ2ð!­ hßi$×ÓüXqŒÕ»…‰¥O&ÈV–lFËã”QžÅðÕ?09ȤdýNÆW)Š”"N‹’|vÁ{™%:p»ef·tò—›¤@œ6?0â”ͧÌ*‰G?0Õk¢wÆjŠË¶«Us²@Ũþ¦úûÚ!!`tĨ݌èN€á[⡯;Kó€]AÂüë}YK:â´…‚"Zy‘¾Ó¡FO§"…ð}¹"²íý¨à° ¸<ÍsÔÅØ©"i>Á®.x'`ø¨ËÓ×ÐSUz|a*ÁàÈa+þ‡>F0Y£Ø™vuØj´Ï˜N®ÔÉE÷|Ñ€¨idù¤¿7€“ͽÝ‘‰¬’ë´¬Ô£„u‘.xÑ?r…̽ž|4ç2_,1ôp›ÙÑUAC´q9Bë,³í5`x\•ÊÓŠKÁ‡AG)pdHhÞ<^dïF›Ä‘ÙEñ˜iNÑ’”©%é Öº2«f3XŽBo‡]ÚÍ(DQY’U+áïÂrŽ> Û{¤?nVZZŒMJËðaRY®ýné¯áÓç/þ ÑšÌ_¯Ô’7`ïÄ<^%Î’ÆGK”ïÃ8R8õH4ø2Fý¸ñz‰³‚n4°êÙÎ…??ý׋itòñ›“Q®8Á/Ÿ†áíÉïN.€w¦~ðެS=ƒ·ØÕÑ\¯n|&šr·›çÅ7??>}óòå/_|Çôâåùkë7ç??½¢6>Þ€¯^ó‚©`h·¿S.è°Äl„¾ÐR¦«;³¶áá¹??@ì;e³p»e£éZuBü–óÙèBvkƒ“Éðª^ó4Ê9×à^L0Ö ùÈ»ß!#%ᣞ‚yYàΰrÚäÕkÏäÄSakg…4Qã'¯SöÇÁòœ«!á¥Êt„s1OÑûߎKÝð<´1E ”eºº]U)®¥^ÁamñÛS²Þ}­_÷ð…õbf—:\uéQºxG¨?nLT©–çÁLíÀ7Â5¡‰C÷¨iÃ>[Àº¢ú}ûæ/¯žJ?0Dö7?0OžŸ¿j˜)?r¼Š-°Ù³çOxÒ ¼J\Øž6ýñåÛ,ŽŠ–Û'Î{—dÞçýcSaüãvÆ?rIˆßí´ú¡²/êeÐt¡7RÿÿêÁ ¹¼ÓŽt_ºÂ[Dé-×nró?nÈC¼å.-_¹uÐYíw]÷ñ;ëô:‰}kdIñ*™_E‹AçzäËÓÓ°3ÓW1'ûâ‹ÊÛjg¨‘RËEþ^,„«Øn?rÃ#~V©¢Ôá.2¥oÛÑGý³–w0!={¶xûå?nŸ¦8€<ÑîÏ.W˜™çƒ*Û¡°¸+ôߘÑÕ6æöueVó'Ù\§%‚œjЛŸÅ+f89Þå³ÃˆŽ˜¢=øL¤å콪Zªg ˜þÄÝQS_u×â$R:¥ò/L#÷KPÒ M¤ì= qcµøºü ÿ r9/;®?0²îèpþ6þL +Õ&¥xŸÁzÞÚ.³6}„‚"½£(»`‡&SÅiè¡rMlŸ ŽºóÖ7ÞDmB¥;–; Q^1:>ÔŸ#/¡ú¹=êlI~ZÐ5éàÔò??èñDàQ¿÷æÛʃÊÎ@–IUœøAsZ„ÆâIÑ3·_É:óµs™]~tƒ”³"Æk¼Ïòëi9- rܨ)¨E„”Øwc|Ï?n(¾½”6yYÕ¸‰]>è¶C$s¯ßÁ/Kk7àzn³º¦S”¸Mr'Kùﺸ ³. Ã=I?n(ööo5½M§ï˜ž»ÞD>–Ĺ5Dl/Þå3‚žÇßg!ó ¯ÿRí5žÃNO²ñŠ;Ò­O²VÏ®òRÒƒ"ÀI&(»ÔìëΚ%ÞyL?0mÙÍò??Én}1äD‹Ý¾yÜɱ÷·\=[Gm³™šºÛ¶h%tº”Ý?ngLz€#c_‚_æDEà«ÖDÎÖ!háv ?ns$'K¿ŠuU”®Ê—Z£&NyžF°U:9‘‹ü2¤-Êl'ö»úÏowW ÿEU¾ÈW^BG‰Ë*)H”tƒ7öÞGªõç_¼UXÞf_„ÚIí/¿+>ù(ov\f1ki?nõCEX‡ÜÓÏæfâ5·ô-¬Pë~Ÿf\àb’FWìr‘û¸&vÓÊ{¤Á×rAaeú‚€ƒUž{«<{wƒweе#_òX›çwI5«ÿØ _Rhà×€ãrªþ; ‚¾íù œì0QqEíÈõ¯Û ­rŒÛV¼€>t·öWº¬F‘¾híÞ¶â´\D…öݼQL­Ñ_~=Ž­o¡]95~y`X@"NŽ<Â0ã‘uX`{Íþy›¦Ó¨¡û8ч¥b3ûÂ(ƒˆ¸v¼_Ó·¯áêBÖL­õæŸ Õ-_)íü§äÃ`v’=˜›žÇ)rÕÙ|åV)‘wQ•$ª™6Î25.Œ³Å ïÐfâÿ Ãï=6Ù?0s³Ü?0·î â#=0@|m*´®Š“µ??áí¿wë¡wk0ÄÏà÷<àÓ}xq; ¦èǼ??EoO²êÇBÆbk?rìõŽZ U‘)Ï¿{ñòõÓo¿9ÉŽy8ÍT3¤±gÒˆ²s°àA@ÍÊG%#¾Þ·È`V\ÍÒx•è*·?r”NåK W-˵7UÎy`?n<'„¼õØTÄÝ:½^ãÀs¬$Ä󣃽·Kêý‡n‡mbR%©+ÂºÇø²œRqÅEZ eL{cƒÂzcCÊ?rÿµ^ã{æCãaêîè ³(³MÍÓwþÍwãbNÔi÷Ô¸&â?r ¦»«¶Y—öTè¦ú½–¿ö¢¿xlD)N}%íápFý(2Ëî>¿â˜ÕaÅUŽu?nçVeJ÷5¢Ÿ²µ›jC%kgu/»Z³=çæÞÝ CêªÝ&‚n¦vKvOåÝón)Píì¾p²Â×o–ï™l⊂žŽ³…pœÒ° a«QË&޵D«¡Fàê<•bÚ¸>æ¼RÓb´‚#‚2ZUhØhôGúÊ/Í_ÜÌ#à'jÏÙxù‡—Fôs•Œ5BƒÏ ³#2K²´S_Áõ_„re»n@çûPyù)Å3{°€,êBÂT¾.Î6‰Ú‚^‡º'oÒàɲê…\S >ž¿#­«‚/ª}OüªEßTµ°¶«ßͰdìJ+{«,êG½©Æ6,“¨PËÓàâ5®xÌ‘f%Ž n +ýò'4~fX[6fCÜIÂoGã˜{˯‚¾I“‘[ƽdâù’Y"š—0Q®ëÖÐK€Á:Vg¢ô纽â½ì:ú0O¼÷Xú‚6ðêêš¶yø6ƒy<õ¾ÍÀ¾|¾£°n;m]t†V³ -¯ÐT7³þ@Žã@·1}‚Áqx‘ò=[Ý¿àþbõî=~€6::‘¬þü§'ßüå ý¨öÍV-û“XűßzÏŠÔ;*ï|›ùfb Ž~|ùâÍ÷üQæ=KæÊ-Tá}³)ÔïÞ·™úoå}³}ç'ïå¢R·8;ïI²Ø£xýìÛÑýш4V蟎b­øN¿Œõcþá}÷ã›ú-Eµƒºª¬/w÷Ëp8âÊC/=íc'ôæ÷ê/lê绵ú…⎪3ì@–$‹éûøBc¢ºN×¹wâÁ(÷Ãy°×‹<¿JÒƒ#ô0˯¢k,a£3/°|J–»4xüï`[ºWjÇPjÇ£¯ü`ŸöP§ÝýƒÏK}…ÑžíqkoIÈ^+߬˜4¸”¯šYõίt›=xðµwz6>=Uÿ7Òç{\NŽË‡Þ+Õ£&wðõôz“ Rñ¿‡ª³¬“ó´JT‚oKX÷S&{òçnÿY„6_û??|…6Yl‹ÄG­!Mó‘A R þ‡è ¿=ב?0MºáÔë˜(ÓC®üˆfjµ‡Há??H_P}ÿÓß¼8þ/Og??½xþæÜÙ›(ô¢wÜgÂ?n—¹?r6åKµµ~ýÿqª®>ÜûÖuª2~z¡eÊ"Õ«× ­+¤ðà µ…¼&­ÓÀÚ —›˜ÂZ¡)OñÃÖ,j·!Ñš ÛÂ!ÍuøO§Üå*½ß ¼¼@ìSÃ]y¦€¸ykÈz²üÖÈÊHÿ€ß 9Håv_u¢°ÛÚs§~ŸÔÊÍ–°»D÷¿²[eŒ-ovy£¹ÌxðÑ;(ˆˆ¥eÞa¸íV\Gñ¾sê??žÿóïÿÿ9)çÊœ1åüw‘ë äüYæ•ùo2UAֿˬ_uÎÿnaî÷TæÉœ[”ó_í×¹??¼íAÀöø#ñ`üá+·€c"›˜\s[•¯PÁĦп¢í5~eÛS;¯˜ÏØ„o?r烅N)Soµ©j'¡—Üð^ó‰~fæ8†òã\…²¥BþŒNu?rÆg…è¹;½ØmÜÎË~avü\Ï<&ýÔÚoNíñp?r„· a¼¸iDÉjqy5(6î8z{}JÑËÔõSÕ~ëXáò™)?r]£ŽÒ.ßõÛìÚÞTZ÷_Lß¾ßú½ÿèñ¯~¨°¸7!и]˜§¿|ºàðMC¿Ô»Þ¢eúWLË2NöGMfkÉ‹Nˆœ<\aï¶‘}ËÝ8Ô¼‘.ä¹ã~-¸¼ê¯”'J×kÕ/¢¢H–|¶ÔK'+؇þ¤T?r ]mãdçäšmÀïOÅ™“­’Šß°7t½=ác_zy¼Üj©G¤FÊ„Ü?näÕ%œ(ðnõ}^Äð‹oSåx”V${è3 ?r§1²„5ÆÁÀp‘o> xE"6ó·ý@z…4Ÿ x‘lG‘¬!v!Uÿ„¦Æ~”r®¿"[O±“ኼD¸MíKWÁ”_¨9‘gÔé&SCwÀÞK;|GrÑ=G2(Š"¸'™Q UkjÄ›w;/·º"ù_¦U[ t„6µL•¿­EìidX[]‹˜ééHÇ]˜T^Ð.´Ëj½š•—³¤\ ¬ZXÿ÷o~üavþ½y¶$6,Ç£S??àb¥ëe+)£$‡úæõ¯ežÍ.mVÆFûˆ +#·§§w6îÇûôÄNÿNÝó»ât©£·F??oóŠ7EªU‹Õ’ÿ[ ®§Eà¢õæ¡Mö‘N]UNâcø‰ueB/PØð?0qÐ|¡ÑüÝõèk xfD²-¨ÈÙ݇¢=|?nD²QŒ æI͵¨æBVs-¬9K‹“SòÁòškÍ¥ÄæZdóß$³9 mnI32dôÊm’͈§TK.<Ÿ«<¹b"ˆ9Y¼¼¿Ÿd à –R¤jÜ€„æíÉœ{ŒE•ÃâpÛ¸Æ ¸LÞýȤ;½ÿV¦?rKì˜e¶ý¹œkS‚oþð퓧ϾûþùÿôÃ/^¾úÇ×ço~ú§??ÿó_þ%š/T]ß]¦½Z­³|ósQVÛÝûëOÏFw¿üêÞ×÷̆'ÿvǯQV+ø%ä:gc<7DƒyÛb…Ëimi®6©£Ì=ÓMëE%¥1wva§XW¦iæöž8âã6ú[*ÏÎá…28?0¶»Q‹;*ò¼*ÕÀ®š6#P²I±ý ¢,Œe'Pa¡²NèöíIºkàû!þ?n¬Ò`V!ì+=rJl5òÓ3s•új¹-áð1A ÛùÐSaÈr²|óÞQ§ßPË)f®ØmÕ€£ Kê»3»üöSC+§FÈAœàꔆ?r)êè1Ù Ù{—"FƒªTA£ˆiS@gIb$Z`ڬ鯟E¨ž0w[D‡«}òÍ °Âbe‹Ë¼Àˆ`±ö-4NCÌY®»*û^)¢4g‹û¯†¥9åY/éþžrø‰8`‰Zgͳxš¬t*@‘ Ü]‚3Oí9ùKSé–{Â"Yå‹cD‚"žùÚ¬ñÓümÌÇ®þXfuJÆE×¶íIjÙ??àwwípÄ©€x _ï/5î²|ƒ ²ä=}Öûî˜ÏÓ5Áíöp»>¸eÆpôI¥—%'ãÛ¤£œNßþQÃ("ôtÍ¢ºf;§žø’3rU‡LþT(I¼0F:n·4pôPÀø¡2<:Ý¡¬v…‡iË=MœÚ0¢ÓHðFÇÁ¬f¸[*Üz.÷—™}ªhäM‘N݈‰*t{«¿"Y†tþ2Päp$¾Á¥ï`‡ïúv/z®Œ.î›nwÖ¥ááê¢õØÙ«`ۯȘñ?rp•®cÚ+n€I kî¶HW¢Žªpà†¤…:Ï@X¸ äÁt¶j¯[\•p×H^ 9Ù -µËJò0Vêü"Ô??é`´$«ËݳÒqJ¶™â>’?nz㮦u'Ñ+&ÕLLãFz°aópKåhH¸é?rýCA@Qst}=jq·dŽè—­¸U!>ÇfÔ2æ6[c¡©ÕŠªK_Àí]ºˆV {[/³‚ÔúѰÇf½@“›zAÃfØZ×ÄPÔ–d¿[’Il6ow{±U×l(þì|öôÅ·/Ÿ<‡Ïièl³x?0R°u8Òµ¾aœ¼ú-ü5 ¸¿_˜+fGóÀ{åïa1ªu²÷ãe¢‚!&„A§¬?n:?r°ÎÓY´¨¶j²ý0[ÏåLˆÑ??½e㤿l-Ú0?nùIÖzš­Ö]Ôf¬üÄ =kÛ'-ƒìh}Ó"àEM¹Á:L®“ŶJˆ©ç½·è(%RI‘èílG»0è ²ë²Ðþ½õä¿ÃhΧ!E׸`9 eDíÅ9á%X}$Eûbw{‡§‘8”—\f—™{ÎÏgÉõ Fz³Œ!…)…p'Exiˆ›^œêÁwy‹­™\Oùe¾ UáyÐcJŽÏ_ºî%öP†8ƒJî3yX„^™«ÿt\SÕ×ñ`:÷Q^ߟ¼V¿ÈÐÁ ?n.(fF¾­&_*Cu×»_™×&18s¨_ÜÖiAýáXÇkjjk¨Ê[¾?r¹,*ääû]v7Å2JWpS?r¢Æå´í2Þö¿éaO¨~²T–Y®CxëZ>ØO‡ ì'gKãE˜ÉbG°ÔyUȦ za—Îßgíe‘³/»Mcÿ‚>ßá“—#Ër¶ÞkëdÁ+‹z[Y¡y«|âÏå*zWzVˆ9À&¦ñÔw@©ª§ªof•íyÔµÖ»Y‘Ì|NTj É{R襬oU‹Bç|çøàƒÒS¡fâ‚-P“ÃèöDÖ!ôʤA«X·ÒŠ•n¬Úim³Uš]µÒ"ó<š®(V0š…¶GW•Â4™¤¾Pç¿+ S · ="“ffžKŸÏŽêwLø“_ï"(£|ÃKW&U…0Jû ­§#×õúP~rd %Y?rP_ŒèYª]†"¿Ã턤‡.ŠiSc4šçE…²Ç¥x—ãœExE?rJÂ7¥Ðš?r~ѽyýJ]6Àßͧ…èAçù:©.Á'f³¨Hb… Öªu=­À&û+ƒÞˆÒ‡VÆî{ƒ[­n™MS?nþ;Z_Ôð¸ŒuL¯árrßPÖÜ´ ¨ÀèžËÁý5˜KК¤z=sEz+!=ãN&{Ÿ·O_¼.]ZÂ2ÿè!-XÿùÉÓêè$[í æ[·Ikxxö6óT禟=RL®1)&@èqÿÅú§¼ÜVéj¸ÎwIÿé‘‹—ø¬™öJ?0uTÃÿôA—cßnY‰ÖJ汎jÕ ]½ÓÌêÚåñð8¢!‹84ÙqŒ^{@Oå6?rY·•î|ð î¬mƒ´ŽUY¢ÿÐäLJe†¶È©ÙÌÈŸ;!³ é¯À‹f‚li5džª‹„æž×?0çIÚš¤Ìì\ñö¿†{W™ôL3¹eÆÃSwpÂ*áwrðöwï¾áüPŽçCÇòÍ~ôxQAÍzp˜1;ÑÒ ¶ðøÀ¸»ÑKy[Í]¬¹¹%¿îÞ€8{+Áî(̆ì§ö›±x9س»)¶YnÅv†O‰]–\GbT¾^ð˜JË8-pÍ£sþðò|vþô¦ÅˆíOm?ndž3 &>?n‹yþÇbŸÐé?0(:ùóe‘`Õ¨Ý?r—ÃÅl ‡SøopÚ€¬ò*Zí¢ô0Pñ>ÍâÕjx•Y²º;~—TOÒòê™bî|£ÆæÓë??Kç(Œý=lÊgn° t“§:"1x?0ê|¤Ø?0k]ŒÊCíñ($™ò_¾ß:}·ŸÇvËR´$5¤.gËB{¸EMXÓí€ÆAAÙÊ?rqE¯=_'×¶‰u[!??~óíËsëø­Ló…Úúÿ<(æa‹£Ì¥å"«VCUªZ¡ o°õSÞuªDøæùËo_þôæCXª~[[Y¡©Ÿû¡B­Ç›Ô‘¢œÚQëõÕA”!í:+qRS½‰|þc¸»ý“–±7A!q®€ ÇÓ( 84‡ð Éⵀˤ2Œ ÌïÀÉ÷1¨Ãóïz3ûóë@6˜£"¥ÿ¦G¦ÂÝë=[¢‡¸Ð”TAòñtwäÝÒO¯ƒñhº+ûú ª+;‰œD{Ç?rŸ2Èà—Oãskcm‰°4j{úešá°×Zç¨Örµ& ?0<¶ÍHš˜~ï.??aÁëP¯b¦ÞÖ›lÏ_¦3ÕŒC`£FfZóñìJO"S?nšÏ?0{Û5ΆûY»:„õqÏy¶â:S(á^Xz8•kAé ¤Å•é<ÍànÓÈ÷¥jG²+?rù˰¥Àý£Ý]Z²;©kÖÖ΂Åu²÷ê{$©Êæšm3í“.‰V›É5P·Gd& …Ùõ¤«…Åå6»JbÙ{ÐS9ŒƒÅWRhŠ3@&I<Þ»ÀÖ‡Mr5Š·|Þ¯¿|úžo*E»ÓÓhÛ.B?næÿ|…©€scm»™µ9‡æƒ×´{G¿›.ýÃRÂOÓŸ@¥Ï+sÜì³?rhÑ…f6®£W?reË÷‚îÑI­GÇÓŒ¸66‡h ?r•¤¤þýÖýðz[ÜNš.ñàð¨Û/£?0\·ƒ]6:ÝÅALu;Ý=X]³–awS-S‰ª`’îô¡k¬•Ñ××-µxBÆÄÈ9¬3”…û!Ë5hÑ6”ÉøúÍB–^Œ;… ¾ˆÁ2ÙDETåÅßJšt–âpjSx•§oJ‡e•«*ÓeÇEm|޽jG›?n1}ì«Ú2©¬³÷,Åa¸ÍŽvùía9TÄ[ý7*•gÅ·¯ql€dàr±›‘…ÜC/ÈÁ~P>ߦð1›Tëh3X”𤤒“b¶ÞVÉ5??+3ÕÀ??ùÕ=??›§!Þ¥FÙí`µÂ¯,ÇÏ|¨tۢᨥ¤`Þ„Øz ªîË[¢í:f£s물T­x©¾Ëšn3Î".WQžÔÖ,Z‘ƒtU }î*×)‡S¬¶ÛJ6p\ÑÛª¦íKlÿìt¨ÿwG©Íˆ¯Gó{ælÔ ô?0@÷ ê^Ô2VÞ§Æ ÕÅͽÃÑW_Þ„&¹4g§ÝxF_ßT­ñøLÕÇΖ~-þ¯?0ì½cG’7~ó*¸ÙG&!l'Æ–÷[I¼ql¯¥ln—èá‹?0YD¶å½<¯ý[Ÿ®)ª§è¡¥8÷û¼YÝÕÕÕÕÕ¿ª««ä_¸ÃYSe™Ms•Ó§­AUfÑiß>ùËQïùë¿è~@eQeIÆ´JžóÛá’5ÇÇÜʉTGYžM|Ó,ÚŒK#Fì\ù7Ç— hÏ×”ïßš¥„=§€bÄz‘.*k¡ý`!–ª2á2{ûw“°n&¸ +#?ró)Ÿß²«û:‚,c飯ë¢b#wø\fõ/Ð:5ÀKï^Ž–ßÓìH3Îm¸›ZJ¦L«Ø&Ór³ò/ûégž^ý­6õVx7—$Øàþº–.NÆoY[V h·èáQ2«¯8¬€¥Ý‡Ij?0çÔ_??—ÎäOðÞt°+‘ÎÙdRüNrú+ß{ã¡÷äD,ÑFË•î2!\5]Œ..!hgoÆ÷àäMyªÛŒÌEÂü¸®7o‚étðS1‰«Mus¯42“ZØEu]¬™¾·ÞÌ•+KXËJ.Pÿãä‡^ïŸÌíÓ”“Ï5YƒJ¨TåËÕàÒÙ¶dR’Ú•øE0lHÞÄ/‡M3ItÞŽsÜÊ29™À¾~õâÝú¥5XXÁ\‘*”þœ-ì[Uä_ŠUºõa¿åþe2tP­l›ß nZhn K‘6òG£K›¨.7Ÿ¾áWÛ`) KEp„WÜUèÍТR!í ˆyL©ÐÆE£±ÜR??Àsl•5ܯ~xñ쫯ž~§Ì&$;ûé?rCEéÒßBÚñÖU¨pZ¬ËX)«÷kh õޝøŒ"½þˆªÍ<¾œ•‚¶`š™F¦£êdÞOkÁ™Ö°d‘9ç-‘KkêÅ1¾M?0ànûæø{A± ùºOw •°,EŽF ³ð?r~$f¢ëÀÏÕ*Ò¼w>›ý½üú?nÖCyªÈ…Èz¶˜KŠf%æ¼Âf0jòCßK ’ ¤¿X÷Éh‘f!II‘a(\›æjžlêÑÎl b·‘¢¸0pšcr#×P6Jaî^§\4qTK3ÂÈV“†‡‡`3:ÁZšÝf(Ž,>,:’Í??ÆÂ†ÏI7??Å=ë??û×çb‚Bí“ö²)ù§68¡ÎÂát£³`)Ë3ªã‹J)ЈxÖlÍ…¼:¬ÒË\oØŒ«ô2ЙC“Fcy†;›{ô[“º›÷g¼/#ýøé}Úµ£*&”§#£ðs´Dº¨€ªwëHØ°Ö 屆×Wë26Œ/ÍãÞóãgÏß4>tgÙÞü™Ÿ–Š—&WLJTÑŠág—ªõŸ¾-ß?0 ùºõN (£,aÏ܇ÒU­?nû館¡Rì@úòÓï3YʹHÄ:ûÊt–‰Þ%ŠsäiZw Ô8Â’QvIäz«9à møÉ Ë¡Øj·§ŒSkKWÕ*%;†‹î+eT…P³>ªi¤D f¼µr€Jƒ@mËQG—ضšò‹ú^õ±Ô^SÝÝj4ý„©ÿPD‰Ô¨ü;€a«;9ÊWiÐ*5 >æA‹Gàî?00t¬zK]n-QðùH3-ÐÀ”3V©\n-¶xKœ5Ó}%E< Km=ÖÅù†¿÷·«QNfö86?0Nòêv‚CÑ:MŠ–32§u¿ ó„\ª`YÞ#;ÝÑr7_"Þ®†>?n:Ò͇rÉu¨"øýóï8ž årÃLÜN5AƒØÒ›ˆï?r"£d/k¤K¶›¶S`ÇÞ b¶·•J¤m3¡Õñ|­åoY?r™¼«þ[2ì`LJ«ósbW¡`»ÛzpŸ ™‡ì1Û”9JYßöÈÙobìnM¥ GfTãi=d[ Ùp·(r‘¦5\òPnºùt«|sòË…cB±¢¹bD¯KˆÜ³ÙrÇR®8–ê6éHÆÊ4„×"ȈûA=ˆîçQ¢‡;fØêçñsÍ.sÚ…ìí ÷]/Ìè?ruø3GlbŽÅyáýYRuX8/¬&(„ÁÖD”#LÙf;9Göï2EÌegŽ-g<æMЍvµ"ñÀŽ…F_ôõ~…ú*>-áè¯Î∵C+AÅdCâ\ýƒU~ÉʓĈà*€`Î `—<ÝÏy/yd´9—vìÁb’§OæÐs–hC‡ Œ2[Ö¸y@$h¯ftÀ+ H)ÌkÜ îʽ‚²k·å6®Cã9ìÝònÀƒVÇ+/}ØLbé“UÃWu‚N–s!*¢;U€3°FÙºÄs“VicBÈ@pF‘Vúªå\f­Xh½RbíÝnù›$f"™åÄúwoR=_,Þñl7 ƒxy²;_ÏtçfG?nR"9ÍxùË‚ÁK?n åuç¾3ž}þ)¡?r‹c™3Nh,¨*ÝwК×l&ÌsÄm“8Ò"Q™ûBì¬óWîáÌ¿­CÎ&4Ï!­Å ¤T??›y¤Ù^F%~¶ŒT¾s—¾ú³(r1à)ÿ¾¬‚¾<"äi^m}ËŒŠ"8PÉvÜ}ä?r*¹‹’7˜ûõX‰Æ‚Úrà$±Ž‡¥íÛ´~%¾0%?nHv‚—©ZF8`JЭ)<áY44€]˜)t£’w6®áýý×Çz?r‡)"¸81ç ?rœ1ÀU—`7|kéJ!a§a=Å ÀK‚¸ø9Q爕BRÒt0Ð,*2¨±ÔM"¦ŒŠ;DþM¨X¢C?r¶S“ú—vaW.èÀ³EC ÅôC}6›âù³¬} UNø™½þjÂûþx陌-g+ù©'?räd€Lž²AÎ!ˆRs0¼ü*ú¢Û¹¯'\LD¿Ðç]r¯êí‘ÝR??0K½k—ñD6ñàÏ?r<Ú½µÀϦ?0ó%\ ¤ü|o4›Â@Ž¢NÐÐͤÛqXí(¸~[‹ƒûf<½)‚Kƒ@úº(½½p߆޴ðX ü"‰ƒb« bc‡’Î64õ“y0Äá‰Á¼&uîAf ¾»O매U'Ü©ØAL Ç1ôe¼˜M5ê§™¡ˆn:ÔmØœÒêk±H±±ÌŤýÝë9SÕ^šú¾\Ñw]LsŒ7ÅÙ?n¿K“d~͸$ª%…ॺ|4ç¹–PkEàF7yýדo_½|ýää[wœäâÛ¢é†ùR¬±óÞe¿.¡J’ªÊ4Í{;n„j*ô”xÐjyiŇÒ1®¼ H8¥âCSE¹AôÓ­©ŠÆÌXnë¬Óo »X|PFðÚ­üxñÍèrÒŠÖÙO¥å-)†S†Y/sSYææ§ÌÍ53Œ7^ªW›ÕÂß:éLÒg“µ=sÎlðÍ{Ѫy*×eŠ}ƒQjá!ó»ìt»$(*Q|Üj¼ÿDr~­³ƒED¢MÚ=H¶Ë¨q[ø™ÍøÜ½|bOÔº§]Þ%æ*h1×ßç‚CW8²ŽsQ2?0~¡×±ÂS9ÿÜ·¡ç!ÄæÂÂt?n¿áð9•YŽBõrÞ÷—”‡;OŠ|ÿ4?0rµt?0íP^þÑåí‡òÆs—w/”×gœ¶¾_ý_Ì¡ t+Á©t'@ÜERŽ´P9¯Ç??|µËN˜ò$“âùEEy:×g°y(^“²Áâ Õ (`ÂO!?rò¼O·õ7G¯wi$iv=à|ç‘»“]4”_â. ˯¼ÐñPНt°4¶kÞU.°wæí=自¢?n× TQàzµ„U ñ¸›Ð×ä4¸çÑlø E¨IÍÔ´F¹a T!½­§¼)ÿj…‡Ql‚Љa'wóÂ.‰B´y°vÑ ‰x¿y¾Ð·²DÙ¶©’fEçx/óZCX¬÷¿P)rûUÞruøUÖÙrKä¿ tZadÓó=Ñ$68Ö̯åÛxJ”À£lÀ1´q(KEšR˜‚º?rVGROz=žÄ½i\{ôŽ€7›Ò ëâ•Æhˆv;æØ;Ò°ú3?rįèÿÇß&7QuTê"؆®x!ú¶>Ma"Ô°õò¶\Ñ""nº?rÁ1ç.[/$‹°P•͹j¥æ*7ÓE€Íæ ÷‚½1æè?nMÌaƒ„0ÝöHÊjý'ešó‹›þ ÙŒÜ7:_>;!C¼WÅ™_˜‹Wí­Ö¿#lS1¯ß´¥$‘¿­¡ožþåß¶¡ÅÒeÛj·ñ½Nß¡Y_ÿÉlHpòT™Ö>aöffßbîŽT®k8jû×á¿–ªaGo´Êêå1Jõò%èt š§y[Iìc§š°w`å.[–Ä—^tÕÏiÛðGì>Å„¤ÂMê•s°I¬ê/­[€7º­Ý§wÒ„Ã-uÚ6ê¸ÏÉ0fÝ3¸!tÕ|»˜­0‹E—Ÿ€›[»êNtu®[Ô!2àb©(dxgPñ±qÑBoYéÓˆ!•åþýw݇ƒ¿™ôN·ß¨†C?r_ÌrlXÅ÷v‘dw+?0ëèc)Ý—,¨¸u§+âý÷d1 Íyë×õuÂ'jÀ@ áJJLº>R`ËcnQeÈŽ\ð°jÏ)Ù?r±¹(ë!y ¡D‡„~ÂsÄ»‘îØb»@»“s¬f™b¶0f3hŒùrIQ¤z"9­ôqM«x®–HµÊü#R­Zgl1ô¡¿4oñ?r‰ÀðarÀ{Êq1YŽ'”ç>-…N²è¯I'æP:ýÕô_ý«"«2'|´©??¿•nçã8c“Å?nÍ HëePmX/Ãï#^Íܔ׿&é²³q@C"’–¾Ø§sQIQÎ(—¹±UFl×éO¡>EB­¤ª•™xM%ž²n»J‡û©Jœî°ü;©qºàé) n­áQ¢âÛ‘\¸…Ëï¢ë«–ƒî&ZZ)B|­a›-Àhú»6ð%Œ)ÊÀx›D”Á^•åw¿þû®Ìº&W,Ég‹Ù¥çàeÿ2´8çÿ®«³Æ¨k0u˜å@ZÚDzi'qè@›hYû˜£ˆkùêé³£¯¿ùöùŸ¾{ñýËW¯ÿüæøä‡¿üø/ý[a´èûØ+զ׽>šBWèÛþ©ºBÌ=¬??l%xm?0>c‡çyëD®öf¸>þgi³TRÎ*O˜P0Isbu_'iC,IÆ›ƒ)S:h»-–ÕoZrÔü4¾DFyoo˜¿Ñzmo+@?0eRì)Yz›f¹Èª Ï‹D€ˆÆP0}ÿ@÷ • $É?r{Œ'_û÷Ø9 +Ç OZéÍŠ~i‹~ùõ×ᢶó½ØÞ²Þ÷§ $J°ÌEîf,Àòe_'qSïô쫳Æ"it³ú©Ó­'??µÉ~È>ij~Zzá\¸Ž¡þ–v¥¡©ì“?0•apâS§cq…‚Ô”&ðýú#ÁGeÊ3!ÃH~•³ÃÛ‰ ¡FïÜUø»mu*¤d7pëAà+ àuµöDëLm§-7=™ª]D>ÿ»‘.xcÙ?0??ü>bdw?0û8Ö“i‰©C•ýg¨Ä??mýïsÈXÍK¾Ãü¬•Ë#u²~{ ¤Y²Â;v¸6@ô³ë‚vÎ}Ñ…‚ì}C¯*ÿÖ G´(´-4ºíº¸H2??¦$a(G’dÄK2dZEH»ÎnûF$u)eò[ð[RNjo&íûž Ë–Öž™kòòk€¦ÁiF†ÔWÑðÕËM79d¸Z‚@Q¾rCFü±¨¥L¹†¦±Ï$n??}õâ‡ï_'§DÝ:‘îÇ´Õ…Ö—-Ú€ßÿéãáî·†sL­æu5«.ö5¦íãé³/‰„¬5³ übÐ_r0Û¯i{BIã3ŠYÿˆší”Â%°^mN,ÀrÂ’Åþµ¾# ½sÝÕékå¦)|¦™Îfsƒ™+ÖHÏBóhPpå˜?nVQž~kÀZ+Õþ¬€K?n´I­vߟ¾9zr’Õï|™Ö²Äctœ»±Ë¿áI˜±ï÷KµÁÒ»}D¯ *‚– qm&¦Y•OÉݶq(ýÛÇJ’«*ºÂ3å“ož<™Õå×Sz¸žvªÍ??ËN!“Ÿ¼yI(¸=“(ŠŒŠ.Ø:Áãïõí,¢Þ«'&JÿLç©?naRåÃUþn°X|ºG…î&¦Û ¡”br›…Q(„O’^@Ö^<ý?nžSÁR‰8<,‰Àï(¦7"L—kŽüþúÍÉ_{/_õž}ÕÀ”³ØZRê‡v¾¢XËfî6ÀFÆ^\=}ýôåÉ‹@3Åq4ç(µšH#8½•˜Lާç8ê4¨·yý®jöóï_¿9:~…f›W–ÏÝ?0à‡Ì©oèëëÎè%øb?0ÊÉëß5$Ghˆ|½j|?0âTTZ˜O¬Û™óX˜Ó¹i³ç—o{ñÊR"³Ÿ×+¼èPZ=c·Ì© RBUåÂ6¼)—m*å:ý•H]Rs&£+È?r¹MáZÕûº’ÆÑº\(ä<ú¨IºS­9äUßS[­¸³öÙ¿zÞ™Úé]ôK»"üVvZrmu%4]@ŸFv5Å)ºT~¤„Ëé¸?r9 8…È?0;Î*Ç¥§/PWæŒOÝ]†ÌW4&‹™LLÎp_æÀئ0þüR%p·†µjƒ4Þp±•1@g°Ý=¤/Õþ*#hÒˆàEÖ§Ë';º¥**ÎተΠ‡¥ï?0.0‘0¤›Ip­ÁÙA?rgÕÌ…g@(Í£AÃõêF9Y$¶ëüñ±† õq­6˜ÐŒ)îó×á‚¥nž¦?rÄ÷›5/ÎyæÈÀðÈ×aâË1ã‰!£ÅVsb×”9$ishhÔ~{ròúé«gG]`??MKU6‘Vð½œjQyÍ£Îü 1?0@†åbR4ùï¿f´&¥ëKq©‰Z üö3©rëGÚV¾}Mv*ß®‹Å&¿4T¾&!œµþ:÷Œ„Òþ’Ƈ 3\•W§Ÿ­‘ž#ö|ª$™C‹oŸIãÊe¥¦güVmôÏÝý}××i­ÌzÁH4È×2?0h8Duådnã!·µœ%Í=”†c±b†â‚¸0š*>??Ò^š¿}WtÏ_ñ_V³åHÌ®¼Ç~ñüä¨÷—£7üûOÏ_þéÉú'?r¹¯O^??ó~??ysB)å”´Öë‘qq¯·~Ø€ yÇxµþ¢ä{A–üt´ÈGšþTâ$¥ O”@IQ]Úiíþ§üƒ§9]/^ï½íõ÷D^ɼõw«£Eÿîß½‹Ïö÷Zú©ÿþ¡}¯u÷`Ÿ.ÐîíÿC«}ÐÞßÿ‡zë߃+Ü®Öë&õlÿ»ïçd'ô‰`ûÿ‹{÷*úÿÞ÷îÚþ¿{·õ¿ýÿïñïÿ(þ}÷p;5¿^^̦µZÒO0Q¯èçÂÍÕ -QÖÒòϳÁ»æÕè1€H–FK·ßj?? Ä ù’!#0.úýó?0RTþ}±\ÎóÎÞÞ[2iXÁëãÞ’‰=FÚüc2>~þê%Jì7[ ¥|õÃóÏzÏN\ÅÞm·v[í„É­ÕXñc]÷pF~“??DÑ\ÈoÞÿÉ/§'”0-—ï³5Ùø.±ß AíTú}±è¯k)®O%ÆÔëï ²†ù§$— *ã8Ý%âÇ¢ˆ(–äwæ}œMÎ/)°¼†÷ÝcÇýœÓ*‹ÈZß[δH‘¶fñÙj¿€õœ)5ÅAŸ%Àª,Oµòàëx°Z"J3§§59ºÃ&…W#†‚‡z,ØEžÑY€jtPæÑ3Ë«–•JÒ×??Q<)J)¢ofõW´Qvmhµ;tL cOˆ4ÐÈwZ`â8£‡Çy¾ûöbï=œÏ÷#RCå£|áÓó¥úaTOÅÑfùd5‹šÆ³=ã¦Ñ"蓺OÉ»??M ëSDl9âÆñ?n…’„Mæ®jθj⌠…}€~ßTÃUÿº~1’Å$G»Wäº~¾®rw¶½®Èüº€©9ä­+ª¦¦—%W"|F&„jQ”öÇk-’??¬R_…lÇÝ”cq¥’ÊŠó!ú S7&½œŽƒòį%ß Êª0&žgß bãàÍÂ9žB¬~‰·IEHÝ©÷Ý•7«¤\)ZD“x´º?r{Ëñ°¤E*E‡D_KÕ)˜B[ô\?nA5ÇCúYÉ0­¤KN·8ÞĺÓ\[ƒ| —{™¤©ÿœd (íîÞëœÚè@bhÊòØ~xðÅû_>¼÷pgxµÓÚÿàîÊ>”½v?rR˜(‰ˆ >KµgT4“9'WûצcôT쯣’v«yÿVwröîG‡¥Ÿ´¯È¼Žá#¸v%!á˜{ï—ç_Jï!¿{.þÙH$ßè£\NÀ«VN}={KóGKR¶ñuе2~xR£ýWq½Yƒ[€%—…;^‹À›zŠgï2P{ªù (Çžâã9Íÿ?ráA¶¦–ul‡É{GW#å„•?r¯J°ùŒ”Ãog³!|ëQ’??zFSš¾u†±‘¼º‚úB+‹TêT:s8ÚŽGê®Â£Zª7£Ï$›Ÿ43!«òú ‘…Â˽+'FpÅZ$ù-Ο†óûWs—ÿOE¶ÎÍOqú‚››Iž9E¸¢)£~ˆìMíy¡:7ð¹yˆ/´|CsÌûþõíV xGÀŒ~F'hFüÙ,¡ˆ¼7APPT(€Âd‘›+AIp”WÊÂÁ¦Î\w_ ®Æ”äkšôÃ}Aܦ2Òö_ %Æ|ÇÖüòþYA߃ڽ|Ïͧ¥òV5ƒÒ-„§Ò~NáÝ;gý‰#S4-ÉðI½Û™ÐÀh,¥UÝÙêKõ™ 9É(.iŠþJS1_¶Ì­qQ~LwïüW[ 6eÔóLì=UžOz˜í\¼ÒÀîcE2·Ä{;¥W|zxtÛ5[Š…[Ö©Jë·R»*t5‰(Ýì¹ÀL4¦¤ÓÍÞP?06âÙ¸G7Æ}|üBÜðáçë7¯N^‘ñwïäÅqÅÑtÖŽ(œ~¬i8?n,ç©È”Ï™&ã<_ctýqÑŸ˜IL²&9Mðÿ4œ-‰÷º´:eÞ¼˜–| *µÃ‡ì?rd§oÎ?nŒ¦Á¡8Ä µÐBÚeMH-> ãïl¡Ë^xèkµÊ=W×1Ž0Étô¾Ž.E9ŶéÄß ©ÃtD`‡`ÄÙüƒ&ÓS’6Ý==»Å£žÔ(‚cƒÁPe…Î 5J?n¨¤³QÐøM'©ÄÓ­¼Óh8û÷[Њ¢ÃOÓ‘„??B]Žt,gýûŠ–!ÅxcSOl.jŸ­DÃAù¸*H¾aÐðйpÖ/F“í·Ã4@»WôRÍ‚‘^£¸@ç X›Ê 8Ìçï‡IVÆ€ƒoõƒ#٠˘^nÄzmRãV¤nmšv¸ÙÄ`/³-P>ŸM™ùQg?rv cTÀ¶;£|;ªÔ€øÿÜm?rµ?r£¿>Xxñîåè5R1¯õD®XsÅçFõ<]‹X‘²$BLÙÕt©ãsŽä›c-Fn.#ª{ˆêP¢“®úÚQMÚÍ|)íw`èp»·Ññ‡«¼¨²‹ž°FôKœhVCV*¹BŒQwâ;96·À€l7{s¢.Éh+2E°Šå—ÊÓ–XöÉM¤UÕ%\\)*³HòÍÑI’­1ëlÚèìŸW9"¹Ù÷i•°qü&RËGÏ:ñG— W?nJHØŸ˜<æxùêèåI‰¨CÚŸ¶î:^ ÐóW›ç_5NyÏOÜ \†Ðñˆk$n??6]îžPž uè³DŠ˜Pø¸ñ5ÛiwìYsÜýêý<É+žÿ*Ï?n «ÏØ–»ÏHÇ/®»èáh;•ii-ºbb‹›Ì ’~š’|ÊÄž;¢tJ¿y;>z½2ÅΖߜpÐg0Qjšçm÷wÿ“Æ^õâ·mXûÃêb¸`*1®ýªÀ¡Ã Äⅵ߀~ö¢¾s·Ù:ÿŽoðî_í< ’»†Åu{­Ä}šø2`Në{¤ìÛ'¨Ã¯IÄä6ƒ??yƒV$mQõtàï"aøöüÉ‹ÞÓW/OHÞBÑž¼‰`(Ë79ýbæí¸ØÑ2ã™VæŠLÉôÕºé'©ˆ8ÚÛ&ödãÉézL³tÂÿ®’Ï9\hTçx†SJs?rèOÒáì‡<XK‡"niíw]3y~)ÁÜ{ÖÛ¡ïž}@×÷twC…Ð_&¬N×ñP=¸…B•Ý„¾åÉéiV 1(@ñµ k¼xI,x&`“…A«˜š4¦Ý„œçÉ©(rH'0Nâ:â ‡M0!xBN.¶Z¾~r¿Ò ~÷¼xq!¬…Kj2±´ W±²6÷œ*uÂ\9µäðÉ·qgÝØæÎMÆ6Ê7{Ï¿~öo:[³{÷îeÀ«ñ`áº}3ïí–<8+w·&‹VÊ+ÊÅ›´mÉlÉsqHrÊ„þ­‘¤ùù^³"}ì§?nÿ«áäì“øæèÇÀ»wïþ÷g êÌ_¿þº¡8·¹Î| Ç[¢ç¦ìrîÏ«)=3½ r±T”3È»gK¬t Nzº0µ?0ètâÜí<ÝÓpV/|ùj¡"‹ÉæÅQñnk5÷•a=D#!·,¼öž•Ù»*S#—°Ǧe‡cåbOA.aþÞ†ÖeŠË0ì>òðŽ»Ô#`w›ö!þÓôïGû¿ÖÿÞyü…ûÛnýÚ¡¤¿Z»7ûšLöë C0y[òömžå…çÇuáî£è#õ§.‘>¤¡+X¹]j®±C O:ðpRÇ‘¤¯[ºÇ¶þ ¥9«åìüœÝÞ¨”°ˆ”Ç‚}âæî:1Èá}ÊŒÛØ®E+gßS5Å,éÖŸ°¶åß].e£œBJp¸Y6SÈ–Zœ³…VY??Ýs9CoU¦oªÐñ¨˜ÂÃ1ÎheÉ”¯¥®J¯«^/ߪ{¬ªIçÓp'ÚWÉAñ«‡9äµÝ_zØ„Ì^i-ôöžh0ÜHšÓâÞ60-·Š ËJ¡z£ÓHgg肤à(¢¦Ð4éÌ›¦ÑI6Ü«R¡üv°ì„‹Æ+8ŽjøŒ|ʬ:Á4’+)À?nó6üü0®N-J‚ÅÉ JÒ4r-??%ˆ²8Eè´ »ZBD@»é?0¾üm²ëlø\C^—@‚ø”տĨ£ƒ~ý›³[´¦Öy??”RT]Ã5yוÖìP©l`â"Ó×a1Üø?rDi½‹¸…¡Ô$°bª¨‡3cKql9Ž/ö†‰[À¹(@}×âŠ÷¡N«Lg·ÀÐ)pž…×Hj/6•‚´Ã%D>ŒˆÏ¥Cçƒ ¼£¯ðfˆ¯t2ŽËHXN€9 'ôu—¥˜¸ÐÄ'.yÎŒ‹Ýnn‰[XO4i½³]û*ýßWû $2¦'„»”÷_U?n£ØXÆâ4ioý›IaÿöR¨6¾¼øžÓÊCçæÙêíÅOSô*þÛü¶»Kߌn”#‘Qí-œÿÇÈØ$PZí·¸µ*»ŸÝ„?nߦª…î­•è¼)]°oâ¡ûÔq~GÃCŸÃÝû#×°Ç2ìl¶¼H‚÷â¨ëjª•Rw#<ßfʹà?r”£ÉîžèSJ™2¼A»¢Sº“ÆBà­_¶è_j­ÒŠ"äÂÖÒB¡} ÃYÄà\E°6a'ø²3ÌKtŽt›¹Ó¤šv—ÙºÝW[´PÊ©B$Úk/=D-Ø ]ùáØRF4'D›r­UŽx½Ó:-$$n5¥e}6›ÕL™ŠËL ¦08Ug9sL¥ ,zl(‚Š”$<Ú––¯ÒZþFÒÔŠwÑ I¡H®ˆz-´¯dåø×Q´v`atAá”BqOE4áa2å<Þ©B*?0CÄ[eF­zXô‹a1, úÆÃ"8$úÿÞCÂ*O~×NQÑim{P3N îUˆÊŠ0›J™i:(z˜õîgÜë~%–³­T`c§ñ}H(?n“´FY¿)*Ôo]Ct ¾Ô¶¡ý俆_d…Š)(<¬žv¥?nÊøPÔ/”óo3&gËßé/õ1J×Ãyê(‹«K£½ÇÁ±Wa(Qt½U[VQI7eå>c0?r_rë5îÄÊfv¤½Pó\kdnߎ膑1ÁàŒd$??M[™¯—ÅÆOí¬ôºý¦À¶%ªø÷??Sˆ>xax?02AnÇ™]Š&ÔÄ5V³™@ À*³u—­Ù›–k(¡G06å:ÏGËpu¿ô®úÐ*°=Œ||Y+˜‚0û~§qUõÇ‚±S Ø´É^'“%<>??þ*QB…œÇx…Xâ¸Tp(0µÊ'›j< f2"É–ùTSÜ_;©ì®MN7ÅdÛÁó#ȆR#ÌzP¡—å²B3¿/yvÚUŽÊ¶·N¤$bžVª¼T§¶šÖ"œÎS`ƒÃ?r6V>?n;ÇÉi½ôèPåÆ Ó9ÀÜWåÕ 3 ÅY;,â˜õKAd¬r +# l:jæ2ã(Å,[1q‘ˆ0ÛéÙÛ¸°¶^òÃ[“ïƒûé¦ í‚v3ÎTY¶Êµh&¶YL¹„Ü#¼mkíD akôŸÐ¬áV|ÐRà1Ü'`S™QæªA?r •½Ã¼çy’ñè”ìS`¹e{‰‹&j›µæÑî¸M ø…Ùw$®uÜxe9“Î"#ír¬ðfÏÄ/cŒ ´TI#9d+h3nn7Ý Fyîß9ŸOVù…Ÿ@=þcÍOÃÒhOû{8.~‡IЮé_µUÞ²tÙãj6¯bA­??—¢ÐyìCwm/wê‘1²Ø»Q³÷Z†ã’¡Ùð1YІ‚aƒ³»•~•-°ÆŸÙ†€£7oôy‘…‹Ë¤×¦3Í>¿ü²ƒf,Е¯#YH?0¨6ü?nÆT (x·¯‰‡Z¹…ÁºÜfû :IÂñt†…<„¯?0TŒF¨Ìˆâl-Ï9¥vc4…°2¨bôÁÿSŒéxú¸Ð‹ñéÉÅèIá—ñkg×&O‡IÑÎïå ?n#R.»nb“v½~ѶF"Ž@Hžðþìdy• Yü??`j%\ÒRœ`?rÎÞ[ºWž³Pä{ä†a]õ¾­{9*U~ôý×Ï_­ëöžœ¢1ì–}5lÁ‰CrÀÂÓXõè$=ZcŽsøY¬ûØé,0¸LÒx£øÜ°G¶ûçÕÐlà7‹^‰K??ÓùuI3O‡©d3î«"sJ²'Ç”‘??¬· º·U›ûÕ=­®6¯ª\óÃÌÇÛª;÷kû?n1¸Öõ§^mÓs© òÃʃ‡õ}ÔÝÛ‰úØYÓ¶ëÐݤhl¾%sÿjÒ~Ù.¿¬Æ£eué˜üŒÎVo!…g³| {vsòº‡Û±Í¦ôŒú?n.l¨¨$Ì4¨»ñNŠÃºr(²-õ7J=á†N Ú‰T?nb@’:b?n±ÊëJñ:á|µµõ°+ØÚp¾wŽiAÉg»3²…¤ê{Tn´øl â)Õ®æËë[b—b@͸Ü6·¦ †¼!³šºøÓ%„=TNcA±Q'…x–Ó$‡|ŒFKA¡n6½Š†Z ;ú-úº¨ÍDqÿëÅ?0œ° 8©ÂéÕÛwZ|)©þ¯8Ǿ ¸´0ïjä?rª,Îï°¼HM 52w.î„rN3!¢õØ<ÄDEЈïDíËÊBi–Ö*7&³\Eû0ñö[_ÜýßøÿñÜ”JgºÚIU±xÄ“`Á836ã¢TöÑ/›}„LaA5»‚DðÇ“¯¿¬j „:Ë?noþ­›xšoþrB¿`?0–$,??‡????¸‡ Æ¢î´KQYüzò¬÷·£7¯,,ùÎ[öׯoY%Ü#œ?r…úiO;%m:aç;vy» Æ…—Z˪øåGlÑ"%"")—æ7­N9ÃI}ŽõÎîß%ϰ”焪yÕ§i”6í9¹.¹ƒ[ì³d·ç»X¡À”‚ð5ªùeêoží÷{¼*PF³ßûÉx::ä9Ôš|hºÚ¥QЕiEI??Iµ÷4‡êäjàž={œÎÂÆ½÷Uø„ÂCÙ—¢g gΗ|Ã:¡ßæ­Þ鋳om$,¶¦ÁáÊâOj|òãL …bÃdðý3W]α¾ð5kÓ>™Í؇ûöâ¦XY›snB~ÒZsÙ(’ŸÓs?n­¬ÞÕ¤ç/éVAž½ùþ4½ñ}½6±ñ9sAïd¾=vÓMÃýõûìdfBk%mOZƒÙdÂ~óuX*<Ì®mÄ$àtŸQ½_€Qxe¤VýÆû%4Ð?0N£eçaHq‹(¾4§ùWõ“Ñù²¸jëS¢€VRHF¤K9r3àªñøüt2þbH°KoYsà,˜¾kC–“œ³#î—ƒ‚Iê.RÙ¡GЉ¶‘8S¼T°”«.©Ñ_ L‡³kÓ`Æ‚r†çª_Íðå†Óßr2+ì^•iÜ‹’j9 ;X¶_. ÿîªQ£cx®,??ü&‡Õ~3ǹ›£¶Ö€y4nú«%⎮+ËV8¸ö(NC^ëÉ/n<8=@âHE”|¾÷y’•3Õ2?0.)þÃn2~7²pßÎr P5ù¯(†á˜pÈØkž¨ù®ôvŸPD]G ÷ŠWŠëcov]Õ0•ŽâY&ãZZ¥'Q}–»¾`RgA{÷Ë{_ܯíUXé^ص‘ŒHÁc[R)AP$‡Ó?0þÕÚPŠ“i7=1XÙ¹tñiLƒåÖ×ò»µçA<˜?r¯y{Á®9Í#àeG²‚æj>Ķª,¸›Þ™¡X¤Zº›¼þ=§0nJC…|ï¦(Î~Q4ÔÍö¯_Ÿ$nM|úâè¥D’?0š?0b€œÝb´D”À.ž³ñ­Ik4rHãàNôH£c¥&)CÑW—Zž<&ÊÌêϳ¾‹ÖïÿĈt~6¾¹Z(L|˜×‰'#ä’³@ú¯Ô`Î(uÛÖÍ7û~V€€,®L ZæÉnïiLW¹VÉd6–I£Ø›‰@½B–Iã•áùhµ5Vƒ4<ÛG±s>‰Hñ¤¹ÁSÙ-•­ƒiÐp ˆÄ'(pÛ ³Õ(M;tì8vE_Œ§›Ïá|ax\¿Û©rRöã“7/Ÿ¿ü¦C·ªŽ¥—³Yý‚bÙ>$R‡«ñ‘·»üôn²¯ˆŠ5À«; xµö²±2aQõKámW¼\­FÒrv\ûþ•MŸ?rè¾›“?0ô¯’&@È„óÂàÓ!™®ÖF«tð¼ä½‘ûZáYÐZH1ÙMiº#g•‡ÂÙ<ƒ½ŽA©’lÙìf™`ꇀ,'S)J¦¿kongt ‡Ã÷f° '`|”3PeàÃîXψb?rŶ9B¬A^Xž QÅÆúlm!mcûA£³áÁ~< íS/dâÊÛë{ZaÊ ˆX5Ëg|"q\Ë™«ŠšÈ=J$‰_L©Ó  °õ.[½Õœ)2튌é |EŽ©uÞ; ¦6SC¤•Äÿ2È®nÁ6m*$ÁÇíÊþ‚ä )—“Nì––­SÿN°€#âïyˆ°0*E£ƒ âêwñÀ@ÊS ?nÁVÕš–1!Ü¥ü¶;uŸ)iÀÌÃVáv0þP>]?nÎ~ ÀÄTz^ÑÒ®ÎWgŽeªò€ó¸© mztÈ@AåànÛnÅŠA­ ´<6?0†?n@ß7EHK঩þ9ª3t3ôcÅÕ?n“¾_‹tœb(m»*Ì\yŤà›}ݰ¡ÍPåè”zÉ(^‚|6@(Þ*ªZ™¼±@e1‹HY@v·ÌXBØ2%ƒãÞâ)¤ácÁ"icZz'ã¢P¸}@ÝýÒÔ5šÂ £Gßp¡è¾Œ9eŒ=û5ýžŽ> D:»DÔà¾àú_BÚ"“P3æ$P,fi@•à??zlVz×äÄ4®Î4Á?n„Ÿ÷]”•þ†.NÁa‘µE_‡9F]2nžVß_ªe?r¦÷îˆÕÞ˜ƒ¥Ûù<Ðê¾{]l??PLe‘‹ù•_ýË´¨ø ÈÇŠkk=»l%ngZ€m•;&05¯½]vá‰[‹,<è`&õ*–ÚŒ·z©Žñ™i„Yz¸ö²õI4Ïý>,aän1y³QQÒ¶µ¥½9Fm•ì??¾??ù–öeQ•ð?06œ]þ>6=¡ÄÙEš‡!ß Œìï.{PòóE‚əչ„Ùì11­ôÔ´­Ü|SRœÅ?rÓššÑ¾ŽíoÁLüat+=¾Ìg¬Â7ê¾>É]BÐ÷/óMrLœs²íT +#hr¤[˜ ësÃnäÕqd8óœ5£ñ¤Aøë{‚*MírÊNFbj»¤€LíS9ÞàÿÆ?n~qG {[É?r®3¦ýðÌü¶ÑŽƒ„4[l~©ˆsX*¥¢V¹Þ˜Fãµó— {EN°(2*U"tØ›YÞUßk@3adø˜N‰v —–!$;5§›ðôR=UÏÔ¸Hxçð†‚*C1ùy'Å0ôúKLíémß*¦åb³'T²yü¼33jåI½¯ˆÏ…4??hu@<5MX¯Ì8 Pq!á¾À¸:h-!€Â¬™_ôi3©ìaìéÌC«Ù•¬ÐºLz#¥¦K´˜úV2³$/!Öɪªh­£žb•Aëš2㤽Ïs{0´N<¶íÑ«¯ÉÂϮРƠFŠzŸÐ–ÉUìòvdB·*ÈSô˜½F9¬_H]œAºæÎÁÁ©µ=·û ™ Q$sR‘­»–v=¨#Q >%$8ì78£aöË—ýnB@rª“;Ç´«K¨H1Ãúº™åf–~a}Pà¤w\ïuQ먲Fœz¨J¶tË—Cz/¢F»W­Þ̽6Öܶ‰‚€?nËݯ³zRLZ“+ íø3Î?0×Ì,dn-´°¼óù?nñ+±4-Á½[Þuf??I¦†]* œ•†(y'„˜aRœX¸ÁºCó˜ b¤8 ³/8”Cž á¹ ”§r$ƒbVŽÒ—ñbæÊƤpžô¾¡wÇkœ­ÅLàrý|0]NBÄÊÕ`Ê9ãYnM¢ˆ‹4WÓyŸv´gÉ=â?nšŽ0¢(“Ò͓篞2¡Ð??¡IÅ¥F·ÓÙm‡#ƒ,¸ ô”¸ÑJ!­¥¤öfÒ~ê²…çCîw>¡ÏZ†pkR_õÞ<{õòÅ_µIa²Î‡fžËùêA3b‡w¥L9Š`¡%!é&dWðÃ÷/“S¢nøâùË#$–bú’fÂý{ëè¤ÆÈ!Ëž®FïÂñ+º]OƒG¯·ÁÔ÷Bû®£5?rãµÿŸ~› u=ÊHo cZQà€ô[÷`üP~2Ñõió^(ÈÉåâ¢Ã<³ÑVí÷Ñ8%è’Ê`¯õð\ œÑ²g˜ÕA=“¡|Òr7ŠÈ;"Å_|µ¶Ä´",süO–ÜÄE ÌÝß…û»J¢æn@H~f…Ù»†(½;ÝaÄ;Ã'Îð ýózÝÒYR’ÿ¶î …8àYfBbmb?rrŽ{t#‰õÀÃiÞ€á[a±šÏ}£¸µEÜq™MÄÆå›K¥gÊ9%ô ³É;ÜöÚ¹ã¨7›MGÙÅ·¹fÈ?n&~£,T`À xW—R?0ƒ„»°Iðæ±úw£ë³Y1|íb…ÑlïÍ8×[l=Ú¾âHи¡æ†Œä1Íp”Ó ãWÕhÙÃ"8éOS"ça` ï*›r¥ÑJˆ¬8`*ØN©ŒÎN.,[C¥~±nИôñ[žpQýsÍ/˜•ŠežÖT \fO.zÙÊë j=©vúÀ›–¤ N:uUÚ¯Ó)ÊØËCø8q×ÒÞÅ¡>7Íi‚é;/;€ÀirÈ×??ª;‡ÖDSåìw–ã9t¡L%Ü?rò?rλ×ãÑdXïÈPÒÙZ4ÒÓßt襖®m;§œ‚ÓTx2Ƀ<‘ö‚t´˜Ú%.:0Ötm·is°½@€ö?n±hv¼É7o®g÷4ÃÅMÑÕ ¿ã©x¼s¿h*c_`"~Ñb2ÀÊ‚«ïû“K°J›¡L”£©arŸ²=æ”7ä7­'.\—É…ÆS‡ÑY Rœ=+jÒ»Ñn~}‹Ž$?rû¦"¢]ö5‹*N×bÐ'lÄW'M?r£a£h·ÊNùÆí¥ur9¸hôU\̆ƒ—?0ŸÇäû¢A55 ¿Gi@d„šèˆÒcZ©Cú¡þÆŸ>òæ*…þZî·™‚šÞºÍ*Uµ&ãíeV“þÂ8K·ÖÎ Ó¤_[ºJb›¯;5gF‘ÍGsv¦¤n”H\)55§_‘×Ðx >ÄÀ‡=D.`壜úØß¼iX¦ÂÑ׉Ë@ÿfÃk˜†·Î Åùm¾Ùã'0 k?n¾.­ÊÂyá Î/²Ô`¦Ö[0¿¯VÇî6©ˆ”§æfUoU…& n¶ê{J¤Raû÷î;]–’NæBŸ|`“¨…Ø}²Rdø,¨ÊJMÆ2!Üð!”EŸ¢ñÌy87æû¡|‰6òÅ7Ù›@šë¢@‡5•øYÜ%­s4C-UÌu¥úå©¶²žz–-û$C;?0o•pŸ”—É(âîç™ÓëærL·k“©eFg†<½#óŠÅ/éâuJ™TãßÓÅoÛê.&~ÓvÃÛ¶ó¦³ÄJõÊ-~í¿zc°O¼b«UÜ‹wåžMdY…ã4 ÅÕŸ€„efרŭ Ø’?0¥•?n"XPªw9Tô²¥ lƒŒviš±WÇOmúÿ>Í•?0/->—rwä—W‹¹{éŒ/ß) øRçÇìEçP²àp¦]bT)CjǼ{|msSÁvõ²'À/^Ç­)ýf·kë¨~E¨5’j'ÿãåa¡‡2›MN¸¸-É}§?0ò~Pž"§9Q'ý€—é²xfQê*·*×?r(1Žbm¨Þ>`±€ònCaõr¸¬%+œaƒŸÛÐëË2¸ÔÒtCÏäyAS9ÍŽ—#'),þù¨¿˜•Š£”Œ&I‡Ž&¤h]Dt(œf?nŠ!P—‡ï^ÞzH¾{YX&$ ß9K<—t–¡t“œ’ø±¦Š°é˜ålåÓ¢+P"WݦÌìý,PB˜BÉÕ2ñ$UŠEJ¡*eBoE$àÞ~Ø‘*éá¦bGN="˜¥´bÐÑ, rT@Qœº­À,މ!“ñ‡½;ø]«Ü@æ‚½ïø¢Ã…¢ mL¬®Ò <ÂjWWó¼AŒ‡`“OR”-òÃF’¹³.‰—½eÎYQ3½ÂjRÁ¿òtSFFóx¶9ðÒŒ^$Ú=+ô¨~Wµ(á£DxYgïºOS/f®‚.¢övzÖ+5¦'ôМ=ˆó“(IþiJ¾‰‰!èvuÅhe¯r¿Ñ{ßæ`—ž<"_ÊálÜŸÈm ™ÊçË›–>í%ðŽ’Ìè!«h`5–|¶Z xëGÛ£üúa}î¼Å«ã¯ »í·ƒÍ xÌé43W¦2À¥á­J"7Êâ*|4̘Øþ›Àë¢ÝBdçª<Ø,,w’SöØ TÀŽ ¾f3Ïggú§ø´*Ì‚¸Ÿ \/u¾ƒòˆ:ƒî÷:îŽCöp§y@ª vñ9tŸÈ€Þ‹U<×\ss¦k Õ‘¡`Æo’ÝOÚ±§U7êx:LC¦2g spÅB‡*ÚUÉlfì”nAîÙÇ$æp\Ú°)î1“Å<9űÿ··?n*£kIi‚'8ì’ÓÛìÙÖ,&<„?0ß’SÍá7ŠÈÁ7Ê©m´{L?0`« m¶ír3­…¾:³Óäw ¾DÑ9ð|¾ÈÙ÷òè#cmBUV.¥6NÝ<Á`³xXóbJ2EôŸŸ@Fðë{{e¨À ™RÁ¬áÝçßè‹û=­é…þé~Ì»©Û??Q?nfà•÷‡L–Pìbdlµ¶p´[èš™°é‚¦š‘qî‘üËî¤]Øý–·l@–mdþˆ^ì¸ú×ý+[.aãjÁ­«+(ð`û†ãRxN†¼o² @>^Jîz¬ÖrŽœõ³Ay0H vµ-à Ï~ÕÄ,ñ؆é7Œ((¶hÈë??&#\”ýà·o¤`ÛÇ¡zn¶ýÐŽÁP‘j\2í>^Ù•Z×?ni^dgY¥êl#?r–#»¬ÚåYK½…&󢿌`¯èùøl‚Zø$[Y¦âòFءϖÛ!<6,ˆì·òÀ~‹g<²›\ûó:S€C”õ ðVÒmA~y™ªiÝr²íÙ4.Ä­A™¾ÐÄÓOk®4E‘)±nÄW21§õŽ¥yêo¦H*??„›ŠÉ–›xc”Õ{r b®+¥*÷íƒ6!zóx3d›`XB;Ò ¬ª÷åªÑ©Ä2ðu5’¨Bˆ-ï(ÑBa¡½‚ÿªÙVâo¬QŸ¡Âm¬þ¿]|Uöç¼~®Æ4Ç]6mÇZ˜¢§µ £þï&›–4&Ö̪ÿ_oø~r`bjº ¿pk´¬ï¾s,Ž–\5áÄÕÝ;‚W¢Ÿ]Æð÷ÏȊ¹?rú+jÅ??¸ûGªØ«šEyÏìàD!ÿ^GN‹$½ãbiMÓŸ€2@­ÚÙ]±²îø­]…[Šy:F–yÒH,ÕAòž§‰®NÙДϩxx÷!^Ïâ^h€ ó,¬ET?rG¨aÃ’gÈ|£Q²#å·nsð±ò¡áñec?n”¨Åc=¤4)ÕÕ$Ñäf¨?n!ÁW!é@nJ=Øj> ¹A¾·oÞ4fVÛ¬^U¥¨°Joƒ…&&)aÏ!v˜¦ =Š??&“ómrA®| T!_Åš&sv4e‚®Ä§¹²ÙåC;3#X€Í»:R~¯'—ïäÝ÷–…Ú Á/’Zè?rºcÀ??ãØM5þ??üCôèˆ 2tFÆí˘ްÓÙ?náΆ¬üI‚ö¥Å m=¤wm›NàÍ}T<ÈÐ)rñ¸ÎHSC§ tŸÌÕp2œ áõý³ñÛðŒØüÜ`JëEH1ÁŠ=¬ã’KL”®¸˜à1XgÝ0+߯–À­yªõ¦ñ>x?n7Mec>\þbÅ¡ ¿‘0&µ‘zyÒøè®yˆ Š˜wÃÃÊ-{`~Ž?ns÷0AVQÄ$\ïÌ­©·¢¥²üþƒ6'ùGX’—ÌO‘×\?rä/sÊß-Û\8¿á,i…ض#¤z¦²åëV ûtC‘Ƚ\ôf,n³nmàv}ë5ÃkˆÎãXÏ+}dÜeãc+"t¯"(2úþnnyH±šø²Šïâ“ `ÍÑÕ| »©XK­}Ù 2ÙÖ ð¶¼1â³ó0ÞV[än5bfÄ4®(ÙÚ"-vß[-´Ö*Ë Ä×90ÏŽÒ?nèðµZ\ 2õ(Ý?n;(Á¶Âž•/EÊL“øgy·~£Æ7°V@æAV¥ô$;ÎØ¿0 ù˜ágÔyNrßÛ§[OµW6ø±Ýi/Ž·¡!¸å“WœïælòýaŠÕ4rÍܼ??Y²Ç;õ7ƒ#wå«þMŒg›žVÊdØ-fZö²ÏÔ¹¼ܦ~¿Í„?n(j Ôƒ °·zñú«õUÒú ‚—â ©¯æx!ÖheêÔ€v??œèÏ{CmxÚv“£ÚòøJÑ_üEÞ9FL±"¼@c½H¦ªúã¦ÏvÏg7’¶^?r43c‰°-nÚ(›ÝŽ2!Ìå«°¸¯ß`7·¡°XùsíÞ†.ÁŸnÇç‘=NÍúü¤â²A‘wm¾J¿Î—äDf8åŒd5çx0äÔ%1Èú`å¹À]ôXßÖŠ#܆Æw–Øòæ"„æÃþ?r‘ˆt??{ ¡ÔbæÈdÈŽ›©„EÙJ›(%8ÂRp„??˜å'¨1¹#[þ1’»kîÂO‰*ŒÀq“nû¸Tp°r+ºC/¦!Ÿ+T’¸µ&ábS‹¾Œ XC‰ òÏPëtÿ7·3Kµ?rŒ:Û¾alR?r·¿†k°èâ¤1–$ÜÔŠÞEÉJÇàžÏ‹ÌMPiE‹UÔ«‰aC{†äàü<ÈÙú2PL)\UR¤BZm«²¿P¶§«ÆE\yæLþVsïWºMù_L꺑 g%>1K3¦Œ©' ý[??S[A~•&„ÿî„ZÚ"t]¾÷¹™”:ßSŠ“m»&ÜÿûfŒ—ïõÙïº%™~Sô].¡$^n©t*ŸØ‹ÓdÅ"hà¤×7{ßîóžê/ÿt‡o[®ß[»2C??vkf¡à¹åŒ‚^Þ‰ýÝÓ oiäøÀ« Cz§z9ØÝ:M"EåGÉ`¡é+=#‡œÎ‡ÕΞ“€;¬¸Š˜9﯆£¾ÓŸwÔÌù8˜Î†“ñº?0ímM7ÇsTæò¾tck 0él†R·!Ÿ‰ï!¾¯Uâiá??þ¿¬—ô§´Ô­Ã*s?n•é(ÿ¹þOž^=!kúÄŽ—õ¼×®Ñ­s*Õ:R!ãž”)X…‘^ž6°ö‡‰zðýVáféÝ…Ž(??:ðˆ„Ê „˜§ØSø/u±n\ºF‘ïº$Á¤®–¶Á×@¡a(‹,Õ?nWkµÞµÐ»/Á{“ñ|0žÏ”^лä c›s ¼¶Ej;®ï;®–ª¦À,Â>hæ6ÿ5=s®ìñ´EÁ¿-LøZGIÿ\R [Àí\;ŒªkÄ eV~=)îÜ•YA\†;ïÀõn˜Z5R.j›ý ,$u?rT”8‹ÝÿKÔÎ2ÑÙªeY¢qººçÈê‰ ?n“R­øú!¦¦ºÍ8•»H¡!n­»aYÍW“{•_бdHœ–Ž©)Õ¢ºË¸ìÎÏ_W¾Tsý½JÖtFÃÁu…éSu{‘©?0€M(.{P'´?0@~‘_5ÎäF?n‹²?n:Ÿ3$ùùlðtOhظÛÒÄ%渳Bn-µöY“¯³åòIÅAJøM署fÊ ’Ô`µE¹¾ý*IÉÜ?0sê2ùQ½˜¸+*õ—¤&–ùÚ^ÃO^…3N-'¡ÉäõóuÜLó®$'=©Vˆ‘!uGí:ªþ¿¬Ÿ%ÏÎàbÿÈ…}pßü€~æx…*@gnnÉ­ì¡éÒ^®-®#À}ÇIûEîmóº*¤a|ØW€—E.ÊòƒBv R¼væ5Íí&5 nZ`Í©•wµ>Ñ”˜v[ÆÝ|rP-FÞlYݸ‹úÞ€R:XK˜²O™ P¡zh-ž„$ï2“æsi¦a.aÿ諦^ªÅ=?rÏKÉ??²« p§Ã; ŽçQ£F•~z¨§ùŠQó;™µuxÅ[,wÙÕjýº$Hx{Èó¸5ãÉO›û"ÌÍÇź9xž^&hFAD’Ž.S5ò_U!Ÿ¾èèòµPÀ>a…¥à6”êGq䆌UÄߎÂß?r>Fà Lͼày²ñQ…£“#áæ/âáæ—¼®éHÔ"4ê:¬¥s÷Bß!¸¹W²^µ, =/Ò™¿ˆhž àj}‹ÿÆÜÒÎãEV¹{äqlêñÆšM‘z67††´¸r"ý@ºb´])wO×MG½ÇÕzù–в›?rzÙ#”]c]Ññì¢^¢ç_Q ;Ô 7]|SCÙ%áÂà>É0L}SJ§>H¦®\¿6Þíæ®¦N×íÖ ˜‚¡á+?0ö(ºv½[êíÖôÀ¬Çù›Éäb8nª-½D ŠÓnô€Ovu¹??,®¥yøà5ªÖ,e´b?0ר'iòÕgèh9??ï,GÏ|ËC¯¯À$¯›Ÿ¤Qu•¦A£r&éçcâÎ*¸S:áé??§š4J´‹Ñ?0ާjôþëZa†Hø¦va?0ã†XLnb‹Éf!"dZÕ‘8Ÿë5bs¢ø9›/Oyûö-ï– hˆÍKˆ7|µ…«õ6„1Yž°ÞÍ+–È!Æ(ÇÌX°‹Z.¨/ï§ÛRòy¢,ìþÖÝþãïq’™6E9ã;X¸VðwÖÕ´ˆ!•øðpø}Éa?rnÖûã—Aþ9Øü3´0ôÈ$¡âo¥,Dy)/õ…Bä$%·+Ó*—„??³U¼góv"Þ n‚ÒV+:²ÁVa…~D')??—@b”‘¿‘ çHµ´KìiNßTßÅVɱè > ¬lÊÔwvr½+öµ‡ H³ì6Û|Coc«önP£D$ðF±Z©kFÊ·Jð»ìË|ôF??Œ §-l¥(ã’pWN‘¤G*øŸ-·ü®(–åØ)ÙÌ‘?0åƒbPV{¸ü”¬Íö×p™Ž-pñ¯/‹v±@ÅÌâ1?0;M›“‰è5H¬Õœ§Á?r??Rd¦r–약¦íïÒ@ÊÑ%¸#[¾-d”Ì¢0·írä«’ ƒ«?n?rX•…¿¬}$„4¡)+Å‹£qÒf­-²%ï÷#þP¾æ©p?r4Z¿ÝFïLËS¬?nWÙÝRêË ÄðfSàW»>¬/}aüHýòâÇü£HMróõ6 ñ8MÔ?nïÅØÂø??¯Þ??š¶À]¯Ã…íz‰ƒÜÀôi¤=›ÅHel,IQÜÜ„±*Òh÷¤OU#•ôhÆD›>ÿ1\±÷«nü¢GÎx0îó‡0¢¡œ+û$T»Ñ±„ãäE?neilmŒOIÝO1èM)ö¤ÕÙbCt©MðÃÛ“ ik«ñ?r vBãþV‰(GØ(„¾Ìû~nÏ¿…«ì÷k/Ù"¶¼”bçÇyÔür¤|[yîs ÷¯Ž*¾ ¤ZÚ‚†¼ÉMe=•½{lžHW^• ~<]k7ñ‡4ÅI²rå’„k«_¢2ƒj[%/2ßo¾…îœÿ??øÿÒJ}שx´È%K!HzQˆæ…óR*t/èËËAF&zv\âW€Û(N%]£?nW»ß+{ÏÈZv6»ŒQ…¯,~+3ÓaZ¼Íä™/6¹–©·¿æÆc =« ._$œbÚÜÇ—qêNǸ[SlૼÓxîÌ‹W“ï 6}›w¶û4 Ç7Ç!Sšãððé8ä@wœFN^ÀÌ›Ÿñ÷÷¿µŸzê¾ëû»¨kgõôEc'GGô»ÿÍñ^ñWþÞìïï¼ÙÛ??<øfÿÚû¯@F]·Rüü÷úV¦Zþ ëÿäðøïõÿßTÿ‹Ì¶îÿ êoÿàà \ÿ{Hþ{ýÿüýƒêA’py“b\n«T‡šI@މsÓëÇ4ÿå?r¿Ó广P-vSü£½W_cÙûV}u²ÔF} àÞ†@tŸ??þr‰Ì‘Åã∲&Àr$w¡!·¼õ*t)hG±Á¿4«òCš»ÂäF~£)¼M<ñÀð¬²d¥]>ê„u–ªEÀKÌ”çúI-7æhø«DßùãŸ5 º×ú>?0"o],ÚàîÌ?0S]­Y•$Zûº ¯‡¢ˆr†k~>P³ÉÙÓ¡ÎÔåtòqØôU£;Ã{CuÇ}ê^ÍÏ'SÕÎz£îðb¦º£‘B®iw<‡ç¨>?rçç´·¦;EŽ 2Ý3êqotÕÇ„‹ó?r/.GCyί&g@q1˜öÎñ¡û~8ο0ñ³á|<˜Ív€B'jð»¾ÔìhŠ|½¨Ñ°û~4Pg“)0uÇ_ÔìrÐvG0=ôæ`'䀓j6ø—+`Œêw/ºˆ)e•W*Õyw>›€êe›]æT†³éäB&3b[]Í Ñw)3ä†g䀽)h!1Ô›cû à‰ð|Ú%.ƃ£á‡Á¸7 ¬†ŸO¦?0¼šÙ ÕgDsr5Ï…4a¤äc0H.xd&N˜‡ÁB¸è2Þ³—±³µÅ+9ÇYdi–`Þ*K\žãkÌéôÌèšìäé(÷}HJµµesxäí0ò$I¬å%ÒË%–òªŠ|¯1LΆE±“¥a$|ÀZí;Qx¸ÉS´ŠÜ”V›\Oh€ùþ®È„ËØ$ö­£fÃÃñœþÙÈ‚¦n*€3gx¥ÈÒÄõrŒ¼” V{y: GëȬÓ)æ¢mÑvÔòÙˆ‹‡­HãI¤@Ç‚Fx 8Ó3Þ™çä¬4??´ðÉ ßì>òC`OUó„£LÓf-?0õ­Wàßc_à/ÔÞãÂþ=¯<ç+”H¯"ÏA9OŽJ0Y(¨²a¶çáÁ+pª² J'ò½hÉhö¾|øÏ“µðVWP`Ÿ|/-‹~–&™Gj^°8ð.E¾qÈ<×j¦÷Ž ¼f§P„vGñ÷ø«„ßoQ±ÿãHŠ ÊTËi ¼%ô2¼ÎäJ¤\ØãBŸLcÖ× "ÔB*(X?rµPÒeÇÞŸ¢?r,÷a¢c>×¾NÞ??±Éz±ÔÒL‰+`®d=«!k‚ô‘ª¢Ð;»÷æ_.ÏçGÿ•½ëloUGÂßó+üÜd9lï½÷Þ+Ùac#È)ÿ~yU<0²rHìÛr‹±yÑŒFĄ̃¦å¦ô­¼-º¼}øîw¿ƒËÿ!>â°3™<íÏUÉD‡JaŒë×awIÎ9ŒjÀyõ¹m*EÅ$,WhõÔ3J­š‚€).ܵ_.J¹Š¹ u#ç@¨›õŸn¢;Ø-º“U+lðÅB4‡‚‘e0ò¦î†Û`44 ýÆ:ËÛËò®Rö.»¡wº<öj»°Û}±ëƒ;ÑáöÈÄFüjêôoò]ëÕݯ œµõïÅÇ_Ý/§Àè#2»ø«º¹qýuæ ¹sŧt˜£>嬽®zÉQ³‚]¾Úç©??ì~Úôw&i^û8Zƒéü+_ŠžÛ¡±Ã¬ G†›¬µv(ÏÛ¢ú{-ì•ß^VsnZòÂFÒ:1®OVËž_¡V{¶sû5Ò¸Ï{êµR£X½ˆæê#§vˆÖk˜·%ú[Ù¹€keü‚_g´o—W¶P3tß_5AÑÝç¿ðõÍBQfùØ¿ÄÃ??±ÈfPXÈÇn4c½‹„xÞ¹;¿?rôÛ??¿Zdи1ãÄkʼfÌoÂüV)Üp„?r¿Á÷Èáæ/dÐÅN/Ò8öoäðsìØÃ@¶¨ÌPöûý¦ž%UmÞ·¶&¹bcÔ4Hü?r§Uá³»dY|O{noÓs›N°¾Üö¡“ÆÌ‡móµ§ Jg Uã¸m'Äi©¯·%²¤ .‘õ°å4ì"<«mÕHa®s—¸Òÿ¯o^Vcréþjp˜ +#§q×ë?nW½uËUeµæõuå—鉄ùâCñšáÈF??qk¯ÑÛôÓ6w %Ø>xšžˆ§X§¤&„!ª¼n¶ò-ÕÆ1+âÍm.÷Õ)”Ê™Õ;q?nXÕ(‡¼–¼¯‹ƒ©9øNˆ6/‹òV„ ·û1Ì ¶E…i¡_øº£úS}xSrùªL¯ûåôõ#Ü|lz!î°#¬\´N'Ô6ÉuêñÕgQ«‰V ËŸx½¸pn¡[‡G§E¿”uÕÎz?rË]­å~Vkî´ïtõËbyU ÅCºÿ¦¥}™ªÔN ¶bY'z 7ßæÁï´úÉÕL(²ÊöÑ??R.fC7±Èc÷°øqª½u’À=-lD¥nïzbþÕ®–!ª|Uw.µ˜€w¬Ö±LÃ?0Ô!çD #œH`¹É\•ÙèB`qóBºÃÛ°ÄêÕ¿9¬oñSDtšµç¥ñüµ(oǦ\¥¬¸Ñœ®åØXCkü8ÑÄ4H˜³ž †¡xéXƒ}Âfcü[“yäá2a—3_GI…sM±ËGmømÿÔ|ÛŽ$QcÁÕÙ”‰®óÑÜ"GèP¤ÛüÁp^Ìà&œŒ½ä‘!v„`†¹7˜KTl£/ñôlÖ>úÜ0Œ¦_Eˆ>¿I¶žßótåeù ãÏÄ.ÌÝš_×è¿yÿ!γ›?0"zi+€\.VÍGßY¬Št‡|óøb/:3üS?n†PÙqÅõ&ÖH}û¤±Û:Vžµº‚1 J<ë› æÍ—+6nž>62û¹$nE»’sš~…qL¯3Ž0¶—7÷{•Ö-—w¾Q+ÂK ƒ JÀ¹<ÔCháPù­+ט)³x²öëÅI4¸¶äíc´þTYÖÔþÛ^¤%³¥=¹8Ù E6ý·Õ)‹‰Ýnj~Å×Ù–»¡Ïôö\•Uq3¦=ýúæ3æWìÌu¬úçOÎðcªÚÏÿøëš»^ XwƒÒÇýX\¹ÜÊŽ6YÖFfõ } ÛmØÐâXM\&×%]Óòƒˆíé´2Û|Û¦R(¬Pd+ðc¿Á=Y÷´ã1ì|wÞ?nœ#Ò”~°rý6ÒŒ¾&V‡,û$µí@”•Òl1^hCX€7/xAÉæíÍr®ªm$O&øž¤}lóÄ(45sµÌz‚á5’ .ÙÆ‰…(€3u£ú+Ò]ÜÈîë*U;{ÑÖÕ”?r•èãSßhä6e¯fìaéÈî}:‰–-œ”±éñ·ÔË`̧a\ÚäPæÄ5•‚dÅ2¢‹'EèC“ìøœz"Uç?rPTt!͵Çòæ¿¢xSüöwHÆñGl(Ÿ`Ù ¤šn`}£ÛˆE‡7÷;u£šßhäp‹M¯nööfrµT[fGFšûfh¥²˜]ñj4¨Ú¸ ‰`)1=òíHåZŒP*û»¾¾{…ÜÑcõÌMª#„GäÆúÒ—9HÓúb~Ó²2Þ¶—s@ž—]=Ôe±Ï…MÆj6œ3f´™®©0"urÔ–žE-1ç g`þ=¿a3ÃÂãÇt£¾Õ&k|éLT-Ƭ•mŒg?ry¢o½Ljxt€Ìx1Õ?0r€êåyf3?r&sNMbÛè…ŒN£¨åÚ q硎™ QtâcÃ'îRH«2??mæYÛš‰å5çÞÅ,tØ\«>5ñµú$R†YãKó,·¢@’Ž«*çÖUY–©§¶ðø??ÇŒßs_¥ò®Cü@àƒ»² Äÿùαƒ ½¶#ñåWwM_TRÖ+žãÀ­ãô”ÛEãÑG{gò[V:ØÏ>6Šj4>ƒGtl²ãµ{ÕA‘ºÔÖÀ.p?r•súDÛæf\ãr² ¹¯Ôy§{éhr?0Å,P“gè<°ö¾ÂÌÓD\tw¼:÷'2:ÏâeRêTiþ1ŒU/“ Zº;]è”ÈíMê ´RÁ‚ œüÚ÷ì¹ à§š:M·#î2˜eèv=û‡ûÁ×­›6BøºÖuyí¬ÈY.Û<ãë€ä‘÷½z•à±7qW÷8ÕgåÄx'0I¹g8ƒ5EöŽÃäíߨ(0°Ä2lô~ÿí÷û¹7ˆo¿0Ì›#UºQv6ø}îöI³xåQ@6Ë‹˜äŠxF<s‚X:õ¹+o… +Jg:oBA¤Æq´˜©0iLÃÿör—©p.vC¬LA’{ÇÁØÐg33^§¦*ˆ‹4Í÷ûìÔWâ¼ô 4±HRËuŠªªîô·¡v /‹ ñ…yŽpþva"Paó[‹ ±?r¢Ó#6b7B^ŠÍ;þÃ.ñe??þùoþòý_=J„+$7!¹¨q??&ƒè—á;nÊâù)üUYèˆ/õËòË%\2\ ¡Â`BøAÑ‹£ ÌËÇ¥Pþ?0?0ÿÿì½q[9²7zþæSôú<¼mï8ÙÃŽó&î&ÈÎÎeyú1¸MüÆØ>n;À{f¿ûÕOª.«»º ‡Ìœ»agc»[ª*•J¥R©T¢YA?nÓ’3y™?0õ.&3\®Ämn¥_Ü_?n¯)Í~½k2Ez‹5—âžæÞÈæ"_ýÅÖp` {ÍGƒe*å©ô +,oÑX4½Ü-qpª?no–0È‹sÁܼd€yàT¢ý–KtÔ-Û~ì•Ò„–4¼¬k+þßæù%ìöFâ’4&†¸AŠ\aÆÔ'B3gÓË?rFÕL0¶L›h’…ضÖ¾µZe¦m,ùbC³?nLÁ¾HÛ¼û$˜-Ybk«üðáyX«%°† DÅ?0„®Å¸Ôïh\‡Á†y= ­?nŸ"Àƒ»ÐÜÌŸ½™{iéˆ9šŒê×0pÂþˆžû™„3÷ÞZ÷m3iÞ„pŠ"ÓPgUbò^B¿«Wçóƒ™là>g\ÙŠW6:®€ËzfW¶4IP\Óük<ô›­Ž[^t»e+ºÑ“Íbå+[ùê¾ÊWy倶Âõ¶rS¼öæn¿ÉбWA>¯­ã%xV£ÈäÁpꡃרSíóÎómr?r•H S úòÅkÅt“’!êWÄ„gÔc¯›œÞüi_Nù˜FÛ‘»ëÖç¨Î7´Lðš¡–4\Y 1/â¶{^m†Ù¹…¨d·1—©å… cGa=¶¨k óËó¸fY:›s‰ºÄUïY}ÆæíY¬í \bse-œÅéÊ/¬k€à¹V‚fÅÌ™¡eTAØ8ô|)?n6‚3\¤eF&Yoì‚@)7—]v´‘k$=h“.=ªü€“.¨T±Í³9¼ææ™l°Ý ®T7‰Jà¶³p²Ä¾¦ØÛüäon*†³u•šùw"–º y(ô,…Ó‰f}5õ¨4‰h{x£l°^¡Ixø;ë3<†Q8|û áHáOxßb‘²R´¸3ò¼99àÕfzPòyU¥¦öÚÓ?ròÝ?raaFèSùÕê|f ѤÌÙræG›Hd•Y“j0À—– ;š7ÉÜ‘ò—}®ŒàdžœÂådOh^Ù{ç{Žjºœ¾wŒÚ™Ö{Ç%Áˆÿq»±A[ÚA«Úäq›;M:ñó;ZèaR@¬øŸ#Dðƒ%Á;M d!@«Qõà½ú .­ÿà]’32c+M4Rˆ½cá$ê®)}ÅU"ýªG£tV6µ &Ý2É ú hÈäÞÂJîù$ÉSØŽ–—èHK’flN?r…°µèŠ?ruûÐaF JÕf­-W½G“¡]’Òª.¡‡zzQK²>ˆW𢽂—NÜ?nͬVc>ÿZÑØ×#îÆ•‚(lYeðX›_\è¤È SkÖj˜!ù”3!J °¯NÔ›ã?0õJ‡Wâ§±†N£$??@?0Dª,€º£)Þöý£®ªªþ ˜‡ÏÍtÂîã«E”?r³•Ü}|’¦H[¸ƒO‡åà(+hÅ£úûZ™å„~éÅm-耚½Of‰0޲”\Ríb,'‘ðâPçÒ3ã[…@RÍ2«ßªIN ?nö¬2ÙI±Å‘¦òŒbø~ÝoÛW¿¡1,²äK.?0{òJ稤;ÐçƒXgôP+êÊ“0¾lvܵp÷›#×AÐŒR?ng«#î·çÊ[ðR-Š?nlóè=CÓgÓã3™ŒŽp~‰aÿ6`©3T€Ú#ÒžË*P ÉB4)»mãlšß…WѾ¬ó›ó¡ØšüäÅ!C—ç/¼C&häо%ÒÇ켦‚­ªSÿ¼‰Ø#ô©N†FÑëáF’Ô‰*ý°hns-íIè.s°0ƒ¬ úå8|iµ„è‡*+{ jù­»´õªú# ¶´MPEc‡‰ý¡ sŠRbúÑÁ†F†ê0”ÕYîD”®?r´èLB„·öoßþ¾âß`>}:½3ÿ5yñ4??ïÜ™Þ}AæïÅóçøÜü~{Ãÿ´ÏŸoüÛæöÆóg[[ÛÛ[ÿ¶±¹õâûÍ‹6¾H$EÿªýÿïÑ«Éôn6ÄAæ«V´µ±ñ}ô—aïzÚ›Œ&Ññ¤??õâ臫ÎÌ~ýÏ+sþrdÖ©×/;kÿ½ÏR3ÌM8Ü0#}a™ŸWf"›Ó~tq½=85–Ø¥Q6)BçæÑeo]¤À?0§öMj§Ñ›ƒWûïNö±Ó’vŒ*°zg-I  $Ážôfg»³¹›G½…9nçž6ÂÈm˜Z7é…ôa>Ÿf;OŸ^™P‹ ¼7_ Š7âµ±ñoWR¦ƒX<þøß|¾ñb«4þŸmnn|ñÿmü»¿ƒþNäu~ûS´ÕÙzu°ñtã??ž¥°µµ³õ;æÙÌ,øqÈmÿvº–WßµÃq':é]GÇôþ*øŸch—ëÞ,µãpÍT"]°åã‹:'0™]=¥÷ÙÓƒ“WFØãf—“Ñ(½té0†×¸’ÇÿµHéu'èdx2Kqä-ÿN¹EèA‚V6ówþ03f‹9ߟ':éáh…w$Šòb{ûÙ‹üíd1¯}?r§ì2ïmš{â‘ñ°ŸlÆ +#µiF2³?níº-wãŸ*c(Óñ)ĦHß?r¾k‡ïì¼øjj4ãâÒP>&æ½e¢·Å^Á–N–6Ϙ¤îIrÌvÓÊ{^ýڥƵŽfbˆ†Ë76¨bqa»¯á!L1?nžä¹6PƇ‡ô9 «ܷ ¦P¨¤–8°Ï¯ÎvÎ…ˆq¹Y/™§3S´7Ÿ¸…Ñ—æ–]e퓌ÁK¸¢f­¨7î»ùò\ô€¡4xs7O3ªZ–Žªð Ë ð8@??ˆ˜10ÁK˜ÐŒa#Œ×Fæ'Xà^¦æÜrßcÜ?nÒ<[ÅîgN`ÀàGÁͦtu_ Ȳ¶KõOU•ý–o™|r%NKæ…kÊðôàP& ?n×ä±{YÔ¢#ŠÝèV¨ù?0õáòÒ,¦’E¥ÉgßY€KÒn>3Q–+Ò0º 0BQ®ØnîXÆ]?n.împxµ¨Ï«+•”JL€šW†‘ãWQc??«ˆÉÇ~ˆäÁù•›Þ¬Úa+ž<1Ô*±]«¶3>o‚¬>Þ9lÔF}91OåÝãâ‰|ÿåC٨ḟÞV5¢30¯ôªªýœ_”´øeDTð?n}c®Ô??Ū‚ß•ÙXMùùƒûNí??Ñ%è€d:KÃÛ¤7O`¶ˆ¦´=R[jx¸”~]ˆr¡=%뉺¢+Ÿ¸¾T«Êf=¼ÃžÒ¨”Q™zF–ÇQŠŠbfÆÒÙ*í[c÷ØðtˆIKÔ¼M²êšö_cŠ=]iIóÄ'm‚¦åÛ>z³Yï®™è®ÉìîÓÐì:TnüŸÞMsÃ0°™¸€AË–`=f¢æú¬SÖRVˆ¬wÁqVraV>œ`+G/m-"Éá†!=ë¯ÒæF{Y¾m‹{M¨_NùK‹³a´c@~gkŸk8:œV`/£ÏdôIrPÔƒùOjŽ‘ b…Õ‹{ÆjŒæÍâ.RR’„c)M?r°ñØž¶õ[¹Mn>¤ã¤??äV–¬ZGP„™)Èc ˜I+Ç›ÎlÄ¢¬z¶q^eMÚ?nE¼¶Ôa0B*ÒÚq+è-ÉPÑ„œŒ6±ò+KäÙŽÆmð.¬t?0£^$í– (&*ë21JsoŠúî=ÿãê«+¹Â¢ê2ÈZï,¨!÷,©™T<ú¢R_Xê0¨ÿ2úÁÓà“$‘Yahv7:‡%Ÿé>ç¦rxÕ0»ìÍú4\2ßQ MÿºƒIêËQÚ3]À~ÏÌú‹xjàð¾Z'¤“&otw·7·ÊÊ•äÅEç¨÷k‰£—ØPž¦6æê"1™kÕ!Gÿ*((+¸\?0|èÝ…ì8MÍp#þF´Ô£§ÈµµÆ^C©`žÉt 4{ƒ.U9ÛÎ A|Ò¦Í<ú¶??ÿÿûý??30¿âþß³g/¾—ûϾÚþÿ·ý??ÞûCÇÓÞßöfÀÞßcïû­Ñ–Þ$Ë¿e)VÙüËøÄÓå¯;.†(¦üûMÏæäÌì6!%|¥Wû»Ù=xWùâÍñþîÞ/U¯~ÜÝ{]õüÕá»w»??ŸîïÕ½>Þ??Ù??­zyðîèøð/æõIå[¤÷¬|q¨oW÷êèàh¿êùÉOïO÷®¬óóáû7{??¾9|õ׊·ÖÚCÔb5öMû¯ `gÍ&ÿ=:w®¡ÿ^²¡ÍD¶—¸Û6¶±mÇó®Ášq]ž¸¸}ÄðÙY2ñî’À:W»4}•nh­y–ms¹ÓÔŽMÈÉ`4¹¡Ÿïz”»h«p0*.òÛ3óMl?nš4«&*ø†ŠEëY#ZÇwðr–º”½Ë”Ýö`Ï_Ó»‹‰±%ÆsSp1·£{'þ¾É˜?nåëryUÌ0;Þ¦U’~26p³ÀŒ*Ü¥íÉ55ß®Äñ”q´`?0i¶ÜW¦m 1€Àôv:ÿÊô¡»˜ñ-©Ü»è‘Æ4—??½ysðŽK¨r C9|Ê`ô>Óá0µLÓƒóÓû£èWðþñqñÔgKâ’.=a¹—¤Üº@ÇQAÛIr5,× :5-OSãµnb²4žœîFgCœ x"ýINÿ.•±WØ#­}rSü™òOvÀúmÐ?nÖâÜFÓ@Ë£•3£e  £aìl•KÜP ö<Š=\€‘«üYî&ô«jÜðB?nÀ{—`w¥wæF‡3ƒðdÍTÖD-Ã/dáŸüSq”³“Òtšwª¾¹of¶›¶í-î£IÏÛC)u“u” ŽºèlœW´®GñžñV‰þæÑѳ¢­ÄŸ>:þâdäÙ­/:f *@¨‹•è%]ÈF¿ì½}® (CNL–"«UZB%#Š·HnG“É”Åö™•[ø¶Q„.Nc9nG—HºúY2Í"™ ãœS‘¡W½ƒÉè±vÔÀ›FËNͶÊau”¼äS+ˆ¶8o¨$ Ø?0@aáøœ_„jvÝ'\~ìñåðMáìÅ*¦Šåº}ÂX…§,òeÅcØ-åg½~F§);¯Æ“YšŒ&W‰ÍôQXþwL õøŸ­ÏˆŒU„Ä»í®RŠÎH°ü8¢0žä­óI?0ô"(<Á?rtÃ×´,j t(“8âš^ˆ­(æwnÏÐ÷x¸u CLÓtfO1XÜ?rå­~ÉÆÝ¨¼š'…¶!#äí¡1\ýt„neÔ<–±ÖÃŽÝâÌobŒîéŒäÎk?nN.ìe|”w‡ Ny/F¸¯é»¨Ñi˜E‰ÿZôFî.†t.rJð´Z‚{‘éñhâ{1§¦v ãoK¬Wap©¸%hÒVPÄO@_Ïv\Æl†×zÈ6°?0ˆ®j2,yeHüÃzõæÑú¿ß¾ÚfQ4·ƒÓB×Óžê1°XÆð® rBtMN¬HüIëÌ×"çTÊOÆ0ªÇÍ>†g¢PǶbR1–äAÿ\W‚ÅT¤ÉˆþAïz8ºëºgÝ×ÉÁ;ø?01äOŒ{1995Ž×·å}FW;1ƒ ¡ $Ý€Ëë÷á÷KB4t¬écO8ü7<3É^Z_ê˜hIr¿H'ûªÛ×w%Bf)ŒÈ>«+95h¾M¦s·Ç]ò&A·˜®ª/’ï¿??1Þò½cYÈÇwµÄ'ÁW“jý×h³ˆ£¥½‹\!K˜Qµ¨%¥°S½N"¸à•/ªsž¸=ë;ë`:Àª>Ïc(|€0Fáv±1xض˔øœ&:LI??“ÙI&Ò * MZö•ÚBA!2Íð4˾:çË×’7t¨O•5ëãfŽÌçdQšÉGßôöXÚ¼¿Óö·6Šâ6™9Ì]²gü—èœr‡ñ{n¸d.Q[å“‘»?nˆA¤ž`2‡x%œ©d‰ LïP~»´uáõ¸ä{µ?r[(÷ Õ9ꔯ\È&íÔ›âòL<ólT¯³ÅN¶™Ñ:nÉ@ÝæÐ<Æøš|Ü1·’¯2Æ„x*ã¶dPÉçrBî1¯ŽK"C°Cw˜ë‘‰:&½3˜Á|ܪÖ¤ñÜ,qÙñ«Wr¢ÞJ­ãÈ Ù>YØV²S‘lÙ-2,]Ÿ2älªÏ(ò½\CŠUu·¾ÎQ­Áª Î‘ôþúšvÙW*o«%_??˜I¢wU0Lï°ÞÇ*ÞùðbSz'ZÏþ1¶k3ãË+µ?níµE°dÈÇx=2xApü){J¦Ú ~f,#ºp…€H"·.‹B$WѼn“’-g¤?rfmÙ\\UK÷pØl,Ë¡MÚd- K¾0.ÉiT…[çfwõð¸U¶WdžsiN”¢4?0£–Ch¼îòÒ‡zõ±™P1Q7Í*ÅB`41eˆ`Ù­ØÿúTŸ?ntÕU4”Û1ŽŒ>iGŸÌ¿Ð(Ö½mr³ŽÍg½ËôÂÄ®³¶È…ÿì`ß•WöÉeåø‡¢0ôÌò¼o·ZœOݺ•6¬[‰}H%W1kÌšYÁ¦F² ¸é ¢/¯Wn³7æ'ÜÞÝ„×,3ßZ…9’I.²­Õ.b³ýk4˜#¢_ëäÎï)Ù>Ó$”ƒò u¨ó‰ä»ÈJlÜŽØË_­Èƒ1 °ÔF¡Pmé°4‚ƒSùÐÕk«ioÈ£›J,Þ kE™éÒ~ó¨#ðó{ϱ%<xè[8zãsˆAŒÐp¶¢tØêãbgÌ—³+'?? Ìgî??…$L)R†Tâ´§r$MÙÙ¨xèÂÕÛeŒg;6g͹B’¨â˜Yí\j€o­òìÜ2tBÄ'¬¥,£Å+·©ã›y17ŠäV(y¹„½K¢[Ïi„µª+ òø"V[˶êεÀèHYM·þ‚© F{çìJ>|¤!ºi*±ó$?rzÃhf^äʉfЯòùbÇHú‹)xÊŒT‹kj×Yu!Ÿ‘èàKu‡Æble¡o›­Ï´Û®,~œºc??;ÈÕ…\‘n={Lmdæ÷QwK– ©¬Íd ò¢‚âÙ?0¶@?r§äå”<»„€¢éPƒå¯Î,DÛP› Rd½”FiZ% ;Tö’­Ùk=Q«UBCü•`Uq "¨¨r|ÁzF3Q¿q8Ýñ¤w¡ ÃËè"ýÐû44"ãç>ë4?nõ=ÛB×v>??sö}×vã²$ë]}ìÈ ebÿ¦¸1J5ƒøÉfáÅ$#É£šöƒ%a„8‡)ôÌÒ¦­¢u*íZݶEáÕ"•ï}¹îéîÝù|6¼XÌåV‘œ]Š›ù€Ê<ö¹—?n?06؉V ~‚ u´Üê/h{nYÜú÷kŠ–WF˜×rþ¦¨Ìþú¸ù_7Ÿm??/çÞ~±õ¯}þó[þgJÿ¼¹Eþ!<°Ÿt)ÉøÊÅ߯9'«N$Á4¯î£¶–¶E‰8úV$ðVPˆµ5Vë¸%?nËû¨Ë_ׄõÍ4rD+Kb{ã»f#¿yªùˆ¹ÎLF¸X'I°Àqð²`á s6Ià îd úµ1c¼†ø¹¥LW>ã’ûXgTÎÂ¥ø0fÊàüG\î©Ú2ˆ%+??ñû˜${j¤i0ñ–&yÎ3¤vÒò‹àŠßåÏ.óÚc*`¸©©ß,‚*¤‰Zš§)HéÍî^ág …Ì3)¬ 6˜ÍÇ??ÞT˜Î¥ ¶lÓMXŸ~–¼1£¹]¤©œd´5±·ìQíÚ²#?r€áÞ®«À÷ލÚÁŒ2w‰5â$‰õš4½ûWSák›¯«b?nÜ‹VësRÔ-`„æ‚­¥9§RYêÁÔ¡`´>á;ƒÏH§Ñ“g è²Åî2ýäÂÅ£®h!¿Óë÷'—ÕÕéUÕò¢DÜCJÄ=ÿOU-í???rg“1–mî(=k3¡w›~GË ?0ñ¶ÅÖ$ˆÛÏèV_ø78­D–Ðß_€ªƒw§Ç+åͫф"j÷Õ«ý“vNâÑþñÛÖj4úóã—áÛþßN¨7‡ž;nã4aw@Ïfo]o¼:²/»©„(0*œ3Œn¿g©i2Ì8$™/Æ8×U繘M>¦c{Fxì–eCS®YQ±ÍÐåéNÔPô‹N²Å|8zjäi‘fO7_lÿG‚gó,uÝçÚrÆ1[ѱw“b–ûÓªü¤ËÝÛŽìƒÉ,1všÿØîô?rï>t7‹wP•Ê,3¸féè(iedõ·›K¸-醌óˆÃZÞìLŒ&ÆøÌÎÞ&ŧž=Aƒ¢2‘aó?0)g)‡ˆIÛQL!¢ÊCì˜Ö¹E»>êñæAËÂXÖ–‰CcLà¨ë7)7‹+‰.ºWÚ>]’x1¬t¬4ÿ üôð„3m<??§7ÙüÎŒ`gŽy Ó$KöÙ"Æxù€¶(¯:xEÖÒyÃûlålºÀã†FÖyMn¢¢ÌOd:ÃeÁó;Iœü³ÍëÚ.q—t[3¼í:î$”WÆ?n‚¢ã»JaûzRJŠI˜ÙêAÉ¢¡–HÌGósJ®kv˜³š–::$†òa‡Õ¹xf<šôl“&8D(T·ã5E¬H%î[?nþüÂIaú ¾ÒkÜï1ý–ÿÏݪfÂÚgû€Ýÿ»µ±Uöÿ>ÛÞüæÿÅß7ÿ¯î?0æëü0ÚÁˆÜkØ®Õ/ÈÑYù’ý¸{Ð:ùÀw¼·¸¾¾ÛåabÂ)×Î×8²lùÂiƲï?r-ò˦F‹^öPàµ,­¬À5J¸k®è·ñðX½à˜z#Ík¼õ¹|Þë®oÞž6d¨[e —1¨f‰?rÖ¢´{þ)i¼lÊ·ùÞq,¯’n&³þòɇÉ5îIöŠ*»q:ò£U¯³+œŽ»7øˆ²…½í°uÅbÿµλ¿L&ý‹»4Y¿” àÈ̵#'¸ußeT(­Ï¢Þ¿s!T6`ŒA(·&äm&Y+££×Q7ÿÖé§(fÐΊäIÖÁÍ»afÊæÀïmÂx.~€£ÓÒÄl¦AhA`MÐØL£GøÂÈJÇš/mÊ9Û¶ýËåq3>„ª¬<½éÇ;6Þ+——r83ðš"„¾ôÌK|”ÞLìˆwŒ—Þ°„bþ]Ɯ˘W??½bÿ¬=g9Σ.^H=³Z¹»ž,èJfn›ô™„¸¬:<(˜˜áÆæG\·|¹_ÅY"§XºúV.áb6˜?rûN• ·—Øv}™Ÿ‹Yf®qã7‰;XÔôѸ??¼,A0~ä™±qZ]ây1ˆ–?0Šc®ª2ßN† ZÀ=ä·Ô§À©‘{š×0“r<玲U< ‚£®#àHº(¶Ç [ë>Ô¨¥ÙeiМvÂ¨Ž¨³|lŸŸq= ³’@±HÂ{??ì#·W˜Š…pòŒE±Î3!<÷`j-Nìtf¤Ó,Ó§*Ët ½‘\,ç£Ï‹D=;AZdÆŠ™Ü€?nÙÕ†ˆ–K*þ PaÊ)½•~nê3??¹r0AÜužø‡©)Zjô^¼ÔX0ÓY6CBP¬V.Š?0]Ë…êKPR¾êj“h<¬yÜMCÑÔäJ-L#c±&L&Ç>€?rUv£ˆe7ýf•ºÃ¯b–;Óç [WEÚ&%MuC•‰¸®§¬ïðO;’6g”ò­dš£S‚‘1Ÿ>ãºÀ^ù~2c¦ö‡hñT«côœœ)]5Q¾õ€F­­$'<ôm׬6îulû})$ P`"ëQG$ªÁ˜ÙZ=/íMñŽD%D^??y*IZZ•*Ä£Ësý¹QÁSô9ŠŠùcZ8+î­e)…š·Œ¥ºAÛ’d²Ø(ZU\53oex4Z$(¬ ¤&¶èÔaQù›o›EˆZ¸Ó瓈‰¶m¡¥°ø#g.…ꯉ/:Þ»n©/IlpÁP O7¬Ó™eÆÏ>mÚÝi«“MGÃyþkYv §çÔ²BïlžíØ8£éV 9•?r N±n¶—/So-–¦•-D%æh-Ö)Ô¢ãB4Iq¹oŒRçå¶åôjDÑ¢ODràÞo~òYÿ??¯™½KâëùÿŸm~ÿý áÿßúvÿÏ¿ºÿ_:û‘®3ÿndtŠr…+¼èc8öQú(|?rÚzY”ÐH>°eØ¢êF2PP…Àyga6õšUo¼‹º¨@)4º‰ñí‹ø5mK\»fÊìá|Îë“øå¯Íöð‡,¿^ǹ¹ÅÿOoL®ø-óëuzA¿ž™_o{3úõ|Ç€œæ¿¶í»;úõP9”ïí¯ýúê-®èט_'é”~mn˜Ÿ‡—óü'¨y7ù”ÿ9{é¥ùÉWáÚaãmštmÆ®Ÿ¡??¦ðÙ5í{/Au6™!+‚+Î ë[JžÙ|f †àQ׫e~˜¶‚+Páõµi«FÎñË¥’LèÛO¾´È½§º” ³ÉdÞŽ.¯ùèRÁ×Ã!õ‰­Éû#ke§¯;‹fü4n•^¡¢y‡â?nóÞûEñŽyÄ · l;ÔuŸ‡¶¡6Ž)K`?0GÓæéL’ä5@ÉAXrÐU¨æÞRƒžp?nêWGIícŒŒÂX ðø84ø…÷Á¢Bå}ÄôÈ·»Ù™¹›‡UÉ%¾pQ™ìH¯G(×4oÚ¾l]1,Æ­T°&hüã?r }Ú¡¥IïÛQãi?0?nÁdÓ³-»œ‰Ÿ>+@œmLVE¶LC0Ê€L¬¡?n½·5Ⱦ|çù¤?nîZ†B~áÉ"öU*àêš$qܹ÷õne%0TA©‰‚SoÓVËcc¶e?0çlÌÆÅL01»g?0dÁòŸ­"þ¢Í®zÉnÊà½Db­§òåÐpR@ƒ£(úXí{"ÃýÆè§þÁàM4O#jªŒ&ŸfxêÊÓijö‹Jc àGåðà7êñÀĺR£½©Ô4Ü)ƒl Ó„ †˜ˆ—g‘sË?0!34‹{ÛÈSN°Í)e"Øáá…«1£T™&‹­„Ç»Áú#,§|¼f‹¹¤¹Lj¼Mé+ìØK¡ÊÝøæ".QÊ??–&ÙÏ”#„ÞéöÅ®¡7ׂt{ž¾ú5ù—ðQ…ŠNh06ü˜ÄyFL–çé-„)¾ˆÉÉkyÂïyI×9}{”¼Ýý;Òœ³ß˜ÛjžrAîËûœñ–ëm‹®‹–‚JNS?rú®oûÔ«¥ž÷»Á?n?0ñÙ“ÄËKý(ÿÌaÚ d±Ù’[<Ù©‚?ny©Ñ´I?0L$ÓÝ??>o†zÄ8m÷õ‘Ï®‰Ç0žŠpœÇ@G>Èy®Òf—f,eó`œ¨À(m}¥°ìŸ<"„Iðž±Gh¯ÎžÒø~°IÖŽb‹Z;(Ÿß[Ó"skWàÄDÄå$2£ê¶Häó%?rK¼Æ„j‚°??8Ž???nšò*Yôûá‚"Âè¨-Ë’ “Ç#*)²ç…{¸AÒãã‰ûq'”àï©#/ÿcãÎÑHì×Džðülð??2þ˜™]ôc£'4>öá?0îôB?n\‘nÍn&wÉb˜O 曦¤[×§ÑÓ?0¨Îô&á…„êí•Ð@‹¯é— “›q:k0k°Ç ³æj6YLýŠ¡_­Â o®f?0P«ÙçóæJçJ¾`Ž-ÜðÔ4N“÷æ ,>ªŒ 'vgˆŠ œHYÞE*³º)ö??ó?rµûdÇù.àªÒ:†?n&\©(*‹k‹bˆ??Bé‘g< 7;b’’«ëye•ÉÛªqÜ1êÚ¯+h4åòQÁžÈ.Ô99ø{òöðÝéO'ˆ<ùÓ†¹¨xë¹ùçņý‡Ëåóµ)å/[}‹Â%¶kq•ñä&Çko1$¬ ¸Ë\b*‹òYzuÉå*ûѸe«îõ{×Q{)þ¥ÑŽænžf92ô5ý&œiå1® ϹÈ??æa—?nûÕݹóé†aÜÒª>œ}êõ•´ê&ƒp[•Gç)ÛC»¼±Vêi”õ¬®rƒÿå»x_º™ÍÍš!Á×"­cˆ7Šºög…¨R9¦LÖß,¼‚5Ä@ñ£ðvA+¦1Ç·"WT\jv[þª\šÃ”g?rAåìãV‘Qƒëy6·—;¬÷£hý—ص÷&zy•¢—¾.xÙòë??í¬¿u‰azš†½´Ér«#ø¼-÷³k§^®ñì¼]YÜ–0V ¹Æ´¾–¬Q%¥^|¤Ô’“­ß¬qœá?r­EÑÍH«{Òý¯èÅæóçv—ã·s’¼~óî¯e¡§ÊˆÑäùAÚL²ïUUÆ_qpôä¥ùç;/5I°6Q´¯¢UbÓžS>3ÿò§üüÿýcö±ìjV>íjM!ž“2¯ÄcV,íZ!”oÀp ©Èå»a:êÛ&çi~‘ ME¬%lÆëQv[g3c¢‰šÈî³ ÿ‡›^hª; +#!ç)ò½œø– ò6wýǽÙM|Þ*ÔÇpp™Ž®§¼mÆ71ÇmæIO<xO?níäV|×5ðcrÜ_Vª¯3]ÖÉ>LnòÛŠc|¢¾…bÔG|Ö•¡Ù5ÆgM(»áà¥Ü·ºrîzf”³ß4x¶”‘ºÛ~ÔÄôÍåÌ÷ºbW^±«úbæý-1¾ÉrA7V;¶GAýþmño¶øCŒñß­-ný’!FÈÞÁ±0BP·’BÖ'u³??³8âN,ʉ¡qæôÒ¹Õo\“ZTÀ~(ð©ØÎ½u • ¿Y†y‡D¿J×»ˆ!%`j ¨„E免}XUÝ^ÇR>ÿEù-Äá¯G<ÿõlãù÷ÏÊ÷|ÿüÙ·ó_øûvþ ¦7¢øŒ|Væ‚»Mø…9aet<963‚=¹¦_tß•¼CÄNÄü=¿%Q¤š³‡®8Å#÷–¯­}ÙaÅm‡F™žœ¼ÉA™¯µ P¬Èåd4JmfÙ,t8ë§³´¿7¼œ×ôʸäUóü¼Z%IL—'ÉÃN±‰ 0* ]<<¥¡þŒ“s•/éÂzçeʤ÷•ikÔÒ~ªÀÚ‚åä^TÐ;©Ë½ù­¾Ti5L‡f°O¦y\÷îð$Ù;8yuøîÝþ«Óý=µàñþéñ/•%víÝ¿5xvqÇÛ+£*ß¾²—‡ÕÕ=NP×7C1c´BþØÞý*Co˜âÅtç%ÙîXC+Næß—IЩ¿1°^'??þrº£yf  ÌâV~FѶ×À¢‹8´bO)' Q'µž Y‚óÚ‘ÃCk³Xù‘xöÍÍ$$úsÓL%a·¯Vêb=²lþê“\á1,ÒkÜœ®d¾é•él2Ÿ$ÆüËøØk¼ûãáq¼cõ\³à¥¦x~ UøññuvåÒÔµ£éhÚOîÆóÞíNQ³wAóÉ8Ã]ÅØ‚kµ Ï›7‡?n~Äøøð–žÑtôÃÉÑKç2ÄÊ“éŸm2°K¸€33¸zW©¡¨]$éèh¿š¤¸¯D ;ŠÀú'VÒ›n‰`ïKŽæ¼‘$½Ú{TCRª“ÄSG@GÍ« pO{3£Â½|~‚ŽŸ÷>› ¯C%ptÎ?0¹eÌyÔÄbä*n&3\¾¡µ·ÿ¦®Çú«õ ‹ë§£tžV÷ÔþÑñé#É3@;Z~µ#ö×áôWŒå_£¦1 ÙËæçHíeã$i'[4­û?0šúÏ’ö´a_ãÜ# š©–²×û»ÁL#ip?0šØ$ÁÐÆéM4H{F=¦l¤©u6’~Ús´?nI·?0›¸e”®áœ3¶Œ ãÍÁI?rgâÑj#‰¬“ωEÖfd¼Ý;}«±‚Pr‘ ;Qz†ëU%ƒÒš4’®×oëØsúYt]VL%ð¸¡Gg"¦˜^L'ºã"ªfp‰V©AoNö§¿Yö·¢TM…ÓG#¥ z4?0†cT?06£a1·o%]‡{û¤aÚõ,ÔTn0déœfg²aì[I×_ëzîzµ!b?0ûâæîJǽ{´Žd)Cö»»oÆ.Í)é:<¡%<[m:?0d±$™¥ÆÑ‘ íÙ§’œ·{³@2€£Âº?r´àh­"—Çï^×ñf°5€\Á÷Iz5$i:=|š?0¹–¦~ŠØ-góTvrpº¿Ê ÖHlGZ6œ§OhZª-½4N3û¾v²²/s™ÄäZÉ'ÌÖª¦îÕOoëäþíj½º„ïŒ0„ØmÞ¶+hû÷g5ÈBÖ®HUÙXbIÌé®j-®Ð?0홋VÀ­Í13%6,‹Î–«Îó?nÚëÅÍjŒdÁ(8=?r*hyÿù´hldbqÈ'ÄÝwÙ‹(0%*h;~ÿH3@ÓÊ÷nê/‰H¤æ³Å%|)’¦_NÏø?0p'ز7‡ä¶u@¦$æô—£ÇZ3´cÐÙnôkt@F5¯+éy²üXJ°-A0ìiNf;¿R|þþêѼË?0mLè‹l—韣@??óßÍÑ ÈÂÓìHÊ[!íÑÖù²°¥<ÂÔe??{ÄÅ`ûÄxëP$iy4{Ó@V˜¤˜žH‘ªç7¡Ûœ–»ag¾qžï^•—æWf£š¸ÖT0ê·cÄhb›Þ®œ“<Žʺ2fè„Z•ÑÞŸOzí\)¢ˆ8¨JÅ%2¥­à¼·P&÷×K&ã$.;­Ù‡tßΠ‡«âÆó­Íè¨uB­ýÈÐÑi”»!jB˾?r³*›¥ßl*íV0ìR)–%â>¸Õ¡=¦nS»§É'üa/gCœ÷Õâd6Xðåö[Û¿ÙÀ,µC%Z†ãpvmQé ñà,/fºœ“'‰ü†º+©ÖoD«ª ëšS¸Ëæ÷ãn¢Û›I‘`ñ`SˆzøEmó?r?r© tË1< =]ÿ­tFCž?nÖ©‚QhóŠ<«¢ÖpÇÂáþlT¼C\Gf[¼£€rºíPN·\ tÓÏòŒål=;—x¸® Uy [Oâ¹Æ®&FßÎCdŠkPé~¾ê?rÉí[<-“ûR¨E¤€ãKZk×B‰R5ª9¿†_€ÅF~xï;—Ø“Äü"Š?0Å#À†¥¥%ŸÜ;89|“Àí¶êyáp(ñðØ'?n°`2oTÌ0…¤½œ¢5;ý*L®tiŽQž:ÿÆæhu„냬¨™RMMWíê1ÔŽ ¼¶ö(ü´´¼Ò ®P3ê76Ÿ¨ËèrÅ*?n³¸¬Õp=^eG1wTI¨žPtÑ0ˆ­h|Ž¥æ[*Õ’åŠôŒ¨@æË O•CÄëë|uC½ÚÛyO0uáœ0÷·%÷ñ÷Hfqø¼$—ນ+í+G« O™ô~‡&µ.ÑÒÄ%L\ýKͨú¬Êö½±ì"mË'æ5ßm÷.až\,ƒt–ç|±½ýìEþÖ¯|]·<€H‰¥Áлɨ'OÌ]²e3œn¯»L¡…»d^æáe‰ñæÛ9¸¦ØdžØÃÏ8ï?rölÔ½'l}Qm‘ô=îÚÄÎŽt]o%îçìPJ’k¦fƒdVÍÄ:´/Ë3ïÄqrY‹çù_I:r Ø$Zâ•°!”§ì??©[Xø;’´òÔöÏîC%Ç,áö£bZ?nr)§ÏžÚÞÁ»¿í¾Ù q4Š%È*•jQNÓ:±Œ~õµtj6‚î-Mzn–Ng$Mr-ÿ°ž™=šÖ +#K±;CH¬šNó?rkÑ$i«†Ö1£½À‰3æ” ä%x‡ Ü™H&û„œÄÂ#]]øuXäåÊSt!ëi?nVÀ®™gW(ÔŽø:ž4Z*œ Ž1´ŽƒÅ¢&3¼zÉ™z›äê‚b9m†O,ãa¬Ó.“©qÇLYž€?r4)Ju‹\.ÍÄ?rŒ„ñÏvž îIpN]?r–M ‰ã‹@Ó??Æq;ºÀEîÈ?nŒËÜ=½—ޱkA‹eH¼ãÌ&ëœû’öÁ± ] 7¸JÅr´ú}ƒ@´ã^,Ä~É [ò?n…/Üe/iuéa¸zýÎä“ÙJ•O‘Ô%§¤;óÐXŒ9M”ÅÔhÕ9jІ0:ÖH0±G¦®˜ÚDI2Y9’ëü5Eté¨7Í *,0‘$z"¼# –Í#¾0\®à|mD5Zv3ÊSJlaˆƒA¶¥ë™QåÂkº$%òè©¿!iæ‹in¥y,ŽŽe”y’AçœÙæò÷’,hléY6§- ÌqòÊu'ÒºQ.Á’ ¼]¢Ÿ¼3†Gªã-’ÉÓ**˜ºê¢ÙèøéCˆ#_âÔ(á™ll‹ì„âïVea„’%µS8ƒduKlÆ`O>ÀŒ³1+ÁONú…ç]˜ÓËäµúÖŒS©»ödÃkÔ'ãÖÙ=UÁ¤öK÷ÂÓ ö/×ӓ¿‹"2Çe=Ñ?0Ña•Î¥í“uõ…˜+åIA??”À/g£D]ÿ™ÂB(]{<ñòn“0³öûå;»$ \ª#¬ ’¶Êåu¦º•¯˜ãÚ$û²´UÂP*·QXr\É?rë½btˆ-À[,"D@‰?r S}Ë òå¶œËzIÊ|õ(±øàÀÖŽvË­áà}h¨q€ããÞÈ=Q&úâEd®fYúµzÖæí HöÕK‘Í_7ŽTiÈN ÞÞFœ>TÁ볬Z¿ÆL´žUûtKÈ¢hUo™>>¥š6Y#Ò&P˜3Çk=À á5Lƒ(Ä)Σç>Õ$•ŽÖû‘Ãì¡cm#ÿÖ—´ª‡t*Ñ0ßx±ünRcŒêäöKÅoÔlâzh©ˆ²¿%c8ü–¶ªRMÍëñðð¥e%åÊ‘dÞSÛøŠ‘)Á,¬¾›¤bEi5o¯^ZäÜkýV;zÚ•kŽêgJwójrømèÊTW ÛõômuIÖHD‚Ðq²–Ǧ®Ïê’vôw5=%ëµVUÁúL£õÖdlw9x±ÒÌY~5?0:%²uõ†?rÇyg??f"¥¹JÔ-«3º Å¿½-a=û.½9™ßR¦¶¼Ð‘¯×ž\à´_;ò¡h¿ý±7»ÊÌÇ??Þà[Ù¥Ž[€&j´:  \•cîN??Ì&ó¹Ñˆ^ð$µñkqäÆ.÷FC3º8DË.6‹Ïjb¼k"ܨáqxÔX¯²pÉ}]1No‰ á5¶'Lä»l”¦SìVU™Í-÷”ã5²Õ5Æ)3‘Þ‹¬Õ—è¥^Ÿþêj??íF[•ôx=¨$Ã+_êô?rZ¦I§»k ¥¹â„R1øµåÛàx)oHtŠ%õÐ%ý {×Ú‘GWcÏ?0[A4_“­jN.L[n†}³a·t“µËZ•Ûj!þñâÚjdt[Øwíëò§Ão ?0‚漨×ÓÎâ[ŽggVĬښTF??[$‹ù뇊[Mç$–éíèºw›dÓ4Õ7#Y»ÁʵE˼r/» š ×õ%wòäFDÛRM(Òõ[4K?nù ªµ¢??JUÄ•^Š­xac[¨ÙZÑr!¥ÜRÕm”¼%Õ`Ÿç­¦­žuíID@Ö'QDŠt]A*xÖSvi;ï=&Uµ¶Å?rIú.ÚTVÔA-%UÃËT¶{àÔ;"· MeÃæ!g à]t€6éf6|_¸mm[¿Ô¶é,ýäFl’Tˆ˜w±³}äïæÿAîæ¯¾Ÿ¥;ð+ŤˆyË·ÞlÖ»+ofجˆ%* Á#ûÔÏ}s.1 ªsSÝf?n&r/9hm<®[¶=Ù-±Ss†Z&óØ9z”®]õ‰?0ãIJgó¦©Ñ΋2²iêC]+Ã}î³°ùú ß¼¿waãWy³#Ÿ°m-}KKHúJûyD¾¬V´|Žá««mÓþÁI°'³{Tô7dqKI‚@ÊÚ4u¹Hx·ñžøÙ9??Ãü‹›î•u}f l©×túóÛÕ198?0L~ånæÉ|2µ¬’;r„¿/âØÝeïPúsEÀ¹4¾Ð­/^eKóÝ,7ƒ)¹,ÖrŸ¿ã”©ôÒ;… ØLŒÊ—á&ôxár\ð¯üô~Ô¦»€QFwÏÝÚ?n??ò6v·Äe÷iï“Ñ,caöU?09ÛñÓSâ’XÙ¸æ_â asžý-â£ÑîÇbØçïWæûy]KÇó8?0?r7Ôæ7FCCâ?nÝI:óK¾ÕÏÚ2&=Cî0{& ãjƒ¢h}FA|œ)øPî<•+ò'ÏaŒaà5/Xº6jûIhò¶ÐÞ2¿Î4Mg”Oigë<¨ë$oîc¶ÑÞÜ÷«O]ëG©ý“ˬœµKMêÀw‡§8QÔ.thKm?r„ îl"ßš?rÃÂü’A\üƒôÂMjkë~QÈÇO`¥ÃÃÞ½1©ÛÛÑfë+Ê‚º<eÏמ¾ù©,£ÂÙbìUæ‰g»2KÂÙ¢³&œ=r èƒE²mõÓþÿ£RçÁÞ/¿§™…áÓÅ›Î`Âk6ü¹ÞËÊ þ{^nážÎúpXìáb VáåYŒYþŠB/½ÅBïý }÷ixFévOò¹}>6SÈhÈ@ŠË’ªŠx‡šÅ’ÞþeÔE`YHÜýåHܶLõ)G7’lXÿR4K6í 0Êö1:Žê©??ôiÖ÷Q«[ˆ<d`ö±p5Aˆz½’ùO¸ôVK¶P›©|@®A*½²’?r!zb£‚¨†?rI;îêøæräøOc³2’ÝÈ?n9§Îâõ¬»>³):Ìà§ükôÕ qx†fYlŸ /,Þ~f>š 4‹º=Ñ?nÈìá´Ø½i'cΩ†E–¶aé>§úÓ[Î×eWUßo×Ì™œYmkkƒn½0í)Ô6l?nŠØ?0z²žÙí?rX5¨Å2NÚxâÀ#õ¼ð#ÿt21®³ñÈŸu¢ãÒ^¦œ*“Yo†Ûmc^??uŠÝ¢dj=Áɧ+Ðír£ã:‹ +Áƒ#™8O£W’«+  9€`“Wm>›ŒôÃL[x*E•ÜúC6•'`ˆ6;Ëi±²r@&¦vær2™ò.~Ït‹ÝñƒHô£ÖIq‹ÀÖÐ\Aý­‹<`mkãùŸŠ¦€ò’+uº¤ÅÛÑ+ºÆx>™˜®5>ò²¹ê4w³²ŸÂÃL¼³Sì#û¸C%ãÅ|𧘔xÑAíõÐ?0ñÈþš\?nÿãeß7ÓÂiYlkGÝå†M‰-JÖFªi+ðH,úÞ;N졈¼~‘yY»Û›‘Ëçé`ç÷Vw°b]ÉËÜ™~ÐÜɦ£á¼G1RBÓÂ:ø·kRÁ3»©×B‚Å€ïtTøàø›ƒ“ÓX@õFº—L{Žš'=Ðòddÿí¹Qé?n¥-ÿyyÓ;eÎU Z6…MiúÕhrao­¾î]?r/›UcVL[1u.`Ax9e‘w9Ã=sÖ¶œ³‚ç§m9„Õøüä«Úë¹G1Yrp??pœßLj§ˆÏ˜.§Çp 46ÖÛÔ?rÃSló„t›¬! ä>ÜÿÀÚÅ??÷tìÛ×oOإߺ.%óõ{•wé“,~IøJŠ`MÉ…Bön[¬¥â”‹~–m„®¥Šü¬Ï¢édˆ<{s#,p ðûòƒ19áÁˆý4ÊWPµ€¾3àš´›EY4›ÚûÙ„º#ÁÓ…nngD^’ü¬¦ªuËJÔU­—²¥ƒ§ÂÉS0nvÔ>\ÆÍÚIàš…?rµ¶EA+p­ÃÞéx²¸úq@iÖ å1ö£3X·iùx®ïz«öL Š<¬ûã8Ÿƒ¤18fñ:5½Õ—ûš0“Eº–WøÚ]«„-C®.#û´JŠ$ÃCÄ'8EÙ³gç­ª{‰«Ê’«@ïæÌú5€µma鉆çš’Úç7Ϭó›&3y`.ªœ}XÌû“Þ´ó)Ñ­îbëG5DqÑ!Ëfdi‰^†¯êp乨wÆ¡j>TÐA2XÙ”âÇI”ÕÎ6¾…k‚Ó’¢Œ$諦‹‘¸ü¨vÒõïÃ)Î*HвŒ…ñ)máÕàd:§©—þBDÆëAýJ¨íÃ7‘›¢õ:ûø"I|È)Ô6 0ÖÐp)±P«Uô?r/_ß²lå Tcë%^"WG,?nKÌzÌþ’Q“±vI'£N +iÏpE‘[÷§/¥Zí(??4¢×7Ê$ç”ñŽüêŽ\7UªØ<´f0^®./œ¬ê¨ßLܿƵ®kŸ;]câo›×y‚??:NûQúŒ¥lÑ?rÎì.çÙ ‹Æ»Ž_C’‚^K¹ˆŽÉÅ<ቒo*?0Œ¼RE?0?r²÷‚ÊÈ”ÓÓxê Ë‘ñÞVÅ-°¢Œ ‹çó»^ “GyY‚8ÛfémI­£¯-ª°U̓â\ÈÅ÷#Èý5mÅßYиv©—rwP©´‘/¬È_uú)ÝBºÞf‘RǾ®ÉK<޾£Œ¹àÈS"â'/Û-ŽÖ©æ#$^ÂRƒ¨Ë,QbsˆÂWUÅ8ª]ºÑ £Ë‹¤S°J?0Ñÿn„V?0U­?núÙÜÚŽÊ©¤{#{ÌÜøé,óÀæ÷p•|y­Gšü¾M~¾øí'KɶP9ÛÞ°¹~ EóEM>öî:&KNÇÏ'VÔÊÞWÊ™n³5CУ¡'{—6y–>麸z\?n²ävåÕ/­U|c¢¤°çó%—8AãQ+ãb0÷rÕ?nIX.ìxI÷Û'#øú‡ý¥§ŸdWW#Þ”µ'½ŒÛFåKÌÅØžÀm ±ù3¼ž¦³l26½Â`ýM ÷E„¯ÌÍÉ‘{?rJBoIUòœ&L ‰ó­§Ñ„fñÚ?0þjvpâÞ& 'ëÔ/ ö\“„+8ÈÍÆ2ÓU°ˆ}Ë,I•nwé6Óg’ýpÒþgÚ¿ ÷Ðt =„±mÓŽòÛ$I>®…ËÅÜœÖø¡ÂppÄÕƒêÓfT–Œ†Ù¡âK\æ\ì~£½ü}üîõqñ÷é¡ÿûí_÷?n¯ß~¾ú¹ðóï\š~sqú]*lŽû¿9D§P?nñôû¼À\´‘ØÊ›¡Ö ^n»eó™à´˜Ô<9Í÷¡Ùé^`eå©€ØÊ=ìóq»œžóVÇ04“ï’2ašM~y¶AAÌϱº-c×ñwsäNôQC«(\’É|ÚöxÇIÛÛQž€}™l½gP§Ã!’£’ËÚ]óÃV×,5FI_åmá"¡<ÓTxÊ€m_m"öc£XBÞáÀ?rð2ŸÜÇ)³ƒòÑX’’#Fá1~Öù8ý¤@Ž?n_êø¡Ÿ@0ÞÙ˜¿t7–0ùûÙ÷;ç•‚\›> ¬20þà© }F°ÒqŠÜpi¿làÂð%È‘ƒŒŽÞ¡0aDƒ]âØWw—®~â.< º.=x€oÑÑæÆÖóªöÇhÿÑáñiÔ»êA©Ú‚ ‰êÛ°£òÍLîâýñtõf<ޝÛß¡¶·ä2í4’ã=§7 W˜ÿ|?ns»úô(|òTDac%†ê»Röé2¯^“6¹Ã*t@/£ìÚH_`ã­-Áãuã·nûû <ÉaÔíˆ8ŠíHÂgù†O“à¶-a)zÍ)¾‘”u2˜F]`z#ñÇŒxI×oø¬¤ãF Â2´¢˜.zY?nVù÷æN²àТš‡ÌjÃEY&ˆVF}–ã>o‰'Ý#ÓŽ^sf!gÒWæ¾½ùpW¾^ý±;ã 4jjC”ÂKДÚa¨¹U‹qa90??nÓ0E‚50Æ*m>+ÏÅñøî2•á8¨‰Ú ³]öÀ‘W%€!’¤q– '¤2ãjœ¯$8Š\eIˆˆ¼i(ŽËŒ&âõ5½®PVS&#º•A]µ3•úÂÝל·íüj•–¾‚G„?0•D%?nR­×§|WÀ­+¤LÖnŽ2.(úæ?0m*çz‘ƒT«:Ãiõ,}÷ŽrüCƒTÐ…ýVÑêÙz=B·ãK€nǶ_VcÚwÒ$”‚,5ÊVš#®GÙm»v¾/ÎòFžËw–xz¬ßÆ! ™½ÚÉ N»bïUJçG”3Âo­ðš‚²ºr!ïRÄ9u­PrM‘~w{ãɸØÿÝñkµj«!aŽÅ¬Lä€í#c§N¦ðžnÊŠ½>9]j~w“([\~XæÐ=9<ŒV6y§fÔ?nF(¡*¤>)ØÂmRUƒ^Žè߯îƒ@†Ú}Çû§Çõ‡ —Õ3Ä´2ËEœëÂ`¥xú!’‰í<*;ž]ÄR8÷ÇŸ†³ÉI¾¦”ʨÙËÌË?n#kòŒ?n·ïtµ-±â'MÛ“²FôÒÕ_u»¦ßÉÒôc3ËxeË6k,P…$†{AŠ'§‹2Û£?r8€vØÕñO%1.ÎÎ’£òµЫË]€ü «Ùð±Fú2E‘àçF‚¥?0KA®VaÅ»—ývEtü*JÌžhX«Í ¡ªµÊéIVtþ„¢Iyä+ÄvdÛñM,çu??um¼{t´£›âžùÛjvæŠb€Œ¿¼.ú¤œ(d;‹¿¤âXk¬~¿©ßoê÷›ú]50¢ãV:¦)`~"Ý´x^³ÏåW??.Ü‚?nV8BX̓í?rÉý~“¦´¯:—½Û¥¯žj´…éóí?r?n5ºRߪˆYjZœÍ!›´³”Å!ÂCC‡ƒ„Ï©”|Ò6°€†+ÒÌ[X¬*Æ€,ÄClJö;áSm¨ëú#´Ø4gB7??ßdÈì¹…º«ÌÊBÝä9í†?r{Õd£=ÙlyW‰îÿÝl2PS…Rçba7Ý#w+­›.°$wW’ÄkêÖ´"˜8 l ¬ÙÒ˜…mŒÇRæ!þÁó…n’Q:1FíC ó×b•­B™k³&SgPvuƒPìÁp{V›øätõúàÍþŽ `~³©«ÙPf/eÎ iˆ2ÿ0nÙK î‹OCXyKªGš†€%d’ü–7ñBÐ_öÊ‘4@I#ÛÓãª{Þ?0˸Q+Ê?nf~|–™»'¯,øaNáëÞìc:£hoÐ/F•ø!ÚøM"÷§½™‘Æy:ý¦?0x¶½®‘)õæ¼b¡IZÅÕ•6.v;î´ ³ªš‰Èº&²3!ÈF–ÒÊ?n%$J¶ô ©mSšÌ&˾˜ÌôÃÔañóJª9Qܼ–m—UDË%àz6#Ä– 7£??RQŽÌNcµåj¬È#üZp®ð¨KúÃ)´¼~tbc´Ò§aÏr²Ó¨ÊŸ°¸—]ó¢®k:?r=¨þñùµªÕ§‰*‡Ìá’]ïè7i“ͳg&’”ÌZ³>†GwÜç$œ<µ0{%¨bjœVÕ‘Þ£Yúi8Yd•23Ûxƒ®½ée‘…‘öcP£È}Ðëú…›25 ˜(²jƒc…œ¤€+Ó‘R%dwCz¶¦L\j+ViÞ^ŸÓöÊ<Îf¡á§e§ ÛtÓ‘šx[ÉcóôýYMé—´NůöQBE²õ–„hÅJ'Òï ?n‰RU†|8>™-Šá¢s‚Y6XãAî(sÕrF[)d>êq½4~Acu¨Á%r<ß]›1×Þ¶k‹Ð°K³‚ɽ$ù(á°.Áx%Ö0?0V ¢&þí\ö¦Ã¹±êþ/º/AÓ£®ý(FJµÖä™êòû5Ä[Ï6h÷€kžj05½S?r²¶D†*8Ö ©H;Õö#Ò¶DXâ6ÿ°ƒLΖ?rHѶ&²ÝSá‹ wi ô £×YH½]ôíÉ{'_àÐŒ\î>³foÉjêK0ßÐc²ïCö-õ«"æ%îQ»^G’5|‘eÍrŽ»@ŽÕ×Z<K²r©¢‡¥«¼8ý¶óM;Úeh÷EVV™Î}Þj‡ÝÓ+g \â7Ex¢Þ)KÕP_«úÇV&Ú=No¬íQ)ŒŸ÷0_Þ`P{û(^Rb¶0µ™·Ë™­Qáÿ?0?0ÿÿì½qwÛ6²7¼ëSpµÇWT«hc§i÷u«¼ëØÎÆgØí´Û›ÍÃ#KtÂYÔ’Tbïm¿ûƒ†#‚£¡ ¨nÚÜ“XƒÁ?0 ÀŒg[j®n~Cܺ?n"k›¬ › «âµá׫ÝvÖ<ÁÖýÖºØ9MŠˆh†›ù2›¯4ûmû£7¿ÿû—zwnÒ¥ú%ÜõzRîžì‡:ÃÝ¢ì’Ò’É!ƒ¬yWV}ãA’yYxq8{ôŸ‡+°­}CßocVîr òfSòÊ0ÊÞãµæϰ¦;gXà ZAMéªdnq( Î’øNWd͉̾T'(_êñÌS¾íóûž©R¨o??(ÏŽ@ºÏ.žÝ̓‘Í;^éÇ•c`e×B¥‡Ù``¿ö__㻑ŽLR„ÄܲŽÀ&©ÙNâÛ*!>Ê×#¶Níéäº`ÂL·_áKØÞúqëzk¼õtëÙÖy»·hAˆ?n»ÝÕ‡,ë8ùv•Nø+dìúP™‘tÖ6ŽÉ±.èwò?0žý!:’+¶€t¿uáã|ð}Myw§Î„èq¨ÖDµÜó1.,UøÝþÑ|ž=;8xúôÙ³óó¶.ùl ¥KÇ2ßšÑ0³n"wã—]£ü=›ë#s–ŽÞÐäØš£«,E8¼ž…÷k¤ñ@ˆ¸?0®8å1¡gøÂ½lôLwfwtl¥T Uì÷ˆÊn¿H ‚…ûVÖ4õËœ¿ý?0ÒîœóJjöjüé¶>yAüY??}·ƒ­ü[ÖÒBÓ åRΔ¯hÑÆÏõè,lÔ®ßþ>7j÷9ꮹÚ\£,¶Æ'~R©ï—«ÏºÍžù;?n“A_W°—YÊáåÖ¼¤ò$¡Ûr"vwß¿ÉQbvû·þ»{׆†Ok×`Ê0߈›0NŠ™ökzØà®ã'Ç0ÐSñjÍI‚?0^J…lß$y‘ÓDñè_ü–˜.⦙;Üöm>½ðœ„?r–L¿:?r‚Ç"ò êõ^cëå8Æ-‡¨viì"ÖÂÚ|¯èðYÛÇAŽkˆ‘^-\”í– ³Wàˆ3(½UµþX0M3k¤GeŸÜ\;í8#Lß6LµÐiV½ë/~<=ôŸì=ÖÃùl‡v"N~-PÁ#xD{ÏhíãoÚ«/áÝ¿oÏeM2ž¤ì’-N´Åƒ(žÔª>²Uÿå«~lf?nOE_݉þœB¿ÎúUðbZ9?n«z,?ri½8{áï3¼æ¢cÜ~ lCé“•wmÁ êê$?nŽÌЩøò¤~ Š‘ƒÓSŒ€3<õr AÌŒëx?n ‰–ˬ|NÿŒÄL_O“ ,B9>9ðmkñmf!eƒ…ç^òU@*ÃŒ;—|C.ñí1ø¶¾}ÛЬ_È6E޼½‹õz?0DT¼ì&iቨ³OW¤â1xCQ;07“جâ›}Ñ}¹»#œ±ûO™E•Ç|Doä4ªìèõ:'ý§Í·&öêÖ`.ã÷ƒìÇÅ—$.KVzvVžŠ”O³IØuZzXÀ°oí³â`×ʉoíè1C»Èâá5ö Õ›÷Íâl‹.;¥`?nõ+£ï+¸O;²ûÖ¼Š,ãôØpåÁ@»”œQÒPÚL­wžÒÕœ1È @Öx§âÀ9¾ vW0O¾wMMòngU‰ ØÔ¨šÈ³¤jôº¥¹‰Œ¡âÍJ ,8©dž?n¸€Çágºú¸Á;ÝfKÛÛ÷ž\œ‘¤|5»+¢ñŠ2nËu㔃•¾ Ž×«î?nõZïKéUY'©·ÓoÖ‘Êð&ŽDug¢wïPts§¢Òĺ±sÑMŒÊ—ƒwähTïôÍŽú]EÞãÑÍ7NþÍÓyÉFeÊ??¸w^¾Ž¿Rܪ©eµ¸Æ~ïVžv€)ª"^ ʓý5Þä]ÅC,ættP©õââ \Øu.¾rÞA/?n@£ã4¦nA})È"ð–æ¤ò"$j‚krüÅ©;þâ°q3££Š ÆA¬‡›o±ZiÍm(]®ßÙ0B^^S÷;ð}×Qêµ™Zf}pYohäÃT2Y‹òÑðÝ0™Ø³¶ˆ¦uŠ,V%¼¬Oðþ¿ø¶Ó,XüÅEif'r„–[p-hÉI¼õ1KâCh›‡{Ï:Ë]Xy*å‚‘7}±€s‘Å–ÛÎè¶Z ]®Þí º |SùtåÊxùn^º1ý–öS'§žË³ê*ÍìEEXYàofÛÿ5ìð!çt~}g$Ãìõ»Ç¼Ó•uÄä·VSŒ9z¨A ´¨3C…îéOQF`óQË4*wðý„õ0×DË?0ÃzÜ2 ™ÍÓø‚IUF§—d×Ã/ݰh`½08VJóŠçÙ©ÓBHu‡/äÌËŽ2ÁcÖv?0÷Ö·î«ƬÙðÚ?0˜zÓ(½4AÌ„ô ù†„J…[!çU×cj±r‹„˜qÏONN=3N59ï'·Á8±ÎÞÕ¬³d3??ÿqÕë÷¦J¶/žýÚwƒã¿H¬&NÍɇc½¿c6Š4¾ŽíÆQiíÑØ3eEOý,R=Ó E5˜‰Bž¹¥³ç+:w”_á«—7ñdÖyõá®#¤õ‹gm¿ÝxzÌÌ"¯(PyØUeQƒgð·ñ­³f©òðe5Éd£ÙætS‹ÏkCScFûŸà7!‰æÇ× BîåýÝ¿Tì•­¬6ÕÁÖ½¯óNð…½ºeÑtƒ­;×ÑŒãI°¨©I¯eY:÷é-÷.Þà.ñb ÊL6æ?r÷¶\ù%ÙÁNЧJ}Š-VÐS‚ˆ»'ÿäMÃïCÜ%›m9«D??è_uõØ™?0b  ‚Å‘»‹zh¥yLD6¾ö‚¿(/D€‚…ösÕº¶¾W‹*²º '§²Î5ߘ&¸üþÄö¥rµ«˜æ¸æ}aè´\“ÿËe??xи?0¬-lÍú')âH_+0})Ö??¯‚}ˆÌ¯—*z®²B¸°‚?n!ËÙb L/î¾òY98Ö’•Ì(¿ÀüÇþ ï¸ïKøYŠ…—Ø~8Ø?0›)¤!{ö÷?r¡†ìtÊN(;{¶2"dæcÆÐùùq툄ê°Ï‡‘·¿°Ÿ‡Ñ^~;í¿Ïã÷çÅm³C«(Ï'tòSÚ‡mà‡z6î_N¸½Ð`®o,]ú¸rÊÖÜSD‘}á•ìùÂì‰róç‹·ïñM.YsºÖ°($Á•-”]¸n%4¾N‹¯[×{Ÿ™©¥7(°Ù²aÚ…Ø×%a½ŸCµ³hAм Y#mñ¬¶Þ©hB Îc³ìÇQuªSö°Á„D¼äç?n•¹@X.ç¯Ã¶E„á…ñ_¡kkÏt™Xvò·Gcáãí`€ÊúÎŒr¨î¹ªÒBY}yÒs¥hžº?0´G¥ª°°ûm0ÂÝ´2K°$hëØ¶2SŒæh›,½Æ3ŸÛ?0†SýISŠÖl¯Ç)ÅÞ¼'-gNqûªú¬š«1! Ò©ÆÂrà¨èüžæ¥ÍiMU‡››ÇE9Í#œ0=ò€Çðö¯åXœ$—ýò«Y¹a!« Wz¬íH÷ÚLŸeñ»(KßWQUÙ˜fª0ðϬÛ$žŒµ‚IöƒA„kb¼x¤´œÒã Ì²j㔚²R}C_Û9‹‰âw0åꃖp_<6ð%lðSÍÛŒ1¯XgQvô¨Áâ£ÜªÐÚöqþáÌèëRì¹Æ#G¤òµB'X¬PÑ8£ì$Éß ßÊUÐ7ï\—ø%ª??N»ëÍ®?r&—´2-³®g9,î÷€é0A‹KËÕ5'ë‡ÌÕ?rVT/Ý¿K¸6‚õ–œßæû¦q„° ·2^¦媄g{¸9>?n?0ïCÖ¦SH°å²hj³…ðÓ“ð˺àðäI§ )_B??‚SU‰N8Ù©Šƒ@Á»u¯Òàb‘UYþqx-<ŸÛ!9T¹¦Ó#t6gw%TZŸ’BCÂV˜õI-Ú%ŠH}^‡’"Sn0eQvzÚTNÙíñ QqªÁ¸å0‚Sð£ê’.¶¯çy\ÆA²È!#ÏüÒn­ÛzÅp´¬ˆêÄMT¹!8­™Kˆ×T0'Q=š²ç’µÁjU5G‚·Ä/gœœ÷ºÚ‚Þ^9'·Î3??çõÁF‹ÏÝw:cþ¸½Nu~îvÁ{¥ßɄԨR¶ÏÌà_Ðör½b†!jZþê¤ý°ŠK'µ£,ÁþIgu1¼Mò%Cƒ"ì.Y~¦ì˜¤™g~"Ô£[È «šE¶õ_œeÓ·qQ¿šìÓ$,¶®_áÝ\×fúï{•æÍ镊®Ÿ`[bSŠÿ3ÎÒ3 £“ìÕݹÁ½GÁbös«þm°ß#<º»Gð,ñ7osÝ_öʺJ(ò§ók[$/2ÊOÀNœN^<>>Ùÿ» Lm¢ÔzÏP%Ж¢Ã³³ç'çÑÁÑùþÉóç‡û‡j‘4cú•ÝÇr‡é´ú{ÍÓ,ÿ–CöGïJ‘vioGÖ9Õî‹"‹ÈAq' HU‰ÄúóYlÚh)Ú\2étß½dòR~÷?níùm%â±o$£üýs÷2ê“*ÌìÅŠPg×ÛK•Y |fnš÷.ØgðÆ<];úJÛYjg5=´ÜЫ:Óïëxç–œ_’]4טԥqÚ„®¿þ0žrMíÛÕƒó¿›Ðó‚{ôü⬑†åUÙ úüäñ‹'ç:ØÏÌVÏá•«¢QêŒó¡V·ži¡O?rï +#È„ç({l äkdgPøà*Ð?nßOVDžî}x??}qqpòÃóe\ãt*k5+›ªžÿÐë‘MõÐmÅùáó IçG·ÿ3E¿#ó¿ŸB¿–q׊$f%¡õ’«ëÛúo Ü}ñ¼y??}eã®D>NÔú†ûCéa]í­·µ¢‚nÎDVY>½É°ùR~cÑ¢uónu¿Þ‚ŸÁ(ÅÙ™ƒí»Uúxùñ\‚ô¨ç>håxÌÓ wÍ@Ì¹Š‰Ám\lÎHa ¦A¥ž–èÜaÄb¹æúO$Çîþ,•¡7íz9㜋¼ÇfÊ^œ>Ř00µ{½½ ÊQ/ôêwj!ÓlTwvUâ»U»\@í9 á|³µ› ¢¸{)T¶%‹gY5ßPu˜Ò¯A Lð¾ «+v±?n6áÕÂѽ{â2hUNR½-Ç gãäR¶Á9w?r½d­óÆbg?r åñѺ«1ààÉüÛ§g'ð×vjþ3ØÌ·õñé#ýIãHâéÅ$·c&Ké¤5ÉÄÜd œü4CŒâ¬?0ã…{B<€Ó’Ñëé(”ׄͿw;¢g‡OOZ5ÈÔž«å%àÉ©yõYðà'‘ò ¥„X4†É´òìÔ¼¼????:yÎÞò´š~âªD—,žjܶEÜ@Ñ€3Ê­E çñãÀé&÷%Ì(݆]¥„æ£B±¤tö^\<íìãdTè "Ý@¼¾ÁïØ?0ýÜ”ŸÑýD=¯˜Í jçüvZ ovP|w~ú¬ú s!„C¹ùŒ¯g—Ã3è4ãb Û×aº Ð›Ô9}|þŸ¿1¿\-Ä•ûA8_§Ebº,*ÏNL[×käÍï€JjïËýŸN_-ÆÀ|úçr@?0­Á/??‹èPMš?0–à^é3«ЕYË??ŸF ì6^~¡•žï½ø¬‘Â× ù9°?0c¦Ó#i?0©†ïv¤÷ʱ¡¹L˜]æÿ^qõ²¨9».‘D¶Å*9üpýä‰_??ù«]¯ãâM:®¡—µ&ù²Úb]Ê9¸:Á)°¼ä @ŸKöœá¼€ïÎå³x”\%ñX¹ü)¨ã·5Uƒúîšê-oµ%@áb¨)É4²º“Û¸Fc,€hq×îb]œkÑ6Ë’w†.ƒ‰bt*MŒƒWp&YQ\ 4ç²|«>ˆëñö”‘ì aöÎÂ)&oO£ò-* ®ë±þ6P>²%Vˆ !˜nsX{E…ûO¹ùkŽ/=°ÚðŸ±Ì-P¥SŠGU°œ_/îôÃuâN³jήvŒË†W½À”í¹½"8ž8Ü×¾wÝž°;0ó×ÃMÖµ%+¹Ó7âþ>™'æÆúƒw]+ImWÔwÀšHpA6ÜIÊKþîòÜÐXÁõÐÊ7úÂã¿`Óºâ?0¯ k;‡ÅZ&'H¢ÉA* þÜÛÇS#ÿ>εõÐÈ_Ѷa+¯¹‰¶PEw]ŠTV>Øã‚:¸?r??vênâé(»µË?n9.ç9iòi^w•Ù¨ô>t6ï‹ezó.ö·ý¢já%M“©Ø¸U,W[º‰ŸÔ;¸¯ûÑQlQ¦ø¦ÔGå9h¾[žá¿6¦íÇtNêl’çóë?rvŠaÔ:3è‹Ô6b-ʈéx_k`˜å‘Ú]º¥ö¶ÉÑáÛɈºO7¬û”´XOí46›¥¥••‡®†‡;Û–¯‹óÊqÉ›}H›Ó®’kO»e±†ªX U@ðÿá—|Ì´üóìÖü??ž$—.ŸÅÎnÿp—ŸûæóõW_áïö7ﻑóàá7ÿ°mâ<Ø1QGîüáþöÎWþ!¸ÿ‡ð™ÃÅMüáéçOÁ¾1|fÉë7Eîw3³¾ þ– ¯gC3‚³tœN†à»×ýÌ~ýëëk£Kõ„yÔoý Qöá´x“äAžÎ³dâØîÒ_§Æð25#÷ò6xvtaù(žæRÃ!ÌÜ ¸2[2«ÃCxí>????´¾§û­–5'טWt¡Œ¼‰‡³ñ4çofÀ¿h!Xüºe0ë~ñ‡´ZˆLô¡Ï eþéôŒ£iaHšÏøMžQ\׿÷hipRÔÙt ^ϯ¯o£x0̲zõ8…Ê…Ê»ÈI§W‰?rgÿÅe¾=yU’“œ‹D„Á,oãe0z\c}û€7Ô ‚bhG>¦“ÑþÞ—f1Ãá{k!Œ1È4@ƒéɵ5üóí ŠFÙ¹NÍ›N“Q§G!(ÉÁ??ßc}ünñ­* »v¡ômmÉ [â¾# ÌáÅáEOæ«»_¡œ¥“ £Ótëiúž³ÖTjHQùùt‘Ö+‹ê;æ;ÕÒy¡„C1³í­“J•’ç}ü??Kg¡ƒNjü¶@¿l»bÙR™solûô/f?0{¯ÃÞ²#=КL nW%,Êb8´[Æ]5ùn {(©X]|w2°ýH¦ÞœŒÿmøœ’n—ùßR&WjÖ¯ª³éú¯ ”º]ÉYWèHp±ˆ8yÜ\å‘??K!žmbéSAY-¾t¹Z£¨š‚L“Ö¾~MÐԆ褠iñƽ,ÅRµFÓwÅ¿k˜âÃ4¨<'âà±;|ôÃç<’äŠ<-—|—¥?0ºUØ" ?0yÛ±£r°•÷0`øë¢ð c?0É06ñ$£›Â̼|ÅrDŽq†øïŸBíq%FH•Ñ•]Ðùn+ÒLƒ¶Ȭ¨en9=1Kó"‚¤¡ŽÅ¢É"¯lèj “:ãŽQŠuJH0{Ï€•ç-‹´¶»¦ûƒ;ý~A¿X»=?0Öéj¶È‘‹”+Ž„Z—Kž°ˆ.¨ï²rwþ*™žªm­:Ýçj ؽÝ]/l.ÄX¬òQSHÈu7‚Ý‘•X”ÜUH,<¢GÅX“RW(xB:4¹Û°R†„t' (Ãi*W<£mƒNÕ˜ÑÄŠ£“ã45h‚ðaðŽÑú‡4¿vHnñI“Û Ñ$E·T»þqº˜N‘Ç¡@”¢/—›ået=œ™‚ÿý³È$µ1ÔÖØæIF¬®Š5èéΚˈSrÿŠI±å ÉSOD–çQd¾]§ 4ŠL¶ûmô£„Àãï(zUGÆ{r¡>†WcZ?n¡êã[·í`)Ä>R°²Û 8•xÈ[…îê,ÁÖŸnì:vvXoé38“1ñ¡Û¼À©WæÀIT➤¸q1¨®‡ÀÁÁaHÖóüXÄG€ð;”¬©5¶‘‚Í¥Ù­ÚD‰0ì6n‚¯Æ½»z9HøüÎó:çy±_üªõ‘cØE]£l.D¥°ýU«Ãc½:º?r\bÔ&SºÞ¼ríe ÞwÅ«Æ*ˆ²3Jõü#T.Lj–°nüâFˆrà1/åø^Ët½ýu@¬ÈÅeS£h3WW……wQ„¨¨ylHAÙz+{j¨É‹ˆiäåtsêF&ÂW­¤[§™Àj‘ÛníèÍf¿0 »Ê?rÃɹþp_}ÛýÇ{JÉÀ4ÜýÙE“ÒmÚI«–ÁФö¸;¶ÎÕ¶š½Ór+^/Ïí]а\F×9¹”+o=_î¾µ3Í÷”¸¡ö—\¾ºJËÌ—Wcˆ?rF-å2UüÃö°É”’ñNÅh‰«±Ø}eš÷¿**ÇœìaÑNiaú{|+O¢¤W§âî·`ñû!T?n3à¦fÀÙUBèÑœ™ä?0?nKõ¸•ëx"4Û‹,¾NßÅà…/l‹æ_c]Õš%Þ²†ÇT\;w& +BPq?nË|'—F5¤è!j¿pƒÖï¿Æ¯ŸtšSÂ^g`â¦??KýÙ3s·B(š¼/¥¦:— Ý‘a¼ÞµÑuewŸ1혢ŒE™€àýofèI$9!¨v×;mP¦¤®BÕû;¢ŠãU­OJÄDVe÷z<ÌãSÓ÷‡?0¿šhr\ɪýÜxS/Æ£¨¥6¨¬bEŸ¾K²tŠm©TYtuÅñ@{ø£ó ¯ËÉJ<Y´UTüÛ y=M3=V‚×Òî_°>á5ÂUïwM¼nÒ뚢pqcÏx«ë¹zkws_6Ì"1NH²••£ˆ¤h7aó¹‰\í,h·Ÿþ¥Ó??I©¼œ¢žÍ„gfR÷pì4Mã[jYµájõžeZJ©u'²wé•VÝ4Ë™ˆˆÓÌz¢MÈ_6=|ìÅ™»=D„U¸¹O¾–¤¾¥L:PÛà(ŠhwïøÃcš@GhÅjd:¨fÁ“£ã‹Ã³ÈÖ^6a`ÿ_SLuýÇtlAˆ‡ßª‘ f³6rTJŠ1ϹU ¢¯µ hÚŠ'+¶J²m–Da??’…Ø‚ÔPJ-RÂç±H§«+Õ¹ç¼:ì¿!û’èÛ/¾zŽ??Ù=ÝQÕÝÏÒ¤Bª/a°ø”›¢Å﮼ï2ÄEÙ(I#;.*»†‰£y–È)‚No|ªÇWÖô\ÚšLp`±%Ä?0D‚óV&ø}(Œï+\NŽÞÜKˆë!qž5îñvÈ¥½ç>°“7㘃ü¥R"+•m¥‡Ç”Q©su?nð‹Iö€Ðßï È Ãv©aÚƒF¢N·ÇɆͧjÓ–î­G_’º?rïá~šÝeskýEVÎܬ_µvV׸UÎ*¸I´µÅÝžzöÝÖUôŒÄEêì郤†¯‡ÉÝDÕB³Ý UN ï1¦¢`ÿqÐ4&—\îz ¢, 4½X5‹á†õLã17¨;k?n^ô??l‚ž6!??mVãÙÊ*óØCJ’÷ü¼ŽUÎò¸(¬›?r¬qe=äòk¬ø]÷îwúÌÛŠ>!}•!î—©‚Ѧù…ǧÀ,çW¬—q¬I†v©¤™ídþ6™ÍÀff<‹¿è¶îþq@r帱`J% Ì;J*éP›)«nòâ>£«®í»¥j®‰?0&”§aÞ&i*?noûýÒë6Ψ˜ÉŽ>©‘ÐQ2©êK¡Ý•ª‘RË Ñð*šONìØ^é‰/Â8z’úÞÚÙòF¯r¯’âèÏú]Xü®1ˆ*Ãp$'Ó+h‡ [àpÄ× +#³â6˜$yQÕ°`ÀAëܲ RÕꦀ‚z?nãØ{½x~~z¸¯æžç]Ñù…‘ÅÏêù÷udG<°›H§Uvýj§a)F>Hn:P_4?rK%><{_ÆúqH§xöjò†Á?0È×<¹Y§)պȵ-£ŒÓ_Föý\Ž·¥agw÷Ê|vùpLû ®;??8*|ùÍî«Þ‡Ù~µÑ¢q™˜©_GÕ 8ñl «™Ðíú1J?rΔ‰´sPWË•±ÆçiÍpîÎ@‚ù‡®nâ‘ÝÔFÈxbÛ|‘8ÏX’ÌÃ+G&ç1»­·^÷µk‰¢¸Ë=[Ú–k­WS¹ Ǫ>Ÿã??•îöøg7vRâ0æYçA˜(›‰ 5Ÿ&ho«E[¿`À„BU>¶iag1¿ gZö†Y^Œã,‹Ê¨q¹Á:I3¾hÔ6qÉ“éÈX{ê?n‰DÈú Î_oÃUk,!òˆ[¦ÙõRHE†(C¿Ñ<¶%&owƒGÒfQ‘Ê›Rÿ†™%_µÊŠVëøðûÃc‡GÏŸœ´NÏŸýäv^n…#¶'ÍÝíþvl…Ã|„.ïæ¯:%dôÌ„ÉÛ÷Áã{öÖ–Ý??1âÎÚ©±cZ¤=99{¶wMÔÖ÷¶®ïmƒ­§»[Ïv·ÎÛ¼I5þÄî÷q.ÍY¤È?r+7°üÒx±ùˆ~©Š¾§¨8?nG癵00°š9Q?rÁ7ÔÄmöׄQ=!tGa^daÛ ^µ±«yeV»]䊭¢–éÄðŒ9 ŽÇóQ’h±ƒ "Ç뻪q²c¿øÛî/“??f×N%_u«Šz:2ž^\;ëáúaïìùÑs??iÖCG—}ȶ]d5\??+übPOœ^Yîü×Ùý¶ƒ³šAdu+'if+áDÿ:Îóáë8p‚©è¥…«ýó›ÉâÁØ~<ü3Ü2 ØÊp…]cá£hœŒÌô5M˜H~þôMû¯ð¥>}AÕ[üÌJ qDxJcÃXsy=Ëâ«ä†7`¥tÚ’¤ye€ÄÆ$êóM}P˜¡?ncE¶%ʳH­¹.¦!ênÎ ÓÞë§”N«'ÏrÀÕTe™ÍkKE·?n(•UÁE‘®3síQÚêéøŠù_›ª˜`¤Fv89®•X(Ã??®•Éǘ£4S»nN˜I¶…hÊocÿÉãìÚ9»ýxöŸííí¥øß_ï|Žÿ-??Ÿã{C~'¯§Ã‰fâà<ëÄ?0ç§|e6Ÿ:/Ã&¥§³-â7 ÐE¸Avž\œžÛ™?rk6­Bñ¸J|ÕŠŸÃúѹÌÇt†kç®R.J„ÌaÎ@8TPék¾Ö’¢YœEÉŒC«VŽòn”fåIèâéDíée/@o™–Œg(ý\{MZC?rý¶j+¿Õ³ËJƒW_ÏNfe—¯TçS‹–tÌzåùl",_Gð*'Àj¦—*P'>WxסsDC)ª”¡v©ÜÖ.Žk²#.yMeCüñÞ\Ò.J,‘!ú†š–œÿõc]EÃ*ÄÆ_ŠhFoqËh˜iÍA!´«ÆèP|ƒnv_îî8®î1tØ…€Òäèo]NÐùV4ßÛ·n;ž¡ ©nM”þ”­\´¡‹H›5Ô‘b“Ç’G›ƒ4{+ž…0fÀÕ¿À/¸²ö:p¨è1ÐZÀ1J–ñÆ´Ë{fJ%—óBlí•Ê7 @V»~Õ’íEVúøÞ(š‘ØCò¶ÑY½ >î zM™JÀoZ°î'ïâÈÚŒ— ÿï(ÝZÐó{è&¶jÒ1âHïCÀïm x]8IåµÓ‘ñ $3ãË ³Ä›NÇ኶ÎbeåeÙº¿È9@³@‹Ó –0é>M˜L½ÆÎö©=§É ÌÝõþrâTlëX–:UËÚ54"¶wJ;z/ Ðùôí4}???rŒU9/ŒekévÄ®¯kÎÁ¦ôbttK(vac¢¯]­ˆˆWx=ÌÍuœÌôc>ß»èòbß#ôêc¸‚J¨Š¡:u娷·îe5œ—?n•7eÈ}—µB÷aë.5ݦ²Ãyñ&Í’7–çÆT€ÝšÃ-˜VižÜt¸Ÿ´êæylŸØ`kît5®ˆAš¨&XÉm6Ë™sGœù\[éì1Ô|:I®“"·uLbh´Á nf?nvV¢E% lMt—^ˆ4¦•Yë‘ݾN§Ä=‰‡rV0)FÄ ‹×Ls?rZi¡"†ª?rÖtÝ2övNV ËYÍ]5ŠÎφ×(à¼uê5f²©–ÀÕZˬ?r®@ ­®D(W¯dæ?rQ]•t) ©ÆÚÎÉ•?rÊù*cª®™7z±ªÒ™F4ͬðœ!÷Vü`;a­WÓf¥¬ùZbK6cÜèz¤^©“RušáR??JçÓÄü<´r&ÙK¶ès»7蘇8ºÛ]ûõ—Øgk5¶~ÑûŠ„§’çr"·ãƒ\ý'3yÇ&MÝN×ûÔàøV‰»“sÙ©ïâd—Êˤ:77g¦ºÚ)û7ÂŽÜ`DNh¢óÙð½Y??àî:¬tüÃ9¥·¹B;(úInd•VTÑmh5?r¿b u7Áª ‹2长V‰oñ–o\Ç\YP¸ƒãÏ➉;™%+J=$‡ù=`ä½n/ÀþlÐÁegÅ€*ú€ãû6h¥Ì·tðò¥è­£ÉdÜéúõƒÕCy—ÃC1ª‹Š€qKã’e2±+7 `Ñ›º€ï¬Ü»,ÀÂ.Kºµfp¯öçK2“»¥ª É½V惷 bZõMŸõÊ£¿þùÑßþ~t||—‹ÏÏöŸ®&¥†Æ|ÁT@êüd®?0‚Dª«õ¸Wh¢2zÝ¢‰ú²V3W®‡ Îh-š`xe:éy<‚¥À “%jÈüº`³úQ=º ÐÒp¦Ä9êÛxE 1YŠî†ý+Œ?rÅZV¦·`"ëÇËàÐÕ˜¥ž ³ž£'o÷ï»úpuÞ??6 a×ÑÝÌCëð|3­Œõ›?nµ %ÐÂ&S )|À}]¿³UqQdpã<ÛJ~ Vt¶„‰ãsÝ`ît u’?n¦u”<Æí!½)(÷—•½†Ï&½'zQÖqJ_—;ó“»ýòùó_;þ¯dú_Ã??ó~—¿>êû¿çþ××¾6é&á›o>ßÿú8÷¿î}q/¥Ò»Á¼¸º÷¤àî].ºfoç³|x³ŽóÑp7å>³)|•*®BÙ0Àa•Ô t_ãþQÌ,¹s °ÿȱº|=(cMx‚{i^À³ùëìzXóµÑžßN‹áÍj¬/¦FªCwsÁ(?n¦¼9D©:HÅûÕ`Ææ£a›¤Ã±†EîØ&éùO’‰i\^Ä×+¡æS{VƒÉˆÕ cü$$S\€ZðA‡;€Âí¹¾÷%æ«aÏ‹,„2{^$“ªS&ñ0‹FÃÑ›8×ò™Í%$ˆì4?rLtš[’‹F‹|ºc?rçEJS— qMØà³ Û;ýííþƒöçõÿ“^ÿéeóÇÿÿðჯåûÿ¯¾Ùþ¼þÿ†ë??nfOŒXüËn0Mÿ5l-$e–¿©ß°nþ¸ ðµÏ2WRÌ1.¶s;&ûôG“Ï–'× Ú³ÛÙmTdÃin–;.Òî¶"„()nñŠdx}97»ÁM«µx¼»C»¼Ò{£§ùß&\¯ã“‚ý°É}œš!è‘jý›ß=Ú +#\átÃNª,“`²(¯ˆ3sPŸW”wmb8î#OË: ™Ç*$帰æßµ?nŠ éú2˜%戚f%u’.–ÞÛ"ÎNzÁ¹mÞÑ •{>„ÑÓ‚È&‡­Ö³^`éëÅ¥8Äæõ£hqVEl4..•sȲ?0L7U™°¸ì¶°²•Æ) /”HתÍÿôóß ~šÿ}–MËH??J\|ê“Gàè°Hw1Â$L‘F40–€â)ž4Dü6«–?0žÑx~m(ãŸ}ü¬åCÍqòñ³ÅŒÜ1M_äøå—©ËÃýÆþjÛ\pÅ·ÁJV?rôqŸ“Cm˜ 8åb @NëC|tjÇ8Ì´r´xð2,ÿðš€à87¾‰GaÛ7 þ9-‡j-ÓÈ«Š4´¦HSG«‚×@ Vü_Žæú€¥/ÕQÄÕ¸Bî¦ÕŶ[6ìöi…m»È´»+‰s.ä/ Nu|Æ?0ÕhW/ª3˜†«(r'yˆ²½àª™š0Hxâ»(VÝG¾êS8·\n v»’0’.¡—$-\C‰£µ";qÀ€uùU´ð×q1¤+»øfŒ‚—Ã<Î¥eÚB,„¸óÙ›[Fb/ Åë²øLÊXo?0P†Í€Ä"@M}ÆÍ@)bÌ×av!§a·g¢iwOB4ÅçÙd’\ög†‹Þ¿æiGÈŽ.!WŒ \dÓ5—C]?rQ½´¬„Ûº£š`x9jª„AœR@qºÀw•ãeq#ÊE±EQ€;­8gñ)0?0@ÇB”·qJ?nBü§]"Çt[dµõÌ~Öß,pýò—;c¨&†ÔkÉKŠ»¬¿õIíÿH©¹Jâ죾ÿEæ7Âþ»óàc½ÿý¼ÿ«íýÿjf?0“¢o°?ràæ}Öþçû/ÛÿOz>ÛÊ.Î>òùÏöï¿y í??_ýõo??ÿ??ŸÿèZô¬q17Ûq¹¶÷™ÏƸZñ>Ã-§¬¥oF°àSžÙW¾O³qe愡ΣÙmñ&Febë.Î¥M1Êt®»vÄNÍ©v´z6ö|jŽØ‹ê0ÕÎ^Ïwwø:Û 6<Ù;ˆöŽöÎý`§{g{ÏM°w??èÙáùÉñ÷‡~ÀÏÌËÔç‡%¨íX†:4{ö}:)³SÃk¼IcNh8’?r¹è»$O`Kâ>5}OI­V:þ]Ú­Oü¯önÐ Ú=ú=ñû‹ß¯ ü~Tý¤?0 ð]õ“?0¾[?0$Sü6ÿs?riAIPlmòϵˆßðáÖ3x³ô)²‚ Âf*Œ@Z´e‡Fÿíôö8·Òiûí9›‹e¦ÑÏ瀎"fçëxJ,Œò"•ób¾:Ù– ¼mqNOQ!{%õÛÍFØÝ?rn“x2¦ÝÌMØ]fá Ó–ˆP‚]91{t(öJÛez¸3^,6ìÈÁæ²”¿“… îֽ¥³tå›1ŽÙêãü/7ýw)Ììa^@u¨#ÔÑd4ÄC®ßE%Ÿ×THþ8?0Á¢?nñþƒSF¾l|YÉ?0w[/Vš9Ã?n±›yI$”îù)Û*ûQn½VÑ#tKœÙ‹1ä  ,˜=°}êX8ªû®î.’j´â¨ÏrW>_¨<Ú‡íýá´S¥2fÊMƒ¢,EHhJTó$¸ÍéÛ©RÍ ñ€{†»mvZkQˆªqÒåÖMûË)K€ð‰a?rª!÷—‘RÖíÈÞã¶yË<¶É\ Þ®r‚xRË:4ö–Yžèî÷3ÖͲVë®øzµ‘_MÒ¡ù3Jÿ¦ç¬Ô½R»€³Cw}þ š¬BÕC€{ífé¿á2‰ïóÕTÈ[Aa5ˆ›`"à›o÷‘?0k"4žHRs»Ô(‚×cFO+ýˆ»ÏO¢Aà§Ðƒƒ+Û ˆF"< Ú›fÁ}†~4mr1$Y'/ŽY1©ŒbC3e€\³è«,F«Ì3¶¬•ØoîŽ6㨷ŠÍ eéY|"ñˆG™ò&5ËÝ´rجzÆpDþ#\­òc®‘P÷?0½f.Tå?nKS_õHS3xƒk„þ.e<ùÎÄwÙF.`*æ¯u€œ4ã`À:rH˜íBL_¢È­Ïþ/ï¾=ƦsKE6\®Ìÿ5O²8JçÅl^D憑½N¼L“(pQðM¶ÏмIª#¦nª(UBs1WaLú#]IWF¿ÝÈØ³Û•/QŸF #]váí¸OJƒ"Q@VÃÀ}KÔn ¼grºËžäÐÒâ1)ͯüNµGÓŒ¨?r^p5Òl’Y­¶Ažƒxk”§W…—Ñ…dÁ;e 7ð'ŠPØž)â OzWê…îýKG?00èAŒ\¶fwÐ¥û´3¡Ê/jX?0Žã¿±º/û\vu­÷îtƒ&¼Pé[þpÞ@˜ê䏨‰ûUæ1Xë™´<ÿ𬾸ªØë#’•ÁTI#H£Ô4H¡?n_£Œz-cµùÐáæ¶ð(•QBMh??jÈñS'PùX|†µà,Ø™úéÓá¹²Šxõ=ªuÛ ädJ9¥‘âaãBð ÅhD×”ö5ñf»S(dIJŒ—ýíP^A¶ma¡Ûüp¿²â«úJd0èšðõ h˜$"ˆ}OBóGË]ÑëzÖXQÄ|)=±mF‘ Fe›ñ~ùü„i.ý×êúÕüÖdz›`_DÁv6oü j¯6·çÄÁéØSÑ“äpòÅ“6Ñù²Ûö©zËÏTu¡W½6íunsíº9?0?0n°ê_ƒáϳ¶I M w ”r+FAì1`”uâÅòÐQâöâÀÚŽ`u‡ï ›qé¦ðÅ ”F-û»åñ]ØCÙàé¤ÄX_Ž˜ð†¼ÖŽ©ÝýB1?n1GeË&µ"»E³áÈðâûžm.0us"a­Ô?0|-eLª6."*t_¬Z@àÆ¶e_m†™Eÿ¬£ÞÉûýV¢Û8ÓÖÂBƒp/Æ!߆ܖCÉÆË?07™QÊ×ŧ:° á•Öã) ä2¥@`cÞßíB%3LÞ!‚UÊ-J÷4·Ò—jÁ*Œ™µ“œÇAãì½{z[˜:E£+j ÆëŒßW39ÜXƒ^WÓ,‰`-2vÇÞ3)a‰[h4ƒ Ÿ?0U8U=ii£ÑgMŒExÓêý~óCz¡±?ré~rBc‰ÎPªå”_ûz£??üx»ÔÑ!##=·d%MƒÊÁ¿ˆ¸{Ð0©ì²Þ³;¬ÂóhëoűR†øE%br·yìâUe{úN¢_㥥”†h{.¿è§ªøb *(4:Qp®\n48Ÿ¨ÄDP:Ý8åÈÔùrxjkŸ„Ωo†ô½‡øS%N½[0Äó{Al‹y*¸›¢´ð˜hLù²ß–ðä®}Æþn óU¨¾­ë› ’4×#šÆ³Ó]ümÕÂÖî¿??oï±¹v(,+|îäÀEÌ™}xÉÓUAühXk¥¡Çú<é±^ûþ»]xNüGu<öG4ü´ûÞ£³^ ´yíºZÔâÁ”Á} oæ§—§*Š8IµC(— ޏ ö"ý$º"ÛEŠþ\Ô#puiÈPÇÆá¸ë·Ÿòº}h©èçržÆHª1kæ‘@ŽTø n‚ú¾…cÓ,]ë%OOذ©[õ7ß?rÝJz¤Ú·|õ=œúÏJÍ•lõ<¦ðžzÖÈ8!¶_þÜ%®¯Qí?0RzqºXâ5JV.dýÄ Áj Õ÷¬sý=%?nÞãnÀz€aü¬={ «$»¿¬åh9÷ßûË_/ˆt픆Ø3ÔA¥~Ýæ1“Ù:¤Ò4•ÛÇ…SYÀÚ~haØýók±-ÔÛ–žûl—TB*A#?0齡zc ýtŽc-W ¨©¹˜x#ÚÄðªÂ°‰”¨»*lB"Å£ðŠÚãUvên8BÛÄ8’·õ€ñÝ?r¤ßUa€¡†¾œ +#K\_YÇÜ:Y´î4?nm K·ÀƒèÅ¿ôÿ]Þ}"p$ß•ôùÒ°çäW6 ýdž¬vS=ÆÌ¶ªkÔžDMMxû>l#Ê‚½]p,ITSé†Ò«HC´`…ËámÍÀŒçn“ô(„êék}.›—´¯ûɲÿUÖÏ0›±XGD„²+46Húuê¢ EÀõéÃZc(r¹¿3oËc±aF¾~ÁûRñÚ^WE·ÏÈàwþÝ÷­vö#nVþJr\¥`þ3é:qž¿‰Ù·`…4`£®…Øñ"<‘ª²(ÒRVk“n‚ó†÷ š2??ødn‚‰41@[WqÀcÈç8pdLò":“£dsެN9=ù‚.èèá§MùóéÓ AXX_sˆq£ßÚ0Ó%‹½nÏG‹ã¯ahWÈ«e75d’ÀKÄŸ—9aCô)(i¿‰apäÁ·1aÛ®lC„®ŒIuSnßõjƒ1™^m©ã¹ž*£ëÛ#ß1â9°w½tÈwlv#¡B?0[BÍòкÉ0²#rR (M'Üï?0Âè¥É£§÷Þ¶´ŠÙ5wª¢/ ??Þݼ}‡·rïÆú1.&B€Ep@JÃö¢Û¡}3Œ§€ÂGåÁ¤G7 ·<ŽÑ†“ÿˆÃ]n¿"¿þ¢õî¡Cù»é6·‰c;¿5¸¹Í]*)G$¸ë’'?rŠ¥·¼}D*ÈvV¬¾âKGÅ“ÿO¸¤R2³u‰%ΘvLõDÔ?n÷¤.C³ewë25OR?n‡DºÉ ò-†íâ2µ_÷ûø»Wæ÷_ÒÀ5a9­yÓŒ½.šÿÚèS‘Ru?r°RÈExNÉ–ÕpiÛipR’lŒ î?n0bæaÍö0Ùì’áú…]"2¸®Ö_Ö¨’áÊîò²®[ü)ºÿÃ~Ö#ož ïÑöÿŒQãü-1f©q·É¿d¼s?në3,š.ep4RŠä/åK›^Æòh7ÉáÙÝk×ð0™tÉ‚…+Iúh†¤&sIKîmp¡ ŒXññðe÷!è9?n¯þ-÷J"¿dœ;íáé“OK–oU›˜T+•|Õb~±óÖk°Ô6r >8R4$ÐîüŸ˜RøsfÀ ò¯q­ÆØ•x’ýæäMH_{ïgzh|GÉZÀ¬ïÝe¯"À‹±xø$t2‰¶òÞãEC±!üo?nº*`D‘7 9h‰Øí‡C¤‹†èÛá¿™=Âæ|ÙÙm ,Ö”°ŠE¿DF-™È–ÉHÒ~Y¤ XûIç§´êZn±7\.Ò¹5Ã.ôÙ¤x[Š´lÕš<×±’kº[-3 Î)Û¹“ ÍXL‹'–&4N»Ñ'R¹bu.é$^gziòïE|®eûT›÷õ&kƒ"ú±NÔ›+sÜ<ð1…Sc£3}EŠ$«ã¦aéFr 0–]±¦Ç`zÐÍñlúï²Ç¸õ²‡V㽪ˆÝT=TU?n‰#ÿt¢8Ûl`Y¦é̬Æê'¤o4u^mÛ\±H´”Üœ¨'­­„Î:mŠùé# 6¹ä*ú…–åÏoóŸìícZÉx+ëø2ãw˜Ñ¶p~ëjà=?nÄ?r¿j+‚³O¾ëO??EXfò¼äe(«??!t?n¾iEì†âˆÆkk‹ÌOòРÞ'ü{\??ƒ’I/g—Ê7Úœ™)ñ1Øñ©-íC´~Zš¨VÉ(\Y0úĕոi˜jÌKER¶Å£~ª??^ŠN–‰&ʵû½ˆÏ9Ÿçq„ƒöRŠX•iŒΨõ½Iõ‘\K,õ u„ÛKµ7©MòGÞ»õ4}…è*–ÀÕykzšÓÆK­àŽ\Êç\çÐîA†DÆ+LµJ¿ÛjVÑš%¹ŸÍ=tUE‹“”­lËÞ½&ZK©‹?r[aÐ æ¿õF!¡ ©-aãˆÂ„ÌU#Tç£SÒX4`Ô-ØÌ?n°ht…~%å;QÖdgÒ‘þ¸¿ƒ•Ë’ÓÞ4¥v«ë{´>õÛ…l~ašµTíÝ^É– ›Ïui\„ jŠ+.NW\Œõ#°%qƒ=Š?rúNðÜnä1y„ò MïÚ§‹b š—#3îI77õtƒ~Úê§FKeTråbKeožõ*´*"^Œ“W/‚³¸3ÙýNö¼Ïäjd·ç¥ÜM›”áΉà½Ãsük­»wmt†¦¶î"S‚ÚsÈn¤ÌRDôÒ¦'¥Ê‹¿½ÿðíi³%/°ÀÞ»??nc¬Í´ q¨S†Å—¸Õ`®tà8”ºÇc̺¤Ž¿ö6f;±m{÷C¯.v³[LõM+Z§½9– 7ÙöPYìÖ‹Â?nƒ ÅØe(ù㊭Áª»K l:¿åèÏQfؼI¶%ã%Þ¹x"š•í:5OFÂÝÜFšaô Q8GAüJ)\Õ¢ûK×ÓêfAWvŒQå¬ÂËËYWÛA*‹ÌFnSЈïi«?nîs:`SKe<­‹©©¯0ÏáàY‰·IÆmÚ–ô{2WRæÆY*»ažeøB¹i3ĈZFØËzµ‰˜}¦ÄYÔ7)p¿ù…\®ˆ#ý@¼Ã¶z`$N°@ 6ÁéÑPµ_Ðeå¡P/…~U°Dã†ÈÏ£ºŠüÝk~ÒÙò[A1Aóç~àée_—š^D2{;ÝFÖI!ÿ–c‚'-¸ìó¼2¹qâÔUå£?rC{3ÆŽ¦Ÿ”a¥ö^Åmœ¾ö:#´L«_‹¶”B/«´{ mls³üXk'¨ì䦱I/˜Ë¾¾^Ìt°a—ªƒ­`‹ßíÂßíÂ4¶XC~5¦W6?rx”|LªÜ¨µ°Ÿ¼[J¢ n]äf'_cpþ²Ò|†¦­)kÅ^ä*òñìƒóoyˆKì+‚ó„ÔÖø'Þ·^Ø™§‹å’M¼ ÏëUàˆÕŠÒ‰d¯Û´JH®¡¢¾@DÅ£|Dà—ûÅþb±Ùo{‚š ·A­ ?0B жG_àí{O‡4Ä´8󤨅å®`‹o¶˜°/Á–"XìåhqRj»\!_§OX"üäPhÀxüÊ£Å5Y@ÚI?0øÓ®2V^@%œþú$i£4ìî[;ìœLSíy¥B[ÉKSè:*/Ô×YÛÕ¾RSØ??çÿcLðr¼_–DPKáÍX­ÐG÷ÇŠ"ª¦©ÕtÝ"Tµ3š€`•&Úfpá¬0d )??n??œŽ½÷-?n_h¬i:O郻Üy?nþî›ýý·??Ü6&סàE#àºT}ͧ-/âìUæËðB‘^ø§-#Œ‡5bSC…€Õû£×z>ßÊ|J-9ü¦`Úy°ÑC±SàXp2ñPèPŒˆ3ÈlZèa†7ÁbtÔµ1žm§3²ŸJtÎ`©K­»Ãµ?nÿ2jdÆ?nnÊ5õÄ+ìš*Ñ,Ü4Ûd « þ]êžn¼Gp_B´è>º!üzýz¨êõ‡F”æcy;SÅåü@PßË)اP€óáóí›ñféÅbR,*³rB)ÑjrKšæÒU_i´%´]z0Jp|[Ké¡©<è¡O¼…ü{yã?r¿k9¼@AôU*mxÄ9¤é"dkž4¯Ýúý·À¸¿?n???nkºê%¢^¼ ”°°N¨×Úý»x“Í<ŒUy‹õ pÐóIXî'µ÷´Íj†EÆ(PŽÕ'ö5)‡Ú)jÂXÆAb(E¯Á=]œ¿¦#°Iå8Æ6ñ£˜/39C?0m¨Ûè¹QR ½Úã)ïÔŒS=Ú›K?r†ÙÃÄÓÚÙœ?0gÄ ŠXq„yJi{[Æq)õ¦*¥ ƒKqà…ºvúç ™'Aü…-½Mƒá9³™z²äÓQñŠe·¨÷QêÙÀ•ú6ûÛuÚ.Rïô“ýÈÓ§÷œòÀ»q3†ûeJD[öվɾÖöSYêÿHm!ŠïȆªž,Ae븼DÈÚŽ^[È~³…œ‚6øêüHš»Ž¿ I&˜dhZeƒð•Lõú[šN[Çáóæsç²ñºÈ° eÇЈ‘ƒ5&Õª|ãzŽQOŠ–§Ý¹¯Ù É­ýꨳ-þJU»\‹Â]§‡÷ÖžvÐŒôuÕš˜gGZsÁòçþºh§¡zâu+—R#–}v— ¯C·Oy™Pg)D¸ôÕÖ/Þ*½ŒH’·)—åi{›€r³0lÑÚȬ?0Àõ˜@ËJ#“†–W-€ÒI?0 H?nšÝÈØW duþÛ|jY ŒEA“+–D»·ôMT YÓ??ÞÒ{}/(K|¸FŸsYÈ϶8P°N6ÝpÔ]móŒ h,œ6"ÉK‡ã¿??|Çã(?r¦5Çòüè×Ð??Øþ·’¶ Qº ˾¹ð>ÒB~õñƒk#ˆXòQšƒƒ äCº€´Ð/âªÑ×ÿCÌOü¦iT½TXõ²ZòQ›ëµB‘ÑÚßÒ¸˜‰òøAqŸPcGEH{;ÒpÖ囤ĪÂW,"%Ï?0åa³ZÞjœrƒ‘F}ÊD¬“6îÎR˜ fio U´³Ý^xR®`…Ф`ÊL64:i}ûÂ܈‚},½¤Êê\ê¹[|:îO&&ZÇÇÁ¥y[ßòÃ^F`ošF¤*ƒ…~ÑϽî8ùãæ ©ì‡ÏÇ€å[þT@$%+f)Å䛿X??¬[&öTô*Ô¼Aœóëð][QâŽÛùt³í'o#´XL??Ú¬M ¿¤’’ÖªaÃzy¾>ò°GSžGorc­R{ø’WÓõ%ØÏä]b‹¹Q… ÊÉ??I®š¤cÕ«m]]ƒáÅu…Œ¤±Bä床€®'¡±™bVOƒcÃ#•Æs$zOo^PYžï]ÞfL…GÀ‚J¢³2ɲÖÚˆ}¿ó®K¥T)Õ„\T >%³‡Ušñ˜Ýâ/ñiÊdé: 1‰2LË0|ØÝŸ¶ç¡/Œô)éðJBK2*¡Nl?r34œ~¦w¡×»âQ_:ß fh—ÆÖ-û8®ØStŠ·?rMöhCÀÙ{Lö ²Ï±M0©èq´;/‰zý¶à<Õ+Ðw„§4¡²o²x‰±¯(d ÌQÕÖ®ñöLwä6Æüš­´$mJå­í>º©jZ;r>òa˹{‹»-ܬ]:??¥¹uåžǵz†›çê cõŸ†?0Û¾@‹áWlÒ—Ekè6fÄàÝY]}iǧm[+­¹k½2#rVÙ +#ÏÊi‡+ÌmÚŸ@ˆ-‹·îuoþÑ??‹r:\t X¥‡ÒK?0:ªm•ïJÛ$Kfk_ ä6KC)?0=-7©ÅÄ—”›++Î#øÍð™â …3\>ÁŸü?nLðõ7[¸Û?0$ˆ¸‹'iõ³”Vïw÷ûC³??`~û£Ÿ3Tiìc„éK<„ÎêOö/Òý3ˆ‚ø³ÒÜÈ×ÓŠ¨V±‚ÞsïûýãvC•‡ˆ­G¶ì'€`0¹ÞV#xÄ„­DŽLx›ˆÂä…^ª^n??ªòo°‡?rí¸ûô™ƒ³£ì÷ŒØB‰2a nØÛU…м¥L{Zÿæòìéj–=]ɱJˆ'°Øâåu#ÍçˆhÙØàÂ#ÛÕ½tmòéßOïõÓ«ìÓ??ž¾ÕO_é§p~|¯Ÿ~??ûôW>Š÷˜ÏüËþK6‡ûûÑeöéOï“Ožkܘ·Å‡žôøç£vØ­ö@,T¾X—Dú§í'ùRÐÈ×;<Â>5Êfû­ Ó…X ºS…Ö¾m¯j½jeÜe§°¿ šæ?nYêÐN÷;¼ÒþnÝÏ*=ŸBë§ú(Ò‰f*å•4s¥Åš˜Ûì>ßÒÜw]­Ò™Zd…ý~Èùª64þ SÞÎ3ÞêóÝŠÞ–“Bì~SÁÞ>û»©wÎÊ&è_K´?rCéÊÂvRb2y×kaW¹ð±MÖvW½’_ù¤¡²§MMèË”šS•…ÃÖ‘;î¿û0Ñüê#îÎÈR¹d»¾³rmRÎç?rƒ??„®½žBÝâzé6¿ž*-`‡‹ Î7}‰‰¾Ñæš™…£ƒìsÆPI@m?rP: ]*òÔCñü1u­ö„¸FD Ëóšp[K=ÆiÔªôÄN FZ ŽžÜ—­yöxùV I”òðÄÌàèyÆŠQ4|U`™$ ¦[d"¦„éº1T[³S:Ï,íÛÑ„qü‡Áð–ζÚj›D§ ¡¸"”]bEmÔ/×BÅÕ??µ†_tÑu1×Ç–øðœ¹®Æ '#\?nxoYmSù»N˜Ø?n‡ä/7¸Jq?nßH’Yõ-Þ{2%YÐ`†\1¸ÿŠE¸ì®¸AjÈ$Ñ<@]A¿þƒF??eT ùgaÜ2aíi)0ðî& ´R¯¤Q+’TÜÝc=ü,^9Þ#lûÞ‡?nJIȶêi2þ„„ܨ׈÷Jï=n€ñeÝ/l¥úmY?nÒ,ñø¤ëÞÛI~Á2:#syª??&ùÿì½ûšÜ6’'Ú×Sp8GkRJe«äKïjî‘å²G§eI+És+ÕpX™¬R޲’9$SRµÛç±ööÅ~F BT¹¬qÏZßgWˆ€À-ˆ¨?nžÄ\YDù¡ÏÀCC¬T¿è.FQT*JE2¢MÌ‹;b)úèᄯxmÚê¼MfªÙ¬àþ˜Ò®Bú_??a}Ã1ÿL¼"~ñ%A$ãOÕåÛº­Q?0?0n„0ëφ[QÝ÷èB±ô>/Ϫ±¥òZ=^¯»bãáÙ}Þ/Œfé½[ ~¿Kák®À‘ôþ}6TU=¦  ×6=*hæîŸWHÅd¬ÝÔ=¤¢Xñs5qøˆöP-”?rU”…Pä~ 9ÑþOö ‚&y]®>¤ áöëGq˜ÙÑyÎ?nÜ"ÛØ‹«Ž/ä&ú#I‹_ ¥„°??´©Ê×DñCäžÏë÷?r\@DT3ÌÕM<¬'ÃGl 5U­?r~S5«òòýÕ7³SÜ·\<@J??Ríù7":1*p1Án# ái°§Éu˜þJPS–zKpD_/Ûz…-']Æþ5u¼6I?r4Âþ¬mÖ¥~‚0—{ÔÑ1!ÁÅÅkÚ?r8·æë‘>x!d!L†AÃ( ‘øÃ,¶wóã†ocíÁøß–ä¢;±jÈ#4q[e‚z„ç³Å‡ö_½Z€>_ÜíFÜ,R2ÓÕE–@Fƒ‘k$ô¼ËŒ¢2,n½"o÷øeRPœ,¨&¢+×Tfæì²]õßIS£ÞhÕüÝuüû÷»¿ÿ÷åúßË»¿·¡¸§›Ëß]û¿;æßŸ}†¿‡øüŽû????¿{çw‡ŸßùìÓ»w??¿óùÝß™¤Ã»Ÿþ.¹ó»ðo‹‹…$ùÝÿ¥ÿþÖ «BŽ$Ûîìö?? mjÒ^¶«Q??“>õ/à׃ò¦œÎf¶Ï/×]ùÎ;÷Åà>ýç§ÿÜçlÍì¶#¼ì7Àîþ?0l§nŽv˜·šŠ4x"QšGô´ këm3¯f8c¦’7ј† Tebõ“wšÌPÓ)ÒƒžV5}ã)¡Šæâ”ᦆ¯u 8Ñ1ser×0T“O??|CeþRHð%#m’ÙfІɻ3•eޟבShÛÖ9ëg!.xµƒÛ´¸ó©0IY»Û/·kû¢û«T@X£êš³óʦXèN§Ýi±6Ó"eØ®v^\¿}e?n¨§ÆtUD&å3+b˜Ü=™ã†—D,¼TIç«ú´\ÑÍWZvž-º(Ô g «c©ÉPLÚGÝošªÝÔk ã‚`2bDi­ñz4Ô·ÞÀ˜pÒ—“$@Žzƒ_÷´ud ñÎî??Äýû:EÚÕ±­™¥*›Aï¶êì—)nÂ9(W”Û¡Ù!û)'ÞXºÔì³<–OW¬F%ÂA©™/O”˜+TlÐE¼*~#o䋯¥3²ö ýñ'â¢Ú§ì€Ró”Þ“?nº¹HÌ?rðsOš— ûÂ1?0±¿£PîYúr&7“¬÷ÛÉažÜâ‡5Áz?nPýWsŒ,µ•§:‡Æ9ÜGÚH°õtBÇHÛTÚ°ÂÁ2…"o*Tƒ|Õ›Û‘9Dã„"i3eÏ“#½Lóh!ŸX(„H??Inìq¯ŸP¹KŠ^ý¬][à”ѧ>èy½]köé¦nÚe(ûõÛXîš$U'ÛñÞšÛq¶*Ï÷ QfOn{èĽD£K¥g)K#€¹†éó¹ýU¾!Ñ´ º.ve±<Ý«`SU†¡ý:V«•JFzG'J4>ÔýÏS5àOÆ:ÕÀÈA•Íam3?n¨†‘ÄêøîåýÜðäÂ[×ìæŽ ”À”Å·•ýžH0ÃljàÉ“¡_=áÁÚ„ö÷Áå|U5¡œÿ·^®ƒ9âÖãààëGOü©xþâþ³æÿÏ>þgío¤}ÎÑãoœô??¥ÆËÃû_??:ÚGúQ2ÞOïÁ“ï¿??zübío%ÏÇú[ƒõèác[Ò‹# ðôÙÑ·ÿ‰U^6“pu–!ò}a«ð|çAêH??Õ©þ‘?n9ú_??=~pd2 e9øÓÑÑÓâųûMîwEµÃ:8àžyl¼Ø<zˆ½‚(µ²ÞsdbB‹å¼3éøÓ§Ìm÷™4êÇ>/üJ}È©ì𤹠?r埄—§O=|ððè¹°Ò+ŽšiÙΗˢí@Šõžæ¶±^3›js¹u]o*”ìçöøî¡i±k¶0µÆ‹ðêmyi2??gæÛz=]l/6mÁ?nAU?0ddg²L[˜™‹Ø¶gð§w~ËÃÿ¾žvÍòâ¢Z˜lÛ¨øÇšÿ]Û©ÍåÇ›ÿ¿¸ó‡»zþ¿ûÙááæüÿÛüÍB??“Ö­s+È??ßVåë¦:£™×º–ÉwS6ݲ\?rä6Õb;çKDz$Ü–gg“%£ºc$ Šð²T­­N‡upA üYUî¶ ˜iuó^À¯ç0$èØãòÐeçÝÈ-h0§©¬–;œI¬Ø©v D ¼@Ù<1º÷·ßUk®}æœò™ï?n9[¯˜0^r£€û«bpM½÷ˆCÉÚ…“u'¦71&¸ÜÀÖý¤lâ°Þö`?0FoÀ°ûˆ?nl”ÞêÝÝMO«Í?r×Ý·õv½ˆ*/lïnÇ’UæaØø™‹ ¶ªÞ¹®+Ì|Q¿®ÖFuQ•}¶9Û1kê³ÍÌÛ?núS|1á†Zó/˜¹ +#®Þó€Ô+´÷¦j9íö¢?ne½*ßTô¢ 0sP‚þð”1#²"yôì‡åüUܤÀ]zW®«zÛΦn&ÞÉEQ??„‘Íq5p»??u!Œ'?r³„ò‘¸¯d ;džȉ«‰4o+”B:Žk80iæÿ†Œ¸dLdLÛWe£mT˜-?0ô?r0o*ÜQÌm»@¶ùTëM×NÞ«0Ô—û@¸W/éÀs/àÒ¢gÿð`“õ+cÅ_®b íé?0G™±L áÕ6å|Ù]2{ð?rZȃ„Ì{{+éŠë¦j)>Ê"¾?0Cw/˜„°]bˆ‰·}Pºa!:ó™à³h0WÇ»ç5…)Nw+™;mEÄ®b¹*LÅŠ~üÙGNp¨“tcÓó{ÑÂ=e$7lNÀ^ É™ b^:-,¸r’G‰E…‘ÄêËÀ5!ôŽ w°UÍn´ÿÓHÍ"¹¬·è53GàÝálë®Jð€‘+iø#=E6èF¨Äa'¢?0c©r‡tÑ–k#päOÚmz%SeÛVÖíB»=µºòLª(HÓ]C8?rióI’î2“‹mÛ%§UR&L.©ÏÉ??­æåÖÔ„õD‹?n×¾ ¤·›z …Ù ƒÓ¯2í3ØßÌøhPôfzK;„„"4J¨”ÌÎô¡(Œ"E0B°ˆÈw•ÅÚ4f¿›O?n i.I#@ÃUØÖmgΡV7 ¤‡ÁLZôuUm`¹·\ayégÁ£˜ ÉÊ:ËrIæGÏô¦g—.3Þl7[Nt(!²K–dy¿I¡%»…ªQàvcQ´ óÙ;>™¢©€£˜ÂE¥`@Õ¥WkHí·Ýòó{:2¿7Xäö5…ÂIA!ÆRéQT¯8ªÑôÀx*9‚è§5Rpl¼`z?0•é ¢NÕhÎ83àΗÆ£ù;?0ªÇœÖI>Bhä+˜®ÄNö3]>F€ðoâDñâŸ>€ B"Š޾̴j‰¥à²e ?0"öwJ÷ g)_žù?nj Ñn7ø¸á=:(;oñÊ…Sw]õX”??iŸév³€}M¸åÄv’K¹TÆ¢‡¹ysg Ôª2Õ»M±í5x¦‘udWe+ÎA%~ÆÈ0\’°ÌÛÎØéõ‡ËÈöŽ!"»º8XŒ–^TâP!Jñ=ZŒé…€ôNLeê]˜ÎÉôòô&IgñLîeé]’ΑiYòôVI§ËnIáè“ÎT³ Ÿ??0"z3yh‰²ÞR—­—{gÀ€8N!¥éɤÿ%MZ5uì@èèè\6-°\¹T(è4T¹kRÁIQðl?0PNô!ùø¦u|ÈsÏp p0nUgÈ|s@ߢÿy£‡½P’F‹‘ÿ£Ce ª°E­(K¬°Pé3RÔi&5os‹ˆ²¦–¨LO—ë…i Ï1À¾§MF¬h{‡¯=ŸãÃ˸"hº¿˜%›n€»ËlwÁ1Iõ|–¾xU½äzµl=]‰ë~M¦W²xhh`d­uÛ¢›™ží›Áiåk¶*/Neòî^ònºi–u}­Sö¹ëãÏŽÒ-x ³ Õ‹ÌpõDk3ÿMÃ$Á+…þ磺67áö#ú˜ƒ‰G”¼šCõ#T«LLÙ@†ŸJpŽ~E:>ºd˜%Õ€üLÄv~iÞF??q0Ê¥©ó?nÙ‚mô,næÎf!.)ñŠ2‘+<â%‘Ld_$Ã"8QåJÝÆ·pšÏ†k –ÚѹHh‰$%Fòú­|°f°ÿbÏ öshÕVîj=µã½ÓFî.BÞj©:¹Þ6ÛêÁ¯%‚ÛfHÅ›—,¿.'¶G*\‹ø}ÀF# ¸÷§¸\¿©_W\?nW*õ¶ãä4â!‘›cº\CužÝÙõêõúÚ5X>ÛBµW<>,S x'9**Î…>-v±6b¼ëõ¬?n!.Ô iº2Æ™1£ñ±ýñ§\m¨C²ÑãyÄhÁk« ©Gj¸Ò=ó7Ý:Øç$p«X?rj‘Þ½HÁOIéó×Úë÷i¡ª×’ 1‹GšÃò¯«J^]³«?rúãty¡j`!¾#“• eç—íæS*UøÀóç51{ࣺŒ+;ªìwÚÁ~{Òÿ¤îØ xa=¯Úöj­¡jF–›þšÐï£[Ó??÷’jê–¸WÛÉ~Uô_ÁèÖw²]©£vþsTð¸¾PŸŒêzbP*ôÞ>l­¡#²0gAdbG9 ²kÏ:Ü>̳*8ý>¦ ´Œþè·:*AM\K3."TŒƨº9´Ð˜t_øÆ;kت±·ºtAM&ê 'ÓÞ;/%+âB§:ÉOÑÊ/ÿŽ",¶d|«bäDɸŒ¥}`D× FŽO?nòÙ”o©Fw Ñ,^-×ì½epÞT§YÂŒŸeuý×?0òǯŒDÙÂ*??õØC3zX'ʃKÞ¯2l¾d—_i—!msxiÞehùûå–4îæü@Ëo!ŽÞ•(‹%WWëzÍ!ƒ²ÚÍÎB›^/RÖô§‘?r?0Ø1dˆmJ\Cs1Ó,9­ê6~<ãvŒÎ毌9PRžá&ŧUÖ|ÛÀj©wÅÅ^¼‚Îs]MC¤êAù‚{Àë¢Ë‚íÑìæ?0¤§Ž\ã-h±)»W=26%¦¨øNˆ¡cžÂ^u0¤è#ĉ?nD!ƒB–Þ˜ðB@ðÁk¤ÝTsI/Ò\ÝRöúuSó_攞km3g‘Ã⤦+·Z“]©L3ê‚ЊûþÐD¶6?rÁÀd°™Øn0…á¢)Ê7Ø‘ºj–ŸP~p# Ì1·?nnÛ„‘+Hši✱(cРDZ!²jç•CPôàwÑ©Äø”ÃM+CÐåV/]«! ‡¼·ôuËú¿QmЪy°ÅÚëh²V¿PnÜú??䢪?rÎúñØþˆ1˜my^Ͷé‹fIŠsª;͵å:FýÒ®ñIšlÓZd;MÃáÈÄqc¤ýX/*R•ñº±‚7Zøâ…#¤RµºçR YÑ7\úz-Ö÷$xùû&‰º)ÂÂÏŸè?0 "ñsÃ?0ÑLNÙþX‰Üîy3\”Ù@ûW«Q|Å'º RpªÒ-<–Ý]O¹Û|gó¤º@:lrýrµ!; "ŽuÖ¢úɪþ@¥C?n$yšrÏw5¡³ŸSw5"°P•Rä""5DGqIfS.?0nSùîq?n ÂÀËÙbÉÛ#I“º°å”Û²¥­úíµÈo¯Eþê^‹D^rLc/•”GÆto\Iáñ¡óe@è\=¢ aZ£šBT‚ LGhù” %—:KK#çFp¢íÎÿlä­ê2ÇVoàÂOÞ*yS2Ç©JäTº&ÿ»ãB£8Zl%'¼3÷4m¼øªåv¾Úí·lÂEÕ½ª²ˆó2HOî½ç×Ê“ïvÓÕ‹÷\îœTÁä3u¨¥÷<Úž“tvÎzòŸ……rî›ÿbH¶;ˆÌ+‰“¥*¾Q0WËÐ⟪©?rØûZð¢^lMm°dÒÉ&¢Š›H­"$#\jbšIÕNšÅnßЂ%ùδXçK`äU—àäÑ¡G¸8&á'ɇcAò@ivñ)Í€9pð<_4fd%¤‡\lßÏòzWr¬BÐjê}’îS±S´×??'VUà@À?nxã”ÍÕ¹°Ý™ìh•hgÕ¯0ÞkÂaÆQÞ^ãSªã`%CQˆg7ÎÎVbÝ¿ÉuÅi–U}u¶5GÚ‡ì«ZÔôé (:Ï*b–üå›ri«DÚ;r†{€§ÖójºgË…ñý¤¯Tž …éîÒVSrJd˜ h‹êMµfÛŽÑ¢PC\Âq1Q‹‹Â'<ªmþjÄÇi9â=Åtò??@;dvØ??ÂäÉ¥‡Jeh??=\~Ÿ¥¨ÊÈá›fâk« ’ï]²A¡ã"˜xæŒEc4¿y‰^áÔ{'s??p9DÙ‘Ëjs˜ó}?n3ÚãÙ4·×óÁ¿­‘.Q¬* PÜ„jéôï`T)êfúˆ<å•Ù¢ÑÛþôC6Ù6ì¿o´/g}™ñ‹#.QWùp¨Ñ¼+ªXÛq—ËvfÙwxÀ]–BÏò!–¤AFñ#àANŽm|—ì¢ÜdKLPï8äÅ,ÍóÜöÕ;tŠ&Æ`ÿ-…ýƒ¢yâêÔ`YCCdØp‰?r–.ª‹Ú¬“7ÞÁÒ’¤"ò6ñP,IPÐâ0ýòF›Üh¿QõTrÊA­øZõààï‚??-¼ž0C:Hå~F_`s„akŒÒ›fê™pyææêÓÏFDá—uÌ[àc©’ƨ$–MÕ&¥eˆÍ>Ó0bW‹Ý®³Ê]µûùhš$??´Õ ú«*¡'øtг¾£xÿéCë9«lq·-{6ŠØ)y:£÷5ÌáþÞ‹_œäj’ö‰8_œ~vËäpY0z‘¨‰³0º1ýâÊÕ«îb%CLI:ùÊÍø¼¡vWíiƒ„)Œí\™_xÄs1??sÔÛœ€%Öø13Ùº§ú™aŽVš@uš'öqMñÞ]9¿eš)N¼_è$#²Ê^;ûF=„XyÅCûÃc¶¹;Ñò£›€mOœ»ÂæMð6_J‚Ø6M<Ç+ +#÷Ï"rrÝý8íu³IKfÈñÓËÖ*;O·p5¦3¬à$¾'y¶é¹~ψ‰»¥X•æ]:°!˜¯ê¶b×jÀ 6©f`.zØÖ˜!0­¯ðÔ??ánL¼ëÛÂÒ·§lë¤Ø”’:'kNâèH´+¬§ÞM-N•1*7Úæäq»hM(š9öbBuäàÝ-÷ß SȘîÀ4žŸƒoÆSPªŒ ๴ÚkZêƒ §6Ç“L-¶±=)?r?0?n‘Ãþß3|Ndl¨Õè*Ÿ€4Ô8KœòaUÌ⟣•«&íŽXwoÛWÉ  }ØA*GbÞ‹š$Sÿl t‡U®bNÕµVÿÀU6—<ÝMûÔUoÍ’ÃUËó®Þ<„ÄhwQODb!½öµG¼I0-©Ãå­Ð¼Ç÷¸t_HÏVk%RÒ­³Ï‡;Ôì¦Æ!ã+‰Å—¥D˜à»ºNÚ 3?nÌ4–<ž‹Æ?nqH.Q”¿ÏÂxÜh!Õõ0óƒqéØ5(ÓÔƒ·WŽÿÁ*@D€ú˜ñŸ¾øü‹½øw??ý-þÓ¯-þþ”›«͈GÅPÁ+ô朆öN¯­÷fË3UAÕf/ÕÁ›Ð97ºÛ` ïEpŸ–óæƒ6Ej׳c|fŠ''­âº0œš#¼½36ì1–A5Øè‰‰á§‹Ê­÷$I›j³Bô¦\wÇv½¨šêÓ³R7É6M%*æ•;ø¿pûÔ^í S> ¹uñÆ™]\’\àêöƒíÁ^è‹p¸ñu3š ‡BèŒõÿaŽà Dµ¹Øe2˜Ÿ-wû~/éòèd¤òÆ+–gÏ€Y²éêh·Â¨-ܵ×ÜiØpé€óq³õ«ö·Ç‘ÈI?0¤Ûynˆ»q‰Óšm?ní«;¥ßÏ—oªuò¶jp“ex¹— úóª5‘Ïöë?rÝþn=š>Óò¥ûêlè\è×ñíÃ̤ˆP?r¢e–õ«sŒV]¿dLB± Ÿ¯£Fy´ðÊеW¨'|5r(‹?ró¢?0ÈŒ'Uí¹¹\·`3p0Vó€; î{XAÆõlGÑ??RT3¹±€Õ«jS¥T%Ù]????·°¡Júä[ƒŽhFºo´Ÿ˜òúaË0¹k5L“¢Ë¾‰ ¢ZCZY/ªñ“>9©v ÐuépòcW,n'‡'¡3øC3\Ò7zÉBVa7¯˜JSµ‘:µF¦YÀOãQ#hΰèJ/.ZŠÙ×O’Ìow§´Í EHÅ~a1ÐQ2ÞeУ^#{×Xœ8€¤ñdšÆ?0Öój¾…ÈBú־Ƚß;…úkÿ\½Óq????Îùÿð³ÃÏ>Uçþ[üÏ_KüÏÍÆ,Ý5}ô´ö’C§±±5¸¶¨žâ.÷ªÑ0íFþ¢êJ;Tÿ*ãY^=läoa#EYÊàG’‹íX­®?nÒJ¶Šµø žðäCÅT6øjúýÁ¾;zñâèŸL Š??xñðÉc„ËÎÒG‰óª1ü\óo³\oÛ¢¡«öÝ Á&}ÙÞ|¹6ÿ‡‹âG'Ϫó¥)ï2à mÙÙˆþb¾swZ¶UËÏ›[˜Æàp'â\2ÃwÁÖˆErËú鸅T¾ó]ëªdþd‘íÕq’ôúž]yNO9;V°3i“zxçÎàÆPu<üÊ@ˆÁx”ü¥=ÈG?r´ùŸ„掅²òg‰ÓGœÁ„Q¬ÄÌíKd2qÿ?0Üv„­ZK?ržó0?n·Ñ??šq qØ?rƒÞßvõ‘}(:áXoùéyYúÓȆfv­÷nSÍ»,µj)¢Èr°–Ó[¶LGÀe‘;¹¦½9œãžÔ‰Ì6±Ny²í6[#`D{Ivî[6æ—+ï?röN$^¨º–©˜&Ôü—öÞð?r$ÇÑІzˆéëê²ÍrgÚ1läpòôwî™òøÞÝ“ä«Y’}:I> ßÒuº9«›‹²Ë¨à•sÓ½š4{2œwxˆ¯b¿\Çß5Ø¢ôè´„òÛ¾áå¹›gôv¶·Ǿö´Zmw¹ò;-–sHÛ‚ÏsD%w†K_1;Zù7†£ÿNÌÃSÕ=v™¾×—ë÷n¼+–‚QV–ö-‚}jÉŸùBõ^B¸‘“™[³þaž£QŒÄ¨Q•$ªšÄ ?rJ-ï¹tÜCO›þ’‰ÚUCÄ ŽµTÑ%=}9­z]¨¦¢€ ×;p~Å/c̲yõÂtSF½Óae‘£ÁªõöB†åí„™ÔråEß³F—p ,æ#°ì<(|æv‡‚¡ºw–;XÕõfNgÌ¡£ÐU†™Lï pàÅÊ?rìùÈ[ìLmïÕ…×oÿ~¥÷¼©ß\~Üû¿Ï>ÿü޶ÿ=üüð³_Éýßo÷e÷Š7åzQ_È] ÿ2ÓÐÚP [6CnµªæîE W²[)Áí^W×ÎÇ«r¹È;oêíæôrðn‘f¥øÍãPn[Ÿulœz¾H,Oçq{è뿼ã¡«§éÁAÜÚ.tm„×¥hŽâßÛz]à-ZëO¡,6ìÝ6+²‚5ð/†K¦ºY„®™ÞÞJ'HúáñÃO¾9Ê{ÈÓê|¹^£­ìÎ)€™ß~Ù¾Ì^þøòøå—'·rE†Nê^¸ìL¶§^¾5§½èª~k?nÌÕq;t’¶g$ÂBAì?nu«h2‡I \ {ĺíú˾C ;\|iî} o{pÊZì kÛ˜…BH?n#ô϶tŒ©N{Àð/¯¦úÿ#›Ê\›s=´2(1ÓS.ñ½#·´VäàNø.,UZ›qY@›È ÒLn/^.–WéË¡Þ+ØŽ7cŽ$=KLR-w –=­4ߘc.,.:òMC%M‰ cY²'cd?rŸ-8Aãœ$7­vBÄÕ??{LtÉÞ™C°öû¾X&3ê[}¢×€)h*ƒ2L-õ£%T•¡å‰š*šíx n”’ã6.ÈÑ.ïÛpI}§ïõé°ZíË¥É}ÔåºËÞY‰~7]¶‹å¹‘ñ^rÍHñ?0˜¦9—×xèI_¹E]óŠv9Áy]^%óü²ãW‰aóvÁÄ"ËݶíKáý—— 3¼ÐY±šª¹ÛÃFkúIëFQ?r)ÿ`¸\˜,ìPÙ1Õ?nÔ+‡™Iÿ›²É‡-óìFk½Oì³úz9(þ£%mÿ$TÄŸ0 þ›dɃawy¼·ÙàNêßae#v‚?0õj1C3h§¶k^ƒU÷QÞþ0¡äYrûщL*ÚÝqn Ç|ÊŒI¢á*Ÿ8@†Á¼ç0??з,çÀò¤|P7 Ja,ÚàžÛ}0WÁšÌxX´JV8ß;F0+lvuv€¸ÚN’ª¹Œ·Í÷}á­œ*wJÀ6vy£± 8.Yï.V¶Í?n-Óè~8è Ýf‘ýÑ6MôˆÙ~b† ž?0Xóÿ¾‰˜f’8“?n`’°¢ØË ÷"„T ›à¼#Û5¢ÀR'•®©…t9׈L;ˆŽà°Ãžæ+"&K:Ì -;ëŽyl¯ †Ûñݲ[?r’HuWï­Éæž„¥)¹E)‡÷Nv"2ä¬bàÜÆ;??Å»¾ò76ý^Oê„ù×ÊÜq8è-ôŒÝòMÅîO/g©’t·”±7Ó@\YXé¦MÝî.@ª•@Ù"÷àõ|ÀVaÁ§Ÿüs½µñÊêõê2AM@"œIÝpQŸÈ  t«6¼¤Sÿ–Nôžj#ÂTØs£>MðéORø~Ÿ64ó FçlÇ­ô@ÿ—¦}¢åîžwü RU??t»l{ëØb×ÇÁSŒ„1ǧ½ÂÍL{??±‹j7j/0÷áÍ…ad<ÈìJ-DíÀíóq[¦­ªµѪ)…ê$‚KñÒÜ3˜+óÈfÓ<.hZ.v• x89>S\,ׯ—ï‚MŒÓ?rí4 Ÿ:ºýM¤ºA<[6mÇva46æÌ…%É;¿íž®féã:)ÏÏ›êܾ!$0šÚ–MMù>KßÇïz\ßꀳc4L L[øã#:Ð_×4˜©÷‰€ÉŽI@œ[Ðü¨Üšô8·ÌZ¯šâªÿ,ð‰ðe)|YÖ«ª\­«±¸¬ÇßmÉ?08ñy%ï)0£»T»»‘*W…ØW­©ù—|­ãP«&_ë!ÃeNÃCÆH4OW>E-rÚ¼éÇßDاÌ'œEL%À·}c(4#ñ¢=ËÉõ9OÑ¢ºÒ]©Ô@`%P,+º£Ö:\‹cÃщçž…y‡&­H,䀷ÐÖ?nÎë†=y¥~áoÙUÿ¹9Ò…rÀ›‚Vçíra ÿûð u6ÓÙbÅg#L¹þÖ +†*FV-)“-”økY»,/‘e+^“Uùs+²3=âõTiU¾¿FÁiŒî[3Ê]šÎ_ÕËye«qœÎˆl<\"¯‹Õª‚w4º;d>]®Ëæ2¸tœ^’'‰³UÍð9e`O1ëQíªqxçîgIݘ¿æ‰•Ø]Yt9ËfFú§å×t¼zýušOÂ0ß3Ì +#÷Ã0ß1ÌwÃ0/æÅ0ÌS†y: sÄ0GÃ0ÿÂ0ÿ2 óÏ óÏ;˜^&l«+ëIV&_›Ü”à ü¥í‘}ð %(AêÓ±\Pùk÷¡Z¶kì¢InÞL²¥ÑGÜUšá оæpzx–ôZÚŒ(ö˜¿·˜9súWE•!@†ä,úf^9­ƒê.…/Ú5Fÿ7®™£KvÚ´ë­Tˆbµ¼XrÑu}V¯ŒF‡·™]‰mLŸÙT+zÙ¥8ÝÔ«å|iGZh³3åü¾Z«þ`—á'D0Í{]PŽÏ㓺¹qåjeOk)g¥9SåljwœRý§&3=Q%qTÃ}í6§Ï’=r”“ž8ÕÙi2é(šå9»ÊgßÁ æ5ÿíþì½U"ìŒôµ¼qølB‹ëNQ¶*ׯùƒ€)??¼-v Âc¶šâÇ^àŽ*ùÄ¡ðIRöª1Ðjð䓼ÈÂÙë…a{µJ}:p9ßTõm×»@ŸNïLÓ‰úMe†#ù†ùGbÌÏ·1éWÕÓw%'×çr·ÚȤeðMjß'º²àƒ%t9ÕNú¾ë›XÑäŽÝ%å:¥ÀPo6ÛäÖŒa¸`êd%:Œg&R)¥iÏÛM躃Ýñø^cf‚MvÇÓÎP†öÜmÙfŽø—RPï7ùÀa¢–Iú…†²ÖTžGDÎG¶KUË8ä1”ôÈŠl<»f‹çûV‡Û6\ÿŸ›Aw÷óÏÃ2ã-¶j½˜¥Óé4\õ¶¼s”¹??_Q:ÝáOeÎb&¦”ßÏZ¥ufÐ3d^Rá—a÷O)½Š«Nî?rÃüyÝñZ'À’û4Q°îøÄ$YhõÉîën}û²oõ?0ûîziÇ}ã{Læö®~'†$>û¯ö‚Ó¦¿™OR˜æã•ˆPgd¢“0÷¶)7ÙV›Ñ¥??4ÿáЗ}áY¬êõ¹Eop± E—çV>\øz]¼ºÜ¼ªÖ ¯_’8–„ ã©„®4¤¤)Ó°Ô‚»Q?n€©ÌæTZ/äkÈáö´ÿÁ4ëIÑ•§<ƒ`ýEma¼}wo0½×o:a?0MwŸN˜Äšè/o¶SSòà=¸³×ÌC÷ƒl«ià顾»Ë݆VÎîóŽÙáȉu„¹Hð)‡”g ‘ªª|Ê‘€ÿ†bïôTÞ×°4Í›¦çžJ¥À8é1n!i=¢¬Ûok8¦Å¨£«ú>^¼óGÀÝ€ZŸj·j0æT½K.ןt …!Äu‹o¹"šÕ%dp·S4%uÖef‹÷©°qvú?r©Û¨q°’بÛÊH¥±7ç*›Kˆ&`à£Ýûá¦KÕ¦â©Ò/l³Å›Á=›­©pವZÊP³­õºB®o5‡žg_EÌk(í&HÉĆSŦj¨ÊÜïß°þ§-±°ï¨ð† §^$é¤ÜÂ(?rN«&YZm×y•ŽÓpx×D~LåãÝÔŒ:3’ùå>¯Rˆ°èÅcÀ6Á-:s)šïÃ<Â@w±Ifhóc[…{†–ëEºJ[·ø|5‹0ÎñóßMõÀ0"9§e7Å’ƒEÆ®!QááêŸD¯ueChÀñ”WÈ3„æŠStºj(ŽOÕH¿7ª-…—/¬pèôã?rØüf;àäöŽF´Q¼àFʼnu‰-–Or*1³¯Sëuº???0ßvÿÈ€ðNR-Wøk–‡ºI3-§=µ‹mÛáMÄ& HaÊ$R¹_6zŠ‹Ý[[Â5#?n¸Î¥[|;âWxï^˜H qï@(¤òä÷{‰peö ½Àk#S†<=ÊR'>:Òs|V?rš?nÓ[z’»ÈSy†{/AAµÄßÑsBÿ¤)tQ«m¸Ç±‰Uö{}¯e/îðYC:™ÕÛA;»âÅ€¯|bÙ#?n'q+Ÿí…Ï3Ç7Ò³šÃgw>ø†–)ò%íÈ&V¼ÖÜÑóä,§Ò€[iÉ$¼C+ðî*‚ÉËzw»Ža«Û@E€.¸®j¾­‡yùzïžq>¸Må;µÀNu·Q½Ù…¿ñ÷*sÊ5R¬·áÔ"G̼åÙq'(ñÛHÈœ/mpÃG^×b›{v³MËüöú¶žŠÿMÙêËŸªÚNòÄ+ìèFbosº$.-ê™ÃÓ´f==­ßU‹½‡Š`?nQ–§û‡f’/ȇ=Q·õóø"*Ë3w×Ý“€7SÊúôÞ‰^T%îÒø½‡$707â]3éH[¡Gq ò`Hd÷»ˆ–]²÷ñæ&5ëÿêK6)T65Dâ“‚ˆ“‘õʶšíÕñ£ˆGŒƒéRdK—^G·}¿l¤iMš|ÿ½1Öz6rˇʰEg%Tð5?nçÜñ¤DJËÞ…åä6T×bŸÜen)%ø”™Ü;¹Ù„ÇÈlH!jñžâ©VÇZ.yé´„ï±ÈeöÒHÈ”ûŽ«‹mh‡wjWØ.ƒ­~ýðíÃG/Žž=ßùMËSøó4ÿŸÐ7xHïñù¦O´ÊAJµ??ûdyœFyòÍ?0ÖH•2é7g@‹–Ú»Ú>eAPýT͉ôÌ¢çbœG_œ ~È®†ì‡Nõ‰’Ÿ¶lZÎÇON^ÕŒfr²¼èî3%A@¤D·¤^C9ýeñ¦ÆæÐoÎTNÂ=,¥áWŸ¸*¹øÅ‰V1ïuÅj¹ƒ[?n\ý–{Òþ¤d,`”h~pm~ì Þ1Ô»>‰¬ç(•~÷d…Jô›3ì¢ úíeˆàÊ7ÐÕ/çÚβÚ΢Î‚Š³ÏÀÏ>[G¤Š¢‡h´Sýö2„Aùf€Õ’ÙÃO&(òíÈ6)lL²{YÅy|'Ôcñ'go/úŒ-7ª}IJ‰ö''7Ë¿(‘?rPœ¾ƒ¬QýæŒÍ†??¹Êü6žQøS²wÓŠýÍ»ëmÊãOn¶Ñ\|õ™ýófÊ£f‡NL”C¿'??üWñÿ¶\À›ñk#77þóÝ;ŸÞÑñŸ??ÿô£øûÍÿ›ïûmœ7³7ËvÙÕ?rg??6Cò(éàÀÄä)=¹ÿMñôþ³ûß™ fí&8•ÌgGÏŸ<ú‡#dõ± œÌûÞŽ,ýÀÉøáñ7&HÑã£o¹ÓÂ!ÛΛh{yqZ¯Zr“m½ØTëŽS]¥†I2Tž÷à7óÁi›ÕWo °‘'Œ¾º™Ìƒ»åL̨ÕÀ˜º]¸ä§}±Xì €YØë¨?0§åº\]þ¹Êš öSÌÅAqØ/11ÆÖâdH»–Ò•ý¢ä -ÿ›oÿÃg?rK4§öwíÊ•1A´ŸEˆ;?n~fSÙX??)ru¹¥·¦Ç+Mèõ5#~î©ÿ|É{V׋òáR¤¤Ø‚ÆJajá…¢û{¶ VHWÅEÑJÛ)­¹´Å,NfDÀ•”ÔÖ©v<¶˜ O}‡„Lí<p}…zûy¢­¬ý×N!¡£§Ëáx®t„«¡(õ"(,öÌ)îýÞðYç€Çܼ¾¥ñ¯ipŠÇŠYe7*Ì?0&0ÆY"ÁÖÔ„/š%=苤´L ¥LÑ‘t¯Ê.I÷ÑñǺ§&GG1| U³Ë«I.âZ?0>k áv‰ˆ „;çq#ÏMg,v²‡Æ-lRï6cè0[|ü³=6¥àøÄŽ7fÎúzUÏ_Û´<û}9oêÁ\ºðô)PÎs³¥¬v_ÏüŒÅÑ›rÕGÖû¾^p_¯µ¤ ñ~Û.Ï×¶¬oÈÓzqùÁM'e#Ìš.ùª]h.(WÛE5ËRÜi¦“<ÿŒ=yS5«òÒ6êGo•oëÆ+ú*Zgg)H§š?rž ÓÁŽâÃrÍ󘋲¥IP…ôÃ??.B*ëØ*î³Q­p·¡?0€"JÔ×_ èúñ•;V°CVŽ N:—??£@½AÃý ZöÄiº|æÖ3)#ç- îI‚ÅE[oº¶mÇ”++Ìõ”ój]5˹^?0ŸÔÅ¢Ië'sÊ5;?n^²Àzo0…RDë°]B¼ª•L-“ú9ñøõ’yš1€îZDŸôÚD?rXm;\E•Vx»–Æ01™ín[s8Ý??I[|ޢɀ÷éÚbct-À8ZØ8ÅH!ߥ¤›òù³êlh˜\…àÃQÔôô¢”Îì‰{yŽ p…„ˉo´\›SY)1(YØãê´>KλÛÓ»™@?0ìW=Ƈf!ü?0AªŠ EÚCך&_\bÐ6ß÷XG>ÍËÜMZ:|ɱËîÄcgâ•v¢åˆ6ã“ML2ímã(’þì«`ß[:/ zÛÓÔ×ÉCøØ‰h<\?0L襙Sd¤N@0 +#öñ ·Qì<¼'5ƒž¬õ¤í“ÀV‰!Ûѫómº ]ý?0 éb¡Ë¨+P&C]½®½ôÛŸtÕ^éê§3tÕ»9~Ò‘4ÞhήxÀbô¿~ûŸUõ®jØôç#Úÿ|þÅÏuüÇÃ/~-ö??¿Ålúˆ~ðaÚ'­ìóÇ•w1òqwA”[o ¸X Ù“"p?n[ñ ¨-Pp?0×&{7[ºrˆEÖž=¿\w%¹L?r7|ôì‡åüÂÚATÌñ•ÌvÙçð‚&N…B[€˜¿¥Tö²y¹þËËæ//×y𙧆£Ó¢þ$;þ×O^¾<¹™ýñÞË—Óþ#¿™’&Í'IMvêf§}všbYy~ }Ï«&ÈËâV‘ß4ÿ7œXÓæ!FRš÷³??~ù7/§y’üm²¨«n_ÈG‡õ¿P&S %dhCiµÖÏ|»<]]&EÒÂlží-8•AˆSÁÍÿlz™U®pÛ??ïäN6ÌÇ·^Þ>ù£W&Þ±×k#zú—B_Ãò»œE°ÚÕ¤)ù%DÃ<üîñ“gGî????Jþ‚ï8zöõ“çGÓÐö©…äæKÏþÏÿþ??ÿ¾ ¾ì­¾ÂirvI,²IèØN†d縼ýçû·ÿ¥8éܹý??Š“›)1i„tþº`ËröM[D›MBOù¤´å¸ e‡ñ—”-{ é±ïƒOþtô¸¸ÿ?r¬÷hØfi¹X¤9ç<nÚÇÍ´{‹]þƒ'ž¸ÙfÊ©ÝÜï¿¿ïå^\”»Üoþƒ“·X¾‘œ'/ÜœºÛåý/'£ú]ú·ž~qôÝÑ3'¿Ÿž¥Ýïï–€1)¼¿xöðñw’˯8ÿÉÓ£g÷_úî¡ÛN§ØdSÌ'yôø›=¸j-d®ÞÿúÑÑÉ7e³„E•áa†öh??»ÿ{d›ò­¢(!Æ0L‡g–£Ç/4-Ì0Ø¡=?r­h2l€î>”ŒJcÿóüÅýGÁò­—¿®ì*Å…ÂÔ¼(<戱Bµe,]c¥KbœH9ah™ï¿p'õEÙ•Îhxøâá}w‚rzY®dò­“[Õgx,Ë2ÝÊû¶[é½d·:M(í6ÒxÌ÷i¿G¯'œ†Doaàœ›œy²O»4žUÎòØ'þH“‰¥O=Fª·ô'»Œg~FÖgðÒÐ'çï&ÿèÓçbh7y6Cz¿Vöi#iî+$õË'Íœ´ò—Rü NšIš?0r"oúä©ôÏ&pÓìþ!ÿ‚T^‘˜Ï‰€b3ѧþON•%Ï~ú‡`…ÈSïrà‘<_ç*l'[KìÀs£‘ǼN:›.í?0w½RÚ¢©7¼~à¤ðö;Ín´°qEÉõªèƒ¼Ë98-G%6ȵ“ø•¸?r–Þåy޽*…zZ]ýº‚¯lÿl~U*þ¦Sè$œHæ)EgÉj*93X @QVÙ%!ïk[,Ï?nã"PÝ=¶™¾&ªšû%¡Œ>æØÂôL³<­¨eÉ}“ü”HÆ’†¾Ó’¢½# Ž…‚çèøÇXÏÝKR;õ'õYÂ3u¤Û ¸™õG?0@?r¡;$?nÅÛÍãn•KìÎ#Œ&œÆQô†Efß§?nþ0¦”7KoT¡X3GcKÁqL–\ܱýVÞä÷Й(XZf….côP??YcT¶‰#º,õ¾Ððž[2›¤lwµ¬÷€ÚƒSdlM‡™)àÔ.ÃÿöÂÒ‚Á‡LáêÈ^¶Ç¿ïÁ¯¸gø«KÀ•º´:—ñþzJ?r)¾pÑ+™Ø7í èÎñ­"‚Ójc?0¶«ªu})šà•—ª0T¼¯þ(Þ¡7å­¤Õtzá€7c–ËŇPÄÏQœÑÁ&LEÏ7q®4µ«óÄg¢[zN s6žf΋&ÙŠ‘<8?0PÀª‰$ƒ¸rª®³®w¬UïèüºÚ¤ÿzœ¼ì^¾1ú2Ä?0øàòri¡Á–,þÂí¢ ‰¶?nQ´MŒÚø_ÿb”º³—Ïóüø__>‡rš[h|ɺ}<¿¥ïszo“j›A‚åZ»¹1k*—«mSùÏWcfLUÛ–çpý½jg¿¾Œíá“cúÙp?nâešnÁ¸ÞŒ†Y×Ö²£mÁöèenùü]¼ •FºŠ¢]Õ]kýÕ’­ ÓQ‹W¶i°]ì.3¹®Á €»ƒ=o꾸á·o¦ÚM7ä“}æ:x#öG÷¶khË™Š["yÛÕFðüºbÄ9ºOÞ9qÌl˜8¯à¤h~Z)K¸Î© L©zÙÀ²®¯¨ùCØ`á>ÛzÓ¢0; ­0q*t¢k„{U›¢\_ö5ºÉ>SUµ\›þ”!U½ÑLªB5³Ç¾œYrtJcèF3Iè¿\B³|†š…Ü..0Ý¡ù¼kªòâaŸ3~ªj-žž(5™õÙvW?rW©þÐ}×íÓ~Ä¥¨é|Û¸/Ôþ|ÙÊÉÀï!ƪV.{yÚóÂl?nùtHäÁ­ÿèæÝì°æ%"ün^ÏØ\5[GüÞaå?nh³m_U‹dFWä™ÊîݬáŸA…"“Ê×?0Ô¢ ¾ÀSyÔc&“äúp’xjÔI’¦–ÎG S@®÷)ÀÛ dfÒB,ÚãžÅ [#1^×ÖõúÏUSÛ•‹¢¼¿ã¥lÇJU·Š €âŨAÆH(HØøºŸy¿FðB?nÝb<Ô«EÁcOúƒ³%ž•Û@>–Y‚ˤAJÑu' ‡JY­?r«îpä=+«&8#T1(¥`§©”|c ##*Ÿ(ÓÚðÀ¶‘ÚJàhÑÃP¯«jStf¸¬`<Øã?0”%?râ€Yɵf´j|#¬¿" í´Cp퉃xd?rI<‘G’§Å¦Vg<é?rù=GŒÒk„57/*ÇRïïÔ`BD Ì/Òê†r_KÑ“¥ Õyô¡ÂoÌ…Ò“{o¤½5;—¾ï&øúžì ŸçB +Ï ­q§™g™:Ù¿dµ}¬æ'ÒlM]w…Óv‘ûAê'Åí™ît-??0ÔÉÿË—ë??RxþÔ–Ń~»6eµDi[Î5êè“4Ç@œ#("2j¦ˆªiB³§³=äàì¡(a4Üœ??úo[ÑÜé½ä˜¡µ†[ÿ›’™V6½ùÇÜh©a?01%†Š’úwÍeøÓ/wf_Ýh³—·ÿòòÖ_ò—íM“lþKëÛ7Zóë/†©WÂ]œ«ñHW%ƒÅð#É£'ÑÜ[£;êFëôéöÖfîŒÎg\@&˵š®Èw¸ÈpCé¥Ðµ3IÿöôÒºOp?0ùº¸À›géô–¡–H1ÁÉõ„‘c¶#Ç??H7ý˜öÇQ||‡K‰_ky½~ôðB1¹É´Å‘¶Ì”M– hdÓ²‘õnò·úÁÎÃÀåc%‡/ʯUn´ÄÄæú§·ñÈYX´)Ô•$#<ÇÝ’=§êeewpͽ|M]>hæ“ëÇ¿zßq;^¡û®Þ{l^ý3»¬ÊØ8™F ìÔî)Þ¿Wß-ýןâ[?néê·j˜ÞNËIßTñu#nÍ‘Nœ¡ Ræ1åÿn†Ì%'}å®:N˜è•wB<,Ì1fÆÌ,ùº0·RW_úUÍà'G¯±FtÄßà³Ý­±±FüŠMì Ûíi< 2!.²¿l6òµx½m?0fÃpBZä†V" wô{€¹CD—¸i¡ÊóÈ%(U‘E2¢­iä*Á­м+ÿÁºE¬)Äþ\±aÏrÝ“$<}ƒ|ß¼¤ÏÔjµ#1›EÆ£¦Æ8û[¿ñäY¸GàA€6×>S뛞¯ÚlU­Ñ©_XòBƒ‰¤??B=íƒ'¦Ã?r?0qY×a°k4/">î›_ôK˜Óe+/‡Å%ÜÕoªXÜ®?rýå”\âùueäRTÊhz )FÚ×Ó.²ª6vªn¸ñ>??>DTí|{JñðTz¾\¦Da3š¿UÙ¾âxVäEEÈ}|¨Û}8²<Þ<úÖ÷ˆÝ&àxn‡‹ö¼Ÿ*ë²üØTnÚÚMGþBa(õn¬#=íó° /×]F¢Û7c–©½CŽ=¸‰¦íùθr!¬¹ïbÁ(¶z*Ü]œëE"´Ò‚åÀR{…UØh‰NOIRe+þ‚ÒIZhH¸'"ÊhL>„pfÌ0˜çS,£Å_6ø¿ùt`ÌœTëE Wü$B ÿb£Œ¦Ó¦*_ï·?nx ]´¥"¸›ºõ¢àP§!â§éÌÐÇÝéÐýX è1]á6²©3c­{À³9c¢µø‹¶¢ÇQx¸¤9˜?0¹£ޏ%ˆÐ­$å×à J˜ü’й8¶ø˜#NTK0¡w’Z€NËU¹žC vM"*Á‹–OÈ?0ðàR÷CWmÊc‹åaû“æ:¯Þõc® ‚[šÛ#¼õÂÕ"O/d“·§ü $¼úŽ6¼u âê–„ÐcV"®®’ˆªâ§º0p>¾v!ŸzT)åUOý³ŠsHèÅ”~ò%ó(Òê;¡ؘÊChãaH’Y8Ia ­s+]çèžÿî½{wOÀÓ¹;þëŠ}—îÌ>…WàêŸ ?rËü´‰­é±ìcŒiNË“{'S +#û`-ÃÜ(£=´CŸ ø%ã!‰ÂVKl¦+9ØØ˜2o¥qpŒ=©(ãÁ86 YÖì»òÜÚ`„T§ÃÍ’¿§çVVî6<ð£v–¸iÃRB¸_%w’ºñ'ÂH¡ÊHN·Ð´­ÊÆLkàgB%†c¼ó­ݳ¸'Z?04)ñ]½àéØ>Xo/°}âákFÞœ‰Ïåd¹{„á··„Þòhðð†P.õ¢…¡–0äb5€ç”À³hƒs™ŠtKí<¹²ÂOYíznÍ'>1Äw]ƒMÛF*ñŽ>ªüôF“¼-×Ë,‡‹¨eÔé‹K# Ë9ŒûFâ&§ÛÎt„Ä¼íVbÄ…üÁÕ /Çe93ãìä= aÔ?rÕ–ã¬(' ªÌ¸¤Éé¤Ɖ@EÂ0%î/YARñãÁø²ßˆl7h’s:Ô­ŒùãÇøÄ¡w»£ÍOi¢¤„ìŠ%ä£K8¾b 'cJ ãÏO8÷äøß Ÿã‹›.Tá\ƒ,b½ýÉö pðWƒ]Ù‡Ë*êM2ÛkáM?r‰·’GâohÈ}ôVšŒ€÷Ú6£Æu*‘O>`‹Ž)wWÏõÔÛFæÞÑsn«&Ýq“­fPŸ´eV„ñÞ & *<Ҝܵàïá#2Ÿ¾Õv†@#à(”î͆[Ä A j¥Hª?rÝ5læ®}#çÍ¡l~å/×½ã?nï¶°Jßkz„1în Û¨¸WÜi±€ŒoÝ;1Ò2^Ñv”OD3ü+Wîîš(LÇC-²}°~€ëmGuÂ¥Îú¼ÒMm2Ñ”‚ჰ „&{¶ù?nÇ{ñ¯×öö?nuG8Ÿ¤ì’Vz2âãØžiÁ¯4ãW–ÈJ{bõ_Õñùoÿ´ÿÿº\TM‹?0Õÿÿá;¸«ýÿöÅ~%þÿóÿ_·ü«½Üý|k¦Ò¦:#wø¯ÊöÕjyš0Ø«ò2êÖqþŠÒpé·Kþ¾^˜è…IPžÎÃg-H^ÉñÿlÈ2ò^ßÿëîÛz»^hÇÿ| \áö<”_oªµÝ5¿[¶]Û??¸´š??«-P‹Œ¿Ô»ËͲšW­\`aׇ4»ác¾Oÿ}ê{¿ñW3m«?r0- /×ÐÍ,D¹ê?0õ³| R®±ˆ›ÙŒ 7e³X6;˜<SP·+×]I#Ê`„~Aç>U;bäÈ]+s(ÖX”¿{Ÿ÷uÙV0Ë?r<Òƒý‚_Î?rfëÝèñkY‚ØÄ7ÙuɈ‡á{E1ÊøýTz£Mæ?npÓÔo–‹*é¹îj»Í%úØ> ÈXL@ZêGw”¸°X¶"Ê1‡LórÃÑ$»WË6¡5‡kAžZª¤~cÒÊÕ*ÙQæáßy#N… WÝÁÚŽóU}Z®Ú˜U¨iO?0Ë3Æ ^ r&ŸV÷Ó®àvÓÕˆ¥Æ;nG‚ö˜u_ÎçêQÝéeWQz8ëò ØÁóÛénð“ lá”°W¸Ç´{¥®‰Ð§¶\& [JU]ø±*7—6à‰ÔS31X¦0³xÙh‹VÚ‚þH±<¸,³,Ò šb¶/@×—^«šl‘??‘„P$NSóç—­)£ŸŸdªñ˜îÌ0c?0ÐZ>KíZž":ïjUCýº¥Àš èÏHÎuµC kïô!ûìÂl/p~­$oqU+A?0k›|¨7¢>­øÙ†ÐÙEi•ÛÁ òOÀiã|]}²—õ:ºÆ{&ºRÔGÕÚo5Çý2ÉNÈmú›T~îc%3µÉ´¡œÄgÖƒ)nâªíÒ4mÕiS™©9gKC¯g¸tí¬Bý—1^ÖE·”–0e¿³À ÂÉ£íO£õPóÀ@!Ø÷ؤ!ÓÈ'Ïíb¥-Nét7dh5¹öµú hvê²+KèÛrõºX,ˆ\;ÅW& þL¤Çà~ÐVC‡Ð?ni€,p)T¶FcP)háàÆˆè¢"2Ú˜'Yª¬µŠT2O"V&½©+Sl«M> Ë6¡ô(FZçòæ:ß»KJòéïymA¼‹ʼnó=¢Aê”=-zÏ/2Ï^-¤³ýâð ¥ÿ»ó‡??ÜýÏÖÿý¦ÿ#+³läôkÒ·ñ°a†øwì.U¼X#†›Aõr3ïëƒý,ñ¼çQ™£ úârÝ5u»¡ø¦_¥$å[ÐÌÛ•©ÞÐÉs¤–·WÎ3??4[_满€ÄtÅYS^0¨ý­ÀÃõÂõvN’Œ¢WN’MiòsÿBÙ¢pXp»º©%«°[Œþª5ÅQòÁ6Ž{ªîÇBmD3ÚñÙ«Ta·Z8ÀüR¡ÍÊVû²ÃAä–“aé†Ëaî"QÄj€xjFÎ?r³ëòØTgUƒ­ Ù‰+¹S0ÜÐdàÇâ"ò†„~@v;=z×áUÊ$¡O‰PÏ)êk=_m}ïGÕ?0€Ÿ2‹Œþ ^[VU×ã1›/`VÏT-?r‘<¼ygÌ‚wð½©GU~˜æ_F“ ÏÞÑmVØ&@eü5¿&¦íÆ+y?rí¼HU1¬ƒÁ­*€µ4bšT-yÊ:¨DI)™FË“ÖxQÒí1®ç¹EtéÞúÏÜÿ­K8\@½pü1÷Ÿñ™ÚÿÝýìîáoû¿²ÿó÷~#"½#¡«k¹å4Æ*Ëõ@Þ²]-çÕÁø­¤?nÊß(†aè¯<«ßaœ%ÁŽ$iˆÏÂý*NƒÄ𼞗­njí~U•Уajʨö6IîrÔ’~¹¤v>*S<  [”Õ4?n,\¤Ì,>ž‡ñ¬¢ó·iÿ–ôXÛ¾!å9bRÚ~#úýŒœŸ€AÖ‡*Ƽg˦€üÀQtfÿ€™Ô*;'‘ßWusi??rM|îöúmË~ÀNÿïÚΠÌ/ªîU½½¿ù_¹]u…U(ÃCÈ@Ø^¨¯Þv›m‡žm;+8´_&C¾?0ë€xO îIžŠyµ´~÷©¼¾(tmÐñ‡øw9·°ÉŒ±²íqiwŽn)æÝ;*Kvuj4d't1/F?nµïi܇qß”]Ô;×újmW½©Í&¶ÛiÓT£šŒ?nå¶Ñ×¢ú|AÇ/a˜…ºí~AðÔA?rgzÊœßzØ+aŽ4"a§ÃIÑçžËøGO¹’²„šæHš©©Ö‹ŠËͲ9W¾•GÔ7¥ËÕïóu®¦`Û†Ö '1BzWö"( Í E"víïŠ@IÓê«r½€7ÆÊÐgºmäªX5ßn)ùuêÿhvÓÒ:ÁõGm· ‡go̵\‹àHöf}Íöu¶§ûÇÁÛ²€ˆŸ.¯±îEQýÇn§]w¯Ø[Ÿ»-‚%ÓAbQä³½^rè úLg‹êƒîòU”Ç÷0&Û龯Ñ#ñˆÁ#û£Ë”SÜ2šj3?0ï13„¢RÝ‘›[zè2ÑЙõ‘hÍ–P/úHê÷r˜=yï©öÂû?r´Ø^lãT)›±–%-n6¸únÏø!.Z‰(¢«âÎ2jJ·¦%Q|DO,Âgd¾U%äé WA§–¦ïeÅn@¤ô#p'6è%œ;EõlZiU®ï™Eqhü#<ÛéâÀŸ/Q.t{17í¼ÞT ‡Jo:Vº@ªHÖêâ»wZwAÑ¥Údý¤Yu!B†×N­ÐF¨½Û4 7blzãë~ /Õ£‡ákôh??j,Oܧªãæ1Þ‘”­gÅ–M1­i¬´í‘›—뢴UŠø§P¯qY–—kË>þilÊíL´<ÅÚj¾Òd·×QcNOfúCcRõƒÚ±}«–¶\/NëwÊ›­I$P®Iðý†õÇ}Sµ ‹öð›ÿp¹OÔýkúc¯ä“÷ÚÑ!†q»Æ˜ßèžPvÜ*(Z”Öes5Tÿ¯‘íÏ‘mù³ÍÿËHÀc3—Ç;_V=¨ˆ‚TìªÔUEg),Sì´‹ùŠæðµýû¢Ïø–3#Ãáóù³êl,£8Ä9U¬ú;ÒGd2¬J‹ïbØžé1‡Y¤ýFÒø¢Ÿi[ƒƒ?0HZÚI™Ñ¼bCì4ÔþØÔ«å|YµÇ)›ªOmÀ.xOOôx’ea¿y£‘?0+!þÂzôÿ¬û<'ÓjÛʲÚz¼Ù®äµx¢??_µ½÷YV0ö7NÅcKöÓý¸)³-|ôÂu4|g>ÛôÇgÜù-cǵu!¢ ¦$ôºçp·8qgŽ1®e²ØxçQ2æ£ÉõÛkÞ]Ž´tz#šË>îãéñ£´óLZÇwû iØÞɵX5sÐ>jPÁ†œ©CɡܜÆgܶßÔµ‘ǶïÉ®6ß,ç¿ÖÚ,ÀÚx‰á=-—MFºá?n!˜çîˆýQGÕ»ÞD[¦ö³¥Š¢H¤?rµY%ìIÑͽÒ)UìÜýHuc³&0¨Lä¸?rV_“¶ÕÑFoñx¨°»á¶Fû@×Upn¯ûVU?0H2‹N'|¿PÚ]Ñã~s2Õ{Âw‘‹Ëudõ¢gh»P¾móð¯ù¢Y$`6”T4®ŸQPŸô…+5æ¸#t²§s%bøÅíÌ¿)ç‡Ñ­î­wÐA¤\ÃÝäÞÑ‚²ZkæM%hšŒíÞ¡óíRŸ…MU‹v&ÕÀÁc«ºöž?0ãúr=·$.Þ£*¼X@ú´ž<Ö3$G¥Ñy¢–ÌÔ£{Ñ»Ëu #ä;“$¦<ð*hY?0Žo]]t€·j.GZû*åhÉñ¯/[¿á»mÊ‹ªí~Eó?0ÖÔÐàCztè`ôp¿Ò´óQ¥<.¨ì5Ë_˜”…ñ2 ä¿«:lÒ?? „{FåÍÐàî—Ò÷6^¼¯BŽ˜úfÐgϨPÈ`ŸÈô(]]ÆëIÕåÍw4CŽíXÀ^¡g¯»—®.(ñÞ¥¶Ñ™€ûOè­çð”ñ¾¾²á¿ì}qWoèoµùXû:зÄñDlßVÊ$†gzíÜc??¸Ñw«m#©àÔ¶‡é8/©Þè”j“{‡Ùy9jqýØ:¶œaÅzX%§5,–S§zx ܼW’p…?0Õ›öãÌãlåkªªÆø·„.Õ“z”pÛVÍTp¤‚¸g}^žÜÇ|Ìm_Ƕ/ûõxxvŸ/–Ç×é×~oF㹟sÇŠ{DÙëRåž±SaÕ°²WÍòMµ þ€ÜÄjã[±ŠDÕ¡Ìç°äåÌÑ&èoªfU^ŽÂ _"ôâaVl;ÞÞG¬¦˜àÅ«À"D+6¦ŽvÊ9+—+³„3’,à$-ôèzîN8ómÛÕöއ(Úz³»örb?nv"fn±¹adå1býýÿì}ûwã6®ðïþ+´Ú;;Vëq“éëœlÝÇwÛ}¿Ûý^iVG±éDwdË•äL²½ýþö ‘ ‡‰çq3='µHAA´ñ¿€³®`o2þÿéG~êÇ=ùäãwñ¿þÃÆÿÇ8èuCYf6Ŷ]×ÍF5ýa.?r(:Ç?rÆ0„ÿ Ó??Ó暑ÄB#»¾¶¼GÔñ>@lО™ ½X.s †Gˆ Ìs~©³Ýßb4ÀesŽ1ˆ+*4'ˆ÷—Bä«#}îùú7òÜŒTé¸T@ü4ׯŒ½ëàCÕ çºnŽ9åù]2.ôƒô›?n¾“?0??&ÿÞm”„ÿ›”ÿú×éÇ~üÇ??úOôþË»÷_6ÅîÞ‡”¿‚0qcô <ð^>ÁVêÖ:?nkZ–z+ªò®~¡¶÷Cä¨9ç )Ûö:ª¶¦KÿSm[Õ¡¤;·ËzíHgö³\»_—pEÊM@gŸÖMÚ5å¶s6pŸáÄ«nn´†@‹–V±O¸@æ~; B{1É&ØiÊÉTÀ›~! a³®àoÕá_L»‚/Œî1ÂóMÑ]cÄ?0@²X­Ò³^Ø~5D´l÷—Cª¶¼a*Rÿ8ÄÞ\•7Cê×CdM?n´9d‘©pÕ+‹«^¹±ÿbDÚ#CúÑ۴ѨµŸ0–ì¯??šiT±ás¯å??…×q©ÞŒ£Â‡A?r(ϰ¯†€Ïk®(vŸ×2üà;Ç̽yÈKJöR.Æj°%§¡w9Š+SŠ€æ:¡µ`?rç?nÂCÚb°fy$çºç;Çä¢áO<äÀò^êæ™zc8 vÀÀö©,›öÊ¿ùp»\Ä—ü8–RÞÂ)óå¾i€Å˜É-WPýÔ!jfFÿÓ{uؘæû.Wõš#óØ@H]sË-¼~¥Vœ‰82vM‹£‚!¡²¼<¹HÂ"2?ríâc¡Ëcî¹TÝåZR¼ÀG€1Ò›wÊ2tËÓ'íS8W Tv˜¡]f??;½`(Å›mÐùL¡‡.ŠjÛ>^lú÷-±(Jjûç<½`$È8¾Ù.ë=35Šò“2Ž?0)Œ¸Q ;'_?n†F4c©?0]‚!uðÿÔ{xíþ²¸¬î’M±RöWµprS¶‘pžü4Ú$?rc([¤ÁWè¡Á³D[µmÓꌆ'-0hÌaS"÷GtÛ±Y/‹–ÈCYÊ •ÈŽ'™õ¸'5úxÊS8-·[Õlê¶KŒÎ¤i.:Ýmj¥™_'—Áh§ýºT¶v0ZB¤‰Ç…¹‘Ö)MûžÒle\¬çýøüL"´|—Ÿ'1‰Š‚ ‹D…ÄCêŽ"Ä`¦×g‚Õ†¥hÀ[œ¡¤Þµ²ûDK-ÛÜÜ»©Á·]S‚›}¥°Õââ\Á4Šdé @@—Îz]>Pƒ·)á\¨b¾¶œ.™W„!H™j»¼ØÞM9²ýÊÞrêðHu!pM8å7aQ7Âd£í¯Y1Ìjö¹ÌæÆÄ~x²‚é¬Ó»f™IwN¡±F³[´CñÌ( kGÎ A0??[ô‘ˆ0 (`áZ£`¬ ¢]Ð#iz °YhWï?0’.:‹¦8¤–ÊÃ(lVEë–"'Á¹$4‘Âiv ~ˆÆ†!h¤*3V€¹™b÷#E¯ñ(´D ³þüy'pZ ”0{9?rtdH ŠÐíºk‰,ÞÙl7ÅJû+K®0Úð5¡j4\¨2ž.Ò)N³Ö_o 8~£qÃ]…£E†ö±óöE¹ËË5œÑUµ¯a(œ\SG"œä°ÀþÒÊ–¨%?0Æ™ª×ÒT‡åIDê4ÇhÇN•™àaæóY ‚ŒïóÆèý]Û!æ rô°Í1 £±—„]±T 7l½v¢à‹!žmóªð–4àŽú°CH îÚDáßÏl(ÈkÁŒ)Õ g©Ì¼èáq†70Še|ð"dz1ÎíÒ53Õú-ð{Wkà1½šH7XÑÕ¾®Ôc+·šÙÁyGpÎÈèT°¯YWSŒªô+Ê„Y"PaC²Í˜W!¿N}|ïèDj×|x"?nN2¥ZXœ1:°"zKœ±'ý'D\afŠÀœ¶ë`_š· päÜ¢Ia (ââç„å£y?0C‚&€©nfX>³½gb?0wåyA' $ïéŒn=í‘Êw=Ì??xîéYèåà¶ ?ríÀhÎÆ¥cìØ€ê 6€ʰ^fy|p}W^„¸ºív ÌÂ1¨œ—Í’Ã86‡t`ÀÇ!ÙÓÓ|‡é…it&U;ø¡Wä±O³\™â–q14¢ƒ,„?0ŠÄDi-Ï< ßòŽ6ÂÒØØõ?níjMÊW©ž.¬ œhS Œ^‰Bðs?nÄžšwúƒ8[1øI 2yX-ÃE,#TÂzÀ÷°ó÷D Ϧ^@Ü^fäº?0£%^öúcÍßq¢«Zô0Ä6¡œwˆ˜bx°sÄDbîÅ£8dQGó‰š<5d^øKîr •½‡q> À¥ýšÎ a@ÓƒZ‚¤ñ;8K•v4»«ikUÁY ÚÄñá(4¹jªËUò—»îºÞ†ì¯Ö„Óþ¦¸3gå…Fx}·»6{Å}«’"ˆb/²êv6ʼ—¤ŠÕè%ªGÓÑ÷ ™ñ`¡ž›ý¹þÁ¡Eñ6¸èÄ&J—éCÿ?0æìLNh}ÔàÀUUÉ@É:9†ÒÌù¨÷íH{„p±Â4sõ!¸m@Gs£ß&‹ƒ:3€bÅR7O¥Ã7¡:bP›ÑSãà0 ÀůmÈÝG¼ÆÀÇéÁn õ2EJ²BÅ¢*3£¦Ø#Ïå.‘†"!âà€Ç¡Žó??85w‚¼®“7áE›z +#Xï×¶ `o'¡¸ö`FwÇœ§£ƒ|‚ ×oWØ4°6·Ž‹oµštpY³{`-ENYˆYG Äã…b„`”¼Ä“(y“]®mG±Ë8¨ùðs^Lîiy›Gÿ<ÙBõ#¤i??^ÇoÍF1ÍYÏÆ©lL·‰Aò0‚†eÔ)˜a\Áèb—ôʶZ±1j¨àfÍÜ¥agÍÈ÷ñŠVbIQ•E{L??py@›Á©Ó73¬U¾¸ÇB´.Ö,bDÒERq÷Žó!fàʔʛ“@]û® ½<­œ¡LˆÑ• û?n«m”ðn«e8Ï¥÷.¬Žf¡ÄFWýÉþCÌ'?r–ñÐõx‘¥9¤¡¨l@‰>ÊaÿˆƒQ†fŸ£’'—d}üY8<Û;%µ‰Ÿ1fÔ@–þ}P ¾bÊ @Gìÿ!–T¤?r viÂÚ—ûH¸Wæ“eñYÀ' ´ÿ°z%¿é…¥g‹2ÑEbð8„")–!–=j+xk(ã‡uWøóV!RÎp™ÎË-”FÊÅÄùxV˜{y€/=Þø¿‰Ÿáiáãdnbd¾„õPAÑWi²'¬ÇèÓG´À9çw9ëÞÙ„Ãáv‰Í°C”›t÷³¹Ü—Á-+»ò2â.ŒD÷ªîÒ³à)›¬OEz„ÐÛöñ¸»ÃXÄw:˜sà›ó§ ¸¿[·¶Ç·0‚*?rÁ‰JÛŒM÷RÔ* ÛóX½²SŸ¥Þ¶€­z…ƒo4¶¼ïxm™&*›¢¹»ç yä/ÃÆcÿÇš÷1Eè°ÑëÏá“1]~ý8Œ4|‘P\‹(ìˆçË$»7ŠãY´??ˆ`Ö÷z=§àB)º™1†ÀX}m6ßèBõÆ1ÜߤÒNBí™~Câ¤Ø&vœÍ’«ºC *6–:NdSÇðnŸL‡¶rCÞtƒðÑi¬’Tç·À7wdh‘߈ﵹ  h‰p…I´Õ ÓÌËë¦AàkÜÃ;L½šîRo§³TTWéH7Žl2qU¼"*ã”?nö¬y³Ý¼ε=D1t”4pà‘ûÈÃö¢Ü­öu²« x2S²¬Êj]»¿¹£íÂv/—µd7¸"Úø,Ú½FËæ÷®Ü©¨†#‰Á¦{XÉ€Ç@Â.?ráÊ6ªîNµÿ9ú’>Ö“ññ¨M²ÿº|å1Öy¿g«³Ød#NP¤Ð˧ⅆ¢Ž5eÜë-Åi¥Å†6ÝŒ\Ù£Z"f»ß\êBŒF¤#à`Ïöä£FQc$Ló@ã ¨$Òʽsæ{’ãU-YmŽ_Äå#%6y.ÕŠm;ât¥ˆc(Ö§¤ä˳ŸÃ߯4¿Î®Fó nœM„Æ#/íœË??™‰#?0:R¬í‰Y0W\DX×c­õÁÁΈ:ì?r€bÇpŠÏà·x‰øpÏ»MœÙ1KhÊ5¬Œc¯qd×âG·š·<¶{â9Åå¾MÍ6àý‹<Á{HŒÃ·Öä5âÅËq¾moRíÓö¾g£~Ø—Ê?rãÉêÅoK«m»ïY€à e¥9¸ˆ—[Œ&Òš¨÷&,5<) H`Éa!xüU=ÖȇmcpÄ$á4Hêh<:ðTá¢.z¸lm$uêk›üУ>x¢ä5 ·øÃ|ÞWF3b‰¦ºUAm)¨GÜA»ïž ]Ë«ÑdϸÅDÞÑQ™h§½ÇÚN¸¨"ÁÊ#Jì(æklI œ5I0¤&cMZ…XI._ш}…•]ú"BEï¬YOúÌ´ÎþÂTÁq‹6Ó³Q(29ØUŽß¤xÁ±rÌ`_bˆ‚K‘ ˆ#nwáÝGgC¿cã‚X"YÍðŒübUÅE;âº2ü‹+?r %YâÊ{^Y¯‰×o䫾Ӡ¡ ŸtxôT˜ˆF6.ãã$ ?rí‹ÔÃ/$Èè0÷ª€í+—cØ« ojl´¼#f‘ІcfOÄh“‡kô”;zºÅï7Œ°Õ3–F=<• w<™eCVl˜vA˜æFˆ‡"0??2CL€©™^ln8Ry=D†€!þÙ¹FþÂ%JfG|§ÊèÒ)OA:l­å!$¿©-ÌEN턺™ëýÒÛì.r¦üMë?0}šñ¢xÂy‘K: ÒË5ÿ½ž¾~÷½ÿÞì·]¹Qð?0ü}ÿý£OŸ:zÿýãNÞ½ÿþ¶ßïRoïðÙwó(LW×½ÕŽÛ"̃%}Hÿ£ê®ëÕwàæ„¹›¢y±ßµÅZ†þ—rÿhR¤Ü¶^wù~[.a~‘¾¸\ÊOÕK9»?ncŽåe‡¡îìêwÐaHÀc\ûÂÙù??ÏÈ3ž?nE!§Ég=n£ºÂ„º÷­}½÷úU½ß®îüʈÃ/øÿ}»RzyU+̬eáÐx(ëO=,‰e"ßóFíšÉDÝB>‚ÏËÿ¡®wØ62~¤÷ÔZ5j»T”a"òÑŽy^†sœòzòéÙE_ìÙÿ'O·¦(©Ÿ8,mÇ”€??twôíJØE Ôù?0¡÷~âdáŒÙÉ!ªczë<Íï(%·=œÓ†i«~àÀxñ6î4quŠyNÜ•³?nOè‘ò¢1OÐS DÚ# †#-×Cð\t?rÊóënSå¹ðŽ,öçtŸ¦ÞZ3Òî®Í†Z³Ìm0v$@d}Ó©¯„¶ór¦ÅÄä¾¹„^w¥Ä¦¨£?rbÏQv/ “ŒªÞâo½¹s¯+µ×E£VNÂUU_• ¢ ÛN8ðŽN¼0u‘üøaE?0ïvh€œðMË7ö=Up²?nó{ïA©Œp#…t¸¬/€¿2öâ½=Ö4cŠÄþkÛn7$ȯ÷\#¿ ×ˆœnw»pÞ÷-†}›ºÛc™yÜw.ŒS& ©)J;Ûsv CÏÿöö6¬ ùîÈÆ&ÅzÛ?rX€,Ò§‹r\sL??èQ¬?nßÕ6?0ÊËSÃgÉIæRâ¼'ˆàxÓÏž´É“æs¸]h@öüŠæLӛÊE²Î¶÷ÖÈ€Èót–ÜÚ~¡§4àæu_°Ò£Í›eKFÛË¢U`£E(kK€’óCðÎÀD°ÏÕQx²“Úº‚÷667ó>в?0Ôy¾o¢Sˆ#Ïë†Þ ÕÃ!„*9¹vŽj"ºŒ 4Ía¿®‰ï2F%e?n¥7Xt“óJ]Ë»¡Ð­èþ³å2Áo”ØuÑvARCï€Ã\ Ô>¥¹‹r¶O]P/‘<×¹ ~i‚¬÷§2Ë4’U(ŒRS,ŽÙ„€²‰4òN¹>>uætæËW¡{í)+g)Ë–tp=`Ë€'ºiÅëy¦]£Y2U‚/*Äí X)ì €–±¥a?rGãhN <üðm¨Ô€”õŒi¡7Î`ÁŽ?0³3VãÄûm]ogDkøÜš,4”ìzÏØiäãhÖ±Ùo%½” úHôk¤Që%$=çyUÈÔcªcªTt½¬—ލ×"ñª–×>콩??ü…ðÛ®ÞýŒc@™€çAjZZ^m2@%—jYì[z±¶?r9¯’"ÆÈDÚá‡täJ5åM/‡g®eL Öp22ÜI:÷Ìê¿3­›÷Ÿ$™3|s–@¥DsÕb»Z:j#À^mÜïVpD?r ˆ Er“Išµ£Ž^ ˜‰€øê®‰QtˆÞÔ·£°x»ŠÏƒÊ9¢GDs-­ë¥‘ÅT–œry2ü@!?0óš‚@¦X˜aúRøH±áflÚóIyÀ,›_ƒYk89²ÍÞ²*(!ÍŒWMòàFU7T=yxAŽ»Ù쟄n[±Ra0Ø]dݰD\¢Z e%ÀâQUQ˜ûƒV>?r©œPþDÝÞõ`FdõI6Eohïü1+l2‹Ë¥~üi·ÓÍÔœ¹*[Ý¥dƒ°†F¾3жþÒæ”L4ÆvI;˜´òæœe,Å<ß`MˆÝË3U% ¬²Wèv?r„ƒïîÛiQgsÑÁÆ7ù|a.¬;„…?rkÂ+Þà"ò#w¹§•>±ÏüöTŽIßä,Åý H9ÈÏÜSFTMY|šû®Æ“ÍÀ~ OÚÀ -hTü2t²cÞ9¬?rx4œèJžö¹íUwí^ È‹µFëíãòÞ†â'_*½.*??µ£’> Ü^™ÝUµw!¤™Uefu›™n¹®¸Uxî†sâdq" zä6áÑUÒÏž«bBWiS*“y…¬Âµ°z0H-ÔŒ¿|£ÒO“û¤‘àY.7Æžf™¤é, ·lI\`'GÉ •½±^,ûDsœ(x'Tf0(ŒG¶SQ#ã‰XŽédZ HwoÌ ÒHÎâ??h‘¹E] )Ò"‹e°ˆÐ­8Š"¬!ÎØI(¡3M‹Eg€¢kÔ?rÅ¢ìÿÌ©à>ÜG >‘p¯Ë¦íÆ8]æ6’ÀÃÈ= íƒýÍ\oÏÌÂèîáykùQAžÉ—£9V‘Q“ñœ£?r•©â,8¢/œw×ëñø¶“|©i¬0/ä‡E;T‰wè›R‡êÎyåÉÙ+ɶ¡büìNs¿ûåÈŽ ¶ðy75g»6‘À7??p=ï0òæ†ÚÅxp¬¡ =öÂtŠbÀppì˜ÆyI­œñðNTærqDÉ3·<ê·r°OJÉ”Ø×ŽõŠ|C(®œ¤ëÙaäÅ®à% 7“˜>H´ŠÒß~X6ð26œÿil7õ µÂ°?rÝËG#jéi¸<è4íN-Ë¢"RËæIòÝu ìRáMÝvZR¼ÐÊNR$Ú€›s… ºbåÆuÅ e¶µ†…üºÜvé,˜¥Q?n9@¥e0??MüPsoÊ×5‘Ës/¿1]#[ð+w#·$Y´ zÙ@u"„¨’‡Ú˜,€DÉX䔨¨¶-®T”·(Â:„€l?r ´û>‹Sú¤¡ Œ÷sü×n\…7??cW>3¡îœ¥@ЫJ‘|§ Çþù÷§¦²£c¹›g²¤#ŽUà7Q^î;¤+ž¦HzOB§–z§x IcŽU CËZýC?r´?r_ÓãícU5Öw?0<^áád•ž6 ôHJF¨ùÙ,X6HçT w+Ïu£·M¾L:’`þTÐZÓÜ*‡È …Kú¬3h*î`z©G>WfæØ]LÇ«o’9“Ô60þIÀÖ)ïxtŸ3Ö2Îa5¾™ äÌŽ©Ê2ÁîEû q%a[|l2b0LàØlÁ ,5b­u|µäøl"®I0‡•)“©ãÄPÀ‘(bøU#¿±‹‹v aòxªèÓŸýÃMiŽÓÀÊ…á×Aƒ?rWĺMuÐ߀¦“7d¹ä¬Àtr¨ß-|êk¸vLë?0ì«Ä[=<}ôÇÁƒý§ŸD›G ”.«_»_^Óú?rîüIs˜Þ´ñ"Ô¦¡D:µÓ]‘ŠåÆ{ïéëoõDz{xgC‰L¾¡üâ^o+ä ³¹°…!]Á®ÇãFÆÕK,• ¶WXŸ O-BæÛy-Aà¼r'yvö.^嫌ÿh‚šcôÇ7ÿñäãOžâÇ|~zú$þã»ø+µ,7EEŸõáè»yx F|^?0÷~¯*b¾”²Í¼e04e¥¦Múg_|¿zúý\ÿ;ø—4›4š®[zºþ7?ngš®‡ê|úÙÛ/Lv½"??ƒ`¤6“¥—”çæýK· ºQÛè’'NI0«–m©é¸¼C3xBõ ?0áA~$Ÿïì©6»¸¹èöÛ„‘È`!S÷r‡T· ¬mª¸Ÿ³t÷)¬]Ûã’Š‚¡:š?n§\??WÄD>Áð…?rqê¥ÍT5šwV±)Âz®ª_¦žæ1qg^¶X†¦ÝîÁh° CƒbåAmb¢ˆ!ۻ݃±9w123Gu3ܰ1ìŸ%½í}F’{þ5þ??ã¼P??ìacuú”jMC’ùœ{Çdù† !‚ýØs#gDU´X3ÈKóatQ*¢]?0i&_¨Š'½,‰<Šw ,Š(kr£xäÀ tÑï¾ùö»o!,Âê…(=Ö¤&Â"C©ð»OvVÊu’Å2ôŸCLY/›?08åÁï>¹Ï”ƒŸD–‘¶”T?nd)¥ÃoL¶Ï3ÉIÈ`jP–ùè3Œ°¡ ø F|P|P?nÊÁ¯>« ”‡Ÿ”‰Sœòð«Ï¢¹H™ôÝgÓ¦lúî³I¡lúî³qNQ&~±`ÁÔk4|~ZVöI‹…N"p®~ 4??Éû¢êê`ÞϦa@lU0ùs7õŠºîª '7ªÐ O¯p¾bÉC‹`ògnj5Œš.œ¬Úê?rf~¶`Éj(ã'ÿ4yû??ˆtý6öÏuüÿÀþ¸ÿ{·ÿ£ÍÝ¿µõ–~×­³åÃ_tÖƒ{4¦ªÔ’ƒ_$ÃÜFkœõƾP—Ë`|5p?r?r3QÀRfÑ.Ë2¯Tש¦…ƒú^•We‡PøºëF+î:ZÂq/È;à×àßTùûºÓÚGo¹¥?rkúGü-&™°×??¦t¯%MUl.WEr{fãÈÿd"â³R¾¤é:HÆXp,˜¦&Žùd’·Ø ‡Ëé÷ßÒsï0bæ«ýf×Nӥƻ>2]{:ÍzîA W??S¶Öˆ‹?? â¨xœ<.–„3?0%ãd¯Œ1nƒcÁÎ&ð$( bkó½ëÈêá»ÇÞH?0¾å¦Îj÷{éίªn’ºšòÝèëmKH*U4ù²X^«v$Öá&œ·;%´Uõ¾ÍüÖ)V©[Õ ÌVƒQÌ?rEhdgå(£'1ÓV¿Èg¡¬4&zàXÞXÀ%†³XäçG+›zµ¯Ð«7Y¸`óvW•ÝT—Ÿ%§ž“|:Cš‡ð»Ó]iF¶ÆÃý?0„Hk}ñÜå †ÚrÀ‰°ÆÐßsMÑEfc»­éoM)³ášyNÀcîG‹=p‡ÄÎ=j›—ë\Ý–m×N×?0]¬Q‹´¹Ly¿Þºkéz=/[(‚å‚1¬ 1l6A^=4’ü“¬À´-?r;¬cçgŠª²°ßTU¹kËv O9)y\´´.¾S:]Kì¦ÜÌåišç—û²êÊ­‰@Ÿö-2Œ=î ¥ÉGŠ??ÁÀ¡Xïã{¼¼ðÄóoFŽQ\ßž;-p6E‡Âê +#F5—u«îƒ(,vwwG¿F.̘ÌÑ÷ÿg#Õ¨~5phHBŒèõëqÛA#D/ãå??Õc1é‘¿Éaa¯ÊMI^ÿªú_]Ñè©ò~£‚ìôÖùY‚è8ö3Lu£ÖLÜðÓÛó3qv%0?0Ýf KD:ŸÏͽŒ4µ~*ð11 ®#ÁlH¬5éôûöý,9;£G 2D­ÎA¨˜¢•©ï©a…$éÓäɸ´ÎÌ2$Ç`@nüB<ÄHBÕ#ÀTB1,tåÌ4¦ˆÒ[vØ¥©)¤¸>GתXAÐÕª2UBW¤)>‚ÜÀí)dˆù §Óóégÿþ‹ªûeö~JhØu;,FõÙ:“æÌ¯šz¿óÞ†D¹?02‡mÑû´ßñáî~ß§óº¹b”ÐÌ€nU ?nÇnh&¼a{ôégErݨõ‚xª§ì“Vß3{Ò~öAñ9Ì^©A3–îÊ??‡d…Ir“ºCðizh0 W便Ÿÿ±˜=Ñ3‡OÅtzvÏLdâôûoßÿòû—çß¿œ??»xOŸí¾d’'~Èm´ðéê3Íu—ãèK2ZêÎË‹d¢ù}Âù¾cLÕ¡­4.$Þc_Àoµ…_¨’,¯‹¦…õÊgÒ,KùmPò½¯…äk{ZJ¹Šèã°`>ø;ï îií±€ü Ó"ÙFWÔI¤r8´Æ ¬ І`{š›0)€œY)CHÏ,BCµáµ«?n7}òü–·÷± s_¤`‘ûû¿Â~1pT|«Ø˲»óodRz²@8?0ÞYѬREMkþû'pÿ¨˜ë %’óà¨Ì®Rk0ò85ÌûÔ1ìnÕqSß( =È—&âéÂXàFMÁ[äLô¼7ÍûØ¢ãç\2ÇÓ3Þ3F]@`ôí!Y„_ä'æ¿É åÏtañâNç9ÅÓ^Eõ.p`«Œ£ºÈƒ)oªS|Yïî"CµÑ5nŽ,s@.Q ë$²Š!w¸'\Dÿú‹îTL{;tYpÓ'†n´Ï‰ýÎ'è¼Xþ°/6"õ¬£™­- ½X¾ð;§ QºD ÈqÇåèàÄzå‹+ß³“ñh8>´_Ïc‚¹ýøäêåÃWR·ÏŸ^@x"?rSˆ­<´º7àÿ'x8ôCžÁ0s3OâkGÏEÊqC¯å½à¼?0z|g £ƒc€â¹‰VÃÀ`ç??&Ã9^ŠóÏÓ`cÏu†ˆ=hzd7h*_ýd æËsÊÄw‡8°Ccú(¦Øg6DÖî+PöÎÀÙ¸™ôîŽ??vÆ–ç “&âF5@„/ê0Ÿ‘ÖÜK)Ïᇷ©|j1dO“—eUAtIäö?nˆBàTîÃùÉ<ù{«R^>1¦xìÓãш`w¡ŠÕÜ{°èkµkÔÒ¼ñò¿(–TºñÕâ¹Í ½ -ÕètÆ ŠXpÎoÏO/L·ÜB;]tœÁåQ F Ç3Øày£¶UxÒÏ”ö¿?nW`rG ;;ù=†Bñc؉Žg& y“¬Ì¢£÷Bq5íö»J1Ê\”Ú 5Êóöræþü —_öÒ€!s™Ÿ4Bµtc_ÒCñPz•›}ôK»˜¦à²šÎø??üïVe3lTÙŽáûÕòn•Ò,??û ƒl…9¸åÆå?rˆð“B†ðÌRŠ`#žSúÂqe²¶Ê©5†â †Å}ú™ž"{í#µ??9ùp™A>w@Tääù'a§ȧ=ó%²±þì Õþ¾‰íé=£tŒ3 9ÿoÆ%‰bã]?nk—hŽ›2àAÂáÅ´›r¥V¶qŽš“,°?nžµ«!ã„=þªäçx1é= zh9B[úÎ ñÅ„=„gYëk Q9¥ßæI¦þy?0[¯h,£xú}û¡ÃW—Û‡tx«v‹=yßV©3 ù;ž¼oÕŠ‡2–Ÿ‹¢ÑcKbN!“:ì,%Ü+@µùOZ0µ»"îU39´2„"LSÜj?n5í[ráDž÷‘¢ Û“Ž1?nÃ;ƒ;çР¾èŸÁŠ”þ´Øôù¡ú¢l“Ô´s(k<•#Ö‰6¤idT Ð0ØâMpÉçbL°ý¦Uj©¥V{·]"âivöý6Á„ÁÒ$RL­å—y!7€šãƒŸ(½sz·íŠ[äTšÝ lëuGB¦4"ÃÉ "êâéStüŸ£ã¿[î^µ<µX@COžZ:8"®—Ë:¹¬³Œ3âU?0xwÿ.|Á?r”Ýݾÿÿá‡??÷ï|üÑÉÛ¾ÿñîþ??ú[oõ\kIºüIвj>þ'ŽqaÅÇáqlQp-À(¨Dø¦xÏ™"9ìÂ?0§ÿ½Uý/M'v`Lþßðo2ù-Žƒ·¼’þ(݇4î¸Eoi¤ë¸]ôVÊä7ßýñzSÜE‹%¸¬ã«øº™Lò¼ßšc_z:×ÿ¥l,n Sì°¹;høÆî³¿r¼a—^h@á®+®Ú@ì¬ég??{ölþÞÏž}þïŸÿãó‹÷>ÏÒl’ë²ew(ð‹éù??~‘üòâýì—é ’Ezhi|0LX¦òé²jû¨à{¸o£¶8’ÑöuÛE?n?rYv)·;R (Íâ°ð%²)(*ÁðêôàéÍÐÂùˆt@ k›H­{XüS¸X­ÄÐ÷žÎ¹Üî3 1_à!#€;‘îáüGÀÌdfs—ª9ŽHÄN7ÃØUÌnEjÅŒ‡›&Ó?r­º§árÓ8½¬) }³§Ä8nRŸè¼™•[Q wÇ•_M…ô¼G² Bå55×ÅEÊuÞ Y9ÕPh8€Íq\|cXúUÁP¹¹|¶±PP¾ÏLxCuÇ9‰ç ßÜx€>i§OÚì°¨­Šcsª1WH ‘¢DÅïÜ;w²!&ªàÀ'½^š>ç(ÃÒƒW<Å?0íϪ¨ä?r??„H40A)Ç#²yÕT61d62˜%p³*·ª}Õ,•‘r¾"`hžÍ ßoq,:ú#:²/õ0ÿæOßýö»ß~ó-8mÞØÈñžÉEïÎù¦¿%êNà‡g¼1Ú5ªVSŒgM™ìx¿ oÖÿüô«Ÿÿo¶,Š•j™>5域]Ì’ÓO²ÐŽëà·ç~þ0üpf™=Î=5´ÖëÇO„YpÞî/§n??:Cs$ÝíóÀ8B 9 Þ§I«Î*®Tgš’‚“Õi7w |Ù|Åô˜—™MhVhDTÏÙ:Ù4tޤd9Ö¨BK9¤|£‘ÐiMTrŒÍ:Ë˦0a0=Ï£º)aM$›ùÐ tÌfìí$z|kW9|îÔr:<—œÍì%zLà«/CæcÁgv3†)2q!„Æ íqZ­¦õ„”¾9IH¢O¿¯?0ÔÀ‘d„6Îí÷éHèË®€Pnª¬X†2Ä?naÈpÎ ý۾팟€>p d”²-‘U5éà?0 *:V»ºÝiqЗ-+û²Ø-‹–Aþs]VU:z’¾ª—…žÙ9ò ¹ç‚9bfˆŠcý]Æ++꤇—VÛ?0ÒÌnÍ+«¶yP½‚äÞÉŒ`ñ‚C…ùW”2?rN6Ä€ZöU¹DèÞ.2Åjä>?r{¨y~Ãhe8Æ{^„ôñþäÛ†¸!»òMõeí~J‰ZõÅð-èu‰¾ Iš¤:½_9æi&ï·iËÉ%-ÎölX|Ëì¦ß7ž=$ĘŠ@–&3L°×úÿG¸ˆ±¾ñuw)/‡Æ –ôs«Â]©[pÕ‹¾?0G§àáÌÔ;ê[*;‚HU¿aÚcŸ–²?r>×u]„T«??Ô5ìAeÝŠ%š†˜· 4>aæ óðv’ì›.¸³Sñ,ö­Açœ£ç Œkçñ/_˜ ee>$PáÆÃf}†9ƒ¢…Ur¡‡Àˆ @%9ÃHŠÔL¢&•ÞO¢ØØ\0¹UyŒr]¬’1&Š3ñ<5ý“mªI~<ù)¹*!w"7c… 8»\ rYAZɤÔî”Zíw-ß;KØñÖŒ»É?nþå¶è´8| z,Àáb2¹-Ä¢ã€×Ô‘·JJ°Z›{ãçÿxÂÇÿoÎÿóÿËÐÀcþßÄÄxðÌÿÎÿæž¹ê0 +#?0EÓóÅ•ÀÁD`&Q°1õ`óØ™yiùÑàÛï0ò¢”ƒT£ 1”Ê( jWꀫ è¨'P HÂN^F¨rt4±mP¯ÀÑj®@ÛÄ~î¹pË%úº?0.PC?0ÓíÐ2?0§ûa?nPýbàô?np8$/XÿTè!¤qøÝ'Ã;/Œ®ÿBLÈ?0«?0z­ÿ244B_ÿolhf>ˆÊÿÑò‘0U?0åµ?0Ê€©ûXo×Á?0|A§±—Ý™Þ{Ÿm)±fÔ"YùËÕ?rð½äû°a®’öÏ‹>ødîwMçôï*å|\{" :Û™ÄAž·òÊ h\/å&Y[È6Ì.Ïü1býX'ÂÌŸüüù­mTB‘¥A5R£ ‘¥@ë>ŒËLsGX]+£Ía…Ãø¬LwÁ¿hË ÉòÒ–w …×2€µ°™¯ÊºrDñu™N6ôÍAMÌú->QÄ=°¹ïjªX› Ö D ?rD"Ð?n4õÏ´E†óÆGjÄè_ÐÀ?0’Mj ªÁKº|_sCÚÔ4 Ím˜zÊgA;À*??^‚søé\xoÖÐð:)HÏ™b¬ˆahìŸöwÓÍ’Wˆ"⨣|†<e'ƽ|öž~"b@eÛ%ÈP&Ã*¡×??—Cd½¤ªµ ¤ªuICk@Óz}GÑ©,´S¼:{È qv]†½Ãë½[RıøÖ¾Ð®Ú5ɬØ(Ǭ,ý?rÞ b}š—§›8e?0ËVÆ‘;9ØVÎÚP?nåGrDé,QVvÎØ?0zÆ—‹žõhß·(©yA2‘ }Ù®5=³Ø8tÒ­t íÃF|ØéßžGÊG?riŸ:€¦ÂÛvÁƒæ§aæúá„®.–ú-àåèé“è¬A{F¾,bû??Ê+reD~¦½qËJi®C:ŠÈáȹÆ;å@­|)Pýˆ²¡§qY6lySjþ™(öž+lͬ`/XÍe1 Ê,5•óFqy@Ç€däXé@¬Eºn¹Y¡ùØyËtЩbṜÆÂLƒNtÇuP€¶ºÕ“.ÏáÑæDÓÚÈ ÚMbD~{œñnqÞ‹\gA¸ÎAz<Ï2D®€3 qàŒw <ÆB‘ǘ[G>Òä-hã{â¤Ý=QôªÁt‡?0ò¢s Ô‚÷¦¾®%Vo“ÃTkÙ?nÍmÔ* ý6ñ%0N5[iæ!õWTFqSPEϲÌý¦™ÂŒ å˜»» ioL#´tœ¹ÉJb-dçµq6/¥“T¦MêE‚”n8ó$JõÂ@P/R„H½pV/2¨Õ ?rê ur1@꯷ÌóPA4ŒŸÓAÚc-Њ…Wb nûI(ÚËÜ‚¸¤¬4¤??óÃ).ïhÓ2ù@y"2B´ë`-]_¿`r· gI"<Ñ+÷ð?r4}®0´áÔÀäu‘÷õ nÓµCûÒo½£½ø †‹y~¦ç‡ú 2J‹-Û|£VXp³£Åd¸ØF{ÈV¨Or Éò‘òçH2ØBP½†´ò—A??Úë>\(´&ò® ÆÃŒÊ›~¨uÄEØÛÎ?ró¼k{¶ úböhcZÀ{¾¼ù‹Ù ÌUþaïù›Û¸ý_Ÿ½ŽËcBÑ’L+)zF‘äD[ò“ä¦}®†sâ¥Qwœ»£e½÷úÝXp±Ç%yQÕMäN£#v±À.€ÅîâWß®ýý÷ÿÅ¥ÎG^ÿÓÀ,®ÿí>Å¿ ø/v qýïØ€Ô»{­m3•ºCcv“¤¹½íî:É”VÚjŸ¨t»ÝñxhüÀC¾v}ž??Å»î(íÆ†·qŸß?nɶ±”Â.,á¼€·÷GºjÑe£?n„Ò%‘ævÇàO:¬ô—@¿Â—×dèw:Ymè_üY·‚–÷·?nñý?r’¤JùÙJbÞI\¼Ólí;F¥üìOÉ0*Y‰åú©èéß-lüå÷><öûï[½Ý…÷ß5üIÿ??Æ??qþn ü¼šä—î{d¶ð_æÒ@L¢÷Ô]Â$¿º¢Ëlåòáևᴣ¿Ò¬‚??¥>Ø24_ó4Ðå}‘Œ;êÝÉÑñùá鯯œ¼a>57#º@?0úï¡Ù¿G æ_íþ²ý~ÚÚ/£+¦Dà :e¥³è˜Ïåà»?01U?rE^§æoÓ[|ÏVCÖ!<'ŒW?0pèÎü#ÖƒmO¦Ã«œPÔ,ƒ °Ð5s)Etç>ç÷UÛ„UWåŽ'‘Ù£ÿvýáñÉñ!ð??AÍ2ýŸËõöèíáðüïïXVÆXC‡Çû'GÇ??øtˆõõDöOôRóñ{VT“üoßžyùA¬ë3žîýìå!ù¯Ïyø·óÃã³£“ã?rÞ.£<¿I¡óÙc"¤üµ®jY£qÚÅçÖ™äQuÖ ™1`…¹z¨?r'w løÅϰ­¾S/wzmé¸?r¿cÀnToÕøv¦"ÆIÁ–Ô©óÂÆj’^šª¶X5m (wÛoørK™T??´µx·wº÷vx¼§ûÞÛ½¿uÔnO<0Ë•ŠJ¥G…x¼ƒ”?0¨Æáål9 s¨ü×½éØ2NAk~›J MÇ;?nÝ_&C|‰Ñ´ 2Ê;Ô­©8ל¹±gŠ_nýùûÙU˜´¹”MÿE{»séÒ z8ró5j’æ²ò˜d¨–"ž8)GE:¥û9Ç1°þÙe1vã_QA6ë÷k'nJw~UnžVþS}B¯]ÕþéO >ÏGURmêq—D·pI ׈ UÙ©"¬­ù6g0Q+^Ê@Âeùˆö•@{Ù«ådëOqÑ®}á`–Õ²Zm«ˆ&üÆ"G“¼.°±Âû?r›¡ Á&:²4©¢û l?r-Q£(oÓù€O5eêjD ¿Œ»è÷ò,?0ýB?rÐ4öQ@á¦P¦¡pÔ£Ñ'¾ï{«bÍåÛî"MôºÙ¬cÿ|¦ÒM*«—qü™Jc4ui<vLRl8£@?r0¥ †:L,EÑ£qÞÖÝÌv£ëdt3Ìf“Ihß2ë˜SüEG]¡GXØB·?0s ÍÐ ñÕ€ööu/F㪠ùU5¡šÉ•‰Ä?ruTÌ‚ÍíGª,Ô•O PW!s0†Y%,ŒCÄ`Ý8áFJë2ݘ]2Sꇄò¹3ú.ª®ß¤7‰wwÍT'1@¼ÃŠÆª#!OO;“ãHÓ!A.19›|2iy]e*Vú£<)FÉÐ%²|–??—²4®‰¦?ru°P?rlÕƒ{€J‡;¾¨Ðù 3º,g¤Ö†Ó„õ r¯ê…X€\ôÂ6)ùyý—ŠuÁ@n•3ÝBz&6ØÙâãBnÔD.¸á”V—R=„®Nxzõ—ÔãÕù`‚+HÜΛŒ:%û(DÚšu,蓾`ô)ÙG!úó8¯)#ËY”죰2´L.`H†8/À¥ú ,Ñ™c^0Ô¤°ª[?rêÓ?rjwÂ?r|ƒ¾??/Ì¥4âÐ÷Ë8ӔαDÆ‚Ì:Ϋé»àÙšŠM Ï}”d  u̵€œ×}Î<‡Õ‘EÔqd1à€¬#¯d~ø‹êZÇ^Þ`Mk+`7l+ÊA7›KƤ?n6gÛ¥úõAÏ`ÍF$ Êì1ËŠ± yšLÀl62çΆ^‰u©!ŠjÀ+›q‘s^ &û(bQ['lÄÅ…_¡0 àhR][$…çï*yn©«€ž¿¹.êR¼+‡‡?rNÅøÏ•?r9QÆ1ƒHØuÆEœå-;_A êm_H$št{??ß•À ‡pìõÌ\=Œ‘Dó1ÌÃ&BĤï¯Fóhòº8vÀ©ÛõlBÂÅv¸?nıB‰‚ê+ˆ‹d!ÝíÁÅÀ±??'Wk8‰&>/m©id€mÆ·NèâƒnlYaåèÅj"n€óü4,r­`UìÄ ƒuá l„…B; ¨yK¸rCðà+ú-áÁZŸìê;¦~ÿþJÞÆä³¿¿}stüv6øJAzsJ=Ú??ô?0ßnð )•¾¥óÅ=îéÊ\ÈEKD?0ÚF~6ØReÛÙr÷‰£žKvOÿz8Ü;÷KûคçuQ®?n§§'§6JØ"!Ûbêr‚JÖQÎ÷…zvòúÁ=¼÷î'ÒoŒÃ7¯?rÊòŸíYøŽ????=yù{üõÉéùéžm(^ pò“n%j"”‘[[;:>8:…Å5ÀØVÝ?0°Í?0š£á»ß¿EèÎỗ¾¨CuÕÎÏÚcÐÓÃÿ†— òýß)ÏîÆÆomÿÆGÙîŸÇÛÿ¹»Õ«íÿÙÚ}Úÿó8ÿ êïéq‘Ñqš™Py÷|ëÏšÝByÙ…x ö€ÂpeqG•}AnßIe%„-où>í.¼)žAd¸GÅ]šµ0 ˜éçÌÖó|Z=7WÁ>×[yú¬,ÄtÀ¿Îo“Ë"¹ó@êkÃLþúÙ÷“É$*?0ÕŽ˜¯L®6Ý盪4³ÙÙDyØ›©ÓŽjÍ-ø~N"™Ô˜baK³üb‹£û+`¿íø×fSúÉZsz€g>¶1‡ù± ¿þºÕ%mºßœG[sÁØhuŸ??+»ñdÒ‚W R¼\GlË”±ÄÛq’f³OÐŒ”—RæÝíL´TȦzúTPîSÐm?0ƒÇ®ÙSq´^MVöüýƒ7oÀ”´\aÐýäÌÜiýŸ"œ¡æÇæÃÖ8‚í̪ÊHȪTj߸$÷ù¬P&ö<™˜õáVûß;›À µq5½~®ÿ»??I“ nÿzTý¿»½³pÿ׋­Gzÿõiÿ??Ûû¿ñG +#õ1½í«ªôTy§ÿ“T*Jûv…u8Ïô°I†C7ML ¸ƒÊ=þ5_¥NÌ‘“rcÃì|¬o ¿U=ºI*÷ËÎ.¸–kºcžÁÞîÒæ¥Ùžzße (:‹ ÷S?0xÄÞEPÖ¹¿õ+|¦ƒEIL?r]U5p†M?0oô'\¸ìFL—FŒ÷ %††lR‚ÿµrhí}ù뼬:?nª1ÐÇUnVYKÜj&£t:€?r¢ñpݦ“û•cwïµ6§ÏëW¤{õè²­§íÚ–B+??\ûá0¨”ÀH¡¢8„–zÈ•†¹¯¼€Í ™ÕHøÅÁsþ5ÂüÛ_¯?r.'7ŽTš±bùš+ ¨Cø0ÏzÁP¡áu+'E¦¬ Û]v€ÙîÌ˳7×^ÂŽ¥Ñ‚…ÇH ÒwêíÑñðû7??ý÷!°oR_)íd¸T^Ošr Ei>8Ê>jéÄ?n‹ï«g1¼-ß+Öš—1¬EqpøÚUúkŒƒ­Ö_ç¿\{SJ>«¦³Š~OͼÎóÓ› ?0G05òàìFí~Ÿ¼??ïø+¦E𔍵§v~ª¯Œ"$è¥G¶¶“’Š‚'òÞlŽªO}…½úYiÇ~:Fð'J˜€u‡_t­1ènœ\ê½fU‡FPÇ3TN‡µ„_Äœú²‘*«DÞJl “Àø¦t¹)ysÊ}ˆCˆ žŽ¹DÞÄø—ÎóuÇ MÈaQ1ÿ%€,Yû®DÍDh’Å -ÁnQIÓPªîÍå´TÏáΖîÖB¯T1X®*ð”dŠg…±AßlÕv`ª¯!§C¨ò\•צwé”DOÇqRÁY‹›™g±Ù‚ÓQϺ;°ÿp_½±z›~ÿ\÷Kû^YÕœ²ãW¬ ’@8¹RÙç<;J'iji¢¦7UéˆYØÐ§9峬j³±{:BF›_¦¿L¥Ù¢Šä÷Ó_i§Ù9Œ¿üj[äW¤Q¿‰aú¥{=Oÿdÿ®G=ÿÿB¯,l×ýÿÝÝßþûßOþ^òH€Z†yfcÁª¼¸wµX?0<Ôïg*ªü<°ê kmv]K/TÒŽbð5[÷ÄL¤Ñ¤a‡ø‰,ŒðÖÎQM’j>VèØnñt;1@:O¶3½?0ðuÿR¨Ÿ›!ÀLÉuçÞM B3·¡åŠƒ™]†Ð¼?01ï9Å’2øC@Àµ‡àø“T°iV%R+°jq6ëÌ2ƒ@²I!’Þ–8?n½æþJ}ÛÝj«ç<§È²£ ‘··vzÝ-f%!œÙI‚Ûx=¸k‰²ô|•³f XjôðŒJ3êI\ ¼'}= ¼ÚƒXð·Nvçn*n”K œ&£¼?0Õ¨"°‡T>ßf&‰àû\,Úom¥¡Žå=0&[| —PY$Šá|gâGïßuTð6úU‡³hÖÀ‹F×IÌô ©Ÿzf™»Å=î‡pG¡©EqNJ[ï|˜ç¾Ñ°Ê‡æYJ»2."i±¬?naŠ0òšD°¹d(À¢†"§öOH?r0nêÁz‹ë~Y^9òþD!Õ`«kþ?0¾W¡âÒ5Rý~°Á&„6­À•þ¥Ï[4=b?rä»—i‡¡óU&¼ºu©»ëp,´N–¯ñqW$¸K''îíDý¬ìªLÉðœd|Ò£^Ó2©° ‡r0Ž€øÅÁØÝ5¿æºJ‘gp­Í%Ýßâ°1Ú??ƒEÓgÁ'Öªn7€&DH@ëãý~¸¿.‡À :[?r˜eS¯ê4É@áš½“¤Jæþ„ù|6­ä¶$èô†U„7ÇýP´<¨-‡É§©V…{û??Q?ră®Ëõ … l6³ õÜ’ÇY(ËïÖÏB&LB0æ\WËan1 :vJzlüËÌPa|)ÁU+Àu¥Õ¼Ì"ÓQPÓ/wjs±1^±‚ÿµâHtÆB"y¯Ý$¹wYj*£?nŸ@õIA£Xá›3MCÔïØº¾-ÒÌŸ/¡…Õ¦!•·Z3h *Ä”2²âé$4&àt]ÿ`–dlk,Ô´N އQ]ù‚”@8äGÅA Èý]zɰ½ÆÃì´¶)¡ÜLñ°½b–Bš²ÞCTääòÞh{øôWDz۰H3Ó]óÙjëÙñCïBÿ—Yzë ¦šÔ¯@ Ø5'¬"ÖŸ(áÖ†POÚcý¯O×׉ʟýþðMŸ×²yV['l@¸H8)ªû¥Þq=¡ÍúƱHßÃçî@ovêÖ&ÁaÎýRµÚÞ]Úé?r•Ín/“Bùd’ÔU®¶Tt¥Y­Û/™Z‡Ã!ƒª®ø:!Q œ4s©­„??gÜ&‰4ð–Yx¤:“.4P‘Œ>‚ýz‹Ë\udº*ƒB¿Cж‘A†=Ÿ©}Ö>wQ‘iµ³Ráw”™™!LÒíí&SG¼¹zòº$ý˜ÄÊE¼ç&!L¢xFä×îÌÅѱ¬·WZ À¸áÞ7h@äÆ?na¼_&XÅ¡ÜàƒŒ!ÁÔ° ±·³kô×êa´—gk/oÈ œd‹MàDÛQÖúÐü:¥«R¸! =tî$¾²~í‹ ‘Y¢À4K?0^Ms~ΠӨKèI«HÀÉ,›U@Îg·)£4Øñ/Øßë¥C¾)CbvÖ¥ì¶Ð®½a‡²ICh¹!.E$Î’BkðR!8‘çÞ¯ø>36š pó…b\€6ï ².Àñy‚Ì?n­³/o%ëÌYuÕó÷Q\Á_ƒ\Öøó(¹m¨3²Ók@ªþp?0°Æ`¿9ª'oèýjú8r`Çì3\Ó¬íɱB"öDÿ£ƒìzWá([ìÈÖ4 Ú¢gÌ‚ÞlF¨Ÿ%•òU9ºƒ2ÝLbk&0±rKøÛjS!l’$VÅTk«”„ö=ÞRâ?0r¨r»?0ø­¹¤6áP‘ÌARٙľb)¡4ûú8%ažÝÊÛŸ*n3ùüJ¶ùfŒ¤J»s%Žüøªwødš—éܵÑj뺵+ÓôC¼¯Í¶’y`%&RŒŽTæLÛ$ð¡ƒÍ`eÞò”TœfP6¼?r^'¥v­ª‘|R™Ì T÷Ë%ÎÀ’q@mÚ¤ÑÉ?n„Vá;Fiè×Qù¦OjöGœÍ(˜ŸÙ¬Y_ÂYViÞ㺌â'œ?0»jóÕ?nˆ¹Ið̘èï^öm›ËúaK‹ŒüJЉY¼/`†¶3/E÷>ý/*  ÞþzY¼ ùÖ^ĘÛ(râæ’WB²¸Êaúu7_µqpväØn¯XÑØ^µ‡Jm¬·83IÝÚ¼»ÖBð²¬_“bfJIͨâ£Gt¸lxFº¹ÔU˜kK‹B.ØÞ³ ÈúË×RO‰-sÏŽ#¾°å'^¼Àûuª;lô‰¦¨«ô#èÙ’xADí,‚ФD=펑\o?0Úlot–ð??ØÖkn¡Ñ–þÇ·Ñt×]k¤=ik¥-6Ioh‰/–r[[SwEZ%k¬9‹,c°ñ>føAî_fùé Ìô£dœ^×ä‰$”ÆV"ŒÅß­Èõªl3â1±/Ìjü-ºÓß™E·Þ ;ýÝtOÝr±¿Ö=à8¯^öx!Ã?nÑÓz?rä3ËæcÈ© X¨Q:n¼õ‚Ï9¢T€œ»ñü‘^†l{»¨yeûͲIšÁÎ7¡‰ù&ÔE¢ËhBu,­PÈ$0þ—lëÎr‘}f7Uz:ÿÃ7Â! Ç¹ÿé›oê÷¼ØÝùæéüÏo÷üϯ~/ëÉüzk~º†+CÎ>)ÂtèvŸ›xÝñ¡??ס/|èÁÞ9‡ö<(ìæäЗ>T_KÈ¡»>ô„eþ'1l—ö~ÙB'ä±g$pW?rÎý|C?riá0ŸBé´íÁ‰Ù¨Ò³d ƒ??ü¸r??ôw.Ä ÀæVii×ÂìO´e;Ãw8IT%¡ŒÖu—Ÿc¡u× ±ð… æqYº(*¤.‹*#ÜÞÖ™±Þ#í?0ôîõxŸÑ£g–ˆ»ÚÃ1$1Nä??X´ ¨»ÈÛÓÿl,‚ö¤÷??w{Û;uýÿbûéýçßüùO«?0Ø©ÏÏ£ù±s½ïî@’4>^ï&L??§Õõ‰u :U;þ¿ž²µjeˆÀUZWÒƒ‚Xÿ'"??}mIt”ýÞ†íò󥀺ۯ€k\=î׺ŠÅ.cµùp£Å§«äв£d}WüÕM«ä¶„“h¼Í.)¨‡k%Ý;…cÔn(A„?0]ˆü‹'râäsÒäs¤EF“R:²zÿ%$ËnÔ‹‘›Ü—ÑÞæm"=V†îæîÓÄéÓ¾«›Za±øDêDàÏjs?nRp›-ŒY‰Ýš”ßì² 1ÏKâOuɺxy1Ä)/‡WþÔéû†9=§Å8bcp ¶9i—+ÐdíB Oû’¢{vˆlk-ƒmR¿ßαjÈÒ# \8ßQV%;o¦¦¼–NTðÓŽ¨IôüÎr[Ád.!K33üNÐÄ{«—5£åPíâ«Ñy©É¬=µªlUóÉ œ>.­Á7Ÿœ€Ã8U¡/#VOœ&p×MA[í¶°[yÍ ̲”§]Ÿ‡0wƒÂl ȸ´uªë¤HTT$ކªr«bˆ>u:õÈâµ??Öǖ4|ù??ì¢Âð¥ÚH\w£ébѼ[¬êEˆÚ ÖÀïšj¼ÂvëÕ²6­>âJãðqý òÊ1*žýD1ô;0Ò¬ç"2ø³;Š&#À¡P¨šhb 2(&@ìøZ‘k!øý«“È.RvLh%_„^‘‹v?0—#òÜçVHƒý(kU˜©£¨ìäVëQ?0‹Ù]1Ã\Pǽª®a)dþµ%FÆ’û*ºÌgfÝ8ÝUâªbËå;åì +#Ò>óîñóa‡~ÒmðE”]%Æn·yÚ‹vq^Äüª¾ÎöµÚ¾h wÖ—¢'ãÍ®ØE[E•“ɳØšW×ÑÇÄô6t,ÙF£ðëx$óÍmY ?0yàh‡8’¯ªà­|€Ô¬×ù4ëâîݲ¥þŽSI˜w´dW‹ªl^FšÀ?rüI¼`ØÙ‹±3ØFì;¦!¥:Ú€­±*?n5+á†ÛœVMÕÜÞ£Ž¨G|5eZî1ºüí5ìH–´Ë«´®ðÇ%[›²n׳¶»“ü.)BY/&YXliÒ¥.Íó,hUÈá$û³GÂFôšPõ/V©}c£X×þNØ9#+Lô²øf–Ÿëaf|%ÁyÝ^»ªD3ûJŸ›¿ dwÄh©?0Ú„0EßD¾XœHü#óæxN?në2ãÿ&ï4ðÏÿé†õ6OÀ“l3‘¾àéfû”?rz —ÚEU„Þc³†ÐlÌÂP&q°¢¸€±¤åÒ5ÐylÙžóŒ‹j¢q”‚ìl¹¢7eÖÈRÄnÐÈÊ—òÇ€š ²ÑñbªÌ,Fµëä—®}rk¬'«í8AëliKŒ'qÒ¹Îgݦ‰kY{Þð:Aªʫh‚«ÜŽ–º)c ‡æ¤hÚ¹âÀ´¼PëO?rçX¤QÛ[9joÇÕ®ÏXŸòøY»Õ8qCî{R®ö`:cqð½n?0Ð6ãCªÑˆzÈ(JÇ ýN½R½¥Ú!vgÏ_Ÿ¿þá~†ù6ä´4oO¢â*騅'ÍD6Š*ÞüÉ»¨ì¯ñ()±á°¥Ç³C]-+I9Ô´ÃúçV¢±¢îöÙ:ƒ<&ùî“G“/…»Y$ßÛz[^áT*ÂĽ<—ßFÔöó?0ÆÞh÷¯|Lsû¤W?rçà¤å¿.KŒ&ZºÕìsMâ¤N·yŽ&“ä*šØî ÷IÔ_æûìF[ïtR\ÔÐvç¬D8Ï{¯ìîÔÚ7€vœ«r6ºV³2)jðo?r™ùgYr•WiT¹¡OîŸÍìÝYæÔo??ÒŠÔ¬’e­J6µ¦ ªòü·Tx\úG???0ç—û—)N⤥‰}«ïP?0 Ö­S¾¬uChuþWUtÝ8o¾b§k:[:êê[\NBäWz†e®=I¦C/&KnLXfg))Ñdí`µXÞÕt°bZÙÓ£õ¨W×i©¢ñ¸È3°ŒŒtúzl˜Hõ6¡z?r IæAì}Ö%èߊ¦–c-¼-øÊ‡\­òÜo?n˜Qž‹|`„ “÷¦zÙ~$þYWo( ~€Ï§^­6IOõÍTz›”et•`-WÔ£áLÊwj~ñ ×4ï6¶‚O<»¤û.khyµpí:¡saãRMðð%OyÝ(,]5Ò@5€ÿJ EÍ£¶õÿì}qwÛ6²ïÿþ¨Ïq%5Š"ÉŽ“úYÞ›6î®Ïf“l’¶oO^ªC[tÌkYÔå4Þ½û>ûÃ?0üQ?0‡CAaì¶ÝúœD ƒÁ`0ÌTÍÅišÎÍÅøqZ+V³?râì…` ‡W€tĵîÔ ÀÂÖ¤øÔæëd˜ ×??”YÓÌU´<»ðêvÑk¾<óãÛpž¦:ðbF 5±¸+kF«¤ŒÇÄôܺOùƒÄ“'¼ì‰ÑlbŸŽêò'2–-X$O´7¬&Шpµª*q"¾B'dsïq¡!Rilãt3,Kè`ˆT”xbOëú±úÌÜYÚai0*‡ªÞÑíçzw³L>Äöfàane:rТç<=ƒŸõ–Í(swüû_s¯íŽó??î÷wYþçÝÁþÿÿIùŸâi|¶òB_ÅÕŠ"M|–—ã“ço^æ¶œ~‡y¥o/X¼“4ÎU%n.êVn•ÐP’M’…ØŽØÇ7±BË«ŒÞJÊíýhQ= èF’¹ Ä®84‹ÈÊì«K__üý n˜¸™FHnUîÎfÔ*ZÚ¨S??~J§(®Q“N¹Ñ,ÐÀkËmpØa„"n&Ж£{îÅ+X¥X^öm)fÖJ›x[tn0~ùâ՛Ϛݸq®ò±??º‚Rn_gªJæã̈EÀ ƒñv'{gäªu;~Òüjž°KJ…†Q…×_!¯UEʆ¯??{ˆó-=ߪˆµd¤•ñ¿:!§²‡ª´žö²Øõb‚!QÑÙÅ%NîªBù,–ÔêÂ^5Td¤Bª7VíåÄ•¤röãñ²Å‹¯ü +À–*±Y·,ä–.oÈA•Èu¯§rÕÐzýþ±mOÙqYhmž")#€­—¤²1ã»tŠ™f'Èhe­þ јMèhâQ6ðÇ!äy¹j Â^\åäËí'Ò@H÷ ‚hˆ>™)€Th$÷žMÈð£?n¬‘5?rs"| y7yÂxÒ ( P(0h°ÇòA2ð;[Ò´{/èØßîU:³§s€#£âwg}Ü0s,n*tíw-’V??²y|FÛFy“=ûºØÕŒdÿ!7 À·õÒ™3=Û§«ºvÛË©<#ÁR×¹ž/iªÝd¤¦¯rî€ccî¦Ê¡ÌDSŠtç,†±Ž¥1-¡É̉ʫDNᑳæ­ÕŸFËÈÖUÆvºPWQ2ã‰Ûš'T?nL¬$'XZוWº®™Ú?rµR]y#A.Ê!ÎW¹nŒ??Ø×˜L$!¶-BåÄ”Oøašzi48–†F"Õ$ñ,K0y@ê‰ó­f:{'þYeƘ£Î¬Ê0??V>‹;ÎÙIS¿Ü©·zIØÊy‰~­-‰W_û(¡˜lÊ † úÒØ÷䟵…«²æÔWp2êtÃÇ”¯UáƒnãB °ÊrhËcd?nݸEž8yë/6ÆâZçþýóTÜp]¡ëﯥÞb–Rœ¡ÓK—è»ú!m¢ø‡¨1r!y¥ Ãe¬ŸÙ —k&­ë¼aŠg=ë•øœ×V#[Ä…ãvþ'¸ÓÈ;½nLUŒ‘$³+-´Ê:Ùn­b·šÆl}!0JJ„‘ǵù¹(§—Kg®ZBëƒ5iP ßþ´éºIª…öS5§­ÅºV?rÄYÆDEü u‘ÆÞU×€%2-–pÖDê¼n·S¿õÁRÍ©ŽÇ?r6©²&Éö!?0¦”ý<1LŒ¹½ŽK°¸÷LSÉÊÎVÐÖXd™v¢ÌRIfÎ{¤öåhÍ×Fd‘’Gf¾•m.š/€ÍÁð…°ùbx· ¢¼(ÊôöÇæ $¤ëoc)¿ù"¯è·¿ªó•=|uo4ª°ˆ…ó0'U)â<™MŒ×¸¦@¼°‹/æñÆú8‘…WˆŽífäçæô#Õï?r8ŠÜìN?0ê/Ÿ>¡žÑ‘–9½è]ÚÞXˆÙvC2–¯ðe‹P@rhÝÞ~h,ÿ€˜þáx ‘þƒ×SÿŸ—³Èv?nù¯œ6@Ñôt“TšåÃõ熒“.´¿N^‚tæÈ›Ê^lªšU&¥’¬¯&2 á«i~â+Ì0õY.ЭÕ€¤k.­mLMH??SF'yZy<4[Ù}‘ˆÂHYVA¤~_؟ƺ“±íYžÜ@Ò«ÊÉ‘·_]g>¡JËÞåé9W¢ÏR­²zÒívÇ×Ûq")ÊÐ[ÙÝê×Ü1)A£Ÿ§Ë§ñ9é&E$"7ûV‘ã­d 0Ï)ÊÁwä?0AŽòÔð‹yD%Wðæä©H4óÄF&86~Á4T9NÚ.xñ}fnç=²…M?0‚çæîxŒ^–P|[ïÛGԪЇmçáÚÊ,µY@ã·*ÿiC“ݱüØ4`òw¸÷‡üÿºÿ£ÏIzå]ðip×&9÷E­GAƒ¶Ânê`®˜›:˜Ñx’¿ /*=Š#EF(¼v¡‰6-(Ú.uUÉÙå3FêGk&$Z@ä&'iOïè˜ ’ìb1•ãטÑs€èÈþ°ÞÙÑÌYVÉ+|ƒ*§¯s'ÛA›U1wdý26z7½î9˜W yŽr¢?róɪ¹i.Üêõ×·+sæ:QØ bØuØCkÃ?0u®Ø€¿Xñ è­¹+Œ)‘“àvˆ4³¬šBɬžBÀëóQÈ…nnªXzRËÀVØÖÅmÔI2•ÁÅ·Ÿ4!Ô‡˜¡Oé|nìÞ,ô?0ŸšÜŸf0²²eq—ò³ùèòè̬ÀJòÑ…¸­ n¦Ù(»ˆ'ÌÁÄNè.:Npï^—»Ã&ïoÇ=r*’A5Ñï­mO£"Ï ¯Û§½oæßýÃi/›Æñ¼=è¯^M"v53ªÔÒc‹ +#%ó* —2í[Ûy%4,ûk“ðHsˆRšL/5™–,@Ù’‹±5`|Ž_|G¶ZB̲ ×çÒçãïZ4A@??R²G^„µØzo|ÖáQ%ÈÒéž ‡:õuì)º©5ð˜±ÒºCЍaM»óèчMvPYù dÛùe2ŸûayiÀÍ©xœ™“®®O‘àl¯õÔeehœD^²§†IÌþútëªÄJËdbÝé…UD%ãy ›~c̼º¶ÓiêûâI…ˆa|lÎ1¸Ì/‰»u•²,ÅtðeèjûãÞ › çÊ=™DW[ßÄ‚›ƒî3—&îsîc.§k8L“ô Ä%p¡b–j~æryòÿ‚ÓS#*NO 6Cƒgà~æI&3³mÏå[˜-ç":a Ë¢¶æ†ØpIvi7môÆ#fqá=²8ùáËÜÀ•+0’@?0]J³€coÐÆpÃÓQ³pÊéú]m7ptêœÚc^j‚5ìí[ìp¹ÁÏòèšf½×¨#h[[›‘·9]iêÔññ©!©=¾©†ˆƒtè¡\D½*\• Ÿ28- ƒØöq½zO O³ª„–S5 œ«tÕWˆ&“±v¨nÓmŒÆ|Œ€ƒÈÓF ®‹À ?r8#Ê‹@DbÁ¦úN?rSà1³ãê˰žwŒø Õ*i¡÷]M¯œˆ=%#(¶"±´¤§xäaÚ„¨ÞÔF2î®`n̨dj™ÅU½à?0-ÐÒ:vbH8ý¸H 5°3¤EÕÞ:§á3:’bH-b'J!jâ¢*î´‚ë »- —K®Ûe×ÙUŽmþ3êŸB‚nÈÎîÇ‹§¬sÆ9¼¦§bʃ±õТÿ/Ò/¶Å€PÌoïÌ<Ð.·+¢?nƒ9ÛMJƒ0›ä;‰‰ßõ´q&ÿDEï£dÖUÿ}™k×gQV5å)(ÎV~Q ‰¶¼H£/T‘°Þh®P¢`föþNL›jÄR»Ì¤*±˜œÂÞ=Yшú¯dÇŸýW'VÐÂÞLª¢?nÚË¢â'Yµ{BÑ’‹ y¿)4Ële½‘²{ãƒMù ê@ªÅì›\‘àʆ¼?0£„Y”dëµÓ£8òû!\…(]O—èVW±t¿Òæ¿VgCM ŸM%„e<¶;ð¦ú5e™Cüñ6tß«"Y9?n“=ÛÄì+á˜/ÈŒb¹[í$2„S¥váJ!©jÖ,=ÆåRH?r¸”¬Ý‡ên±}¨¼.ý¶.]E—ñ8»>$‹¬6ÿ®.S¹2¼ep«“Tç­u›;?0LèFøíP‚Ê©^ô²ù4Y¶seçíÁýAe#T 0?r„Õ¨œ]/ñŒv› eבjÑ,¤ÊÌ ¥:Ï`OéÌ4„èÝNtu_¶Û^)DÊhV7ò¢¡\¸kásué ¢§wú¨ßïÜ&óãj2|µtãÜ•vj$–42]ð’@+çò,­8,XIàZùôFë±É™íùŽÕÇçtOz©Å_6eVÔœsA¨®× É¢Î4ÃPí0< ˜Yz©M 3¤«RÍ59½vÞ›if¬JÕ±†'O;›ª·??ŸnW«k½šhÑ3QDŒÛ«†R·4òA ^ñââì t1®.±¾äõO<èÙ?nYÃŒ±‹  un¦ÔnÊÐ.dɹeŠsŽûõ&MÉg†$æuA–•Q‹oP=Šå¸­žñ´ãÃ0V??nŽðŸˆ0˜ùÌ '3}= ™àį®çè6:-ÅÆäÒ= àÜ­0dÝPQ«ÒP9VZ]еë)ã¢Qq"Ju«£o×êÇk<ÿ}Ì—)9FS:W犯ãÖ"6ÑY ‘j:ƒ•#žÑ׸ ÂÝ„ºOͳN¶×??[äÌ”S•Î@&!GÿÍ÷$ÍO‚Åóçä<z‚~Š*3žàSëÞ°cÕåk'Ž®ºÐŒèȯ,Ž/%†‘•DvÁº>žj^\ý•‰F…N-ˆ‰-ÂâÛ`n®¾HÓ ›(@é‚óž* ?n¤ÅŽÕv5„I2™µ¬G”º)G”cUÂXÒž!ÁÃK8o [{Éõ“ tÛ?0¦•—ùÝž=ÍãxäÚË…™ëW¾?0pjÇ…fAK},| I9Ó7Sï~•¥VQ£Ú+¼Å¬H?ni,Ý·¤ÔøK??ÆTŠgŽšoW´ÔÂÛÆW7Ò?0_6•n_݈;g¶Õ=‡¹7a:é–K¼ "¡“®pÎJ{3U{›]ÝUbdwŠƒ]y²9«´¼¶ÇôÚ2mùÎpF,zâáöý_4Õ¯Òe¬¨˜—ð‚ûÂ?0¡pntk*…Àƒ&¥j@ GàQuj¸2=7Ä?rïȇq‹x#˜Õ‚ôf‚9O™Ù8ÞfÊwM£ßj¿§Vm°ðeZ±×d„ž,„K¥ &â<$96Îç§7®û?0®ë§× gÔÃÝL¥ÆÎ.ËwÁÈÛ“N8!é̸0¾°¤@¹†Gº°ui¹L¦So·a?r.‹8›ëñŽ%þáëQƒ¥??h®½'¸ÍWw¡éF\DóhÉ,Oâ¦A§½}tu#èþò†0µŽW¿­ü÷¯tqËCulTÒü×¢‰@»a{Äc÷þuŸ?n7X{¸õ3ü gS©$1‘ V€—`ô¸…Éàk#8;¦SsÃ^û†»¹¨™†»™ÿ¶­ Lžã¤äWkNhŒ0”?naþTØÒ¤?r¶n¤tŸ{¤4!¿³ä›&&ßo¬Êt~s‹wÓøŸ¸ºƒèŸwÿ¹¿§¿ûñ??õë‡ÄÿüýÄÿüáøÕë“ÏÕH‘ßåžq½üæû“gOÇOßÐÃa¨îwÕ`_³÷ëñªüvoÛ:¤]Eó¶I2Ÿ¿ëtt9Æö¿úý½É¿ïÿkpÐÒçÐ|n÷(W´^q¾BaÝÂxŒXÎc]µhO????½N¦“ñd™¿@­ßëøÏo†´¯Ìu, €[Ÿÿßÿ»Äÿý%æÿ'Ïs[ñïÄI¨c~tÕ³ä<Í¿¾\$é"YÞä??¯æË›®¢ˆá¿þùõ»ÿ»$ó|ÇŽ¹‡ó°»OÉJó°wGëÿóÿÛt~³HÞ_,հߤ´Ðûi¬´çNO+zöžÅ³ÌÚ^¾þÎ$0Z¨HQ¸ìE¢/ ‘ÅûEl¼|zv¹—„øM–i:Íð@¯ô×gËòBÝôú½ánK??'t´“T"]9#-BÿèvVùÄ2jšÎôíùܰò<ºŠö~PÙç04>j„ðš¼,õ`Ë ?ndãl|aŸS0·Öÿé·Þöß9æ~2¦†)NN¼ôeüA«­¿uJª??ÝÂþäÓ ¼µ¤î]ÏhëÑ>m}ñM««N;„›qšQ‰iæ]5ai¨‹&ˆÂúa¯x˜TO––[@/`+ƒ"l鳫È6ÍâÌ‰Ì ª¯ˆŒ­Óä}Ëé^æqµ}1Q³sÀ?0æt7ȧºœ…J[:ËSôqx¨wÔ=ÝD™À‹˜Ýé,%#fÔ˜˜(/ï—¼§)Ô«'Krž·D÷ÐóÜÂU‡ª¯Ò~ÔP}õ•ÚV7X¾µQW‰ßÇ ?n_CñZL¬>CóÖIKY?r\ÑÜÙ.ǬpßI«›·oKÆSíÁ~8Þƒáã&ˆÿýï›a®Ëw ŽÔþ^ñëKÕÿx^úCï0wëówHV’­=‹ôL2RŒv9¶”ii†á xÝNø¼OzÎû„H{c–®b¯$wϤW«ˆqž1tiá`Sœ%6Ü£¹¨ffÙ«ãùhàÏ?rzF.–ÁIîRús˜:¤Ú«†o’x:QÉŠ‹ÈµÀçã7é2š¾XLL&ÿ%“™ŸÒa<Φé23kR»ãFJ‰ÿoü$¥Lˆ€ÁuëÏb±¾¾ÒMqäš·d˸–J²RC•^~‘ƒ©ÍBsñ›.›õo*÷›£{¶!Þ=[ˆõŽæ>¡Ko+{*Ó %¨ª‹èûesD›?rÃ/1Þmƒvº°`;A>ûÈ5ÀÓŽÖÖÉËöž??ÿó›¿PÆ©!ýÞÇo’þÅŒ~b·7??Pª+PW_3M/¯¢ìr}q"M2G|;ÿtŠ”ýЉ“—öž”jm9§}m†q—c…!€ÖZ×Úþ-´ÆMqîkµ]&ø(x£ßÚY¨IÛË” 8ʯh©ÈŒ“º¢°[U-ÀÓd¢nÒkƒ-N‘ED‘á~¿¼Y^¤35ì ’^ñK T-_Y‘ÿ§–ÚAƒfB 7N76Æ\ìôàvoÍâåÏéârEیܲ—#ŠrÈhÏ«AÜ2ÃÉ­þv/Œßãó?rãÅyꋵԻ#Áv[í5 vAºPrس;¹ÔÔ„“œm*Q•ŽYìÞϧ‡»ûéª4¢Óôç2Xç·s:’3I´ÔG–ÔYÊn=vÃdH°]ôhÿz4Ø¿û.í]› yø¹h:žY6zFOÕÈ5?nóh­¦Ø—›×”ii¸^|m¿˜MoT:‹•²º(O¢fg±íÊ?r‡®ô ]9Of¨-v§ìârG’¥™mžS¼°î£#M.ú0÷ûÚ‰•ï†xÉÜný?rû¼7Ö/¾°ÕÌ÷{j€"ÎÍ‚ïšbÞ[4›ÌñØb€'¼::mv¨ccë$G”1yÍÒ7kçq<ýàùV«sATªáù*™µéY·jýÿrH_ª¼Eu_?r:0Xv}u-´;Midœ®päÚFo_­…(=þ&Êâœs:Jw×DN…iÄÊwؼÑÉ!MÑÒýŠ¢·žÆz®¨‚Aº$#±æd+67U‘GL€ßr“5„Ú£El`§çÖÓšÂøåõë“Å»DôñQGæi¤hÑ{§ƒè³—·ÌÓ}­îlÉä6W#W7ƒ©«¢Ö>¯µï×Êâ:ÜáY¢ÑÏ¡8Î?r÷«QÞèX§ZÏñyòQ3ìjŽ“$_²³ŒH†˜É+«‹Sïp„¢…g¶]3[j¦e¥ \;•ñ/ªæZß}-j:fέ0‰­ähW4¦KÎL£ßxD•ðÄ>R²›:®Ì+ŠPÓ4¤$Ÿ<{6~ñüøu)…‹f¨Ë•¼šN£yVˆƒ8³J™^|*¥µ]QÓñULë×È$é÷„¶†¤±GòÂb¼Pë D<ïÍÓ9üû?r½æ˜T®‡ŸN‰"¬ÏMš<™(é™P¶2Ü ª¿EÕw¶QÇ=Ëñ…yYrœÄSÆ+Qô“ôºÙÄÁÌŽ$bXÞ¨ïÌÜ?0m£“&#e:õV=ªïf€?0~~‰Þé"&gĸ4G¦;ü·£’wf2»ŽeV]-ÔX9ƒ‰Z?0='®yké–ÌÔqû«´öÀ/ëd^Zx¸õxnD??}¾ÕQ³zcgñ iÄÖŽÛ\Cð§ÑB¾ó4= %s??& ¡»’®žÈým÷ÞÑæE’ô@ü çƒõ@rÅl+ÜÞuiÛyúòë"#0òéH=6r¦Ä–¥úÜT-«ø4ŽÂCs±Jå'N§1Í{Z“§¦ôQ €­Ûº%+S÷Lï òê5a|•|Œ'æs¬³DÓI‹#{˜Ì ÷¹Ðȵ ~¶¥‹ôƨŒ7d 4Ñb]$¼ÌüvŹ’n¬èªx´ô_óEª—£å?rÌñtÏošN≔ˆ}äÆÙEº°Z–Öq²%á!·BJ!ÔŽ»Ñ¥Ò"ÔELÓ$ÏSÃÁ](דÀ,`??ÿCkgØïgê"¢ÅEU6Ï’ó$ž´hê+ÚÚÝNÀÙ,R1àèô">»$®»ØS,†žb€‡t€}P•­bg¢Úú]ÊÃj«e¹Ùig( œh;?0ºÔIU,u!;?nìÓ…é¬ U:yÿê«Éíãë­›Øýùýå#gíUÕƒ×-nØÒ6ÃJH³²ÃšCO=Qê‹‘«Š¤XC|Ku›jïL@(¡a¯þÌ6kÉnÝù¤qq6O—Šœ›­Œ0âøŠÒeMVÃÌWnG–•ÚgäoT°U½TÒeW3Bý¤Úþƒ£#P¶”EâdÑ +#°ß–šªtyËl–É·¢Îºe!¨þQ¯üÙ¶^G›­(§î—ÑtNŒ£ =Ngw‹ ‘LªM§(ܦM¶C[ÚT»“Ì…M“U½fy]ÄüÁõ¸´ƒ\jÄtóÌ:?rê=uiñÉO”@J‘ÒÓ™´ŸLåÃø¥"L[ó§ˆ w{L%ºŠÑzm‹0ù¦[`ªímS¨ u8e<—=-áàøX£7ØYܾ`í6üpƒ]øýÁZpG€pì„w(6¨~P½¨ýÕàs—WYm‘mõÊ„ ¡×®OÆ$9???rÈ•äç±}:"uYÀdðœã«Üü$ƒª°ºƒ†EX1¤š–U]??§@cñ¢ÅC${¤¢ 7iá,šYUhY‚@óÏÁßo°\väöô>WÕD<¹—*GÑÖ@À&ªbP”žbÀÎXà^©q/¿ò‘Ì,"¢ŒÚU˜ïLˆar\úA¢Y¦yØÊŠÚEA6üx6‘­÷Ø=8pã9ù:ðí4ë8‰ ©{®AHXÝÜšL˜Æ<3½‚ÚH‚BWãóPÙ‚;e·!\ú¢µ¡à8úL‚Ãøéÿú$£å}§îVýÜv+ùpå™- yƒ (4v;šws\Db•»–Òl¯R¾Ú>òeN3‹~>†²àKÅ?rEŽ£+šën‰’F6&ÙøêzºLHv„éÞ ¤Ò0Ïe« µHјCv6^m¢®:-D”'Cä鬧Ṫåv¸•Àbש´€Ÿreu¤¢à£Óê[ rº!›¾ŸE§æâ¯ZÆz@-áa 3ڮϴ{ª;ÓUÍ!Œ#Æ1 ÚéÁ ¼._"šBw5Sh›\ “&306œ@¨V‡Þ4™]޵nM è@ E‘U¬Cr¾H>D˸†?0±z¨U‡ÛûizD<˜#¸5`¯g¸]Ö„s\0¡]g5ëù'Mç”?n¨ ’€±!†¨V¸wÁga7ë}{í£Â4¬öòB8^¡B& ì*^àêÖˆ+€Êž/ÒƳŒÎúƒáîÞÃýG¿FÌŒ±õw6÷Ò³e¼Ì¼o‡vÕðáý7Ôÿí=¦ÿúú¿¡~6øZ?? õ³þ;?0ôožŒ$m¬T:§Õ]ì t¬¹†AÈ]Æþõhñ^Tµé¥µ„[gsuÔª:‰Öe7Ná&µˆkâõ%ùÒ õšÁ°Ë.¿Üò)Í%s±)˜‡F^ÔH‚Pu…MÚfññx«[Ò§Š€ïh¶e–ê^®îvÈG=¶ÌAÐ-ÂVþÚýi¬bJ+Ñr6ܘ§9`„ßêù§ºt˜eKvH›Ü kûù¨÷ÐN„+ø–ˆ+ª”ÇrOD·¡M›]•#Ž»gòU›%ö,°s;NoÚºZ1~µBÃÁŒPͱ[??0ôÍi„mqÿòë6??!3´’o~qÔÜÃUgm¢WÏ«hª&Éûd™±pk!·õVÍlqžÃ+u¤v+±x²TWi¶T»êì"ZDºÂâó¡aŸ%3˜¤Š2z¥ê{è:EÔ#ZãW?0óT­~«ºWš/¯ÓëLühú §iG™€9¶ þXnÚ!O½ ¯c§†‘èþº)Þ)áf Ûíoê&„€›]ñEZLÙXîº-çl÷lâÚ{а©Aui‘U§rYäŒ/Ê".¥ç—d¾_Çš•â4º|3ZØ(EWnÜ$K?rJ®“»ö`‹È½w²T•æÊ³Ð° ý2í†U6i–Ú¯6¦²&^çò¹A Å"ÊõG‡üo4Z" û°È,Ýê%³û´èE‹yÔ’¶'¤#‡¹œx%Ã#„p@yìH<Ý+ß°+l5›”–žø-ÍøãñÏqt©¡¢ÔÞOáG¶bôãõ?n´íP‚æXa{ƒèÈçlA™9<ÚFQoÏGæôXOÈm³¦ u*–†?r‰TÛCj‚¸^ˆêêP‚¢²"þY* “bY?0?0 æÊú…¯LÛFÛâ–Ÿ(b’àÁ‰Ñæg2–Q‰•Ø=K?r-³^gÚŸÅ®ÍnÚhQÃÅ©^€Šö³­Gh”{c›ÌÛ¼>&gh’R*lO?rÌþaãP?0V¬]¬Éш5é‚lhlºªï$` ?r,­a?r-b©)EQ§_îz£QÈm|•kEIJ;íö„ƶ¤›~p[G`°?nüœÐ_Â??eÍY^ϧñ¦½!ÝŠ¶ºœÔŽZë;AÚ=@?0R´P…"Ø,Q„Ñý*‡?nf%óòV=çð|íÚ©FkC¨o—«­†Ä)®1ê‡Ó.œn›ß¸õ]çKëaQô04¸‘žÈ)V‘G¢ñ«§ ÷Ap"#¯ÎZëšÛ_vÕlݪ¼7 »Ø TmŽ$ŸZ¬ž,®Aâ$Fñ§ìR©Ç¦Š‡djB]ñï4¤oÀÍ»Bþ‰M矘¿÷µ"pNH5*]¹í­Þ‰*Á\@Ímg?rl|e'k.nójO3œÂ¯?0ÝFh¬8ôVß??e¶?0ÿ*/DNHùT©mº­‹êÚZÝ)…ØÀƒÆµ·x…¬fÂw¥Õ×??t¬Ô–XDÂæjb¸ÎgN9Gj¥ãI×,X*Ü7§+´Õ¦¤å'vàš?nb¡Æ—üz3™8í[€¨hYš½’o,Ñ7š‰Öô›ÅKšÞÔ"pªÕ²Ãé‹¡þ¦üž÷gUŒz š„ 9cº”SFÈ=Ä9Ø\¦¯ÌÝMÙsK懇•Mj$]Øw—ßÖ ßïíïõú½þƒA¿Õ ðI8ök§P¿t??¡*·`°eÍ.gÀ«px¨Å;ôöFʹҠCÓÍ,~Ô-žúÊïMŽ÷A®êß>žÐèmÅ_þ:°µ›û= +#u&rÅÅ ÃÞ%«×åù?nô†ÞJ$œ–HÜ«U÷Ý`ˆ°2]Í(µtGúÄœô™KûLòk³åx’j;\lTÙ1‚…ÜÈe4‘¼º¦¾X“†4' J¤‹u,ÄxAî•y;N(Ìn€MÞ”?nÌ u¦ÔŠý´È×?nG•tZH^zV˜²g溡«}wVUwG2æ6M[èÑ=†I(%;L«©nÚ®uœñ—¿!îzÛj½Ûªx÷¶ùƒªß) GÄŽ³j_FÊÂR÷ð€I<ÿä{DkÂàØB5ݵ§bë²·Ù1’Aý9??Þû¹wrX&NÎÅ:Pá$cÀ6b4ÎV Ê:¢ü åÍP~[B~0hÈÏ5Õcaü ˆñ«’ ÷<”Ç'FøA Âo&ü`&ü&DøÁLø‡ð+ýA\sàvbî‡Mî×$¹×!™f”ãTÓäa ^¶ã+ãŠÕOriÞºÁûE+V€+‡®KÆ:sŸaEè9¬¸ta’´"Œ„V„±ÐŠª0,]è³áêûê·9O\«€Ï_h:±œQ ˪>{þ´úýéü·¾+-î5lA¡G„Ò¾ðøç€E‘iRùÉP$pE5Ñ(â1;ˆ$"Õ_Ðý3n‹ØëÈQA@³r•$:fÉÉ4Ǥ\ô¯²ìa½ld:À³$AuÐ& É·E’ï)ÉEÞ~bDœŸÒ?0-ù1n.PÜÄ 6Øoë?rv‰]·©í–‹zÚÜv¾üúóíµº{µñ§~ø¸¤ÄŸÜ_1ý'6é ±à’û8ƒ•X0ÎïØ¹îí¶y,??û¶âlµÏR’Ó¦7Z¢mNÔ†”“¯û@$$!&.@ZѾÖ>¾ØÞ À"eÒV¦·è+ÛqñC!ÂWo^}œÿ1Páå=BþVûŸç…T%I̾¹_•<;qUåFÙ¿vq‚«rP&®Ñ;,ØÇÒ#3·?rðÈ€‚¥–úrNË߱͘‡ù}‚ àQ!²îKní‘3­éšý5À°ï÷ûO64)ÏHïçöZ#r¨ªW(g•ѵþ1.ÉÂ)ïxK% ÖÝë(ôÇð|Ä"žƒ”¥”™¥ƒ´àvÅ3¨ˆÓ× æ[K`Yn™H$ª8ä½c¬¸]ËV}%î+Ö+ºmV„¤6Ù^¬©V+¦œÕððcÛu_»ó]ªo?r;vp;ú㘱(hÎÚ¼}Ï??++e:Q¼(¥z˜šîiGó )š•U4wì¦üc›Ÿ÷4³ÙqȱîæhÑÝ‚q™ëÜÇ v‹OŠÂQwý£-󸤮è~j\O2¾4ikBr’f™-l¹HåVßB…¶5EFË•T9^;¦ÃJûdóÇš˜äö^òô¶°d¸?rµ·ÌD•e§Šé*+]ï­ Ñ]‘ªµnôP‹—­iÉïÙ­ì7’‘ÓÝ’A`øAB??1?n°P³$’©„ݺov}-Ú¹- &šaH2©Y³Â¸ð¨BH[ÊðqÚÀ(t)­é]úp@]7q!‡ .]Zô™¤éqcœ??š•pQÀ.kV šˆ–ÛûÝÐöìø®Y×GX­t×H¸šÓº‰o§³i`‚âžGÁÅÍU³"þr2§o›UÃÙdq|È÷.U“pÜ.¾œ­Ê`:œÂéÕã†C%ÓE8½9¬»†– Aô.¸õØ#ÿ}ó1ˆ¢YTW ßVZ—3uãÂïfŠg—‹Î>G;Û‚ñe7O< ÃnõÑì²›çr-"ÚÍ5{Lãƒ0ù‘??¹?r§£0ºø_´›¦þ$èlA£oç×Ó›ÉÓÍñóÍÓÙ"ˆ»›£à*ø¢»éâË&×o¸5Jd±Ã{ç»bz0œÍ¿„dë—Ë??Úúÿå«óïŸïÅ›óׯà›T??xëøóûŸ×ÿŒyâ÷÷_Ÿÿ ïõzĜǨžæ)#æY®Ê-Uæˆò““MYúǃÁš—›jÙ<üzxP§ÖÉŠŠ¯7%9MÎÈËó??",=ó„ œ'á¢SLA³Œ•zð‹´-åüdn)jQñˆ¼ê?r3Ziö¬Ìœª»ªÐtÅ:Ì;ÿ­WY2H…Îø²Kdþ‡Wnè¢÷~¨àå³?nv©’bÀWx‚Ú¡á5‰K¶¢‚ ûdR1°_Hï='Š4/¨Ì$èY•EŠ^tEüêŽD2•=Ö‹.0@ì®;0/_ O6”edÞ'±¬2ÎÔqÉ‚îª;>øuÔÊ”õÖL0EKÙ?nÎ\IÜ’©¡??.—nª¢€8”)zö„¤Š=s??¥9¹úGb¬|åo8°FTpÁÊÞ—ém©ÎÛ†½<'HE.¸ú¿ÿ=î+®R‹uñA@Z¢vE)ûðÐî§WÄW%ËILÞí–R y\øŠñ\j>XÒuÅÊ’]ÈÝÂ@&àú¹Rÿ³[²»ã’qø²dÙB‡Èþ—B±ñWq»õEI<ŠŠëR¦È¯Øj¥ØŽœvèK6> ÎŽÛ…[‰œã¾Q|ó”Çüƒ ×ð|fä§¹©ÙürSž¡¸Ÿ*Ü×FôqÝ¡TUFõ®‡aIåVôXÊKÙoH\?0‘®xÉ<Фßa$ÄDI&ˆwÇTNEÿx–Ió•¥~6Çi*—¬gèZV*a= e¯P²Wq:ñ‘©6.Çdv9&ðéÃN—R•þ–i™3(ƒ}î¡c8½$HÀñ0Ú…©[ þœóGb‚¡#NH‰9˜kÜw® ô8[îÈZQ¼èçHLFäÊ|Þ`ÍG N»HYn¸®Aóˆj-NAIeRáBZ¢>ÜÂir?niFþ%vÿrf”¤Œf„ ‚mMûOÄ(f^r£ ˆ’¬Âc¬}sÆsî4 »‰ªF¡•fž±Ó#¹Lù?nÿdÆ-óR½ñHʵ»Fè•&Zú1Šh–e(3¾6¬³4¨Å~þÁ…ÈèÝnd~@‹‘^UJ€JfxR !3qÂÀ$_É,“[t?r² å葆7’ h¢KyÏH²O!!Ul¸Mu¯º&½¡`û’¹€Ù¯SІ;?nÕ›÷b_H…úZnöAÿu@p{õÞÆdÍÞ…£`DþÅáù_<ò>\\Ïn(` ´ø²•øÓ/É[Øàx$øÂlçÈ,"ád>¨ §Ãñ?rn8ÉðMg¨!¤+]Ì*t¢Â Fa“ ^ãŽÃÅ—¹ S”y B}”E8¼û™ßDóY€úˆ†ÓË´“`ºèƒV¨#Á;x ñµ??Uþ?rXûpc…W× r=¨¼À2ÿb *ãÔp쇌|؆kR"Cæ¬{`êóáßá"œMÑ?rÜAGðè—ÑbÏú>ŒøQc@.£ˆ‡p"Ç … ß4°R0Ô¤Ù#H‚Ï7qPÛ2?nü1ÈŠ‘¹I ½y7–ˆm?0‰Ø~Ð`ºa?n"Ä¢¨©YrAÕŽàöÌHÅ„s#ÖŽFžP;¢©bo¨?0üÞó”a^ÓòÉaa˜rVÂðxÑ'M“\;[ÑI^i‘’:€éLžIHBM]"}d¨K2Ês¦ ¢/ÛpÑŒÀƒàÞÿý½Ûà€óm„ÙYä´dŠÓL×ñÅΰl?rÓÍpãîñ~ñ%&S{à`³¹ÂhÆ¡äpÌt@@c€7ÐÀ뀕¶Ø:qõ‡tkÆf£nTh{‰¶×>v!`”¢;ui‘ûƉçÁ0„#?0güèKÏÉŒƒ_ß?04îQåôHH O†7‘5ŒC|s/ÂÅÍ" W³ÙÈáð?0ð'd<‹1Zˆ[ £4Ï"P‡40xDÑͱæ <a}`™èÎÒLÌ"ÄùnØk ] .j2Ômá¯ö‘Lƒ«qx‡Á(žíA1´jßû_:d¬1‹ŒõLO’f Ñ»ÍvÄsˆBhm°!^»p:*¾úŒŠ£¨øªO¦Œ#—iÅ7n©Ô0w#³„¬‰LØy©Ñj+O*?rŠw¸4«´]2‘B-ThH.KFlT€ò{ [AÃáb{Ÿ8î¶LóIaΈzMøÕ??£úgToCºÝXL˜­÷ÓïaŸÝØ/G??|°û°µ¤Ù~Ôf=…mú¢0Žj]ò¼ÊhÉÈVª,ÝâÑDÊîY&‹œ eY˜§Ì{e²BQ…=DÔžQî’ŒkM³8ÆV+h0¦Ð„â PbÐ0¬8¨N@xžW‚—œéz³jqŽP»ÓÇZ‰^¬ÍÞ”CÔÜnx²1úöH¥7àTjèyŽBàÁ}¬\0¥7¼°ÈiÏú6XxfA˜µqÀm®S'¹”ˆ/Á«”cÁÎN“ªç¨Aƒ³¡šdO ŒÐÛ~âAf0Áî°2×,»gºO.lºyl@Å΂ª?r::?rí hβi††°|ÉÒKV¸ùÃjCQ\ȇÓ46=Ü4u“³Ö"ÿ²eHÓfTÕh7r 飼ÆÇñËEY`7S¨ÑMmx“ÃÝÊUì¿+®fNÁuG@åÂ?nq“­ËèT2m,¦EÑwçAnNuùˆê5†¶ÜÛng@®`VŒ‚Ëp" Å''ÿb†Îþ ŒX1…æ9šá±gFµ›¶ƒI}zvm¦ÜS}Öe`˜\Ÿ»é¹VNèªdêÑÚAƒ`†òÁ”>ƒj.hFÞÙ[ãƒÐÁ´ÃtJù…Ì$‘®W2ÏÆuNÜèëÒùhØä4e(¦©éyéAkÆJxðögK¼¬°‚ô°[ ÀÈŒA-ÊtXÚ0ÙÕ<€‡ê’?rk‰+HŠÄ?n«$]>?nZNÛ&î¹’#Œ®úU¹‘ªí ækCŒ¡†¥Bɵ¢yŽå’%Ášá" ûp¿”´ÇèY½Òc¹m›@ý<ˆ&aãÎà¬:rcâ÷}:ÛVQ‹¹»ýù(CõêÓÒ&ôÙ)DAºûGw²Ø¥öSÏ:_œÕËìV_?néή¨9ðÚV4kd"}:Àð8§ÂÍinÆ@Y,[á–åìYÎnÏPZ=süfó…÷hÂ`4ÙØ®Ã¢T–Ç0^37•Õç¾.ž>%ðôh¡"%ûX>€Ý¦Ê©è)FSºÌÙ@\¶ƒÕZP€¡…â´D“?r¬&ÏY ¥’‚4–¥Ú¸‰|¨@*·å³ªgo©Ù³™QÍ«ï9ÛÖ`Ui¦p©![òdŸ ›iiô-Àó9zˆ/;6M„k;òjK©Óiˆy-usJãm…ýN;Μœí˜›€ 98C`0?nàb¢è4Θ¯ÏÌÌ-œÙrõäüç6ذ¢#2îµ?0fPsÿé6^s#JÓ{¦J®fl+ìžû rÓäNÈmÆÒµ x@wÄfLáGF«ýœ«çºbóbÓš¤j€“×1!q¥1s$%W,ÛµW-“‡Œí5Ø{íÕžf8ÍL}œ[à¡)zÉ?02Qz•eÆ'¼/KøÊák?r;O—МœáºßlGöÑ%lÿ¨ï‰õñžÕç0- Ò#°ó??zëÕïY É'Ÿ5üVï’ê 즃¨qîÐ},ñôÄïñÝT½WGaÎ^`6u­„@*än¿ jÓBrýuÝ~ûü«/émÙr²Br.xoEû[¹Zý1þþ×›W¯áþçë×ÿÏÞ???0wÚ<{àè7¶±±“mÛ¶mÛ¶mÛ¶mÛÆÆ¹ïû;×Öß§ŸêšiÎî;;jÓi$ñB߿ʪ<Žù8ÇÁ+;«l¡5©3’#†™Z²÷Óz¯Ög» Õ- X-¦‡¶«¨úòɧû‘…ÏÚœü2Õ:Þ>¶¶p3·¬9:¥ß¸ö#^C§9­*õד§F˜Ášö{CrùtÈ–¯1Œ|%šµKÒ••ê!#?r7LG kcÌa+|¥-–ug‡kŒ¯¡uPV4ƒû¶€Ô`Աťd‹E6÷Ò¼Ò÷b¡ïÉ9÷»º´©qù]Š>ÌiÆB`>¬´… -7{RôÓ4:U†úrtRï]×l·ZƒzîÀ_õ§ê´>&TÿÍI~L({ŒZzC©Ýeƒ„ºðòí#¸ºŸYZ“Pgv”HüÅ8Z”‹ðÈ)'ùMÿydt…Oëöw›mÙ¦äìŽ7ôõ‘?n?0pÛ0Ô3ÒÓC4„Õ“Ó?0‘˜€F²Óû”Ó€ÿ=)?0’?0£@?0 I  J `gà MPPÐJÐs0Z07°a°p9?0àßXåßø$ùÐ\ë?0üŸMläDÿï[ºŸ«Ÿ&àd éÿÄÿeþ÷8 ôô?0.#àà„"ˆØQì?0sÐ\ ±Ð†ŸŸà91(9—ÁŸ]‚ ~?0?0ü¢úÿxì\w???0ÅÁ6>J9P´0`@œ XÖÙõ??§·ú&úúÎÓFúúöà‹{Å›þ?rqë™ôÀ€Ìk(`áðxvôÿÝN Þ!fb=1HXt*=¼Ñ¶¥§l`B€”¥—¬§b(hXãó¦ÿÖûKø 8څɃói÷9˜Žpʳõ}w6xÛ…Æ{áÓõøÃ "—”Ñ¡˜vñŠÿ¨»Ç;Òâ‹÷#õMïƒàcê#êáƒâÃíÃâ£Ò#ë5ûFí_C?0?r?0èJ rüçúdŀĀè‚{Ydö74Á Cà°WYm—??’kYCgßü9´ìí¿?0Ÿƒ¨ºwT¬ý©\ȹTY¨¥¯Ž‚~/¨»2¿ÀH5“Äã ÄÑM‰ºÎnSSñ©h±†ÊÂNŸv‹±ÓFªê0øÏÕ„~EÝ@ñ¼íËîÚzÿ;iŽšnw«­¶Rç`ý¾Â ’¤k<јeÆÙBë€T?0ÌåX)ÿsL?0??­˜$˜u?0?0->Ð_À??ôO, üŸöµþ+ý³ ýÓþNÿÿ9Úý'÷cü ÿ÷¯íÿ,ÿcü7ÿcdcüïüï¤üñ¿ò??Â"?0Ò¿­”Ü??}!?00DÍÿ=,rlDœ p=€¿$T€#$ôÜ>+0 H?0L?0?0”?0,H‚?0V;ÍJË8$„ëÛE¸ØD~øG¹‰A`[_™?n'X (Ë"zó ÐAšœØ1–V(;ÚÒ¼ÙÛ+©£àn_^™N4v¦w>®>MuÞù9ýè¥vB飹!"Æ(ùôåÓÍ}j?r@›Eã“–ðSˆ¯‹·ô’³y[»ÂͳvéK7•æç«H¢?rG5A«Js´©†ùU]Yö£SôÓû­Ö´¹ú²Ï³`!Iì¤dN_›)ÍìeqxTʲß1Tž3ÏÙ!‚a”n?0P§ô[‹3j‘nòÃÛñvv{û¨Ù±±T ^ÞO 91©??­*­†ücž¨’Ž ðmê[«xáòžÂ‹º)ã{÷ËÛ¾HñG±§²…¾ôN¯\lP`ðdD®`­x@À(«IE:BEΧ;vïg—9q& +#5g ú&"=–Ôï-˜”œ°ª>¯v}\Ü×Ä™Ó%õheåŸDî²{CNBx&??i:¡&iFá®??)F"ŠŠ|¸(½ï-1vÚùI¬ˆÊºWYEeÓ[˜?0¬ÖAË=}Å—oæŸ

G®Ð_ˆ??^xE Ë1•#2r³Á̼Á¦‹ä4wUàYýú­ÉÖ€=©!`~·jt—(RÝŸÿÕoÆ´šPSnpL+Þµ¢e!2(8Ì(ÕJ†v$T2a ‚$H”d¼ˆˆâÑ{îgZˆÉæÄZKxûiü žÌf«T-?nñ8K4X?nÙCôŠo0-RD‰«#vùY‰):H³f§ÇèÌ®kĮ׫ƒ%莋­Ç,žö§~9ƒ×ÁÒ?0°Ÿaá¤ðu÷d~«ïÀ–<ˆfAçûÚš4Uì¿mR %€}ÕÜ "< &Ó?0¿öY s ¿^??ÒD?0*(ÆŸµZ0]ÕjÅdÈjŒ••‡ }<ËFb›TâÀ ÷TÚ¢ä7PPæk®9Ñ\Ý%ýbeY w£ì"v=ɯÀlðX¬k@H€(*‹~„‚øð¾Õþ^ [ËL ˜$š DÇï¼_¦rºìÁDàþóÑË©©Å=N¸EšS§˜ŒÒÎdÅ'ˆ•‡ØYÉ8Ùµ=?rNã ¿›&e«šy¥ž8»nüÅ:N qUú¦¥õ¨ŠÎƒ¶m?n+E³yKb1Ì bïûź(eó}՚ș5†{U|[¿Uá¹Ú§šš¸zìÐÍ(KŠœµY¥*×ù7 Ì!£§qÝ?r‚Ý%ò1á`ŽRô¡jGq†IõK´—Rîþ- )XÖÜ,è:“ÆYÐ’Güì Œç¹BH,’Vþ‰¨}‚)nd’Έ¸¸¬æQÅQ:+ptØx˜ÏF9¸ ˆvÀ·;…sdw·ž5ža·w„ Oíe­ƒ"zTÆàs{ò²¸îˆûÎKMbéð’ky/-ºÖ¾ÀˆäÙóë@¯÷‰àe'êÔ[VZŸê.!§CRðù«ºÌ‡Ýx:ÁD§þñø‡(Bv„ 4oÆ¿~}GD_^6€ ‚Ü+ðûYö(g2@k6+í°ÏÎóè\fØEÚÚA¥Pe¹ÅÊJï»ýnjCT~!T±¦ÉÜ6¢Í~ô¶ÁL°u䓆2zÎ1iÝ:)¦·€ÛÁññÐd¦Êx³EYÒÕL-u™I§!ge,¦Áýü¨pÅ b}âÆ\,?r4î+ê4ßTÇJƒ7ÒkÝAЮËÁÏÈÌ€6&Ì]2Ñ Õ•Ohh5‡Mm2àó7ÇÂÑFŽä½ j5I~™Ó7;̉ybè8ÀÕÄÍߢ%°!\_…LU¹?0ʽ„›'U³Ê»” $û|D±8ìUaD’£˜Ú™ÄÆ6¾…þäP>áW3íZÀjww›ÿŸH—©GY<ãEfgÁé&f¯§ˆ3 &‰Ã=Gý šRN;ì­ú†l‚4³HUË_õÚŒâKy“ÅxJÔ ´ô¦±løÃ“¡íðÀm/§¶×¹ ]ן¤™0 ÌD. ‹?n_a6\ç¤ñJÔ¥ìöTöq궘#'È¡±{èŸ&Ë8¹¯??Èí´V+®£q1ü/†aµ=‡¹HSz ÈÒÁŒbh˜™¤‹Ã‹[<Ô>ä÷aÉ)C‹ØúœÊ•qØ—Þm*5×¥ÐôOffʽ¡ËQ96V‰éŒÔ ®DÆ0C%]‘>‹7¯Ð¤Á#iÄkYÉÔˆq‹®²äMÙeoÙKkI?r°æAæ¯Ò™‚ÚwzØ ov?0 nÎ+$éÂÅtŽ-‹Mä ¾ôóú!ˆ0ØûIü[Û`pü8/;–Ó±cBΑER8Ø·”%" 8Y„{SïÓº Iw%ØÉçÎNÞ8†j¾K\!ŸÎGг{ýÒ6éu~ZÓ@\‘{ù«iK°»âȵӯ§c£s—A>™Ìnyµ‰´i}iUÊ–¼A¡A‚qQCT²½Gy9ÓÈâ3%úöǧ‚šÉè‡E÷óÞBùY4¾†?rÄüK¦2ûêJ(QƒÏ@­Ã~î´ï®‹$€‡|Qe?0cqo,Á€géAe[«6fVÜ:Ü»t†”­YÙÞÀ˜t5ÿo²ûyw‰©lyÙq•™­:Dç©Üp*'†ëšŠ“v=®i¦-SÚ麉“–ný—oLÃH¿äÃC—o™L{’†¢0ÃC}^!©‚IÝ:ȃ\dz¡“(„‰L4­KgadéL<à/1.RÓ½k«Y”£‡y÷ˆÝ??;«:ÂlÒ©ÅÝ%†c??Ö-æŒ$Yoüh”Hxï'´'øË1LyïF%ÍÞ É‚(wŒ‹—»£÷$Î…KHx”FŽ9 q •<\|ðëè«Ý›¿ÊŽÇ憒ãð‡´XÍ¿'œŠý-6”Z^}˜+ŠÐ¹¼0Á¹·©=~†(=6øN¦í’?nÛìc‚t_`€q‹Ða«‡¬M& ©ý!l¨ ºHº¡Æ*:g÷hPü»Ñ:¼ôð¢n¯ þ”®”uTPÖŒã¼ì2žˆ’,#Ãö{¹ôc™‹l^BCIÏVPxZÏÑ^âVa ±¥õ)ï-oquÅ–·ùïÈjùÃö¼bÈc±r8±®Í$(>m'/r ¯«¯)³} ¿#Xaü—ùë÷Ì-ªˆ=DÉ’‹ÂVøMýåÿ’áiŠ|ÄF`Öz›šÄÅ6d½€èwŒdµ'_M­u­Qfbt˜??h¬žÑÇÙd‰gB†¼­Š¾Ü«ã´ô0â´è11–”êçé(õÔA˜D™H‹@yép³ ²>¡ªSþU×€•aJ„Œ‹œ`ïõÚøq\EŠ[]E3‡KóbZyÓ~Dc+bš•%¬©~#Ì”_±ß´£|oËÓ¨zµØY 9lPšŒ`í?nV73üªÀ÷/rß]ßaF¬û±ÙauðQ:õÝ„ñêÅy—%´Ü« ÔeÔÌú:RŠ(õ(ù?n±‹ßt$ $êjWM¨¾»ÓÍú}”× Ûát sÇ.)_?rÞù>@jJ:Û+{ô‡n0ãdÅ⓸ŒÖµzÜ6—õXPŽØüÏÚÈï# vd¹š¯*–ýuÐ??=7 #Èj¹õ4oU]Üé{È.JªbOKiɪ¡L¸HNŒÔ/ûNÌï‚Ô*&Kžk'W½&'óI,ä‹Îç‡2ÑÚ÷¹ HóuRh÷¤^¼Ó” æÚŒ¹,ˆÖݾàd_¾Å??óÞ&Ëõñ75êóÊræê$Wyo??É—íY®6¹ÜVXp¥zë.F}#Æ’I6¦É¼8 èQcÛú.T5|ÙÈØ-$ýfBšV È!" •ŠfUb„©˜’Î&k¡}Ršô¸Æ&ò)˜0fJœ ÆìeîïÀêËsö’¨4áZ ýJ"Ö}Å)7ì”®SbŒ`X”‚‡êõÞž!1DH±^V9}‚ñœŸÜGêÀBç¯lâ1>4iþÙµ+1%Ç4ú–ju{ u@Î,!¨qœ!òß +#ǹ a^¡ l'®jŽÚ"êÂÎ8cíõDÝ”³z·†ø« ¼‡½YA·ƒ{2ÝG’ÆCÙ#÷ÁÇg˜þ§ç8Ûi[]ùœ¯p‘Ìtv‡‰~ù?rµ½­a2ÜÍ)…|Ößüªjè€p‘ÓnòºlY}+:èÑ£î…ù퇅Ú2ÖL>Åý¨“[µGWW—R_‚yO–ÎLJÜÀ?n¾²‹´+å)»“[ï]•ÍY]oøŸŸ®Bv‡½ÒFb´þy:FѾÔ]XOÑQ“¼ÜiêÊ@ßý\^lø¨Y7)'Y´??1MFI;Ó]ÕƒØu.œ«Ü=s]á—˜ø —Ô6¦½¸¹Ò˜õ5òä»õj¼j?rÇ\&Ý׳S úý¥º ×í¹¸ku²??ogè ã ??“Žpž?r©qý¨á\–L¹cÏBä0hZè2vª^ëÏÓ¥º˜÷ø„5Ý|~lh Æ[І8oÅÌÛæ¥gQ.,í½Ÿ¿|¯Î.j·?naìàÞLs´iá±í}y¨i¾ŒHrëb`(DÄ.U0«¿ºýÃlžóZÄ!Z¬²›:«¥Ï«5)??,X2™N4åÃé‚2‘š}¸gŸØW„ÈÒë>w±Ã­FiLoàÆNRòrΖ:i°JÇLs9XÕ&¡P)§T8g§½??L|,:`¼Ü¨2Gkî–ZX°—ØbQlÖ§…Èé5d´ßl“JKhl†÷S£Â£(iU’cˆšõÁXÓCN‹¿»:ý+OÛå¿MÊ%³Fûwѳҥõ§lÓøV‘Jz);Êf”\…3%Íݬèï[Ô2xfÓ¿j=<¡Ò;¬"(ΜPc¦½ñ)lêyÜv÷<ç5ÉiË“ÎY&_ã€D0Eغ7'ÓaW‹3ès5£ÀœJÐNIÙ@nB0TÇœõ`ÝJÃd«PÖÀ½r–„XÕiÈLÒÐ$«¡W„r)먄·»S¿ÃwtVè2B5àbÅÝ⸢¾È3Å15sšñ²ÊœÄ0Vµ]˜E÷úìÜ&(öBBÉ)“¿GЭ*¬?nóÕl” !-ñtýb³Ó£Aï5³ùÉ2‰10™†,d#ß,¯@ƒ–¤Õ©­?nɦ^O_܈cú›öŠÀP¦¶å†‘ìq1ß5",M3›¶ôö|Ø–è‰qÂÞ‰ßB$¼¾ÃÚ}KozjPM¥$‰võ}ˆÛÐ¥›,ˆ’¾(i€#ßôê› oTt8¬âÛBEv'??Þ”ÓœÛm-~ÛezXaŒTî-˜É¡2Û;.ãÆOšÇõ–ô²z¸Ùøªý\þ ,™Þoá=¸Þœù¼¤÷TD¤Xž¡¶‡ö²ìQ'ÚØù;áS´Ð¶ØŸŒœÑ$ì>z7¼žK”ƒySýYósƒ‰ÝÖ®þîМH´Gîv§ÒHU'4|"TcgóMHm¶ÃŠW †!KÈ¥#Š'?0{M5d›K•û%ªÑÄ¥­ ¿Š…íöÑ!“(Û­‰'TÅ¡“—+h£OÆÛQ7²‡O}‹¤j€%~žZ®Zšá³}»Ž´àjVÿÜ$”útíÏ|µOzIìù•_Iߥ®®8@t±T t7‰ ‘?n ×-¨ ?rpC†ç´ç˜¯«??]Së¨ÇïÈæbÝÍ—\?0Õ„™Nô¾ò[b‹ËË\ZND½5mBœ¼WõAÜ—¦„.)¿••)6êdz_0i(³ÈïÌtŒ(;dÿVи€º?0ø6ç&ô.˜Ý&Œ»6gÔB—ú°þ‘#BG’]ä¶è?r?rð”ä ±ý'‹.œ¼?r‹Ÿ:bj<o³?n3˜þ¹6§„í“°H¿bev@#lý˜ÖÞ@ËÐóÍ7¸º…#ÉQΣq¶&vÞîÑö,੉ˆ\7HÍ€©ßÝýÀ¾‚ŽßëGj"9z¾Í9­ú롟æ¦î!EÕPF'-º½Ú× NvKj=É„ƒ×ÿFýß /΋£áë'ì+FY³ñ==K/¥ò|ü%ñS\K7eâ@ð9ˆª@Ûîývž*‰£‹Žý?nË•¾ªNÞüd:Žoö=5Ó5¸ƒìËá-ÝÏe´ÒX¾@‡9¦Bc “ [KÕ÷&ì À%È:¬þ>ñ$Á.DõÒ Èol÷•¼;^ e˜I@^¶[Š&T(LÇ?nØ#e¯f ïò!”áxÀÂòÆ?rïŠÑ”ºÿ ‰ÏILrŠî‰ v??§î¯9‰ˆH~A!;oN>ú­£ëY¨#:AmíÛÝxÖ??ŠI†R—?0"õ鈥Gõž#›ó¸!'×*ÕÙT£x@fœåÁëhç}Rc‰)ç±ø¦ø®x*½¬&WÍ[4ûÉ??÷û­pz¬ÒÉ\`ûÐbbÎaoiòçUâPŸÑÂôdÈBCî {Å"•ɽ‘ѪDþñúžÄˆ:Ð`0c9ÒU0 ¹+—ïî°r1 ŠÀ'ÑGÆW¬Æž ¦ˆÞ×ÀòC6&•Ìð4d+ÅI+¦‚ÕM^Èx?00hZZZWg7·Â¯Ìs¨›^hWgº#$2Þëf£[}ïôHH,dðÍN cTÒ5O>ÚËÖîÜ€¢¯7¨>0¾ž%DaC†ñ¿›b0@ZVYÔÞKYÿ^ÔÌ¿ÇÀìÉ«Òóí ,í“IŽÛ†V; l‚²Ü’ÙÅš*% :ê@àÝLW›V…D1àÌ7Fí0^< Ò+ödÞ-{;çr>]åÂ>AáîÇŠ«kR«¢“jÐ!ßšë8]’¯µ±ìÀë>¶0mS_ce°Õ‡x¸fúÙv9õ< 8`<2ßãöå™(‘ŒqþC|˜náQ¿HÝPÂyê¯yÆÀ©¬¶Õ½¡‡>?nPäÍ^pÂÜžýc¡Â;R^ñÄ~¤†ŽýØÆ­äÅ[Œ™&ôWö‡ñ?nî3£œ ¶Ø¹2ùå?0ê—•ËX}fýG©}?nê8çŠzk„¯%€!•U%ó<ç†?r9͆ÓÓ°æd!«¸ÃUleÊ%/Úóòpª@üÎÜBNÁèݻմ‹¤?ný< jå€ñ1mïNýûYã¦ô¥¦ÙÍLÍD¿trle??žpw^¨NCcö³ì{,ê½½eä ¸ënÁ{ê!Ž~õ-ô¶c×Y¡=¸H˜\Á b"{»E„—Uò|¤˜h§Ûÿ’ Ÿ/êW`@s-ú[k^¹˜ÅŒ!Òm?0d†¬ˆb¥¨%YÁ¢FPÑ;µ†p@vrÉ3lª—­èp•×õ=??,ìš•¢¶.;Ðrªâ!Â/›ë?nÞéxW8Eý:x¼?n9ˆJxª ’§þ]íq?ròg–¼ë, fNÙJàºÊyø;Ø­Hð˜QŸ³ÏÏÎïê›Æxmrï†à°AŽMÕ_•ÎÓ|òE*‚3½+‰*ÿ§†Â…úæññSh«Œ½Ðµ’¬l%]¸ë>¥‹…–2@‹A̵giMÇœéàwê<‹¼ÒGhw‹à?0بØÝlY„š_e)™WÍ<Q<5ÉÄa”?0«BÁàªäÖÂýu!k…N›#ïå¬þ់ݎacó&“­îº›ïŒ ú…žG8>ª½qtRu(ËÙþœ$kíoAˆ7&»ñYRl9—SšxÝßýàswy{ø ÝLàb³i_Å*ƒIØÆêq‘ƒ­…šIxÁüB¯¶Ë`Í^bçdœ˜×嬭•è4âÄ!.¹ü)ê+3>hôme#ÃÀ„àÄ5oˆ¾2L‘W©6¶0Ë¿¬Moÿ ˆ§É›“õácýó/ñbIûßœí-ôçáÆKÑл<¶Ð_ÊÝ]çN2§Ç•Ð (~˶6¯—jÞ|<×MƒOÈ:A˜Å%i??.ÒWÉêîSq¯¸|¦æÜÚ†‚à9â?0­'«c½k!!¡#ÄHßYn)cŠuæ©`¹¹c2äb??Ôù©#u’˜×3žêzldU!ój Ú7è( ˜e»c‡ý/2Ø2|û 9"âYAK›‰Äûc½ÍK¬ ??¯!*ö‰´Ó·Uˆ÷ÄÑ2Հئ™FÃמ-&my(Õèq`,Já??{z(¥o.y”Ð`¡a|’äï\ê@ðxIž2ŒU×#ÌĶ=‘R??}F„púêh‹°ƒ©F–¹<¢Ò)‘èâÖÄÕ‡+à"l 8ôç²üÉØT…_~Wy5:Pt2,Þ«ø-†ÈðèB÷%^X#z $qb!æŠséö5ÁjXF 9D´…rÙÚÄ´óê;LyXëZoO¶ñz,>×o¦¸Bâ„vLMŠ“Í*ë­ƒèµg"Ä¥¨¬F¤öªÃO?nÂN5؇8PšgJ gØéœ{zŸ±õ1åƒa# ­Ï&ã$'÷/q)ù†í!@±??}Í…dX]ó‚ꀼ~T¤¾Ð´ì›ú_Ï™?0¾œ|ClœgF’`{??w¥…ÏD–"ÎX3£T£¡ÿ¼B± WšŽœ ÇËÄŒ—IEŦ£„¦ACWk-4%ó6Fe'‡?nQ‚ qFžéZøê5F6*èË??ÔÕÓw޼ϟrt-ÆþÜXÙþ­.q4’%•R*¥¥àË&ˆ«¶ùsÖ7úó¶Ïyáúe-´'é3ÏýÛ‚)c3æÜõ;Æõ-¯u‡×˜,?0¸¨¤ÜpY ä‡ï7r6J;I‡êêGFþGœ‘dž,#*ÛqjVndùö~Ç3.*Ò¾•}·lgÿ73=˜sJ|9ˆI"»sEcÐvÒgà…0°¢ìÃ2¨­£?0.ŽÂý¦b(??Úù°?r­…ÉS´ôööˆ`Ât?rG¼‰ä°óÓÅ8q$¨YVò2·QQج5ÿMi…éøj??¬ãÛõ2r§a¸Ç +#(–:™ý ??NˈfdËQt¾2†ý˜šq¬1Q56á’¸Œp V¹C0vŸ`£dcŽ¡fsÄâíHк(C9|k’!HÞÖšäp ƒ9ï>Ê[ÿÚ Ï½ŸÑ{p+T1¿óÂ;w?r§³À¶IÅ„h·Œ¶¼£¨?r”ybw—??¯)¤ÿ£Öÿ88Û9ÒZ8Òšzü8ÿ #ëÿÍÀJÏÂÂòïøß,¬ÿ=ÿÏÿ(„ i½e ò¿¿÷€`¥Ó„CrÄïÛÒ³vÐ$ïµÔÀÅã,[G“â•0LÌr€4uïk™^ma^ë|ÆÔ冟pbá1°'_-Œq¾5ŽÔ¾ÑPÚ;#Þ™/ XWÉÉãÝ §ÿUÃQål¢ÐX@§¬b8캆•¾ø%q£©wëãA€Au%u# 8ÐçŽœŠ¹´§dAtt¤Û,¯Œæ1…¨z>í› ÛãïÅk¿X?0—ò,ÄyOb{É“¦ù,ŒûüØMütº‡mË|;!}GT«Šéóq æïmÀ??º+ï† ¥ì˜?0e¯÷µcÚˆ„¯›Ø™˜›@†£ž-†\õ¼*~kU¯án97õzNÿ)q‡½q§ÜÈ%˜C. ò¼nY«a§ŠÍ´i>S°›€†e2!>ÏGÌl駺ÏǸÂÓÛç‘:íÐà¶Ó`KR„HÖh±#¡1¼µ1|Ö´cýÚ–Ý ‘0<¡-®ƒúî¦l­œs¬xØà]¦ÏY6½åÙ¡±¶Ô$ÔÄr%cištFˆ?rtòµfï·åÅMÊí”Ì>uÒì=QÐÙ  &víÓ_ £ Šj¼ÉûÌéí”Ø4ù{FÆI~…ò¦ 06??Ï.,ãL¥o7Su}¾i3kVpVJwkÙ9ƒ ÷MµJ„Ã)Ž¢‹ÿlv`•€ñ-Ò×ÅŠcí°&ú>*1]±T%[%?rœÎ@¡î÷!öÐ5´PÃJ[pCþšLxa±Ë·1Ñ3Å{ªÛ<¹®C}3"‘ðIËWctfE¦3æ­pƒ¸ÿøF‘"ì6Pu5M¶Jò~&÷ߎ,VÈÍßRlc\Y}‰ÈšòèAO·ãaèn-nc’¾{‹Å§íh.±$shÇ;–£´eö?0AOôÛ ò¬ö?r/¸Ä#žŽ+>/ÜzÈFvÃê:ê÷•Œ{©•¤ˆ¿‡ÎÊØ$;_Ÿ‡Ü¬~????0Z­Ä÷`×’®ÛS6Jz{I¶JH­X]ë™?nmœ/Ï:‡M¤µ]S xV¤çÐ!ÜÒ÷¦i!_¤)^Ѹàöê'ˆ¸k^³Õ7ÓY}Çõ’÷^íp+½Yé"bP|§O$(``fjìîˆÞ YgµßxÜ4.ÇÛèñ…HÀ‘F!‡±"ZNÃÆ?n KS2ˆ×2b…GÒp¯Mw¼è …e)aÍÕÒˆÒø­¦!âùƒòýï4?0yù1§@¼.ï)P«zнjq¬6‘%2??'¯žRqÞ’A«ôîiK.'…«-²ˆ„[²½9³Hð-ç˜eëfWÌ[Ùýì¹äÆq!…Ï7mш¾0³–Ô“FO›-?0—d¿XÓ¿œ­VV?0vûµq=o®o¥ƒ·¶cB$8ŨsâCOX΄.ñ??ÔÏ]«ÂoäñÊ.Aäè%xˆžÒÏ)Õɳ¸†ˆR„,K Û7E“ ÈÐwSšf—q•äØÎøÕ“??p¢H?r‡ãO÷¼r‚ÿ2€kR%+8•–qD`ü¨t‘J+¢nµ«ªžd‹_¨Æ8à?n[Š+ǓÒ“_Êâ‹_kìƒÚw}¼ŽÓÄvÑÚù>+\š¤ 3_,y‘+ï6?r>£æ€~½Ç~†:à»KÖÉ*-Ý¿½E;Þ°ðÚã¾EFVTÊdÖ—ç4tñ@SçÛìAŸ¯Ð1AÖÛÿ  …Ç}›??«¡ÒœëaÃOºÀÉšØwHìÐîk”Pæ’æÊ÷ÐFTãÕÀùK èPÏau‹áì†ëÖ–“c8Z`ï-Tg­(´_„= `ï+ÉHä_ëÜJâ%¤L<"ÂËW ¢19ÆÔæ*AŽàÀ$ãô`ICgJ¿*ŬH®ÖûÀ—N3e*l¼«A_£Åq”Ã*Îhä”ý·±ô³Ýï*l Aûhô;·ÜpÀ"g/öSÔgô>a¹³àéy™9BœjZƒ +ü’˜{Ï>ði¼ð$8??Àµ®Ó 뛋ÒyŒÄ¥L3?0vÛú*«Dƒ™äÊžŸÆÞ›lbùÈÊ@å̼ÅÎ)LP¥È-‰ØÅ„¶nÙGñ¶øó†ãÈ ‘:¡³Ø­èUíã`宦…ñþ}Ç´x…ºêÜ0øÛ”/5ªµ›@c4øõ}?rÙyz*¨Á®ÊÚPãïn/9ZXÆ9^ð4æ78Eø¾’#l¬‹ˆz„u³ÑÉ …dèB޲Ëð+†od 4w³ GîvÝŽaí’c¥˜ˆ0ËÖ: ÇäHŽÀã…Æf‚¹HJë:„ÉÙöÀëøÈ<ð+"Ìoó¡)„ºž#¢¹°žI93mÑx“G[¯òif&µ—9˜PDðР7õ`û?rÜS­ïxaËÌøo ¿5o ‚rÝ@ !W;??>ùç°ò;gñ÷Bµ(HßÈd. ÆìÃzÂûž6Cñó¡8¢ô:^»[cÏþˆ#G,oÕF0˜8ÂN„¹`ãß´rò l¤ÆêÑËž+"•2¸Cú_ª¶noÄҲz,¿²:@Úˆ²¡–Ü ÒÊçþ%g¾’³_ºÁ¥ˆ©ù#L‘VÕW~â†à‘°Ë!=“}³ë§»åÑ‘ªƒ÷–ÆÍ˜¸˜ì¤^¼+ º¬ðmj]QÓœ>îrÛb{V$5äDÀžìB·¬Qw 5þíT¯gÜy4éîSõ÷Y1ÔsO0¹“œò=¾d´ùØÔ}8«ѸyV/u5:.º±×5tüñÔ3ÚyPm²’ÕìyàÂÆéиY?r9ÛõïÝ'ožïç_dä±ã¡Louc>ÑŸ‡›­1cy˜ö«Žc­¥à¯gíÀöO_ÃûˆÞ¯·s§ž¬ËfY¥7Ê។hŽ l\ B£fO?na×9QOÿlb fÓ6’‹ƒòn/ø ‰±U+˜y—hœ•î0ÌÂ¼Ò ö*EˆN¥†E¨=F“‹eIƒ¨hM?0]_nÖí\lç{Öú°]RÅ¥hÌ”?nöá¯G¦U2+ßY1ðÁË£±%²êV¨Ù¾ÊöûC™¤êóãfgÍ¿K1?n\ áDž$‘ûf¡+¨5Dš^.àcòORÔ²ƒ«HßM†¾×tl©Éáfþh~Œô;’pÕgÀT$ôèËžËiXîGÚoÔ/ê¦\z#tƒÜÛ0c@µF¿ØôrÅA¸×YeršŒ1é¥í×T0AŽŠÃÜG¸á ,VM¤ç-ØOÆcº®°ëîÖÞjŠ–ç±ã„·í¨ÐQÙi¹@A+9L!ƒ$w~º½–M!w9‰ñœ~ÁT30½ô6Z?0¯c’äã’üzgŽj¡ŠmÝ!jÊÿHê1»Ïl§8\í/ Žª”ÙõXù{TÉn™¸Ú¿õý¸œa›’ÛŒ<¶«Æ|ü:¦Åo˜‡¾‘¯¯Q؈cm“G'F"¤¸ÙY›™°¥®³ôž¦—–‘öZ Ÿ,Z {ê·.s—¸Ó÷†jÜÛÙRkGu:2ŸP ‘ãzMVOfdÅx7V,,N̾T¸,ÌÿãÔ¸=×uk.Âή:¨ŽKC_”}çåG…ûé²K±&AVçö˜IæU)±I‚Õ¿x0¯ƒ¾Vø§Qn.|ϸ޳û«uÿ”Ú–Ä–™K¢]ÆK“‚öh9úê1WÆ[©Ð´y{SÞÄx +#4Ö˜ùý ;}4†ŸM}÷]räzƒ„–Y‰O¥Ú#¦/ˆAJŸ‚+??ø&SÔøÐ!ü·ó…ç¯2­OYÙè’3§N'b”)¥u­G8\÷?n7‹Æýk8Ý}ºÐ†DVâÌ÷Ñ´ŒÎ™ø]uðõ{¶-Šý1JO]ªEóËmÕôèÌÀM}T¸[Ýø«H+J¸öMÔ¡2y£¤µmiŠ´¯ŽÝ[:%Ç™åœçÅûˆ§v‹Ìƒ»Ñ@³2œc|eÃO¬ }Ë^{ýæB«ÅQuŠÆõQY-à™Á“ÇNTGÂñ{×sgAY­6÷\æéÎw>•ƒiaÚ•EHù{“kræ^OCèñsè_•„GƒÔ:!??Åãô×5»¤ƒ3>®ùN¤ñ,Σœo„2AO™=›¹%ëŸzàr†ïv«Òfâ3À1I#Á‚•“&¬A"G³Ãª¦z%kI¦xü ¾=ž]~(¢h ñ]hn/uGŸ":è½”0טßY2qÔT¼%4_äÈdOo¬ëÜm¯•u¹8ÙcㄵÍ9÷¯{™’½>­´,Ó*Ê"ˆÍ¿wëU1ŸZ¢™¬%ɧ?nm¾N8®§¸»Sw²I½Ÿqg0Õq™nk³[]w’­›Î—‚a(Óâƒáh"K^è¸Ú×ÈÂOè|‚ûùr$m7L÷fªd?nfÊ·„¿/s??÷Fy??Âs<0Bw³w19h´?rö??crŒ[í­Úuõ70¤,¥êÅ/ÎYkìUÕ“¯óôåI©ªU#¸mˆ2«aâªލøß¼*dî“.P#[È??ÀÏZÆËOµmÁû¾ÀBHŸ¢ý©ô@©x¸Œš")hØÀ«Ñç?0ù…ªºcÝò7(‰èºrÏÔðÞâ«JEÁKºq?n< ·<'¥\='ŽÉe!^¬–:Z1òüF7ÛÓúÕrd‹I÷>Æ„”j·Ñ€¶UR™Ñ×*@öêQ¾ìo;¨bž;’ä!FÀ -¨2"ðpÄÈ=ß_/=î‹MtFS?0ŒÁ£â??-óX]£ïqjëñýC­·—¿ÏUžÓW-ín¡1cÅò^(ø# a[|¨°ûGá@7ÕIì‚ÁˆÂǯÚZ@‰0\¤’î'"uú~C¥Çôr`ü]>ouIIÝ?n{ŽiQ÷ø‚IÇ‹Ä~X@Õ˼FËÛ 5ÖÙ²Š3²’]Ñ9V¶˜/·jÝP9û‹*–H„ÌʬŽ0¨é±¨$Zã ²??uuù¾‘t³]ϼ‹º Ò|É]åñ·g郴ÎòãË`†O"2’ÚÁ©޼¸Q€òÆsT)ߤì}»íÙ7 ¬—^½òÙÒ[ïl9¼à“ÈÔ®86Îɺ75ej`Z?rìNÿñ=.å…4v¡?rftÅÐ[A*æ’ß‘Œ£*×—h֭ݼ6ï@J N©»\¤ÒûpçƒTKŸø«.‹‰|ô‹ª½ÒÈs(aï¨7œˆEv×ú8Uë‰N½*–ëÄ÷ûP÷ûü´n*gn|UP€r††ïç«ö‡tïÕŠÕµA­<Èë¬x:âé°r_û…x²­Å¬<ÔÅNÄX?0Í ªÄÞt j†ï¥.Êõo•ª+–Eü2Ü͸SÈÓåùS±Oo¯Ï€Š:]ˆMì®ãß ˆéQñs…ÌèËÅ`L%kÝ|*Éc‹]ë+èâ¿S×<ÞFûyÖ¾ÞÏ@#nÁŽlûÜg¤dZ‹ãýZ=“&øì(åÂëmk1üòs#?rŽeA¤Ó‚Âo9Zïòõëy³ŠhÓ³Ï]¼T½µ“‡£$±Ž¯¹Q?0l‚Ós'¥\^ï„â„7Ÿs2Ì9;dS‚3†Ó>;²jMni%a’·€[Ù_ܼcÍyÒx»öuîôÅ; ¿£ÛC}röÔúˆŸÕ”ÚŠû¹¨2Ø;|TU–G©³??ø–¹Vr¸š­w+ƒšË^A:Ä7„«vžÍûe.¸µ›5ýŒºT˜Äx—ßyúÅ/­ÚÔÿ(üµ) t~k]Òi^»:b¸C >pâ,]N3„Öþëñ æ©¡âF¹¤Á‘´̪‹Ç" ó4_ÁEa[=ù[3øé{-Í€9²”·gÌ`Ôÿ* ÌDogö±Ô‡AôL¿P¼^ÓÖÒ^i¼8r=u"»¤ Ùp--ú„;VebA, V-ÑÚsë¤ÃGp(?r‹‘ÑkD½6gOΓ}_dh÷ÙË–y!ºS¤c^VáwšùÅJÁIâx›€3ËB×ìèôèðÁ"=û•Jw›¼ßÎ!}¢Ivr²?r•nëz½K>$(Êåö;hâËiíqe"™2RrÙb.Ñ®ÝRS–âd•.†1xÅÔ9“’1Y¡Kz<%n7Âï3gµÛ êẄÏ2¤c„Љe4%Ò²V |(ÈR#9Q×^?rk Á®eô¨ù¬£ôGÉÄæ!ŒÈ­®FkÍJ°Fb_߸êaÉÄ—ùZN}o[œëÔ™‘.``Ëö¦êJ𚋆ˆýYôo/]ü½[ø+Sg%y~m²¤2Làþ¬`Õ–&ɹ`Ðú³•æŒyY«’"‰0ñý?n…2f†%Π‰=iû óŸ<á6Ш‡­n 5GdX£›„&N¡°žõ0êH㌲šKl4›Š;Í!ÝŠ¹<LJ$ÉÂ@ìŠÎu³¼KµØˆ>"²€Î1PdÁ`|n ?0°cKfOΉ_F;ÇhASëE²úk¼ŠÉChËûâf/‡„Ì0>iÛ­«Ðƒ€Œ ãºj¼ÅKù}œd£xë™¶èŽ/ bºít²9 W`æøÓ+aË_±5~RVDY ]y‚ˆŽ+¶¾ŽËS3”!{ïzÈøµâÓnnwŽ!³/¥©~r,X'rƒYb_(Ò!,¢þ)tECõƒv݈eûð~¢ÎbIŒÁt©E4ª·ñq¡Í¹ë¢/ÑñuïX‘Æx??û÷©˜Êñûꨌào/” 1SÏ[¼E ÷‘°e ÌL¶L|­˜üãd6¤Î¾ñxŸì\V‘V1vŽeÔh+¾â2k]ý,›íª÷œ¢‘œ¿ÙnÒ¯"³$Oõ,;?n陃¹Åkòãp–¸??Á9Áͱf.Ñ“ˆÒÂMÓÑÐP`3‘ö+/öS3uSòÞ†ä%¸=óËt?n¹ðw°du¾£WÊ1À=•ô-P¡Žä±š«± )Baû‘¡Ð,—#zI¹«Ç’ôDY¿z8–ÿ†J¨7 —p®ºdœ+ãiU;}Œÿ~¦aDZJt‡ŸX^L‰yÐFÏ®~Šs?0@ñƒ×»øæ½2?00u¡°:Oåc*NÑ4Á¥'óÂ[`‰>ÀMà?0*±'—‡8+®Î7½)ô/ÁøM8®é¾×/— ±›VYm"9+.°Ùú~ikX¸`??ä ü3?rZ¦ƒÔ˜Z.·E¥"A°çož¹Àíªðf~’¢eü^}}söåîÆ›àݯøPhüâ®ÝTIwÅÕ‹T?rÏ;êÊg©Òüˆv)âQ´FäFMCÂü›0$ïp݆š©3ŠÍ*6n¦ØvÐ:О'v¾¨…ÊF7hâÊxS5;LÝ”fP °y¨¹ f±#Áïñra™SޤIëˆÓÝ1·ùÃq5¨¬g¥ÝXm‘R¶îŸqr.ÊI~PW,¡ôÌþh™Ôˆm°?n¾™3ƒeVók³ƒõ¦À^€Mç4v–;uOuØcU +í êpOZr†þ0@ÊÆt„I‘ÿ)rûlüf!¨ìó‚ ¿\ÕZFó¡¿IjÑ--É ¹ôªÂÊfcà¶Z=!òlKª¨¥ß‚¯î)1ØãßYËWr??ð??Ùçv?rƒ¡çz®EðßTÝä»ßýy3=Œ€ãªw™4O…Y;ù*6UW•ÂîPPwgÖ “—þƒÍa}î„yߘ֯úÊ£Áàã?r Ë7Ū‡/gå<§ ßzOC ¡m5“]ÚÒ=yÜý~4ÕöÛÀi7z €œƒ˜?r£86\‡??fÌ=t¾å_½_ð5¥´8jMo7:¥Dk?röùÓ£Eµ›‚Lddß*&CŽ™A$¨žÉbfS°3Âð%™xäFãŸK&(ätœ½Új𙓆Êûœ½~ _Äæ/I´ ~n9 r:áùdM{º6~ý‹ï‹Ë欪`t%«±$ì€I³ÊôI—^?n>¿R§ÏãkN–ñ7~Ó‡9þbƒÛìJœ“¼Ñ%Ž,fE?0§ÄõO}æâa9YI‰»öÊÀp„v¿Î\ý„Ʊô®±>0x,Eláˆq9\¥£‹MÕçšò<Éý”õ{%ö,Ä©á—Ç€ °#*v„-îÆµ0FgÞd>”…oµúÔ-æo/¥¬Àè=ØËV0ñIÒŒw +#‹x˜ï%®_ÍUÈ5:A}óÅh§¸°{âbYA„1eD€û@nV€¬Ü@›&ïh6†Ë5ºÓ´% Ë`ÀîêÄã°‡¥Ù’?nû4T×%C¦cš0ÁUzù?nýw)‰¸T¦ÿØT:,Èé¡U¸×;ÎN>š€ÜÞ¸ëú÷•}npÑáÎÞO³óÒD(RP3Ð+ô$ImÑÔ¸fèÕ[?rL·± ýS$(œª²4ƒÏÞþlvaWůµuBô?0ï2×v?rH»gÇ«û€©–Š_o%CI§ú8ô/@1áÉÛ¥à©Ì[ÍÐ=›)S¸ÈÓÍã{€?0*íôÈq‹ØUÿdûò²íó¸É2QñÁ•&ºiÒYªçÑNmËÕÄ"ežêXCõäÎ\¾ž¸õLÕ™jÛXéêI&;±ò¸œK·Ykhr#—&F½ä“8[_2v‹Y2Õ[‘Ê/º¢ºì øâ€ø ñì«US\4s‰þ“ŽowàUpr?nŠºpE"iò+´êåá´3Ÿ)Â>p/jð[Áñe^´yz“ë?n÷šþú8«`´ç÷yž¢fú5&þ¦¹¬í²çÙ%¥aAx˜qZ àLpBN,` }4|Á“Rwå’¹?nþeÔº€Ù+¹}?r„tdÍ1u§Ìâý±´å³ø¢8r¹x©3l_ü{(WË+Vû*RÚÝw™}@ˆŒfY~•´£wd[àšìû;ù­!Ñ£‘šÔÅP»¢Ã°Óy¤Ä’©Æ3;¹eú5ì„\F^Pp8ã?nrM;Îê2+ìº`ö韎høÔñŠ Š#¿7Èd_jŠ¡hWþ7y̸ž~.I+½ôyì÷äÏ5vÐa:Fê˜ð…d9óP5†ùæD‡™k'—ª=Z\ Þ’ŸÔ«r—h’I1Û8,O˜n`[dLü8dbFá~îüÖ°:àÚoóV•ÂñÂÛdÔSè“0"ÈÅ„€ÅŸÎdfgàÝ܉éüxЙâÌåXâ9§LÂ\°Q)û÷(M~TߊZy´uS“K*[´h’LŽœ˜µ<ÔqÊ !W‘Eç¦`ö¿µd!íÌ…†’†‘?rmµ‰]Š$oÙM›‰)”ð;EÒ–m —˜ô0IòRóÇy™ýo@؟ׇŠíü„gŠDøno=»¶,Í–îåâ¼-´œæ{ü[z`’CôŠ?r0PÚ•@(©kåË-Y ¨z¬-ÖùN?0.ÕÓB›@w7(ŠªPøž‡/Ì×£îð e†lõO~±ÃÊ« i‘¸g YÇô´2cBÍ´iŠßÁ¸+$øJ 2ìÜýÈy¡Ü-½,°¶Ö± $+ ¨3í‚2…Eé+°+«Ç]Ó~Ý“],ó~käâùD ùZ?nÖcyLMç”·¨Í`ŒúäËâÒXC-Vo·6Õt¦“zrM;WƒŠûª#ûD°  ‚·FÍê „i3¨zG-ÈVaäÇÖ)äÙ„ µº2Þ,Q]Wt±4„dZÎä·¶„—·æ A‹2jhšŒ–߸FgÇ>¤ØD¨n@Á4‰¼qJÂDƒœ¤/1æ)Ãh^á»É{µ!1_ƒÒC"üö¼·œkD ÜÀ…WQÝùÚY͈á=–kTi!ƒ‘S°çt›Wn[†Ï—½ìWªpÐõ]~ï¬Rô?rúyª/ËA?nPewåî…ì$r÷WØ*¡®#•ÍÓø¸X÷”Ÿ­ƒ Yk|s@ý@mb$R,q1?reøl/!Ì}GÊ=á:Ìíâuó‰PÆÎxÖ<ü3vÈ0îÒÉ }rè¹\o@¶×x–*â«cÃÊZiôœhbû£ŸyFت™œFúö£‰X8[JD3t5î"î²æ^”Í(´oVRH¬¥“¯mÿc8ùÂ3g9ÒÓ¨ŠÂ¼„Ûƒ»šÁM0'–WŽÎ…·½b'­×Vö¯445ÀbpððÀSR›(àX`÷×?r÷d;h‘‹„ÑQQcPOºmˆ«y«èÅ´[¼kÇöANÃòäÂ/ó?n)P¦èκDòíbÁf\ô7 ÎìCn=:mšqºU×V?n}½è…kÜbi‹\Ä3€Ë?0ìx%*¼’›fµÕ%Œ:GT¢Pr<‡-Øö†ÂÔ° Ðu¦\íÅmðq>õ¥É•mW5æ 1wî=¬ÉÄ í-?n>v¸ËCþZÕò’ê= ?nÎl  yõÙ箑Í&²&*8/Ò¶Iïð#¯¾M™äHºôÆ[[#ŸH±\¼0Ã"Ì ™gy“Œb¥CQƒË!d 0>u¹Ø€¬3ŸÆ:øxÖ1.Ñw×ìD¶Ö‰ò2ÔD*øRñ˱æà.‹î5ê++yÝné€Hðér$’HC>ôTõ’ãºKùí ·qñSQÆ”ãM©4‰¯tvÑA}s´¹+§Ý²Äa [C…¥tʪ¼Ç|+–û1ØÒ‡2ñêJ7;Œ3£6•Hâ P'ºµfÈûVkz y¤š•);ç£Øàʾkzåß–¹%N)8;ï«qRJÁ­<ÓÂ,FîÐ(…[Fp=)3îµûýyñìäpÚƒ)æ³TQMqÒˆ+??8ìC8³°¢Ì âoçË]l@ù-cuÃûü—NK-9»Þ[c-xDuåQxeoTÞ>nlÈÆCC¸œòžiÐp?nò'?r‹?rË¿eéÙFrÙ]MlXÞ@ÒñI_ ™µ;G8œN|û4úÁò›ê…¿©X³ýv nn6k’ç)ä·’ÄòYª?nž¹‘vô—#iÔ ÙL¯ t0‹Uß]öü³øÁ_JRÈÿ‚9``PÓˆ¸01O9È;¡_­ã¿Žž4ŠE…??É0fë7’ðŽ›¸¯á[3*\Ï"O5ã?nÒc™bºdpŽðúâK ÜŒÒ^2`©¬ê®¼½ÓJF!|Û’–[0Sªº¶mB<"´aë2Ñuaøú‰¾<÷¹}Òš»#Dì{¾’öl&Ĺm>ŒÃôÌ’H@7Q©Z9åK`CiçÈÃúRdw ± ¬ëí*ˆ)!Æ_~of>74‘ƒƒô¼#çÔ^Õÿæ??“!•¿°·®J™ôÏ¡‚L¸YÚ½™àȶó–à‡/éÅPI€q š‡÷-bÜ›ë©(¡3‡:{2>©/1??—±œõ³ýnÈJÓU˜ûž1÷(Î(ãû´ ¿ú”¥áÞ´“1Å 2¼ó¦í’Úà{¦ûu0§Ü~Ô>$‡a¥ŒÃ¤ËÙtú( K4ª ÆÓjÒdŠ2 ??þ¤’Ö«£,|W‚…1¡á\CÅ‘;’’¿'ilaXó×1¡u¯ÜŠz2Bm¨”94±¶)ÅÛÚý©rMñÓ]Î$Ï _„_æé‚ò~k· sv“’èsö¸nPÜÕ*"愎:eü’Ó¤,¢5—ò.B³ˆÓÈ—œÆ+ÑS¯@8h·›M5SBÁžP 1¯Ñ€cOöX{rñç10É?rÒBòE”éÇcë“/x?rþÕø:œÁ#??H£ØìxW² Ö»ªªÅj©Ž´»û%t©œ_²q?r©n¦`Zž k2Ó×Ãó[똴ÏÎLùÜ9ïeYk¨â‘âÈ”ò—QÔéØuy??*\Z?r”Çe Ìû!G“Ì•ýïàÛÄ3ÄC^ÌÔB¡ÎɾŽ0[ðCqÐ6¶ÁsmæÐ~'ÆlôyòpF{Y15«£Ÿ_À«à(óGºQ iÒŽÕ°ùG–™Kj}ä8 ¶§NyxÕHÕsü.ìuV0F¼ T  b2x³å%¹I†Íàµß:÷®TµüAmN–Wá^º0ÚúuhK˜×&ßÒD<\RÅ\JÔZÚeæ#ÄDÅà@BJò“qJÄÍÈKLéŸ, TÆw¶¼“*Í7þN%‰ÉšE{Ô‚xó-ý]PG:W{'à\M pçCìÒ8vÆÂS«íÆsú\àèÕt¼KUJ4xóÜ”MŒ_j]¨l›uºÂôùr§C@”VVÔÂ[޽0ƒ*# .NNRP˜·T?rœ"ÿ«¸5¯‘ æ¤ÃÏy?0…Wò…‡¹ l8÷·~Úç±M±üâzûø?rÖ1Ï/É??ç´þã<ÞïìÌ]'ï›{ãbF¢¨M?0åD³OL‰žÄïdJn9u'tôlyy&=µnu“?rMÓ±…ÅÕKKów‘¦Zz)úK‚ǹ¯ýf.?0ÚÊýh™‡†jÂÌ“ô ß0…0ÅJñgЉ×ß±`ö¸·&ë¡4çPS´Á¯îÎaÚé?0ižÅ¡º?r·Þr¾Ñ—‡=/½š»•ËY44&»<ƒJ“^¦ *‹}dÒÌô3¶ˆ’£=˜¢!qYÃ?réöŠ>ƒö×2grÆŒ°žGÕ!¨h=j?n7”Å•xf˜ þzj£™hOhi“ÒCLSWti…ú¯j•ü©þ Léb +# %þV%µõI™²¢„RÕຠ•èœ,·hz¿o<ß šè´ÝO?n\4žèÑX³–åÊ*‚^½b•GTq1¿A£¿e]`YIíN ž±5¥Žë¶=êi±¿_§ÎƒYÕ|K\Ö é1øB?0Jã‰Z^>û©è‰??l—×ú¾\hœèñf§_ÖVkp?rÁÉ®ÉoJ<×eŒÊÊjƒ¸Ò?nU¼ÙÆËÆr1V–·ˆ[LJ2+¿S†jÔÎ,7zöŒk¢ù]F1¤†Sã-|qáóƒ†~}3.Ý4IìLùcÈ"ëÂø&d³Q¥!gQpÁ¹Î±{ƒ4¤‚hí5®E€> 7p|Öx¹oÑÒƒËRÁL…ñz1ôXÖ}u¨áßï嵉s€ý¸úš]¶Ìb…8Ì5-(c¸?n¡y??Y ¹±<´†ñÔôE0N "ncDAöÐÛ‡¯Ê&Às`zmæ—o D+†£¾ºwVú)q$?rŸ×t~;xG˜{ê«wizþý±«‘¹<¶Ü]¬>€”×PM³Ã;ØCWKºábóU¢÷ØËÓ?rÒ«¤{á38o5˜×Ã~ú=oHjØN«ü£F;óè?0„ŒK‚èÙüžd©$z]ûW¾à¥]¡|LõØBo꤇…g­þ³È~kímÄê?0¦5¦í:´@ûÚ:vkÍÁ {?n]ôÉ(êÓ7a¶ã»ã??Û9˜;Zÿï!?0ÿcà¿YÙþÿ™‰…‰‘ž•‰@ÏÀLÏÄößõÿÿ#Õÿ†8/Œ€AŠUVs*¹à{sK÷.1½õ>t¬oÙ6Á ï†Öªhd|Üÿ6›–ÊÚ©KªõØbŒ‘,‚U›J EþÎ/¹ÉÜ‚õô¤­º/(ߤþuüw:C·âwÜîÉ>D¼H"°¥¢A~r-atL?0gU‰ò’ê!A»?0R&¢a%4œ™âf2xƒJ¤’ì¾ã+2<½3±‚EŸˆ9¢›»;¦ ˜Òå®d#ã†pÿb]ªØ8ÒÐ+è€A':[}™ˆp§ÿJ'e¼WdÄ|Êþ„ÐD+8œøàŸúB Ò¨W©âÝkm2 –…¢4Œd.?r×(~ñf™!®ÇöC·‘+?nÔ¡˜¿u­á9"÷÷dwa®ôÒŸ?nŽmðÝwœ¶˜¤nj8µX1;O–{Ócêû3¬õç¼ýv?rÓu(tÇd}‹g&ëÜ\<UÅ!Òúþ›þjqv83 ¶RmŒ·8F»1AÒ«Õ¹òiHv).Õt…1Žìx¬“X-$Žž÷'VÒ¤íwe?0Šä?nLžÑb„œ}‡â,œù†ªŠô¡{N=x³ü¤¶†œÙô*ÕñûƒBáñ(/Õý¿Õ5¯Ô‡ln»5Qj °îܶ[;øß‚«õP¤Ì—Ý'ƒxfßö?rV Ž7øžÍÁ<MA^š|s:õÓB®kvCC¯šçQ÷Ñ"N«Ûé?nAËç8?r]å⿨²äÒ‚ˆ2[W¯KÈäšÂNëëxàoê/¹6F¬´zõ1“WÞ ~»ý v?ruºùeŸ[õæÝ3t“²n匇ßÓÉ )8×#2üp^H*|ùì¡Ýu?nîÚØcµaØ}Š"msZ[5Xú1aE[¥»!o$n´ü^6Œµ1ƒéz:m|„“¬Ñ¹)??Þà«>Ÿ\íüÊ¿J CïMÊæá›RØ÷fºÿ9ÔàYrq›ß ¨mZôâË6Ž¢DnÓÙáñôµëü£¥uözgÛÿ“Õý6À´JꆥÚÕ¢¨ßi_eŽéD£Ä™gé› Í(#y£«Úéø—Ìî½ ¼sñ™ëê9ô}?rcé(D—-£WU$k«Õ¶2µãˆ—–ò7O??ýs–ûÌŸ.⃨óŒAZ‰6)UCØ414ò{·_CîæO«£‰†ºõ­œÊk§ü·—mæ†æí¡•6Kìs³ „ ª??ÑÇz‘ÿCüÿí¬ôÍþ÷ðÿ‘ðß ÌŒ ÿ{ü7+ýæÿ£gùïÿÿÿhø¿þ¯ñßÿÇ{(RQõ8b…õÙácÜ}Ñv-m/ù»“3ˆDŽf%6StÊÎ}æðtD㈒KBñ6B!».ñuFà3‡=§®•×~±FɃëß ]]B^8ey÷¤¿Ã‚';úE5O#PF!6mç G cÀ€P­%‰îvë`”×Üg¶×.MÎËÝŠ\cÖonXÁ$iãW¼yÙ÷ÛôÃÙ^à΃Xd†\QFƒÆ(Ø„ÀVÏâFD¤Ã\€B6TG=GÚŒYÓÃ@ÿ£À%pDàãSó‹]ÿ¥ìO°¡:þþ5(qaìæ ÅgûpbÚh876^ˆŒp-ÏcÄÈÈm¬¬ëÍcý Ó?0<[]Ïaå9r:tA}žRÿÍŒU‘/ËZ-OÒÏÕ+ „—‚ÿ‰ŽòÈ쇊á³ó?rªèQBÈø¼«j°Xê ˜¤A1QT¥ßSŽhÆ”2çÈ("T£÷tR”+Ôxڪ׊åóǦñ,ýä€fgÅE̳ƒ¶^ñ–?0Ó3'6#Óȱ_êHY$üŸ!WŒ!Z¦†ÍIÐe†Æ³ÝÏ+–ˆM+û¨üe²s.@¶Ù}ziHM+ÍÈQîõ–ŠPYM˜T䇯#·Áóв@ò­ÌZÅòäû?no0¹ßåH ŠÓóŒ¥p3ÏÏU犠Éÿwì½C€°1Ø?0·¶íýÖ¶mÛ¶mÛ¶mÛ¶mÛ¶Ýõ½îË8¹gæIsΈzÖ) Þ¢‹ jWö9®Ö?n©}¿¼ÍÖ©@v%Ø ÉÅíé-ÞpCá=¢#©`éøÈ[Ûþ¾ÁÍκ™ž1úŒma˹ÑÇvö¿s^­J–6Ee\¡³³>8 CW]¡Vyy/jx2¶ÚîÑ÷³ÓQŽÊÓ³†“›PÎÎ B£@./è2þuˆÉô1t3??íl´ß™Œ¹ŒŸ»zÞýN2ûb˜Évq-!¢U²ÏÈ…‹JÈq‚"h{byjŒ<Šy6Yßl(8}`qØàŒð'\[±P[žê„V<°Ê烮ÝÝ€p ÄÁ–å(áÛ¯}!æ,‚g^̬ÜÔ(³ñ’öŠ‘ôY[³ÉXsâA“D‹é„þNÑ?r÷MlÛÆ‹Ó iâ•(çl’;’(«ñwêƒM„ÀÑ•¾¹»ïì%F{Í”#œ(ÍêhW_¨`&›Ìz3å ²ïëVâ€I?0¼‚9Ñ©«¹z˜ ²WÜñ›t•b‰U€µ”—EÜO€›îzµØk|zm4Á2¼?n£·Ä¨Éu¿ý;yc8öJ\¥íø}ZToKî˜'¸‡Rv¤õkÃ7¥Jü™[¦• &ŽÏóìŸbîvÜ?n×H¥x9Pœ/ß'OÄ‘Â}Q??˲íL6n¸t&EÊ&)—Ä)Ö¢´›B9uú`V‡W¡PÞØj–¾7BÂ¥¨V×ìú‹Ä`]^´ŸîÙˆ§ ²À5б¹§¦+%?n8 úóŸpç¬&¯‘˜ìÙOJcÃïö¨¯säêÚ—–|¬Ð¾¢•^ÒVðïŽâ?0‡3Áuȯ—îJJf£ô9VƒÿÚcö!î!Ö =2›Ö,˜Ëb¥{탤—G¯ü³º–´+=d¢°±ì—Mµø.ùQ›.3ɘԻ¤—F5PG…×8}b„ ÆjC’ÐùuçØß\$ lNéŒ(™¼­chAá¼+ÏUOþü'²ã'â‘c¬ógê…A@ Tx¸è–Bß‹(àø5¯bÁçU‰™Ü _„”´,‚ð°6Fg6dtƒÂÁ0æ ç}î¹°Ò")uØré–O˜ë‰ 4¹üY®KP’šM$° 7çeÜŒ{Eͼ`§?nlØB8F i)È®Ñ0Y=â‹fÒyÆu¦ ·Ý/žù9žmFîr#{³³¢¸PŒ­ ÐD]cã¸=WŒ|PÞÅ»êNï©n¼V=Ÿïí¶^h(슧ŠQÿ‚ƒJöuçkêÛ ¨sù•—ƒ,·I$F¸î"ÉU2^ÿp’ää.À•³åGbf7ë%€†L^¹¡˜;I]/iMo?nm°3BÓ™€]/\U?nXtHsÖVy¢÷ï1•Xþ™g=„ÚmÉþ.^ùáVë#ïL—7ÛV;Q\Ìø*Ê—Nß–_Ée¤ûÂA>ã¢Åô?nï—À¨eî5®ÀÂíœã.&XÛ߆¬›¡h¿Hf,D$Ìò )»T£I(¢FdØìæÃvRæ.xÏ]Ä“¼p"¨ ño¤÷¿‚nጓͼ­$ÆÛ|³&©•=Ðp¿ƒk,l4!Túìoà Ìê?? õeºÀ1Œ ðCõi8Ó#BЇ'WeæPz#IDÛ­Çé50´Äo0wT?nIúF5ÍWÅDò9ýeL«æô^Ü/ŠÉöâ(µÆˆÎà¨_ʧ¼(©ðÕ®B' ÏVp{æ[GkÅÑðíöÈ[f ªiöèYK-ÅÑxï=¢A0¯¾ÃýpóG?r<ìD!x $a4q?nÇžŸitΑBD®¨¯ðRhÚ9º1PŦ‘coe=ggôë@Ž T-áa,ªÂt¢‚wª‚úC(½™òÀƒÆDz~~\ÂxvÊ©îºt+Oä.S²œ•އÈÖa„tš Y»Á©?n§VlÎDP°%íñ 0 Ÿ;\'9BchËP{ŽC?n†Éƒû> Ä©i-nàŽ`kšÕ¾pïöÚSËÆ¤ÄÔßiF/¼"Û†Sxkæ‚#)T÷0µ!ngŒ€ìÑÕ ß±µ%Q!ýô2j£çÚ "”•WsÈì‚°!g?nxpÇ(K-»l¼£/+ ¢ÊKr»žQ€™h´\¾"1P¡¥?rÏ­Î\¸ªÝë¡ÂTÔu ›ì‹¿a¿`vkÆ÷¯aw¸È É@IºC¤¥±jÛŸ}‘ć}îŽ2¤8²F¨™ÙÞ.Äë(uUV˜ÌÖ2ÀPò_z'€tlô°ÄüµËlí/íÃÝ#­a*ýö>F:ÕWØ€q/°~ª{÷aŸ£œWU@c×2.dîP5ÎÉÓƒ!mï ŽÖÜÍï,ÝÄqHQô?0ª2‘–çÅYP<å4»˜åŸêÈ`'EÕiN÷êy9Nhwõú }¢I6ûyxÞ"ùï@«ŽË3–ª”²,mU þCO&,åÎíAÎDûÀÚûçCf"NÔòÎdbÕú'é|ã®h%êFS„º¹àk1ÍgDö›Ã†¥v=˜¸€ØH8éÂ??(|Y¶3]¦r¥åx„ø‘øx´HÒèÛë@~Ÿ‡D¬E$TÜ-N»Üö¸ó®È'h[‰ÎOâºçeí«?0KŠÔ/ñQ’¨Æ÷©ÃC „ ™ áLÊMŠÒëc|L†x ØR½’=dwo™\çH‹šº^…v}¶þ`Ø×–ÀϨ„¶ÅwèP†+Í ú›ããÂþâè£/Sq‰ä©{ŸEÉ4XÌju5…f½s#Q®¼JÊU³L"5}p—+º1«XÍ*gSn'1š{ 0AX-¹ÿ"S ÅŠvÝÀSàl5ÈÕ8r??¤c¨Ô"y‘ V-©æ–ŒhÝš—ú™h|Äh/áj™õkä.Q+ª\JfÑXÇËSÝë—ü+Îkgˆ¸ò"n±y¾¦[R‰ 0ÞxÕ–ŸˆvX4-ŽºµdÞ³3Í[<ç¼ßrèüù“{žc2ï¼kaPd#?r$O Ô¬‘Õa/‡ èøC)0ž¾åL*PM·Œw}ð1€Í¹Ÿ”æ›XùbX{åáîKDŸà¿«eÖ|Ÿ©ýXíc³ ø-Î6U2éãðXέ×Tƒt1êÓË?nÑ.Ï[æÍ¾J7¨u—¯g?niXpÚ??"<Ï[OÐõ(?r .þŬn¾'ð!»ÈmUÐÀB%¨a½×xö/[vsÿckE^²àŠîÊìw¹©rŒÌÀ{FÝž€Lªªóæ °›™(†ó¥·ç´m¼k&™s¢,”ƒ·7µS_¾ÿ ÂçQZ‡ÿŸ?r‘Òû¬£˜4o¤œäK/‚,f՞ɼ¸(¤ZwÀù \ãªi>ü)Ÿ&%«–©ÔRe‘ÄÖ|/iæB’£¦IuªØÜm_Ë¡_xõů©Þ”o¢š²ƒÑüTR¨¢?nz¬”ji1ÊfášÓê•©âC)I%PöF¹æ°tA?rb@ í Îv³üÏà@(ó˜ÛÅé<ÚìIª!îgŠÆþn3 |ü]HûE;8±œ¶ø ÈV=.‰ñý]ž·YsdâȾ¥Dú·ÐçS÷¬0“À?nÁ’£³£3”6~¸±)h£¯$‹¤¬ô±š°5Ê?rÙí­k-g©£¬ö‹½|¸Ì9㌳C0Ö GÔœQä÷AAVFó©w‰ý”¹XăÕ)AVo½¾†ƒ.ŠXÇ¢§o“ê’t“âÝÞžÔ µîD‚Óðˆ,)þÅÀdN…gÛ¤hÅ’éÆYÀúðøψ†En­š<¤‡ÊØLmy‘·¤iF `, Y ECÜwÒ}ã;®#%QØ<†Ž0ßÇéÝ»=“]ùÜ·¥W¿×÷¶ÝøîàXÿïÄs_ˆÿ2)/¶W_¬I:±}p+î`§z?0ýÒ{Fü1ÄYMVÇWÝ6öîÞ¹µ¬­&·öS =lMþ*YÀýªm3´Øu ýÛžü¥ª‹(Pšä“â+œù±ßÓXŒÌ7•Œ?0MÓë6[=¼½à#ïâWwßëE.oµ…„B8ï¦NŸÐ}ÓÀiV.Ëi7‚õp¼5P¬~)ö¯WÅp ¹à°FkEâ¢6ŸõÍ“'‰U•“Á(6L*)¸'_«æ‡w¢»1 -N´ÁF!µÌ¼7¡-(O‘7YiJwƒžDe¬`x€jª„WŒ¢º¹¢3eEXÖ`Žþ;ž¼{>dROãPŒ¥,Þeܦ6Ø\M°˜¿&œV¬8 +¾÷ï=˜b7×¹Q/e?nã„™8.œðÞ²9¨Žª?nMÂȾÓþw‡,ôÌѤþêM"¼˜ßÕmDGîȨ¶9u¥I®N/@Ï‘»+CKmT-mTµ“lVÅRžk¢5¸r¹'·ªTð“«2‘qØFë&1§;¯1¢z³/а³Ï0ÏÆ~×Ϙ©sIûQxƒ1®l_ e!Q0L>|="šž#àD€£\žÃšÃô>™±ÒY£Kˆ{ïà‹W¶5Ž"ýß+?r‡|4”¾‡>9³”Š#Éë\1çNþ3ò +#)dz ¿¥»¥°ÍÅ·M·'Þ®þºTXü-ΛGB`† º Lý*JÖ?n¨ï&Ýq%.Ø[@Óº)óq{Ic?0SöBîöQ9ˆTêªf¾ F*GA:}yqUŒS0qŠKXNÚrœÚY: ÕuÇA"û™-UØ`xõ$DKàµ.™&ÈØ+&“1Ñ],»˜1´©="ø‚«=Ojð˜ásÂi²aÃâ$ƒMÒGæ¡J§0Ød -hN?rxˆ£52/œ Y…‰À ´UEOÏmz??A ^æf?rKJÕw<)áæR൭òQiI])×Ð)Ó,²NGKk«‹×“¬Úl÷ͶyCð0ô´´g½ã0qæ—Àß›Z¿=¤‰IäbËcâF®­ØºlyÅìÓ8òú ÿYÂÇl·9i?07Àÿýß‹ÿ1þ ÿø&f–ÿ=ÿóõÿ'ïÿýÿüúZjçcO_ 6§;DÌÒ4³õ,ÇÝ”)”¼–x51ÎèØŸ¯Ô >ŽmöRŒtв@ЇïуÎßiÝ.sfðƒŽ‰tQÒÙÙh¡ž‹°ÉOöTÌ~ⱂ‰åoFE>ØÛ??j͆M{ÂÉÔäðšÍ–¹–W?n3ª{½‰LóJu#ïE,?r‰¦ÀœKŠïælÈÀ‘Q[þ (öÞ[énÔÈõÏÊ×vó#«Àk––©¿Ž:¶jþmYìr§½ñÌ/Óµ~…ANd*òQ“‹ºªì/®@1ÞúÇ‚›‰à_^J8ƒ‘ÉÈ##oðÈÖYyX3:áýŸ¶97åÓ{¹I…߬»·W¸??\æK0W=qƒÁÀåè»Ä¼Ã’\_:ÐF÷ï†qHpx î_«‹8ù˜*[ˆe‡ùk`b¥r¥æjHÈKr$¸ 6T&„Î$HËxÛâIÜqá ˆ?nËA.íRÿêYzl‘L¼…¼ë•$‹fé!M_Ø Î(æd#óî†?0ëiõ›L¨`Q_ œÑ<¢ÑþƒÖ ¬Ä³W´ K:ϸ´ºA=’$DÊ…д¬áN²ÉVB9Í‘<<'CñAKã—êµ’¾IªBæô»¶Rˆ=ð=¡çjQÒL ¾¿²%óO?nÃûòy´VLw¾¿‡Ÿkô÷¾?rëÂîÄJa}´1†ßàRõ8Aì­öfcˆ…úÙZò)˜‚ƒy¯rÌYÞŸA_ïfoq}kâ½T%ýG1ƃì°×$´âˆ]zSÈ#~¸$’wâ_jÚ^¡KYÙ\??ûŒŒg^x¼a[}øLÍ2’$i”Õä‘ÌI[òjÄbb! ÛžpÛQ<˜ÚEp(§?nô€’_“ãé1ß*p”ÊÄf5²Ôi?râèÖGÄ@¿˜@ˆ?nؤðã`žôiË«ñxÖÉÇÛøv¢Ì5ù¥$¡ w½ÝÝÏ]ËûÄw$µÓ׉ç:}iáIÞ°ó)y ß`É/õ»+Ç<ÆãùšÒQº?0 ¢!ªA¿ê«Œ¶ë†"ËýðìòDrõ?nÒ0ÒQ„¼ÂAÐå«»?r¹Ád¨óå Ç<Ötü¢„š,ç}¤‘ ÑCú‚O×Îð+Ž£mšÈ»pׯ­Å9É9´º§< UØ£®*òƒ¯îª”ÝÈÙ8ƒOû‰ÕUYµì½QGuÙ ¶jY¸aÅC8³Ö?re5‹äå@»Ï\öôóUUÿÚPém^—hùÓ v9z¦’ $ [s@Þ­à}`óê &ë•fP«71‡f–%2™vì¥Òëq´*oˆ^ðžÞ9GŽú+yszúJ>sl8Y¿„ ?re27PDÝì?rÉšë•Ôò¯#[Nrà¡È8-&´€+n¦$²)ÑéúC# P˜t½­?nbNxnµSNÍïDüE8Ô‘GîÈŽ|¨²Êë›Gà‚= ©/†½ÂúÒÞÝ%t’?0P±4œqÝ·ÿ¼DèÄ¢†â,Ç1°BŽ¿¤˜žü·n²¿[VHh?nH{Dqim2ŒåÅA@;ŸÂ¿÷ )䤱—õÒ‡WÈÚ*`Lu¿€l®OÃf)´¼«žk¸ŽÞv~›„ï)ãkaºÒafl¤ ìŸ4 ¡ÃÞ\=þ¢‡’Ê"æ ÀF"ÚE4yt‚£‘|‡EŸªÐBgŠ68'±ÙX¢GM¯6n=ß_[g"š¤ØX%ŒŒo¬ƒ»E…êÜ:ŽÐµó:\÷ÿ9cܪÿÚy9›Py»?n° g??À¤lŸÿˆD#Íö!çá«??竨…ÇUóê™Íå^Ý,EÉþíÇ»µwqæB¾??B¡tîçt!¦O®bŠ€]?nˆù¾{ÖNêJažyä}©ØßäW)wÑRé|ŽÑadsv)Ãt]9G°»AZÆ{¾;C2 †uÛ7„@À‹4l7­Ü,Xr/ÌÐÅÅa«ûÄt ãô}õºk/®ì“–b?r¯Ãë2²ÄrçÊ&ûè8´q¡¯ ˜AXµ;8¬é¬ýãY”A?r"»ëX†sÓD?0øÿ¯ÿ‡Åÿo?0ìÿþ‹……þ¿ù/FVúÿä¿YþO;ÿãÿç¿ì±bþWñß-?0œU<è|eeòRݦn¦™˜‚¬åŽ-@bûIîvª S€CñEAƒxR¶¯ß´.¶šàí±{{|_¨GOc‘BÛçïüxLM??ÖM<Ú*“h`;™s&ß°žLšº*·pŽÇæ+m¡£BéJ™†Z™fDÍXwZÔhÿ:·rú¥;œ<0±bÿWž—ÀjŠ:²hÄwV˜$HÚ÷?nöˆDÒ [óç«?0×ð«ŸÂŒ‡±çVÅu¬ÝÅ ²½WNË6¨d?nÒ }F§È†AÎÑMO%fuqb7FgPzœ>Ê0r‰ˆEÇ$.£¸écBEfŒRxø%¦R:ÊwBMQüšD·)(|GDHÃd³1:žZ'/Ì=Zb^?n%°¤ÅOS§lxÜ;¼ƒrƒ•-@zc±%޶/í£$%6®Æžª˜YbmâI—¢("[0k¤¨`ŠÍ€Ó¦0|Td†µŽlnzlždX±3hÆ-QÆ)ç–’_Ù»‡·”%W¹e`ÁƒpÏÂüåezŽ÷æUI‰Â„$ÆD‡áãG‚"”¸¤äjC!X5R†ïe÷Ã鎊Õ(ŸŸÍ–]GÉVlWPÜ÷RÊ¡6kYç”óâ*Žpf Jn–íR¶M$<ë-¼ÄÿÌž4òDÝâÝ]””œÜ”MGW.jRœHJª­1011=?ruˆ=)¡áïý/¡+Jh¬ÜõîÔ4£ B…–¬òV¢Qªï÷>ù‘ÛÎé9"ëú@«Ä³²ó|±ÿã\D_»|åçÀÆÊ?r§Ò¬ •lƒÏiµ ›éƒý¶ñ‡_G_X¶€[j d;xVš.ÖÁ¶1yaÕãö'"úêŒâq*nbW;¾ßXÎô¯ä–#þ?0æãù[„ÿ¦þ7 ŠxsÞíÒ…äµ).ž$ëXO{½ˆB€TTÕQ È}ƒ¿~™¿œ)`ÿ ]ÅåÈ+äw+n|Ãü(ISE”ƒNÖe‹3Që¢ ÄO ¿(ú}žZlŸœVàæ¡ÉɸÅá ŸƒAd_¼lð•ÎUa·¶Â¦?n—à¾É×”ÀÂ'÷Òn›LãÁjÞ‘±2vêæúñ¶JMµÓvú¦‘f‰PF–«¹0þêØ«.—|uüþð9¸??œdvØ??Á„ºhÚpë®èE•¬ 0ÁïÛøÂFjâÎAï\SºÖÑ)·2E2“/ÕõÅ™ù¬¤äkê6dIõ·ž{Z—÷àLó“']ÍÃqy³-v|9báÀ:>@¸ÊÆ«C5×ÕżXG¯ÉiE·í€Såmª (÷ªïgoš×X´×‹õ_%r2fDqkYÍæ¦ÜɃ4¹OèÍ—“E’?0€.9ò¸ŽÄÏzhceS÷ê*?0³Në0ªüzŽ7hÒrE=[×txPˆK!.ðצ‡cV˜ëÑ_¿a-OsZxБօ]ÓðÄÒ`£¼°-³Ccr/Fs’©\Ý^ïÛš®\Q8 ° F,Õ¥+°PlÝAhxÙ*Vôé@çù”ù\~#{,£»*8Õy¤€š«³ „??·ò?0'ØÕqilöI1ì] ÏSOsU3rö?nÓ;Åx¸Y(ûÎlJ`Ö1-ý£6&¦ñ¥˜vôëª>§Âé½Xæ?n«¡Ó£sXŸ°¦J+Çœö©^ëÁé»Ð{ÃÅD´Éf’3͉¤¹”ãÌŒè8B¶å¿u"†ñúÃü…¥„s±:MÄcFYûî­ðÕ×F¯`ðúÆÜÜÞ·ç3—öç×?r“öŽzUæEó^\d:ÍõҖ‡·??æÀÞ/8a¼>¢É®;YË׺yJcyt ìZ‚—fEœ-ƒg¹»¤'Ϋ‘@Ú¦•øâH[$Ÿ×èbض`mˆE?n‹y^ËÐyÏâ­v„=÷·\ºê çûzN¯uÝò»³¼÷½IKD+'»…fddøÙ9¹3¹|;:œ?rЮ®ŒFâ¯ßOdgdd\÷"\¢Lês›©½³"²xl®Îƒ?nРxàîOÕ1.ÃÔÉ?n¾"9õ¡•´¬G?0šgÚÌHXÃíÚ ¨˜c€Iè¡v'ü‡?r©VÊÐ?rná~ƒÅ> +#©™gäsOÒ€îÿŽê†„ÄÀš¼B?nå<ÿž{âÖí%²œ,‰œ‹úio=ƒ•ÅÑ‹YEŽÞ›ÂðÖÞ­Ú•qŠ+žkVÅ6—ºvÚ󾃨‡Hϳ~RJ#V–Þàg GX¾~V^Ïû”Ïá'Ȥ)Z!£w!®mh‘Ô yíÐ-S}:õp„GaNö7”ÜØIЦQLJñ jµù{E`Ç÷,E&ó •.@»ó; )&ÂGÊ…ëîž_ösÁ"|~OyU Ò:Z³5¿sKØñ»2nJäM?rÈöÈ÷ƒR'ÿ?rø!™ÿ}“Nø(Pæáx4??^+÷…Øû>Äì}o¸ñß®fÿMÓZI–Ñ·K™T*ƒâðPúŸ…ñhÓs_fQÁ££#ÉKŠNwUÄãSÅ«cÅ¿Ç(wøVÙÃW¦“K@ö}-ßá»<ãÃɳÕHØé{2‰‚' {}oêú@ã{Aëé6íGyïG*5V‚V´¿•ÝÑdßpØ»¬¿óÿØÎÞo`³ÇÜÑ5Ó°±Úd„ç2es ¯ÃÎà{~î²ù>©ù}åýÞ’Í…°#؈YФ§ —Zó“OÜ"˜óý1Ö¢eÃô“oè²??Óÿ)$õ=OÝÝK*֌ޑé—ú£Ôÿ[f0QÛÞ¬ aÝœ[OŠq·ÅöP{ ËË>z§`ƒ-÷(ÆV˶o–õ>>I(¶A`Æ´¤›r\Ô#ôÁ§ÕaŒ ˆõw0ï’—<á™Ý7ûýî ùÔÏÐYŸÕU™`wCÕÝj9/Û1òb²¹JÛÞàLÕus¤Ù@}D3uíÂÒØ=èꉎN½»?0ªžj¸ðR‡çñжf× NÌ0æÆ¹µ„×Ù?nLið?rŠsaËúôÁò_]hÞ %3,ó1&rÛGèDR,C5® z)Ÿï8>ãÃ"ŸB5nÛý2T~šÍsK‚'ªÔwÀj7s*Š,([3?rX›–f««Öûˆ0Ó2ÅóK»Ù°È›ƒ @®r†hØq³Š¥* ¢5²QŠ}ŠîgDm1–šÿ'güLËoƒ öØõõHäl#ðo$ÖÙ€ÆW©†(6˜Å“(7ýŸÖ’¨¬¿6‡Âí2ø|àg*1SÙ¢tÂTú¬ló>¦??ðÌ8=ghÑÑc?nIÇI©D#ž‹e†+ð²¤9ŒMBp0ŠÉ??Ú&{F»žÅ˜¨w>P, ;9Ѓ5Û>ï·”£^`ûVÏœÊÊá׺ˆ`ø¯fÊóIççswTOEç®õà=Ô3èN»b*²4xµÜ‡þ^2¡¹+I¹Ú??’CÄD·aŠŠ§•¼"ºØÉ9š#»ÙšU¢»OÞh–?n¢ZIª:ç˜Q!÷ïÓ èüŒõqQ»á©·‘ž/»ÈfÍ¥aCN¶Ä_Ïbµ”ìókÝRâ?rXiÄÆvbÈÿLTiXgR޵û?r¼ªc³ÔÙ9ài ¥5Z–3 T*Û¶ì¶}àÜ]Ä‚Ri<ÀøÁØ©*?nQtšò´–õŠ©2gÐÒ!ª½xôž!\§ƒ­ðBÚ–c5ˆ_/‡Ýjþu™N1¹èc>Rq]Ê/d ‡êPƒL{v±G/SFê3>$”.½á] Ò–ºSÜkW¡¾GtÉGÁŠ~–Ã\\WÑòCo­˜Èæ…4Šjn??ä¿›F'¹€T“Èìãr\ŠBô™/?nªL»E’7M@Ù3ñIÉ&Ì¡R¨²ì1%??+MY™L¥» (GMráÊBÛ/hñõä¶–:!”WÑ8ßNÕ ˆóó–ÓƒŠ\[+ê¦fZ&aãÏõÒD¶1p lkvΊ-&3;¢Îr«‘ÊSëÖz> Œò7êØÀwéÄœ>–ò:¼ˆëR??‹2£?0uf×4}`Ï/óàG±íˆæ›Ïí¯úëlSq.BeËPzëm€à^Ç??¡°U<?0e`Ð% T²JÍàú8ÿeÊ”]ú„šT&¦Œ“¡XUÖº¶¦MJ»'RçOŒÌì.ÉU´ÅñåLgÅì»r~ÐF¡*;0ßfß³„B«åëƒÚ” Ü+@»ò\²L›ÜTkÜVvLæT¤w‹vÖ²m €W‡XŽáÀ&MÆh—:ÂHª½@™ê5ÿCŸSXÿ7xm)y`¤ñ+b·•_븬v%¶ ŽïxÐÌK'¿é[Tv¡BÐ6IÍ'1ßl“’–«ÿ°Pt&XŸQSÓEœVÈ©³f7¦Û磆s½ëå &„øLΞa»§~_…?0F—Ëç!J] ÏŽ=LuüÆ;I´?0õEoàIãÓg‚½·T®Ç·¾-–Á8ÍÕ‘EèãsƒÚêmÃjU\fu‘dy –uJ:œÃíF7Cê¢gßàH8,в¶Mg£¦ÑEj=ùñŠ” õáozEtqŸœç~Ôî²k³ûMÈ=¼2XÜŠø®…ì™9å?r?0-– J{–“°äÂê8XÒD´Ú²ÙɹwÏkq3ÃÄ_íÑb8ñö˜@o´OÅ€6½‘Ïñ1Ìuû_™µTSmLxsô;ÁaÓ@÷a•§Ü~:³!؃f§žÌïFdv´d†"–.äÓõä~ˆÀÜ(û‚ÊÞóò‰ëG½;>£–?rÛ/«HŸ‹@ Ë>}5³è™!«i‹C2u²kN(¶{@±-“zøŠy"ÐQ†šŽ®ë²,ÎÖM'Â[J\:'šâSâMêt€lÝSP}6V%ì~'sS´äVà’Ñ}9¿ñÚxßüä9I¦àØF%4)s-ïÖÂ@À1ÐÞ.äqD¾x¥¢°& Wm¼êä—œZ+¨Øâ#¢ýÝ1ï£4??Y¬??ſӸMû#,ã´¸¼µ@ä|Jõûm®þaÃQk8ìx Ð÷Òàc?rv(?0n÷‚1iñøýÝa}?rÍ}Ë­&”Õ葯 ¿KÉl‰¢F¶Š˜é;U ó[?nàÐìwO°vÚõb’IRëµ5á2‘½;??@Û_Ç¿…Úô{–xÖbÊÍ„ïmG¥¾£à<WQ•%„;Fä/­÷Í?rº^/šÿÈm€E(ÄŸ£eÒQ*Ê[m É8ƒdïZÌê ‘Ù߇9‹0Õ:<ûøxÍÍËÎ2ñ‹:TÏ&·?rx0&øœdd]È•¢aZŠlŠ®Nío“È[Þkýh{éu,…`óÓ??>˜Cwñ‘¦¸ìQªïžM)çÛÔV¢K¾›ü~réò74??6î¤ mÿ›hGÚRKMý_O³ÖÇMºl‹Õ!"(£øÌÂë¬õ??ƒu°ÁœPw?nP:,Jöín•UÓènÊèýWû?rjÖ}â¦L7”d)K'&ÝðßÍ·3~(à¾íem¬œK ^”¿Ÿ1€ £9€Z uÊ‹qt÷Šèl<4*«qü”¥“'S܃òeÃ;ùÇß)[4BL.ZµÃüâ.÷®ˆöÙÉÆåsoÛyØ÷EºïæÒÁ)MÝ’Z*Þ{w÷Â)ÑNõ£&|Y]ÿ??T9 +±…6[k,Ö†»ûLÌûâüQTZLæ²nrÍÔ??š€]›¹U^6!Òñ9t¼Ö??RÏ„œÄFªx]ákøepøð¬Hïο2Ï\Vs^8÷8þ)oûøÁ‡È![ºš»|Wfüäëèó%£GuÃñGOë$PzÖùNªè[J½ž¢è÷€Uƒ ºZEÙ??0]“HáÖ÷)¯•—¦×"Q$É€[ô¾è•Ÿõ™X+Ê4ÖOFQ8sì©Y™ÔMyïS‚@»º­8ìeãr%<Ê×l~…ü±¥)7‘ŠCY÷y„"£À­S›B•kÜþ‚a†Ç‰Ñûo˜® 9Íúrþ0©RQúàì¹Ýº*ù‡=›Ó²Þï¶‘€d07\tij%d)Ì^î½…CöRäYí¥t??Ç{Ñ*´ÍÓvN‹Àäß4§„š„®ÆaˆƒÃú" ]‰ˆŸoãÙƒ[õ›kœ+?nGò§/COaÈÊüèËL0@øÂ]•34Hiø¿âæ÷àŇՅ€’F4@EM,,áåêy½ï=ýCMh¼þªÃª´P†ß¹û9¶1T‰áæ?0ÕæBüS=›X?rS­¡>@–ƒD +#[uNb@lKuÈáÎF‘\£gíܦ5öî¾I¯¶côÑ|‡£ðVNº -ðŒ¯‚¸“Ú¢2þ]OX¹ï>?n‘??×´ÉV@:ê‰ÌØÜgÇø&e0bÚ6”CÓ à¦zSí…õ†Ï$yU;(B>‚X%´ÕA¿Ç‰Ã‰?n²á—í‰hàdMKIIC›ZìÈ??9"Ã@N接2¹FèÛˆÔÍJ†“œ]£‰¹´ÐÌ^…Y÷­1"‚Ì4—²¹÷t?0Òd:d„NsáTU?nøÑ¦[ág`Ú.-BgÌ_õVø®q‹Gβ±{]r‡= ðÉLa¯‰ûpûå^„Ÿ,ÖÏ«ëdÐú®Žß:çf&øUÉdÖ :;AE»º#{OsMÖãÉÇnžxÛ³äDb$tï"ã®÷¶ÚÈ›ÿ Rn7¬f’”À+D8‹RCã7¼²ñNó 4!ol­Y ©s¬ÞZN:3EpŠÂ á`Úî±Ê6§X>£oïO‰+ä@F¨ê8±+þ2ºßnc$…û$#?0¬‹äé aÈ¢Ð*li7ñ'V1†’Ê»UÄUTMs¦©!äëÞv½îžO=ìžWﳪÜx£öîDÑ ˆGç8¶BE‡_ÞBûè‹FQi´1Û\«!¹í?0É’ôŒe?nyúêGl­Äô™½—M+ÆC·«KGãr³nFàóx܇ÈâîÈ6Þîâm÷-Þ®G†ÈcláN?ng¡ª*ðȬmÑ‚ih4q]QZ—ábàöNV Á“ÂMmƒÏÚ,'äíF0OTOÍÉÍÃÊÓ9Znd8Cö7.&WU¡×(&M\ƒÁ½ð‡Eá-MÕ<_ß9ŽŒáß’Êç‚m¶˜=[ö?0a,C¡.@åC­Üº ,À콺ЄÖ´-½Õ’˜TFÜ??/úTÒ¶oÍ~€rs8uæ=¶™Äé[@?n§|ç*Œ¾>­ôÿåBŤ•?0/äá ½xêÃ7n§~??®XàÊ3O|ìß{aȯpöú?r÷¹§ìþꮕ*!ƒO]Z!ƒU¸Ö;Ö#ð¨?n¦8òmüùt¹Òõã—Åvquž×5¸’Ô²Á%<Ñ-Cíxhd&?n^"Ô(Š˜‰á£,ÏMØÒ¼P$Ñ¡,.•ß{- £ [??‚Â%5Kb çÙ“+øΘî—ycpÊLZR¥ÅäbÓÑŸœºNEuAj8\ˆ–{½á€Ÿ¨ÆÄÄêý9Ú…f¦Mè!”'èÊ­¯´I´C”OIH¤qåËŸ¬hl(Ý©0žGجâ…òì??’çÐçò ñ f²«ÂÏ"è{øZµÉœ/™ð_7ô6h»ð–—âúÝáÛd;SüJRqÑe®¥üi¿¶8ÙÚB+!¢9däâ??NH“2Ä(»/”©˜ê*TvÑKÙö…=ä÷õH©‡0beqÐ.ãt/ÒÕNñ?0X»,k&¾D*·Q0ѺpZ Ë9£kp²€L75w¡G6šeGk·£âéI#N÷ Àñ㪿÷l,³ªóÍAÂÇò® ±œ`2"ë[´·§®é¬&¦è»´ù‘z©7 ’ì·P1b*Îä/™#/ì5?nƒá ª–Óöcû쩈:>½LóQ/Oqaã‘©)u±0þÖÈ?r[`¬™™a¡bÝ%zLCÔ#}Å*½K‚êQz¸hpÄRq:Øí€ši™<³6\küRk&„«€Ú6–u‡f"ñ¨™Ç)à%'—ÛãîþØÎ'E??Št:Ý#<E”õ81a–Çí-òüOô­±h›¬%w3äþ8AûëØ£Ýo™µ›²Fªÿ™…f!sÔ`utq¯¤kÓb§ô~è[H憚ïLå˜ù{a‘'ËjX>{èñ0s­F´]ÏÀ‡¼HÉÉ;Σڀù>¦tÏïüÖqòû‹ïýÍOK[Ô7ñknµÂ2ûÕ´’º qg‚Sgè÷3³³[0¸-ƒ|guW6e ^žÓ™ºÇ\a’-ÊÈÜÜ• '6õ@Ú68H1ød†>¿o;ùŸÑf·¥ò@Ž#xúJ•£To¿Æ(@A>®ùe‹…~{;ãIWÌÎjkAÊü<;דnª¾Ú#´Û‚SÀŽ×ê–¡5‹43Fk"í¥,ÊèŸX ß+;™*w•<„W”èuŸ®u?0pvi¬qôãpm߬³ëc–ê¬Þ‰˜®ž«Þ–ˆ®%⟫*ÛP4Á÷äò¿TÂá^Åqq`‹ƒ˜z0¦U=ÉÂ=³ݶ0üÁÙrwÐuR\«¸ó*6M{ªVÀMÑ;/žëw•¯¹~??€“•ì¤p©K“U°Å³rÖ>½*Ï Ö+ Zc._’!ÿîJØÐú$°9*ÇÜô Î/O¶§µªŠl·7xF2ŒT,ÖUΚp„RŠßîþÞù¤ÂÏ­3#F©@cXWeJA[a©ÉÁ°N+Ö#Ó@M>’ÊäðÿÙñü\‹ÀwòLQBåvøam"½‚jhRŹ#±šA0†›à°ƒ€œ¿qXÚ%lHÈ›ì î©YÕ-ˆù´²WÈq"¹L‰)ká,;Îñ>OîzÞº¬Ë…>(CŽ„‘??p/óúùG°,að-5³ƒ.B]¢ÁA”ÆÆ2S7 ‘n69e4æqá™4HŽjÝ=ß27Eݳ!¾?r—nªb—›­AÊ3Ó’O;Ž€ºÎGÓ2lñ*Û1 ~ø:|ê™æÝ?r`?0áØ?0˜™ñ9d|ÒÓbpøíQº’+©œ¥nS}*áÆ[<´‚Ym¿‰IXoàúÖÁêÖ›¹&È??‰HŽeÌéêWû¤û5A"ÅËCÌâOåЧ˜·vm§rv†v~X‹vœÏ1]ÊuuÞÒ‚ˆ¡5Ѐã쵿+‚ÜÆÈ²oþ]—hoÛ?nj±¾PEÉ5Øìl©‹¨Üw}ÿÃ¥›ÿ°•`›OjÑíQ??0Í3%GïJ†<£²>k'?ngAhê¹Û>ÜÅÃÔOÏ– ®­ ‚+íô¼ªÈΞ˜G¶¨þASSkŒ¢ÖJX;%á]µ‰<ᨵü‰å)5ŸJþó¼5÷LÆP©ƒallT^Ù6S[w7k¹Ü„õ1ð¨€ã5)émµ(ÊVsc,‹î*Ä» ÙB/Û˜å!Z‰a\Øcževè_6Çãsø÷:Ïp @ñe4v”Œ&‡³y7ßuÄ$Œ}ˆ¨Y»)uÛt²S/M?ràºÂia„ y}ŸrêËÕ)%•³^R>Å@ä~tÈü¤ŒO7u#º|6ûíÁF?r$!Y`šÀ?nÛíÖVíažwfñ$ZÍT ò?nÖ‡øÝÖ,ÕãÐ0ÌñÌ?r?0‘YFy‡qYIº$ÃjÌÂÓ€BùˉŒË”lBÒ0Xßæþ¨àES_4=&åq·Ô/É8êCh#WÙ3_“½ŠG³\úôÏð¾ø ‡q/ØÓâå•ú†˜{„Ë|´AnÛ(k±îé{k9,ô²uu£,v™]c1kUö­‡ž²s˜aÞ—VæšI5ÐOwèž-ïrh 0 åFUØ~ðˆ¨…º´1ìЕ*Ùy.²“=Ò6Ì-•˜î¨%ƒ{ºâÞÜÂÕµd½ ÙÊÐ¥˜»eøš›ÛüIB›gÚ7ÑS^gT(»ÃÓâ©jïô‡Âs WFŠ ‰7@ÊC©ýF&lO©L¦V¼ž^ï@ÿ)³”ÖK*»5ÌdÖP½‘Ãïn4㧋Ѥ%zÎQâšJ ¼yŽŸœ¸Lã~?n—`_ye¹8ÝÌÎÏw?n&)þU#êÔ•[4,ùQÝ×½Y¢?r¦&½Gd¥Ÿ¾Ö*…|W8/©)!ÿYš¬£%ªc´LÀ‰ýžüj?0J.Ë[>¶4‚.Â÷æÜ*#•†´rþmº1”pYSjC%“ûó£R1R¶_?nF<ÑÐŘÎ5QRLiÒ¸ÂÛßóäẳ3“õwà"ÃHnâæPwKêÜvsÙ??!Cr±œú‰Ø$_Ù¾±Á?nBÛåõÔí!myÅ•ØO€šÏ.??À#rG¤ƃ¨J°Šz‘åU¡áåYß´¬ÜQ\€¿„Lïèsú^ý`¾òì÷>Ü-–?r°°êBöà«{¿T=¨ÆêÊ™ú|+ÐPÃÑ=ˆ$4YÜ?n/pá¦a4ßM~l!¢Dª’.’ t¬­ñýZl2ÏÈ/9 šöÓ2Méºi„”5šW“êu©£~?0½ØHoR`úÛ“œt—?r-CÊù­˜h&x7màƒ^—+ò&ñŸ—½G¥¼Q$Äô×>©ôêƒÆ‹)e!‡•W¢!¢/?nJŽÏå??&±ãÐ;¦Â8ÇÔíÈ60ͧë-[ª0²OÑî<ð@̾wêžõHŽÀÃr#ã1Im=Ë|ù+{v0¯Q°›œ ΤIþSöÁ|^˜Ø ?0`>ΈNšÑ—ó0ÅÃþ½”ŒŒï/ÙWyš*·­¿ªá;ºo€|+õým³Á–”s6²vßÕ±í˜Aú‘€Æ[3‚,­?n''¿¬ÔƒW!ÒFšÍ£&e-:¹£ˆ.—R}‰Â(WaÅœåMÝ6×òl¯©{0úZÓßÀWýöî$V6Î7R5’Ÿ¯Ó½ÒRYd_‹¥¥?rtUÌ™}[¼ÅÄD™Ë<_‹ê÷Þ?ršûµwPp,÷ö=ÿ˜¢yˆ5DcOj‡åk“«á²/Züïj£Õ+ÐzxëuµàÎK»îß•GÆ¥L¹vÚqÖÛµG(†¨mT$˜(M¶\ÕobvTªõ>Óxºì?rÞß˸’“Ég¥"¯'¯J¬µÚ]CFá<®ô5?0¶k2عġNVœ#éâ=>¾À„ûžÙ??´w¿ú~ÚŽ??l1\k-IQjn˜g.ó?rËHKøí(¬Ò@µŸ*±³£š4}’k?? TÍÃlÅíx:|fÛ>•l´ âøY +#Ñë[Cù§Y|T¦OÿyŠ-*þ[Ð8©Dþ|µ%ÞEè^Hÿö¨ðŒÝ·IúО±„Åøì{1yvhu+[Š})~Øn³Ü`–n\Áw×Qc̼OR?nMcÏ?? ê˜D@›Ep‰Ö»”º{ÂÄÝÇ×Éçí†{º†tê|Z¿›Nnø??*EÑí¨ 4µk+Bh6Þ¸ µ3|¤(¸Yd7?rQ_³*ö®×­¶6N²¾ßí\='îOh޽X÷Î*nTkÇw•Æ'¬tvúcQçØ/ Ùdèj $Ù±xqrÓ㨲ùè??{¨ÏK¥}þᓈÊPú¬ Ãd>ë€i°0ý åkƱÞkÒÕ®z>ĬzÜg)”9G ³uŽ^ªÞ<í†ôáÂcv† .ªlé<ò~ÈÔ†çð6XóW×,Q«aßÜ.|Éf(Ð_ƒq.[PmnΗ‰b[ ÷HðppÓWë/BXèhGR@&û>{;ë}ûçñ~Ž„É%=?rõü¸‰lèÊlB­æÁóqo?n´Z¼??d=á‘Sá4’¶fEKÔPÔaÙ«–ðžp®\í¡‰·Öyié5¥&RƦžÒžóÒŸS?nOs?r*÷žy$¹uÌÅæÀ´¦;ðüºÄ//ôÊFµÚ¹ƒ¨¾ë6B4ÞôI˜'¾Ö·…/£š* ·£²}›ïX7® vÀ’Ë)Ruv¸äçÑÆ ©\9ìÔùۦő_ýD´22w!>dØ#îIpángåÇL[™qáõn™D3UiU2ö¶¾\p†úôÓNµVVŠI0ÙMÆrÕ$/?r¾áïæl=N’fQFO˦˜]°,å–p)¬=:L÷¯>^âbtzK\;L]ÒBlÕbB«xÕ2õ.ňzÒ6YÞÛ\d?nDGü`uôbÖbŸH3çQhT’ßÒ“$®Œ­â½‘’;~pw'U—ôب*À9u¢SzÂßÙá)±(÷žtç¤Gê5<ÝBÞh® @ÒÛ_ØS75ÁÃh)©Û±»KÝ‹vjÒû3ö€@½Ôá×è2˜À‚o¼ ›˜„å £¼Û?0Ø{¨S$S+lÖË`ik:š7|ƒ' µ_ &¾ÅAå¬X‡¤ç2 Vý-›ñ?0óŸÙíxGǧ%wû?nòqýã=èJÖZ—bzkÊ1ËØt‚”ë‰Èã€éAÌqic.-ør:ì-–ac*ز§†O§«AeµÄ†ÿ4í©>– Z` uæÛ½ò&¿9Ó^Tˆ¥”×ï#-ÛCCĈ£ÈøéÉ~Ù•mß5ÿ”=&¼œÆ –ÕýOø‹Ò»ØÑ‰NÂsíÓ¥›a©QõwéŽH ‘Îkø„ûµ×!„_ˆ51|Ÿ ¾ÜËiN©Kîö.WýeÖ?nAÒY_ŧ ôÜå=cqÛm¡Jöz÷BÒØ¨K‹¶Ö?0üˆj¥f’±ÍR»•ܼ«þù)tvôý} QmhÁ]BÂzpÔ?0MNú4bÙfÝ@HßC&ÛòËvþ ­,'–Úýæ2¦ñ1í4ëwá|zá/ºi: ZA&#k€Åš[!¼¨­ÄÜO%à˜§/OÏiw(yŠ–‘…qŸ„L{$dv´‘7œj^(Ÿov,úAi³ŒÖà~ ièÛþë]l±^Ì©òóýQï&S\vÃp«Ðô¹/'«¥C†1“ö¿°Tҩ19Y„ ¨3(¶ŠB ´ž¢…¼´“8wÈSQ› -æÚÌJ?0¸Ý(F¬¥ * Iæ=+çåUwø?náŸ"g´vŽˆ DWÑü¼Ë½Þ\ÌÖÃ)­î·¸±£¥ñ9‘„[šÊ÷¦J3#úܺbc;Æ©»‰P÷¿!ÈP/ª?0?0<ͳ@þIHòc´?n’qáCv¯Ç Ük›ÊhûƒŽÞq:¦2¨é½èÍ«Úæc÷,0hÏf—a™Ãlëî=n?r$›–Ì=?n;²™,0½” ’5UµÁÐN¥?níV?rd_MSI(ªl²)\ÖBÐOµßº¿ºÒ³K?nŸL ²](• –ýgù¯é?0j!S¢±S«RŸº#bÿ¸›yÆpbS½†ÐÃݤa´ˆÂwO¦›®£µÜֆ߻ÒÍÇ­ÚBcùÍ~ˆX4ß|©ñLªoÅD|Ñ={Ý‘}=·Ú `){âF ÝíqMc„* è† UŒÒλº0eÜu@Z ]Züÿ"kdeÒÄ‚1ê ^°¬®¹c9¹/÷hf]Dse¸©ò”l,x??ldÔBƒ­Ž/žŒ– ³ñ¯ûîžær2y«@??JOÖqÎdˆ´œ³AÉñÇ…pw|ß‚‚Þ8Ñ$9È= ÿÅÀ/Nç‚XZ=šÚ –öQ —öþ{Ýkó[¡o+î.r*6‚Uø6mêd.X¿[tÓÅmÂØ¡ ¤@­ÝVbþà¥þí¶î²ÒsRqV­.f\˜vÅ[l?0áÓY€ ¡¾n$/¨\zì*×J*9ô¡£¥»rFÜ ÕÖÌ9…?rÀ’r®ö’À!X1ä²~~Rÿ‡'6 ?0]£`³ìE¯],óô@Ó¹¼÷Ó‘d–Çó}?r>–)ª2¿sLÄÑ+¹ã›Ù‚_¦ýdƒ©¤ùJÝ7ÊÚ•‰ÓFnëüt9D…#I ‰}ÞúÑÒj}ÎgØiÎ\ÖõYeÚoh:¦n4¬—ò¨\ŒÏ}DÉ—¹Õy¦°ØïuVüâ°÷øÔ?rÁäS#\Ã2¸² šç¼+8ºB.zNó¦  <‰ÔOcK2g×CÆeàͦ!oÜ ä$š^ôöJ„B(›³ajBÐ;áƒ1bq^îC“U›¢WaÝÎ…ãœü¥ÜÀ¶ªä¬8[é~™V‚Bæ »=ìÕÁ?rºæÁ¦`˜~È(_Ð §9‹ã|ÿrHní®¢–SéZt öÂdz&¡¡¹-;Ý(/- œû™ïÌ»]åLÔ,$ÈÑoÕ%dqCîc_Œ û´Å+» Zßû¼ä:Å›‘,a?n›&>8Á—g½4¸ tÆëgÿx„¹ÒÕ”µNp»ûÊ=¨AW;R»@óÕ輦ä¬ÐøQ1$)Ww,Ù€W.rfã,S&®u¶}“—ní0ïôÝÄCmMžùnÎ@Dî+â·˜£Ú®ì_§íìÀôÐãëR_YÈ/ÒQçøNZ P磈B{Üz“©Üø1?nôaÊô1aís??åBi.ÛC/9OÓoN°!9¼m ¥»·¹Èhâ!UãM´j9?0¥œDûíØ§ ‡w3ÈðgScͺ°Î®Hlf’•šéN}›Â“m*ö$í¨PéÆv”ñ<€-:ùÍûÑWÿˆ‘Ò[uEæ­RJ…¹Å/ Ó¶ÐTW0*-ŸZKAŸOŽ`5Tµ‘k뛛摵yc›. å9$jJ׿ùéì[ÚìÐ`  º5nŸ†nÊ„3ÌsBðÊùŒÎ”@;Ñ´aaàžšyáÅ@£û·öÑ· ÎdÐL•¯ä›²³Õë,X_gmú)ªâÊû$;c¿|Üjé°Jþô¬®OE>c¹ŽP%‡M…âd"¿?0ª˜'ØèÈ›hü¸º!µÌDÓ™Éý5&F_,ÒWOÊÌißPý#-ŠœìpŠFAyoõ{E»xË96úÕÑ6`8-fߢÿ³A2à?rØP€™oNÃ)½H:ÎßÇž?0üm­¹§ZÎI7wjm/vÉ)ìãÍï0-)éÁ‡¨ÇBÏOmÝ —˜×?r>¼‚æ‰ovóå1Oì£Y¤±,8'GmêûÅOOŠ?rXp©ôl„óþ¬!ºZý‹‚v˵º î:Xòà?r¿‘ªåÐÀï}Êl¶ôŒ„óŽŸw0?nÅnV6iè_SxeßnüÏCóï›ßw]~ú$?0ñëµ€#,©ÆXÓþ½z$h ÅËn;ŒŒ3ªvrðŸ0€)±Î½¿Â:²Žëâ—7ëõᛀÿ¾™Gy‰…‡¶È¿•æµ7ûéŽ;¶0s¸¢d#û;ƒ??â­´TW»NöŠFÓ– MìS™)8öbZ‰â"òP+Çy:ãù¬¹,Ýç·±;L>];¢°¥¿‡wH€ÙùÅ Òé¤%gÞT.ýú///|,µ«ß¹ñ´Ÿ<‡ð2k‚àHH–%˜¼r­r~œW;†‘ÓÔŒË'ÃÖF¿†ˆ†‹°œ”–'t“›ï“]ämª÷‚ñeC­)R#ÊG¤{?0ïÿú瘟ê{æ•F§ò?nºk5µ¿¡ÕµôüÜ4ÂuÃSÜ—8çíÅ«cˆ B†?nöI˜üž"|8}™}m¿²–ÚFÝ57ÌhýTŸAy’¤Êd’ž³ks¾ ÆßÝÐÙA{¯=†¨ ÙQÕ<3Uœ>¿ºívSW™«6kÑnéˆàÙ†ìq§Ÿ01pøxpž :1¢^_7‡Ö][\Ä~??À–vkÐ??¯ß&>,|,ìœD~€øæ¶´‰ +#`˜ê3yù<^¿ânJ4±F0Ù„Ë·ªÚ¤*Œç4c Z fW¿ÝÏñ…v|tÕ¹Ç+m~Ž?nü®rÑý8›åàR©¥Ì~Oäûžó™…ÐT#BâTŽ£&0Ì:$æÜ_sά2ô“ˆ– P>ò M¤U}‚&b¾¿¥€©Ø–@EÏ!œnD¢ ‘m`Frfú!†ÑD9ðöLR5{_'t;”¬xìNè/"‹„g:<~v¸U«Ópqü‹g©Çеï»6ù7OŸoï™6?r5ò?rŽ ¶sšÄGSßÀîܳï4†³OZ3ÑYjÖ–rÎAÌ>Íg®ìšþì?0}ÜývHgSBüÝæÝé’0{2‘5ðŸïxãÕ±î°é©4>Õ릲,}pP"Pæ‡Y9¢1芯†dBL0kUpõ­¢$Ö¦Žhç@L54˸*ba£7™dš7£H¾Ö2¤Ù7ãoܲÃÊ©}8à?0+¡Ð´o0]‹¦DV¼PâBöà5ªµSBÓ.ƒA –‡¶3 ƯøTïÓ…²²á8å¤Ü0ÕmD2-N‹WZ¨Ssê%ñ’VUƒÅÇL_»@É5#ÑFûýÎû^,|fh\AC²áô ñ°"ëÄö+ $Å|µcîým=5A_ ÖhäÌ“$Û/ ¾ŸM[_ã5Z™A-«„I˜?rŠÓÆaL†Áò÷Šå&2ÚŸê–7qÉŒGÕU'E¯-"Æ8-ºÔ:¬ÕÇýÌ¡ûª3îš2í`ìø{‘ÑÓ8ynûX†+!ÔjZ‘¡ÂÌÊC_¼6mʧà\T⋲ï>8Ae‘áv¼.n0ä²`†ªژR w¹ˆÎ/Ö™U¸¯þ‡ÒâŠÝï®IÞP÷pê%‘¯?rÇ—r˜ŠzçBðól67¥üêí›Íž??ŽÑ??u C^ú&Ï\çˆ;†*­«éí’ñå\ t… Þ‚/Æ`§ Þ­{1çUÍÊX3±Â¹†©ƒckWáÙÓ(×9™ÚÍ#seHÆL;Ë:gï²ÀÁL\-;Î@Ö°mW{üT›HŠ2?rê+âÕë˜híÄ$m’Á"¡¬C0³y¡4uT…üH¢Ñ‚AÄßÍn‹.=ÞŠE5[pòÂÍw‡ŒZM°øÙ¯–Žj7|åòˆÎÙ옱–ôËD“råªI²¥XˆµãßÎ!fA·CÈ&¬ØJ2mªM?0Ìßw˜"‹ZXÖ¤Â4˜~ÌÊ0ˬ÷„Ê?rÒé²ÑT.-·¯³Ù#Ú¹M†y[=;1½ï;f¡  ­¢rkÅæeô?n,Ôv?ržì7 ÌrpÿÆ®ÿ7â Ðþ® ÈãC?röŒ§Õ-?0r?r?0Ì•fVžy¹Á•aÙõ<º/ê.‘’¶]¾$°œçë²Îê™é?0㎪½¨_Qu ƒUÛS˜YÖÞM9Ü8g˜™Á·šà<÷<® 0w”“tኽìxWÆ÷•âwïö›Ð~‹3›œE²!z…*é!må$‰Y3ÌÕÿeå÷Á~Q)9>¹ƾÞ%.I÷û³÷y¦)&ð`ÊÅlã°-ÿj‘ä¾w0I ÕtµµuX› ñƒ ª¾P>ù¯f/s'Ò–³eQ'/7H­ÿb*â©U¾o˜h+bÆÖlÈÇÅóŠyúdóX}8(Tð5ß6¥'ÝàÂo—ñÛÄD§_¦ú:T–U¡˜Æ¬àDkt/ÔQÄ„Ýcn>Am«Vã´¥ÝrNr‚—ƒ¼™ŠQª(¡c„rœ5eÙÀÌ ž¢ÒÉV»¾j<Õ§ v‹ÍÖ:aˆ= §¿6mÆÿk$~(Ð:ï¤]~w…àWŸìqÇc??A'£Þ$¼:#.¦§aiÒn§<·°»~Vï¯Í…ž7ö{?rP??ã?n«v²xbÊ6°áP>Ì5yެ©¨û,ÌÓEx'»?0:|NJë)‡÷1ò3î˜Ø#·»!ŸlB)›ny>3ƒ[zýª/ƒûñi„ÚV{.Ò;ä#?rÕ$“=Å?0mGmŠŽ—>ÁÄU\š—-&¯@Fƒ -uywgââ%[À:fÿ?r P¡ô ¾j¡]£Ÿìy¢vWrUu².•ñ-1É`?r$4EµIfYÜv±’éÓöšàžÅxêÀEÚI¬Ý±lL˜ì†¥è™8¯ìÆV;‘ð/Mà×??tr]š#âF¾°q׺»öaq€ü"o!CP ëS©Çf)5ú+«1L"¨ª&Œ½W$Â⦔’›\·Ué¾Bwð~m<8ùižÊÜêÁi…ˆ¤|${½²ášdûÜI˜;Ôô0L¡83×ýý÷O“åógâiù8$m^ÈÅq³;%<;][Ud$­#—Û÷P¯ëcD퉆Uá²yŸRéñìš‚DœÂÒFŠðÜ ¦XºFH¢µÓÙÂ=“òËC5C$M°C†}æ^TòìI%§UâãsKg¾ìßIJ6%kJ9e+gDjÁ¯ªÜ ·ìœï–žßV³(¡$Q; ??ÝÝ€Óç¹t>W“ÂDðƒÆc¹rLüjúGΫ6-Ó_ä??5ß“ë# K"òª×í$çÜœ2kóÖN7ÛY¹¢à§Ï>º™W?rÿ¾ô´§—<Ê#šxë©BQöŒ€oÍ!¹Ì‡ÖíŒê·™ ¦®KÏ&²ÆÐ4£$h¤=ÄtõFË,„Éé¨ýóÁDS¾_WÛ=Œ¶ j»S«“mcG`m’yð˜U«ÜHýÁ??ªÅJ óÍJ¸+ã9j¹-¹ô/nø4¦ÊT©Q<© 7Q¦4?nB”„L5‘¨£§[?0Rvá· Š#˜ìQe-ÎQÆ<û™§Ð38&0·JÃÕ"륫»IS¼q¸$rÍÍg”((.6F#YÞ^â?0¨äÉpu ß•ü :22›Ÿ´¾Ï¾?nïáÐÂÖ™4ØÉñlÈ©næ® ›`2QÚäk~gÚŽ¯»¿ó??p2}.šâ`šxÍ'LßDŒðKºþÓß??@Ïà×U,Wèí*°ã~‚ö˜¼¿ûƒ2:öºSï€Zª®”&¬&Î) õ9®ÊŽuW‰ñ¤EæuŽÎëPël'—ÃÖGùED×±¾??½ßµXUÕ–?rµ8?nez‡TÄ+¢Õ:U$Ük–4ƒþvѦu?rÊ”ÀLÄDSH¼G¨YŒò–‚UÌ@ØÓpqQ˜&Þ³Ýw·hJ?n:(éçÕ›êMðkíC¢°§u$“öäºØÀÄHW".ÚZ??I_2·¥4EÒ”Uq.õT.…":áÈVJΠ‚ÈiÏ ÁÕHr?nPèÛ´U«T·’ÙD5߯#ÃÆS–:ïiä‡õ=¿*7ùäMÊ·®ì:FºTBå6ͽðK>imiøçýF»$28MØÅékt,4âM\¬Tš¥]æ> ÉÌ<ù!“Î2ïàh´ô×\ñˆs'{oÜïÁtü:@ÇãŸ-_Æå«9~\ô“'Ñ,Ðõ,NYýì¼gS¬WÆ@TMð{N“ÐûÓ+°*Ráa ÛG„¬ÿÜ›n³ŽúF>`)7X«µ»Ai–ñÕþ$:V¯(ÜBC84KÁ¯yÒí [è¨K¸,‡§~W:(\!±(ž\Õjd›­-l›E:÷<>*ãþ¢>ìLÕqQ?r­yWöM®[iìEeâ° sÔ%~æî‚ìÝi«èÊÿ݇}`”?nõ©aÕ/ÁH*??Q`<éoéz3˜¹b&8|¬ÉøÃQãß9u>S»Ü™ã¿u(Æ”#CQuwÚåH.6 +#«Ãuü2®–µJ¬é4þèL<ÁÒ¼ óÉ‘|C—üʉÜ^§‹œu9‡<2?nÊ4öIéX´Lé¥tV„:ã´±Ð1Kc±/¸‡¡”-®¡.†å™ÈbëhàFÔ›\WBö\Æ(05›¼ ×ÃÚV迳Tk®ë’ö¥“çFœªÁr±*F·P9[lɆvB݉NÉå©á8hÄY#dÊ$4Ãð&8# _&9¸7’Ÿ^YÚý<ÈÔ<”â´°Ã#4%" 3Æ8]4®¤´tØ¡+ÎŽ)?nq€Ká5úÙ¥«’í8¹ÁAÃ5”òb5p±V×—Õj±ÈãNpi%ìÞè뚀+=??8Ã×ÉÑuw#œPð?rÑ©VdX?rL7w´ÿTæ£|÷Ä!ÜûêU‡?nÞ¢J}H›‹³su±,Áš?nÄ‚(%®sìYä›ô‡ñþ‚Bྉeýì]Ää&ñP{0殲³¹ÁBÁ?0‘Œ°Ä\Ý@›”½ÑæIŒ´Zê~KJ«ÌÀ ŽQû{4FÈî¢Í&TØd>š €:pØ¢zÀLk#–»#ð€!z4w½IËEvK‡RX(^IJ2ˆøûDÒ™VÁȰßS݄쪭¿?nwÔw!fò ö¨Šèóö"Õ´ ^è•KyÀhì³o]²cDzÜŒt,gqH02þ_{ÜyQ“ ­6-—§·Ì9ÍñttTŠEð‘hPq‹žàYÐ0nþóK™ßíØnqÉ*Ÿ% Íoóô…˜Ðô!†¦Â ”˜¦fõícä€ú¾R•)qr6µ(’sQ¢¦¦<ª¨'??8RóŘC“ZÁu¶lëS™Ùt©­2 ¯|F,'­6L¯­½¾•KÏ©ô¬?rýÒ¡òm¤,ÀòDì°ï½b~l¯P´…CôBMíSØÀj’ìN˜vi¤ÉO.Á¶þ±nØb„IäÑšJùD\w0Öûœ¹{’™#>Vª×öœµ5ב£–DΜÖ*õ—fÔøUÕDí('5GëlåºHúp9˜ÂSëóRì&×âaäçPÃúB +#ëêøÎÞÏù±Þ;í>î‚–th4Ho¸32ROf"!ˆüiû§¥äôH—|S?r ÇÍ‘êÍ–­lÑ–Nõ”¡GóR]î'|™Cø\~¢BÉN³ƒVèj?r’_|É?n?nVÿEÒ;'(¨7nÄr¶àS¹GÑÞ^×ÌÔ#T—¿s·çÍýñÞ& ¦öŒñ$ä?0pñ‘DZa|vcpRè™ T¨@;Õ‚åCŒöåÂÜ/äúÿ?r‹÷nÙ“lq'=@;àµ/FõÚ¬E¦$ˆ5þJM}ÝñJvüªB”Ø'ÕåÞË>¹D³Ì¥EÛk´§ùÈKÈŠò“NA¼½ZHgÕ¾ àø¥šûfë­ûÐÓÔã­ì?0&ÅZ)³PÕ›ù??ãeË$'Õ+f¼ŒÛ|¶ä.A†éb‹+ï Ò­¯¢ ŸŸ¥ .°ÚÉu L?0rÅÙB»•’ì6FoH [®`.©«üª!µÖJËMù¥_Ïm•šCHvP¼¼sü}Üò±?r–órfå,„·7Ûº£m4„gÜù÷[òÐOÛb{™•¨ÊjV7Çæ¾·„O³¥÷)›óÕ”‡ñœP|ñ¶X¿Ojbt»ÝU‰Ì^??:¡^„jaìðY$IîÖ™«Å9Æ\]ÃL7IŦ´èÑ®·û‘Ë–ÚÀ»èUËßÈ›¬¹’ð¿YˆŠéNF9“²»†vmËe‹ƒÉM”ùç4TúEA=ÁóÄA ¬&IÍÿ±4øl=n¯ûï0‰×®Ré0Áûÿ”r(*ÇŠÌ,2ʯù{ƒ¢'æòW&é‡?0•¼@¿T5Í”‹˜`.”DÃî ‚R*âp®P8’èb´„˜ ƒX¡. ˜ÍÚÌNcSÛR¿ÉZ_˜)“¶O6jÎ㠾ح5Ñ.<êß?0aä ;Ø?0/GŠŒñe¼_ß=|eZ}7L#önË'wD ‰ëºj&†Ëò.d·CŸ¼!mPBò kn¨Ÿû±“—Ûk$“ò叿œ‰ÃæQ;òßÊÐÚÍu2Õ}æöû„GÅgs–Þ'ˆ^4Shz¨q“seŽ0l{Àªö]c'”ú]¸'”³¬'xÙ¿wmßnÞ¬Ç=0/§ ÍO*&b¢ál5jAér!¨èõ!“ø?r<ËN/‹öcøxùíÀf“›H)Ëëä.Ô¡«_Ýêæ?0DõpABp4??Lõt194@×h}x3åå×aoçŒÌMþ­š ΋„)å•ÌUú??jhÚCVáH=å±V$öÛa!EýÞ@÷k+:Ù]¦UèL¤ø¯iïM¸ÔÅ99ÃÎBƒ¹^ѧÙ2•¬5û/Á‡?0(S‚ÞTp±Û“Sµ›òõð(ëûÜö|&Ø8:ý̩ª‡ŠlN;àï˜'á÷½Jèhk UQTU„x”C‰©\ÉTú®–¤#M`Êê<¤7¦?nèå^Ö¹ðÏi…™lq°†÷.JZ޾ÐÏ_6;C*!õVþ,SxQ²:YëÇEDzšŸàƒ¶½zŽ_Põâ´e?nt£zx^˜­qêcÌ[;å1Ýqr§ôÆ•-&pp²ÌÆŒ.ÿˆ†#çÿ.P?n_¾’›7¨`Ùf. /­›ƒ`uWŸU\‡)´—x!ìu ð\ö°Òçoƒ?0*™ föëçc ÷C•#P)ªLë·žŸÒÿßËžôõÌì9Ä®²g˜ytbÔÿ=}\ÕéìÒ?0ûö™ò‡W¼ÒxðZ:ÂÅ)Ž/Á¾-ól·Hodw6Ç•Šº&ñ?0}Ÿ1??Û¢òKôIN›ª^ä<Ñ,m‚g°7:|V V¥…æ±@ÈÆ@ÊíÜ'V+?r}¾X%¹ù]lج·Æj[V…_ã£ËI3iI÷Q±(Dzë½a¶Ál1Ñ$j!€ØE„ÀqQT2 –ê€ÞÁ±µÌv¾DInZ†ÖÛP ‹œ¿X÷ò|¼Jc%ë1éLÛ–kÝ·È`µœ‘ÙòÝX¦#—Ö¼½>…R›Ûº]†Ÿ˜8Ûó{…¦s~àÙÏ:):›ü"ÚüP¥ÍéD®…OŒýµæÞ„Ûaù ^Î~b½¾|y:F4,žZu ü¨®Ö·Q¾¬àŸï©ÆƒÛGËj½Óh+ßd½éîŒ5žC‹-ï[0ß…Áò·¢€»²*´({©úÂ{÷¤üB–{+(U„8ÚõûÅ÷î†>xû÷ÿ7QµÁ – Æ-~öÎV]I²á¬„m«áÆM_ÜÛ|«…9µ‰A·Çgo ÍzÆ]óªÆ1D3K·jð)é&ì‚Ë|ÖHæÄD8?r"#S÷)l‚LÚ®^¾ÅfÄZ¡¼ö¥†~IU[0TÔr†ü3àˆ¹sc-N×yÊ2ãÝi4)ª?n®Ž-k§pK47A±v;uUEIFÕ命HÉ÷??ûª|98}SÔÉAF±?0㈘þ!ü°õX°K¬5,|´}«oSã×LŽ ð^›oª§¢ObmM˜¦‘‡kÔ=֡댆¦ˆPgÓZ†1¹ŒA.9²­Æü1ûE¶®ÝŒ7YÑéF·U¡³öº¯kWêW½uGX–±ÕL­þå§lI:6™4®^–uѾ½NB©H½Û[„uɈeŒ1²­ÏSÓ.«äþDMYˆ’_)ÙŠ»1Â;0Cb‚3CjDwÇQæèA'%åÅPäÞ\…0\òs~©ôH#›‹±SXRy?rÑb©GѯõTT}¼'ZÚʸÜßßMbsøÝî ;(Ž ç’ïw*È=ÕîŠÔ a>FJE‡’%.åpM*b*Sb[KOÍ µ±òËÊ•2šý@{ý€ëö 'Ä7þ}³ROŠˆ?0?nwÓ(!·­¡£ð“½Gç‡_ÿíë8;@÷úOåEs Wñä%b¤Ç˜hNâ3J*2U6Uf 6ScS­uŽ©&|]^}œ”Îg~Ï”à+tî4ÿ;o¯¢Ë¶7ëÆØHoœpÆV¡ˆÑ­må|MÄ_¦J?n‘ÞËÏé°\+¢l¸9¤pe[å·èô©gZÎTm2ø8¨Ïö˜ëdµŠÝéF1Hø•«”|EÎmD-ºñWÎjQ'?n0ibÐéQ¶êÚ¦k5:lÉ1NöîHWËzEßÒ%¡H(ç*ÛýŸ]…σÛ_,(|k9»›ÂÊé‚Ëkξë³ð‡ºDw)ä£emG]C²;j=·~?0I¤l¶ݼsV]Þ+UÅói²fû3Q,Ý‚ZûÛ¡)Í0M9Ñ!Ž,V<Ê?nŠDõ¨]ngíDsñAÕ]ѳ¡·7 hrÆW¬Ä,‹?0 Í$t=!'ÇÖ`â¨#Ï0ÅÏ ªb Ô;Q\[+¯ú,ØOì‘$mÍ7?n¶[=¼ÂB[–?n?nZaE^??¼C2ÆKÑ«|Ç×Z:ð‘ÝB`?n•èÈd™ôå©Ú/ZÔìÖø¨ ` õ¯#­NuENËØc»ú´c€D†øSyÆÝÝ00o`þÊšáYOÙšÅuôow÷=_å˜Ì¤ƒVÿƃWÅ, ‰N*üº-Fƒñî÷³üçßEÈã.nHnþµÍ|†í./W$ðAŸV êâ½ÆVmD»1Êû aS)ÅÈfeŸLfÑ\}D±Ù3ÉIßmÄÌûš9»\Lû7ò1ÔÁQ`ÓɵçgÔ4¢ë¢'CÒãEßycT~é‡]¨fÿÂÈ’5ÖT)q—ƒg¾˜ •×xçèšpŒ zï¾L«x ÿÝÖ´µféuM7áŽ`;—¨;ãQaw¹òË=ððKÖéÐu>?rÙià&hPÃÛt{j?nbAÂÇž3@çi ÙY·Ç<Ü?r§¾ K5µ—èš°/á:<Üߚè°a—ŽÄªaÇðâ$ÓÜ €ìjÁ?rD?0b—° Ò9”cÄ„}ÿ“µ…0R~??•ï9'×áð4aj}‹.ò?nžÁØ•öÑs†Ó)²XÕm²4XeQG3ƃÂx;¸u< §éÛ²¬ÜãæxMœ+à•'}^7+¶œ í@ý‡é˜G(š&±—lXtŒ¤Y°Îçoè§T * Ú\?0ƒm U¢ç¶„wüMÊ,»õÚª€‘ÅvXh‚Û‡ép³SÉ-#èdÏÖi¾6u[”@½)òedÞÝ ~š€CWszøƒ*Óßç% 6ÿ~ÃÓc§”/ÉÚHú$ó}‘†aþ¨2]HôBg¸Y´—p9žœÕfQˆc‡¡ëju?n®4|±[A@*,õ¬ÇxŹØPÐËR3¢íòy•‹²Z(ÒMó*hoeñ=QcøWÅÐ>ôòþ´÷ÔøFQ|ÑãßE²ñ\Ãùz=”í°ð6" Û©‹¾¦ûuv¹H›vYùø;Ár´OªÞüêô‡vy•ý ÁÔ9's±9ü|~ßz¼u zLQ+eèþ+Ò^­‡°‰‘6™ÒëzS‡ͧÊoQ°°èêB;%3NˆdÖß–böï¥s*SeåŸ?rñ”FÓ¿„‚£Ç÷-¿’¶ÉèüÌ»š›~®ºð†ê?nÉ)R|ƒØž°w烹߉]œ X‘¸˜Â$xÑa  é …®JºOdzR“E˜·sæ)¸ma‰±šÔ+’œ:æÒ°ö—‡™ /ˆ‚**_Ø¿šÑGÖi$Í`i.`ï÷õ€ÒÑÑrƒªÛ’í$°vúËí‹|?r³¯\‰qÕç?r¶??wÓÆ÷ÊkÑb…¤CÅS>Nº¿ÆbÛ#¡??'êÜßÁ†ƒ9æ…r¨ \€¾%×S–]7B’”KÎ*,àº~άÉj©ý:Î?0¯¹b­ÜDO*þüS?rœ‹!‰ç›bÊ~ÿ:¿œK„_Ð<Êl™ÃÍbðôNÇ•õS¬ií4n8Õ]z²Ö?0Á‹²¯!©»œ¬ $é»/®žn'ŒÅV +#œ#éâÇÁ+ˆ iñIãÞMGæa@=E¼q>¥Þ×Ö/ª<ÒšG—”GåRÐøruåAQý’¦‡øèf-_Ø’ð¯ðLÛO×8ÒI¦\þÃPÞ:rAªš·£Q*ó[­¶ržJn ÞÝÑOdR±ÒW¦ßDà(m=žÊs›ÍE®E”&À\iJµE’œå€‘½RÁâ—}Žt)ªÒ×}ßHeGOѰÛͬ7æçŒÝBh=ašë[°Ø#*ÒÝyøzdý<…>3ØZ"ô._åGQ7üŸäáŒ2êª*5¬Ï¥€¥’HZß2qNÖ”W.7œ,òêRÛÅâJìñl¡í”^Án1Wýê”n}‰È¬pÑy¹·Ÿ@‡&WÄyÇ`Î`\?0ź:À›ù•ã?ni—¢\”??–ÅnÈžè™—ÙFô_ÌãÑ<¹’–ƒ'\aÙ Ã^Ìöø^ãn‰QB‹õÎÉíÊ/;3i{<á|w^Ä‹<„¦æ?0å2ˆ²–ÁåP©[ID"Qneš4z÷J×/§Y‘³©÷x<óœ€ð‹2Ò##ç??Z_ÈúɘÉÉñã¥/'?nÔüÿŠÎо›žèx½3³ñooú¨?n%Y쪔ÜçýÞ Fý°¥;·xŒ7OÐU¨)½U.ƒ4J]‹™´D²%‚nàÓ;OoØÎh íž(áRCÏßß@wÒy ›??Œ« ö=ÁO¡i½ÆB;)¶l:86h JA?r 2WÕ4ÔIŒ²zxë!\*ô&eM—ë€H–?0 jHnT¾–UÅ.Y÷ë¯ÓÁi»|_6ø78Š„lÓ$ÄL@]‹4rÉ3¡X÷˜'Z3 Y¢`!Á?nÆ¡–}Ž6bûdX³åMO#Þ莒L e̺ŒÔ•!¬W±×$øÀoqݯC?rŠiIWÏVRÕ¸7ÔR›Ijš­g¯TB7~·ˆ÷Œÿ yu¼#vØk¦•-:zšöbC?07Bࣜ0g\1žµMüê/³ìà£s>òn0Ýwz Øf§çG(eÌÊÛ‹(oìT‰²¤h©öN”XǙݽSfM«b—*ÐV•› Åöïm@¦Ù¥°Px·ZE°¥Ë$*f÷Â?n>­ò«ÚŽfäCñ¸d-Î…õ\ŒË÷|G„晦ÙÖCqÇIÉ£%Úöò).ɘޖ°?03äbAjŽç½M€"î·?0öÇ¿¤Ýïë°Õ5eåQ/<÷áòä¡Ì8AŽtqb˜‰§6ík%{sÿj4Vþ¯ˆÉt*±Ï³I-½Î¸âÇj¨|é–Þ¤ó×ùR@(£@Òë¥<Ü%ì;øW›Wƒ<¸P"74q3¨½q‡)½û‰Q|ÁÀRã†GæJÅF˜ŽÂYŽõ?rAG¥MÇkÜCî??Ÿ·ÑÌ¡¯Åp¥B·LÏB‹oÍåv©ß¼õp8FR§i!ïÊ.JG@«´Ã¤&:T’hÒ¶xùÆ’·ªEüU\Õ„#å ú„õw¡Œï7À†7ø‡ KÆÆ??¢Þ}´]á¶„ð\Òä5VZ7":žE¤4üäò«ÄITBæëô.B[œé°@‰Ååk»?r•Ź©¿xS]Ú\c¥¹iVÒp`ÚúX$õ[tõHöHA‰B©à™ˆÐFp_q£S]¬"§e–ĉV{]IÿZÚMª‹ž´]”pñˆîÚˆ¾Q5â1”¾'¯1??½’Ãt½îú5ªá>ì§d4l‰´puCŸWu˜c—l¸ûbž,;ƒTZbú!gÜÖd|ó—8¶fχÀá×tæa‘±R\„­ ÛòSTà¨3üΟm§£0_^ä[ß|¢=ö™Èß.>ï|ŸÑöu\eÁ ÇHP¸”šO;7¼¼’‡À?rºÃfj9˜¦‘ÿ½13p@Xl¢ðp.²ª(„‹áŒ^aôàÙ®"9Qob¶°EÞ:;nL²QÉ[¯§4£û*ýH„!ÛjSD316UŠhÄ—£óè‘ŵæbñy??>_G°ÿŠÞÕ¹…ãä™.¸>‘Íéq‹pÖÀŽÆûþœÏL=.¥ ’Fx?rSorü£ÈªT·¢V×3]ª ï! ¡Ikc"à)m4 ¾ÌÐ.6š:m˜jÝ Ö\!ó'´ƒ•ÀM@‰¨FWÐ02pŸVį‰ìÂGýØÉ¸âÅ‚þçï0 ¾9î??åi `ëÙ¹±¤½ÉúfP "Òj‰ð`r!ÏwuŸ~8-Üð|þ؆ÊGÿTžŠÑµŒWkšé“‘wWEˆ3›At€åùùžX¶os¸“Éôa)0jÃÑÊ7æ±T-È4 ”Ìl|kÛ÷50ॖÁŽ¥¾§ý)c>ÑrRÚÅÁ·îçgÃYÖG±Äánð¶ÕgýþZ#kã`E6ñUxQgTy8¾i´×7`Í–ÐÖ¶ý:èÔÒ¨5?02q|·çQ?02iTzËá¾X¼5€.õ6DPšW@Ÿ§LrÇàw88é×ñL@3èuÄS+³Dª¾ˆPšÞ(¬Îãâ+/Þì¡ÂJtð!Ú`ÁjÔ!XR¶RÉ“+Ÿ¾ÊheŒ}ƒ6eL¨´ìí•@ì&wëçûxý46öëîìÍöQ®QZr{9cwáÔõ  Ôõ§*Åäþ"D .^D¼ª/kRäË(?r£-Ì‹ÕäZnoºyº¶ùÉ"FègduÄVê?0ë¡õq5ãŒÓ¶nœHÄê-Œ³#H§j™ 2¦•µÒQnøoBgEÚlU:Ü%ÑÉÉt.–¿Çorꤲdr??JkâñHën"{oÕ,ñks vw+Î??oSûÛpxDJVMìÚjc§±éãeO¬ç??½ å/憛եO˜6¬ÑSÕ ~÷ÚÊu8¶«G`Pg%´wNgŽ-Ÿû…"ðÈÂþËe›e{ K?ròÊ×qï°ÇEƒÈeƒîtURF\}¥·p’Ê8lšJë•|Å%&xÕËÏWj1¿bëMæã‰Ú~¼`Í£¡9oÊgy¼Þßß/{ÞÆ[àgAUDëG¥•WSµ%6›:nB,ÌI¡BX®í(×v9Æ)ÜöÝùiç-#n#áðþt{ɘãðE·’®¹š«ŽóJ?nñ®œ•ª>Z$s#„ù*ž4J’êjúöóá…ÿz’ú8ÕUz™û°pðhIÜì+†rPFøoaGœRO»*ÍwÕGL•¯D$”ñAW~úŽ–‰ÂðbÓ9Ñ?0Æà° a¶cêyê¬|6HؽÞ7àñ¼GI¸ÏGD»WÍ“eDŒuí4oî ÕÏgnÏYÔÎ/NŒÄ%_??`qEº;>%óÐUÂ̬s­0„Á|x??þj&0“_ù>þ4ºàjYM~ÔNíŸzZv³‚æ6²|1¡½ï5Ê|Ö¯vÐC$Äóˆ·þ !¡Ìs|)u_f,H|ù~Yö*̘#°õí£î¬üç•ÒiªÇ]‹²j_â0ãedz¶žqã¦ôê<›ëwŽÂ¸FÁ¶"êÅkgÊšã803¾ÜýQc»•ô£­{=º"k˜ÂW¼†'¥&ùäJ©;$ºyRT¹˜téµõºìëb9w÷¡½¹Ó3céVÁÇÅvK—]àÛ÷¦oàâÐÖÉxˆB–SBÛwDT7´såÚ=Œ½.%¢„Mê ºÙÉ4»‹Ô:×»v,˜±óP¾±Á$Ö%Yþêèrº«cfK·‰m”ø<ùçóÁƒ«ëƒ}†êÎüËÚpÄ„ùDò—²™"‹vWP¹ cF¯wgëb¹óÆŽmqv<±m÷i†IwOxg–Pš–.3Ös#ù¼uš”î˜ÆÖ YüC¨`z«Šü°HÁkOº:­ÿmE÷®/÷ÅVm–¥‡ ÊT…mJ†Üï.ÇÙ-ë-µ’±bÉ?rÉPG‹/ÂÏûÀŸÏpeñOûÅr“øuèŸãö >¾Ç3w†ë[=&Wò¿&,Á×?n?0e8ðÅŠe:~¶VQISöÄSID ¨ªIwí†W»lô¢›p‚Ø—æ"ù"û"}0VH}Ï$‚QGÜUHuÁƒ}á6è/„PÒÙÉþz]ïõè’Gt‹Hû—?rýðœè­' Aƒ.¡ê$À}æ…ɯ‡¹ˆ:ç—9Pª:Úêtå:nCG¶„ðw`F(hS¿– Ãi¶B¦F3;Okñk«5·‘àœôfðRÓ‰äQ’å +#0_4R`/(–j¡vM/_ªIB½î!!ŠÄÜ{'³,Í“I«»qs[ŸD©d½W×É\/¤é'ƒ§-  ¼ëΓß'šÙÑrL¼Â̾!!@ý©§¾?nÔÖû4|üRSð@ánó°:,6qøl[Mn2kÜé*šÌ8ÕI˜³©—îϾ⠭wY2p`AÉÒxçnɨjµ7ûGê7n—`‚œ0¸°î†‘RÇßÒ\óî ¸Ú™­Z}-'A[OôÅáiåèl3Gñ`žsZ;Žº×G惹@Z/fa½Iˆ·ö½J·ä\Ç Œ)Ñ}î}{¾=>•©ã¥`ô¹†ÎPØö@©›uEgûïLÉQÙ•—ƒ0ÕJóïq‡™Z× i<®’±ŒÜÛQç‡ÇV^óa;~·±óüZ«ï½â_ŒxqÙ áÖ]ÿngßMIø„½«'qç@Çù–ÏdÖ¾Ëø²Oª'f'Šcöè3$¼ø„ù¹—€û3‚ýóòmvt­>\NµŒ#Î\R9ܳBš <õS®CóÎøÎƒÎø-:l |X¹£òO›K†‚@ÀiÐ il†›>ÿ™ÿG݃ÞÙÀ´z1‹.^il5Ðfγ8ôQû„!!Ý!ï³Øµá…‹GßfÅ{ËéÕ¢´Á½¨øéBÁ™Ìí4Š´_nRó¨a{Ê}æŽugŠÉ¬O½OÁ“Ý"2ÔüÆî¥žõ€kÜÝÆCŒÝ‘έÀ,γNv î7ìAìP#çÖ=…¼´­ÇØóËàHX5?n”«Ü(ÅsúA0÷ÝõGj¼9]Ý Vk±;áç°Òm?r-ú0ß 4À‹Œ€{Ôžÿo©ý)qjì¼0Bûƒò ý̨ýÏ”ÚýoµÓãâóXÃ?0”þ²Zy;`í¤Á.Ê‘© |”H ??ºF\b³Ð#Žà6×ù-Ã??+™‚>?nˆ1Œ"úÈþár©n‡×µ>k??Ñl§&_¶ÇPFe…¿Sj^þÏéO‹xÃjð®yÇÕ fZî§-3Mß­EEð: Ç¯}ã¬épŽëßZv©=”Òš¸pJÚ?0ì䀻H?n9ÊÖ›TaÒ(¾°5…¢P•ûC¾@[†;ê±$yö£&Û¿Zž\¡jÓ‚ô/ÚÞÅÍBˆZU* |<§Òø62·ìŽj?noÍ£Ù?r+ú+îΠ3ÅZ¯ÚéÇ}þ²²é{|ÿ¡Þõ' ¦z˜Îë?n¥\ äÊZû¡ÛIP£î•ô°¢”E ¨ý#SÔЋ…RK;,QÂE·¯@µqR÷ôälù“è˜Ö»5I©{V™½è &ôÎ*¦!ù¹cWñ¹íSÆcPE+ºZާ á üM˜º¬ &ºJ]ôŽÙðrÙ€ÉãëZÔs}k¬ù¬?r2=WVð³’î}éi„ºœ ºbnBŽäw3Ó±ÇóQROÈÖþƳW ò‡­U¾{½ÀÁQ#Áñxj;¯eЬ„ŒPúŽŽ$½6¥ŸYË]¡Í—"ÿ7Akæ°«} t!EüÔ'ˆæb=³íø´þlz„Av§0Sö…|=uT‰`ÌSw¨j²<,¦QËñ É?n˜Ùx¿D*[°Þ¼9 IÏfÛ[ÿ§÷?rÕ¦Ý̃—9r¶ˆ¹Hm(8 ¹è\Ê›D£Iðž8ä÷ôÁ00G,E™ßb½._^©X?0!‡ ôÛ`Äc!´"îfÊb‡Ûí4ÃF“Õt'“#t#›Óµ§zª¼‡,ð€QUü-sðáJ(¢î75-‹{ß»èÕ{A…œîrªÈ‡¸Á?nm>ÁvoQÍUÍŒ)qNVÆñ¨ åø´#ºTŸÑ–ÎìÆá¡j¥»_=tº|Ÿ½ ´¬^,L…jïu©ª]ãú¶öÃ)Éà?n^x!”ÄŠÕöáËîgïp›‘ÒÒ×jB;Ìš':š©‚H)JÒ?rb–\AT:úwU±ûÚ9®ë†BX€)5/<‡ÂÝüÆ0LË`æ|ßæGXªªŠà™éæ.$Tc0«!˜CZ!´L!Qmº”mBc×ñ´”¡©UOGb|8#{K§r0NçÎñÞï(k’ÖWÿW,[šûvÁÖÖ”?0ÕP­‹òwçWÁ W¬?0. U('Æ(èÄÝË?0Ö×Û´ýÓ|óVG ´¿ú5éJX'†nÐüª¹eQ0©G-ç‚„”ïV'z;n¨?nºËYýj?0Y;•Ùù¦Þ‰PjBؔ̕¾ääêþL ÀróbyUµ ??à/cxTu¤Yè±Çmê݆Õ){?0 6„ âÕ!o¸F™¦)^FY»†vòSD0c£Üº…»Òý®ñå&Ý.úèOÑ (óçõ€„ÜÐu¿TåkYúgñj#2‘vÉaûM4ôD›×Âo3h'ûaK&TÎà”2›XÖÒ))˜Àf4øòV†¯{#õ7>V,]%ÿ¿@0lg"ù‡L‡¤Æy;P:ŽGè|!:ÚFr)ˆ4ª\¶®i‹?nŽÁw[¶á±??(ç6¬Â!Ä¿&‡„®-_KHS}…ŒÑ^ÍH‹—†®¶UkøXÎStWEWÍä3á@H kEBÿîcr† ©ŸàôxhÀx7†Sn~Ú»ˆó¨#ݦÍ#)¼˜öJ;µHK'狦—fí8†Œ?0Îc@]ÿÜ#PÜÞ¼œ?0Ÿ,¤Q×åUOï‚»8WݪÇ~~hލ­¾šÏóG|Ü»´Pµ1³mmZsÑÒp\»>ü›Nï£×ÍRZ}Þ¯fÅ]~®:0\½)Ý/rn›¸\³4^‹K3@=†ŒNRvýrn"¢¹9‚~Æ —L’˜£ôÜ@£è§Ú³ò©d³a,zNé u¦6ðù?rÙÉQÓ®¨ bd#·yÁ9…i Ã±ZcAtMg/n›úóܶrÅÕ•Çèµ W^.lEKDU!˜‚/Bw|r$ñhá Âr”Ô"ë÷ :•e à±ùüùX=pÉú{$FÕ×}4Çpƒ+-ñËßa).Žë¸¾&&F«ó•ñs™NÉm`™²ú'U1.˦‰AM®bÓ€#Û­h+®nOŒgáA@Ð,…ã±ÃæML5o¹Aƒ©l1¦èél9Ðæv—*l˜Ò›Üóòž®¯]xNÞëÏêŸ*,›{vª’æ¦[Fðy*òæð…×2FvœƒÙWù´Ö''œèD‚EÀon7Ú›æ4þÿ {ì|Ò’©w7®Àà8Ò˃'®“äà;`ÃË•??rï¨X\²\¯î?rй²Ñš2¥)´ß(µ³ ‹6u¦pû¢øMQçíÞÓLrè"ãÈà`6  Yì7óãû?0v‡$‘ A@??˜-£çé÷ºûÆ¥»¾ÇK)|õùûï+ÛF:¨pÂîÅl€áºþ IHÒó|õôÐÇÂõdt`$,Ñ_2èuâ[¶õþ+¹¼`Nã_â—ò/i7ŽMa‡€:0À”µs[톧üë…Ò§ÛÆœ½H 64l: ø€ïD“”ˆü“sCÇ 8sÍ?0Ù8Isç†I´Q5œ=É—AÉ ¨^ìä¦^’K·%=ãx@«ˆŽ_ªÒ C%yuð|¿Øýn ÉI²âL ¹Û-»^è»Þ8Ž“þQE°¶_DëÐWP•€}ÜkW›* Šo7Ž vK"Ïû²ÛRÁ®‰[Ötþ rèà–‘rÂæâÛZ,qeïëH6€Oƒ%`)㜞sÞ'ñ?r$ý­ÄdÂÇD¼âÈÀ$¥ù¯ÉH3"Dûßo®¡Ù,šþÔþŸ¯ØvZÔ£…†ç!?0c·»' N™áéW…{³o6ª_VHÎÁ0·íL¿ê©]ìÚs"NHãÏ4t·=éAûéâå{¶˜œ–Ö"þ??¥¸ˆ@ÁXl$ÉRráGÿæÆÑÈø’ÈÑ8éÉV1XÓウр?0ò«&Øg‹beÇ-èù÷3ô‘È G© :Þ£t¡YW̬}·gj?nÎ#»#Ò??ˆÜÄÑïÙ$VWijÓ}ô©\û=’(ðÈ+PZëž±iîÐ#]ÜS…Ë ßÚËöF•@~XsWwÀýâ‘ïƒò"@Ri¢O¼²Y~0›˜ =FÙ½Ös9«–T‚'ÜRôÎÉÛüäî'kŒÜš<¢DãEø¸x Ê’ä[Ö¹ÜùçI=MV¬ËÐgE‡ã‚R­Á©?n6ûå‰á©Júåa¿Ëø ×?n´“¼åÈ‚âµÁ?nç"¦QDÓíOëáwµ3‚_Þl}Nã5ÃF£nH}»¬L¼ì ¶Q˰jں桉ÂÂþáÕv" Ÿ§/]S·w@þÆ(”ÇqøëÄg?0eá³è\qn®šÒ3ŠÍÔŸœ²I[ÐM^º·L3,p”Ø—,XKÆËbHp˘Äí+W€!ÈÔœhRVTö£ÑD5ÿûäjB©ó&@šZ\W¡ÍÚ?r1æV"ÒÇ”Íåá;ÂNåœÄ¨ÝL $…ÈE¼s'½–áüX1•±ˆ'<–&XÒ7˜W*0Ñ&3ƒ,dXžT9}‰÷w@K@’@ %%Ö¥v©ÛÇs÷°Ÿ]ðø˜uJÿâjwk[» èÃé¢?ràà°”ëA=bø×Uqmù™¬»4v“Vg7N•BCiji³ë-ð2Q£¡A_•´Îɬé>Ž$—??ȃoØœ??÷?0¶€¯Otlw'×8ÎøÀ,‹'sÞ³´éA#ü`ˆ÷:ôù¾ñ¶×Ü´yœ”nÞBð +#Æ·1 ?r€Å??œÓ^V¦SïØßÜc€4×íg5,ÀýO”lyç<À¦É§¶(ˆd´¦ªñ="µ¿ðMîTM‚È~v8a³@Ò„á ÆÔ]vÒf¦ø´öȎΚøO=y"ø:??ˆ6F9–mûˆÈ‹"(§?n:V¾XRyüj¹(r¹:—?n^‚ñöT¿#Ñž¢BP¯Vf _ß x»Ù¶3e`¦F§‹ºx[\ahÛ‘j}Ìjx‰ñ÷%º a¬šñó!˜b0ÁâÒA¶?nù«Ý—% çš×}>>º¯4Šþ{CŽ^À˾\î*Ëúú!R,jTŠH-ܪ‹Pð¾½xúă¹Ô×±k~d|x¾ÿ6´Ýë7Q“ Ê?0ø[Ð6ëí(UˆŠM/êâ~gòCð&Ý;®:éq’¡êUßa?rÜš[8‡øksX.7¿íºUeM÷*ÊnÁ(õŠ=mb0òxP cû@0†¥‡ÖØ£IMëËmÇhW29¦ê¥)žc8âNÒϺî@ùSøgžë^öú†ÃWãœÈ§±Ëp—!Ì1œÝI!?rëQ† Z"pµšÞ½"2?nö!§ÐH…,â2`Äò¢áê¶fù?0éH¨³ŠƒË'¤Øšò$Tâ~=ë.„j)€Z:5ñ÷ÙÒ„r­²•^ è챈¼Ñë áü½mô$‹vŒ¢}I[”’”¬ƒ@Yeí3/7¤žÝFÑ*½å)bóò…ñ ×3wÓn "½8,¾ºs`ú.3&¶s%ã4’á96UÏë°®ƒSûLö] M÷¸®Õ݌̥楗©(oìp4=ø(9Æþ±°)$ùaNL´4œ ƒhÌ·Øs d=W­0p"?0!x¾—8ìȧgoù®Hy¿ÿïÅ3ˆlÄ¢¸žâÁÈúm$EÍý" ù™ù_7þK!8аƒvŸ†—ø={˜*ˆf3 ¼†f èȉ?? ¿ˆþ†yÇSô3"L¨Ûa+ª¯ŽZÀ¦>”‡–nùaƒ–®dÓ,~ÞU<ìíeA`„ç×ú8ó·<”C¢lÑqÉ®{ž'‘¨*³ÇÀÄ-é¤ù€ª52N7‡©ä\ç£,¸2ŽiÑ??wZ†Õérfj„I^"aåkh½Ü`K)X£{“#Ó2èKРw¢xÁîù<nó„0|uµÂRݤ’ïW)Ù…a[m¡?0^Ü©Ø%ºÐÔ‘ÅS:8Š÷›«†Uuðœ¸!]bôìCŒnµÔÅ…ðPÛÜ—ud(¬–`ѧ^ ©Ó}Ùþ—#Â5?0YB±q"x“iÃÔÕpù(ŒÒcŒeýõš‡šÄ‘_}íštáሂJö±ÔL')ËtÝÏ@KX°O£ÚŸî—c çWÔ/œAú ?0k"‡«Í³þdvãôÃö1Ïd·’ŽPHçÑ~7¯Ñù¯Û‚ÂÍA9FQÇÿšÈ)pôýå)n??hËA13?nÞïš¡GÚIÀZ ‘óXù•³åe+Tò¾°K¾ÍlTg:a3¸“?r§JÛ(xÖh5ß凷«öÈL8IÍn§Eå' DƒŒœ.󥾯š t-a_™’à$mËæÙ[|Ë?nº¸\ܬª‚¾žÞŸ†•˜ñ½?0“¼¼š|Ð*œÿ7¿gf‹??^ˆ@©ZêðrhÆ«mѵ¢vÜ-²Ž‘ödŽ0?nÅ‘G_(j®9ûCy€ž$1q­úXEšÄœáì³`OÁüÏ.¢âØîè%φãí꓀Ϻ'”ò,Š{ R’~Ù}Ùuà3b??ÎH‘Ò™×g)U¤dTëß¿y¥{´e_ý;‚Á]¢×R…Õ?n2±•Pb?nb`ÉjN²5ët[òF’qØrNüDXXgqµ"{pˆöòÂh"ó€Î‘œHv…s“Þ6î˜~ºFöùX‚w™2èåZgÄÓp¤7–«Õ²!’ŸŒŸÌf->¾ÆiKy,ÑpyCAvíœ?0œF^÷“ô=¿“6ÝúÍÜ&~È$­â`>î!œ“Û·Ããµ8€ð—Ý&üîÏÖÊ«îfE“óYyÕë??²[îö£öî_•O÷€Åš‹ƒøÙ›YŽ»Å2¶ìñ'i¸Rñf:ŠlGÇm~‡œ!Ó3áÚ“»C2Éå¨7ÄWøoKfv˜¬¤™ Z”yÐ|yÌ‘þ’td¥6??«9“¥NËq¤DV'[!Èý·¹Ï¡=Èdl²Üž½ÖšjJÑ–èBW ?0&Éèì(2Ò Tq(S©qöÐ^Úæ¬z*m:•ýsu»î+3u™œ·—QlryI€(Ð~†é3„ZC”Ú÷V+jí[étÐ??ÌîãºÄˆÄ,¿†úâ<€‘å XºB¤†ƒ–.Vð¾;kåÎkËdÜqp`³\eðrPÓ_ 3¢6(ÈPÀb?0ò1•C$üvåû Yü¬#ßÅ6Û²«ü¤¢Ž«÷ÿP ]MJ©^T{ê™r„7F듦7©,䤱ü§zAø!d„Èœ\P§tµ¼ ŸÖ•݈[FÙk¥î¾´™Ì—y D‰W‹Ëžj?rU¥¤ˆÎõÏ<;­Ù%¦Z‡l!Ò>[¼Ñ²O¦|R‘P¨ ëCø…‘C-?n|8Ë•ˆ—ºˆ2ÐñOhDúёԘÀ-;:-]ÙeÄRuT 2yµŠ¸,ôÙ®ç›$‡nV›ø—™d{©‡HRXùÛº»i8­²ê»ÖGû(÷vŽcO…Ÿòsn¬ôUQ·¨ëjªr*PÄ¥/xØ n”Tuj"dCsI2 ²-ÞqpÏ5ƒa'lÆÕ&?rA ë(1•Óâ¸~ w:â¸BúÑorP&7Ä?0ZU6å´‰¹{¬;Qñ¢´?0,:çÉÁ?r”nE¯%üˆ^¥×Kf"ѬÐóЧN¡ç3VèóŸGòÒÀ??B‡R GÍ:¸íª;ÃõÆêiŒ"]‘ðÄZçµô cÄÍ<2É@’g*´:f°ã~”eVšTÏáÄf2>w½\•. Ì\nþÒrOPñ êºÙ1¦SD¶rmp÷c½C^Z8Ã|ªs)‰y??Ç×÷!Äüy}‰?0U'«Ø?04é=@ôÏ'î??³0±àß—ôAªÃËc]ÅPW™GÝØùýòîx±N¢é§Ê†A–YÉü+ÜÂR˜iÉê²½‹äÞ‹DÅHúáKÑ€c™9Z˜ÎÔÒ9;­g¾Æ «Å4W€µZ^b,zå:vÑNbÀà)MÀ`8aí0–âãË?0‚Çö­X»aàåÐðì¼µö>Éú A¬ÿ®ë޾ô P`óùÜk[È™·qÂm)¼S‹÷¯ÌôV4Y¼döŸ—[’8kz¶Íùö³³w±ãò˜„ æÀM–¾[_Å??H›r|g/ Æô’p”=5PÙÞpÊ}×ø©6ZtHªEfåMÝ·œÛ]ïü#E^½ÍéÌ„£?r@[›/¹A/pÀ}½SÙðñD”UÕjp(´¼ ŽÑ¢gpˆZ7úô£¥8DÉ[Ñ£AŠU%1T¥©“™ëÖ al­™ôŠêtJ;ÒTãI˜-n+ Q"eR]-‹|ZXâ–¹—bvÖ"p%µ´µ¾b΋‡ÔŠ'q«ù_¥Æ§á±?rÚß?nQùsš¡[@1ˆC±*,¨ÒÅM&9Ã?rioŒµt5|³£GìÐÞ7É9?rÀÑ}”:tÆ%Nµ­¦†Ç5_¼Ú!VñšÄ¤ÄîÍz\¡ÔÈäTÓš´Îöš^ó7‹ì_¥w¿}i‚ÍjF‰~й!„2£b÷×öú^&ªéÕD©„ž‘›ÄüÄ´ê4“qCà êÚ8P¤¶9ϤRèJ€½&L¶ìm  €ˆÐ€"jÌLì¯"w4ˆ'saHÎ)ÏÉ äQ?rK÷«,öëšÆ£,*Cä!Eú÷'ÿŒüI{UH›Q n «EN¥|¼-kö[ šJ$i sÐÑ”$>Ø×¨õFz4üáx÷[ʈýK¾Ak‹¹ÐêùògwG'³ñ??³a›?rŒ©ÆoÍc²bä‡Nµ~ÂSý>¥gŠV#¥â{êÀÿr(ž?0«Ë-…yÖ½_ 7“ãóo—ƒ^Vóâ"CÛ[A£0Ârò‰“=5¢>‰…nþAâUi ÓÏ]W-aÌë½Xwö àí㹄2FH)õ>„•­hf¥í*bÜ®.ctÔõöÆ ÔUuçVö¦ü ±ï2ŒLñ¹„©T*|·õ/EñÖQ¤Íw©P‰ƒã¢þÖÒWZ3dųQÙ=ùÈZ^Á¿¢#5 üÎ4Y­©‘ìwSu…d#8³*ÏWì¯Øº—Bèo~õMÓ±h~½Âv~Ùetš0^©SÌP‹HZm¸ÿ²a“ÉU®VÊ7SþŠ6•H&„>6LvñK©ÇÜÄ0hòDk,ÕM±ÐæÈŠvY©ÕÙ?nv:FÍRN]* ?0\Ÿx·*àeVoxc„vŽïÂ`ã:Ü6¢Ðt®|úÕTkÛísUØhóöz<·}×bÀÕv…Ãl¹ 'S Í*bJ*4âÒŠ³è3qÏ${µlqçp?06 ëÎݺdqÝêÑ|+9¾6%pED/.E^·EåŽûtîxøAŸhŽã)JÎ?0gO¢l FI 1Š'Mîr^Á–Ú;Úî ‰BÝ%¦NuÎ4‘Oü‰.ÁŒwø×U˜Ì?r»©8îåNͶš»à!ŒõO(˜Ø€1V¿*ß_X*Ëx»«ùÓË[‚Ð’.…ŒËB>âXm«ý?rãH,*¸¾ÑD‚É ¬Ùì¢8“Ûj'TUHÈ1ÏÌà_Gdwü2{VðFž+ÈΊ(,•㿲°Û§Ôã ??R+É©ü`ØÂ ‹?nè.…nk?0 ÷+‡J³·Z{F,BO‰7Ñ×Þ]HŠói6žð—<ÂúqÔ’£”قê’Ýîû ƒ6Öãœu¶gÕbz @ Õw5æÞCF·T–i78ÊÖc®)ü¦ÂýEÙ€â@OJ +,5…pä¨!.^¼àhn5#QÿìÍ9ƒ®G»?r¼n…#I†ßL+˜]´Gú?n-GU¥ 3°*u0pÿŽú_Ç{¨ ’4{Þ7E|‰ôdh^=øÄÙ&/®–×–“Nëa†Þ•§³ìk9à”jfsÔhfßêzzÔ|¬Ñ0wm1LÏ¢8é_/¼gÂnpÛr  á¥6Ã¥Æ Û„ê??ŸÊˆ9ƒ ƒp¸9¨›LæÙWÁÃà…áÉz+˜Ÿwî8ô:ù¬üh^#ªw³~ó–Üч-Úõ0Ú¥{Y'ZÕƒ°©,XP/EÌ÷3ÔfýO:”œ‡UD! âÀF??–GSÌ+m‰"9|TÄéï.AD'X„%jÌ]}-”!k0õòÎ,ãYZÖÏk®Ù\®(É9kŸör)aFãå‘)}µˆõ^Åe» ŽÅÅéê’IüI鯣ŽÕŸf•˜{i®›éf[8#4@åê"‰Î_4ØfW¦Œ¹Þ‘þ™#²Õr×~Òv­†çO'›C1â@x÷öl??CÚ…y”A­…çh¥Àvc_fÀ‘°Wɵ§ Õ??£?n0ÎxÿdŒÙVç9lTÓUS^ˆÿ¯õþˆ ¯¸ß55ÆEtúN£Ï*;8xP§);ËÔ@md†ù{fˆšô­$Ô{ܶs,Ó7¬všŸˆFU’onVn¡k™,"0[bJÝÅþÕ9“d×lM%! u¡¤à”&|ôìj+š”£vn¡wN/ãјpš´ðÜKuݹ¬øÄ†è`NT¢&Ø8AF¥«ˆ¯8ë…J>]‘íg`eZ³%°fæ.´ð;«éÍŸÌJëA.íW0¡˜o°Ç|…±Äœ"¥ñ¬óÖΡiZ!¦'v‰®Øv¶¼~­S5*² Å¢Ÿ{Yˆ˜A³P¬f?ré÷0Ü_âåWì’ ]Ý‚x????,L«ÖL¦ìâv¤˜~e??í?nÁ‚Qó#ø*îÙ64??cè˜^'¾!`Q÷k?r ðrTà@qÅlXØËЖqjÅtºõí­å;ga©DÕ_…"F#[Ñ+ÇèkÀ2¼??µQ›¡å®Ê´˜…Èï»6^ÁÏAÌœ>;iQ’âXÀ‰¦„RÄœ$lY[ôa¿å¶‚UDzª>…°t3»±å´àBŒIÆÉS€dDÓmGÁ'JJçÉ”‹W†ðô™auR¯··WÏô™–°¡=2ð×å]†Pÿ\ãA³k!YŸtD‡ÌQñ¬„ ²h„Án&n\?r·–ºÒŒLN1 ¨«¦ºá–àŒ­t²ÓþÛÑx«Èû˜V€,áÆYäCë»±*ÊñŠxü·Úìª-Ü¥_[Xè^Fí75Lr)ÆÅ¿í¨ŽªRr·&WuÛ8Ém)L$8)¾­¸ì­²æQN¿Öì¹*ŽèÍ‹8pGÊ<¸E”óˆ±"å×Óü¨KŒuý?rÙ,™äÎ…åçGpõ3§¨Ìk˜GZÞˆ´þeȸpä&AYô•?rG€ìê|?0¼HåUáaYW}.ØëäZß½àÌä„Eå›¶Ë|Q4iDj«dô1µÕ-?rª@^kåRŒ`¦z*è×8ü§üÕ£@Ê.?r*-òÈ[@)3:;3d¬°†$³ºÎæ÷÷-ý‚Ja²¹î„Ƈ†Ë«òïÅsñý‡¢ÆïKÖ™ì]GäpŒëè}+a¡›‰øæÜ˜ÀB¬,Ä×Òz§:Lu1H$e¦d«ôNš«“ž“ãlh?r$þðÎZ³ëÕkæ¢BЗ4Gj9å~Â3´úý²Œ·èþÔÎûCLï[ÔÁýrub î²êÒm¯æâsßÆÄŸœö;zíã9}2 ¥{Jž±³>ˆ¼½øÀ,5p¶ÛVˆ˜±³ØƒU‚²b`ò ®»3 íèB¥ÂÇ@.í;€^Á??Ú ÞåªÁu%8ãûä1f‡ñ\³†¯]!ä‘ýBæU½Ç³¨±š9[Y:?nlÖ3/“ßü–æ7šê¬#Hôqo©£ÐKWðtÚ2f+„Àp lðUËrSWR…Òˆ€Eÿ¢ÁŠDµ¹«xŠ9dƒG‘SÒ^’ˬñ\õËS‰QøÒ1ÞÒ)FÇP„óÒ‡#x†Ø·Å:H¥uýP¿eo=‰+ªÙÈÊÙIB £¸™¯ážRÀ*tå2¬‹úâ£ù<‘¥.Ñê8Ðw $ºî2œ$hT]ä,ÚµQƒç·óCitž®Áí ív„CZ¥Bãyù>íO%~0zVŠÎ¤óðAñf­ŸWm Ó*Yž)ì8ü”#ø ç6”|©,š%.–€Aâd?nÀÑ^=š¶1Ãתhìô„JË£Ó•x ¯î?nx™Ò'jXëAŠ—…ü#™±þ,"€zb·NnK÷~5èÈmxçnA³ `ôJ€wøÅo*âáD»g¤µ|¢bJ×ÓÎÞhƒT!Äì¨4½MnÏiÈRcœuuʆ™(ý§ƒ/Ðe}‡½évÚÉ Ì½ºqÃGÌ—¦fGøòØòüÅðìÌ­2*–¶½Œãåe!Ù&[ÜtÂr;½¼îžÌd%9„òq$òoÁ›M‡¯m{(¿óËEOÈ{J+ªƒ, ÔŠˆ2E™‡dää=Vãq}™ã#Yq"ðÝ[ΜJL­T\J?n8Z/nžÑx¯„Ùî•?0bw öù3ÿ-µ›X’c뼄ñÚ¾×??Ú“ƒ1’¦KÉ3ùw¶ŠRÀ•ÔÔ9ËëÖS×Ñ÷Ól5º ‰-°"UÍØDÆ×½ŸÂÕïòmûg5çÂuÜhĵfíÕÙwp$‹Ø´U•t†{x)xâm/’òéÇäк#¡,ÌÛªØÅ5]'N³»RwphýÉ¥ùçïé]Ö{¦ V“8!¥Ý\5zYJ’ó–@‹ Ø!å:j’`•S†ÍG¯®}§Îu±‰¨;yŒÿž±î|‹vÎ]"·ìjÆ~¿†(ü.úgÎh+–‰œ`A…-•‹%Ü{ß&è`²ýNg÷¯Z»B’Lè݊˾ƀ¾6Ýo‚,)§À³Ù.X”Wç3ð\-ÂÐY­Ê¦"´™ø²,^]ãñS+,@Œjˆ e¼ íu?r*-KÖS›Ä° ³¶8P©(-0Ì\…8º¸‘Eò•=±Á{@hc]]u;T¼î& ~Sù*n|B6ƒ&Ò`0@›Í”ži9Mf·_ƒwEëx ÎÉ`7I\«T ž>|ëòجü6!jR߃1ÌF¤aÎcµ™td%¥¬T»á_¿–ùèhê:·l$btP"Ö‘ô*áÝ®÷© ÿ,‹*kT½«Œª—ÉyÒäs3(J¨22mF-ÍF»õíåjX¹’;yøöÚ] üè^áV³/nýw·7¹×=ä½Z|Ïv’&‘²1©²yÙä½RC¤_fÝÇŠå‹“7½ˆÉÚZŠ %hêSX8ÆÉ ©®š‹ÄÉ&µOéf<ÃùBÊ—u³ø;Ê…ãkÏÔbAþ–¥n´;xêŒ!|—?r3¸»)Ž$;KÊ Så;æVTFf‘îÿæz??5žµbnLÆÎÆXL‡Äça+‰:@#5ÓH©Õ„xÌ’„ÑÃ0ÉÆ8äÚEG„¸"¸"¹ðYyÎfï‡ÒêcM*‡ªJ*P‚‹/„H› «mç!ÜH¢ÈѾ‚/RÛ šøRÁ‡¢Œ¾”éaúK‚ļQšAªzÎ[zÎÓž÷wA´!%œµD¢óLŒ‰%Ê ?r3wSŠ.\í&Êé×dáõ]Û¢t|*¦SÔK,b0w3½ü-%qþZ³‚Ljs|47«U‘पRMá1¿,SŸ)ÁôœÈ)(?rmÜR??¤óBý`8„¡/ ??GÇ`›9`”UOÕ¬Æae?n#˜ ûXþ”AµvV"dnÜb°Ú’Ë•¸U:ʨº3\g‡!¼ÅÎßëÛEc»-ÿŒUp/áÑPš‘°4Ü”±…ZeþmåÜ–#ÖtDÕ›™Ó–ÜÅ N”­*†U1Š*®GÆÒ*ãÆ?0‘õã.{õ}Ž©ë?n,ö}wK6*(­¥Hèè…˜wíXu;éÖ¡|Ðî)T[7¹qϔәmÉ×ÿkÿ}žÈ¢ªƾàŸ±sÔ”÷-“ÌSíÌÁ¦Ã[z‘úÍj#ÁD`¦™á´JhP¯°Z1ȸ¨$?rQ&¨†û„¡W_.qiÏ??jïU³ë}ÊEác>©vÄÿê¿\Ñõ»úS¾eµtÅälû“‚ò¹NYãÍ??LHí;AŽS[æbþ²ÛL?nŽ@n<Iý?n}‚ßfÛóÈOöãuëšà‰p¸ëÍôÇùGÊ-ƒÔÈÚ~…4cäô´VtŸFž‰éxšØ$ XnQý½ê‘¾RÒú)¸tÒŸŠq…lÑ >™ZÃÑÙÑê{4Le@ã}´õùÿ„S+·kQ,Zø[ƒWȤ}¿8¯Hó[íòùs Ý·šé4?nN‡)³ç»¥óÂ%Må˜@ÊÀ¨Äfb¸…¡Ó„iù'c?n¡)æªpjІ×û­ÜÐ +#ƒ0gÛë Yei§´îW.ÚÆ'#@«CSïy¢ISþ¤Ä-‚¦mnþ,#ö#ŸA´ÒÁ|úmK“†‡ \Ѽt¤„iŠÚ{åуºÐ†KMBž¬ÓÔihª>ø[e'@>¿biUÙ*ׯG ã!Ø&K«KßOÁgÆÞ>‹/Ý?nÍI›3ú?0[J‡*SRür‡ÿ´ZRiáòñö[ Eöþ–†µSßsý ¬Žx˜?råî$e1ÊTmC´!!ñQq’Í»O½^|Êú¶âMÖȳN?róé7À¾¤Ü¥$¥üÛ=À.OiujÉ.M9©^3G ¸†Vk'±¾×ô|“AkRîÐUÈ?n¬Ë™f‡LŽæï ƒõÅ©£ÁÖ8 H*õ·S £°d¨à<†4Ñ[WSR4–ÉfZ$aú#ÄK¾ÈLÚÕM»ú?0W˜ÙaÁêò6'½ÙÍØ#B‘´Yr{=Êë?n;£x³I±fbø$FÙôÚùR´a¸Ê%O›ÊF©PÊ??ÅÇø¦}s5ú‹Z\û㓸uZl]y]Ãñz;úx[J–Ï^h/È b~óùºëuGsrÒZ¶üäü#§¨w'×®ÄïŸxr´í«´ŠÄÁ„dQ|ØD?rS¹ÆO"%p÷þe»„q%¦ÈgLïv¤Ï&¿ng9n(¨ò–ˆ¿jyËdAò±Ãï5³yg¦Ý&Å—7AçÈñqlj{ºM«Ìµ‰úkØî8#”¢…¦þe§ HâO\Œã´åÇ£“/âÏÏ Ž&ð—½ÀS´ºÆ-Iã[…ÙÛôþÌD?0ÀT“àw«õŽ 2íÃÖïÂåâûGK\Œ]s˜½±ñæöò±¦ôàÉ™÷GÇ„ÁÉãOdG§[?n©¦KKÔûz¸#E¿ÒC}F71Õ[èØ7¹È׿îïL+ÄÚ¼‰jnŒ??KÄÚ ¶C~;m®ž'¶Có5`šé©?niëmSYµ¨Åu ‚ÜÌ4yνf§=W&ë~4_U¶Ó@S½@ÉkÑ$ŸN»ÓlÒ¦´|k¯R&c3×vƺ\Hu¿ÅÕÖ²âyôßaþTGÆ6÷“[ °??¯€“à!šJXöp.GNá3óJJƒõ‹ªèóÂÕàÁN?rÌá?0ª¸Øg-¬jðÑoóå$ÿªäQصÜuÙÞÚ2(œÌ©‹bõT4twü¿sô~+±nvIªÒE‚òiûŒ#d1òrx1ze!ŠVi^ª•É%ð¼`O“|0v8ƒô©ìÅ|ØVʆ”U õ—»u1ÌkõyȪYæ1(é]=??LÄÞ•ý‚1™iPQ%{'‚xåµLô6©ªDl„„P ´k¼Þd•\ƒnM£L÷½c®î¾ïn}ÏÒ|óôã6GWb8Á‘Ú¬}/¡¨éŽha˜W8-µ§Â)®"œX@÷WBFSÞòÓeD¼u}-Tu.ÃCäS¨y-¡gT²ç`•ç=«æd?n&Ep^ÓÄpY[òØl@ ãâ^>µÖócã\5šf¹V³ðîdö‰ò®O²0-©–é?0+j8ÇüŠ`¨/Fº‡Q×o„?rD¢–1»Ÿ;¦¿Àx‡R=X!aIú-†ª¡%’,0'¯Ø‹Òy{½¨¥Zz6Ñ,¦Íºä аŸFé÷È‹õšgN²Ì[Ðc¨$ûÚšž?0[aj6½Ð}GÕ(ô@=%è`æC×0ãF\BsU?0* I*]Tý‡vÔ=ë§·;‹wXGXm[ˆ”.€BÛeR«€0ó‘þà(îj̯$FTÏg?n]¿Ô)¦èór ¤îÑÕ î¢º§µ:¦½Ö•/7ª;?04!̪U<€›^pGãAã%ª uÄĉÀj“’·{ÏHÃÊ9ÿ$\Ü;¿>i·*±Ñ²kŠ[w×ÑPb$¨&x´,Öˆ ›:­mByµÆ\zÖu·JŒöTQqéJîœ^0wÍòˆ ±x ²¯]«žämYñ< ´²•Ív\­Y½¬¨Gvz]YƼP‰wŒõ¿SFdþY„¬ åòÇ`½ÌrŸºpŒŸ5¹w[»-?rùñ¢çvAÈ!r\Œ»i…C'²ôŸU÷??4€{“á±&XEaM»Åâåòx}óÓ*mm?nJŠ®á6ñÎç~Ü»Ã^ªNaWuyÎwYDÈÉ›qG_å@êô º’ÿP®d Ì9'ÔâSA²twÄ RØë-_ßî ã‚Þ„£-’¹.¨R^FÜÄF·[ð?n¾¹Ú‚F·µŸK˜7y2^Õ[÷Gr¶¤ÛSE+¯²§¦¥Ôæ— –¾Í>óí ÅdÈžxîxu{lÆÚxUâàœË]Î:W‘1±<ö2`ÒÌðóä\z¿5ÔêçËjšê¢Ë>?0f&N½{öÔ2}¤«Õ’¬$˜!/u¥–ºÔ0îT÷‘÷Yæ ½??ÐòŽÅMç!Bñ½"€©]ÀØ;0$³$eô%ìžéYT­Àõ å\13‘G·èª„ž#èë/²wò³ÿ}ÞHÙ¦”wM³Õ!lºÇ×]-;d¸Dª*‡oMÊø3ÅþêÁ_\4t®slkà"}ÅòÑoóaìîêaŠ©Z¢ÞëÛ×_î¿æly¹½¾ªÒÚ6»*Háw{w.q˜i~ÚjíNb¸]ªœÕB÷Q×ö—Ôi¶ ðL~õÍ¡0ã7ñîVêÌ!4E=­Ø'¡, mnQ6³®•·èúg®µ\IÝu7û˜òø3þ§I{­‡à?0¿4 8¨Q´<ÎZ«¿z_¹X1·¼Ü÷Ø1ÏÚ‘/Kä¼\hc‘’æw1/òûX^,µtLɶß2ŸF*ý©]ñ5ÌLSñ&«åð²ëJáã!rm9Ü#°¡ÎS¥ÿN}aoõ;¨_Üä<Âåý±Fh-íÌO¤‚¹.Ž‹]#GÜ*1KµˆOÎ@p°'™øû—öÏÆÆŸØ#¼M¢ÍúŠ.ßêx7&¥]Ç…Žî_w“”™EšÝî:‚mõÖÌ¥-ÏvGŸWÊi?rôÀ‚þ½ÚûyÖ2R™Ð–³4<ÌG¶¬ÿá%¤|qsñÞcH‘–@WÞrÒø.¿4Õ¼­§úGwgù‚½yÝêÐöX²´ôpnó¯çµïoH”ÚEãñg+*¶¿¶V•tPÒDZË"Ê0§æU”ê"Åãåpxëè(2v”ÚZ“CΞÄÒgf„ÐÔUUÜá<ô™‰Ï¼ŸÒ4ÀL†ò|ãÑËGÄDaËw3nrg²ÉE™HaS_,Òµ¿e¤XLCÛHÌ·¨ø¤GfdBŒ<ºiÃm :<çîJX\ÎSKxƒƒ×°qr·ö˜}ý|Ì«CoU»Åd^¤ñõ s7ÃP`%Ó‘EéR&$/'VdÚ‘$ñÆâÐëû?rçµÊúek7ñþR?rgÇO5ºGj6U‰Œ‚V£è·$hž¯uÞôÊ×?r¯Iî iãš•­0Ö­øZ‰µV–Þ „??<«'›ü$hÒõQõwæl)??¯?0ŸÌ hÂ$[-ï-žåBãO%¶âRgÓ~¬¼UO7eÖ߈P³ˆ¹àpO¡Ö?0dZ0Ý^þ|M÷Úúûb'…‡âÆñ1¡ì7I=¾ÏÙ‘ß×ߟֵ·àÛ¼ÿ¦àæ|¼åã‡iŠ 8ÓŒuÂïýÛmùTßù&ùîÍ?0U²Lœ!‘Tu7*â0¾ÁÏ’Ý“9öƒ#cjÃ1 h•ñ¡4$ì…h¦w£“ýÃz®z “ZÚ8ÔcBDBã"0ËȉËáqRß??Ù#‘•B¢,µ¯SÑØq??ks‘Ó4½¦U¡cI®[(þ?rhh$}< °]”cB—÷õ??÷*!Ýå\¼L˜l¢?rèß{@ÌÛ’‚vC¸Ñ¾§†až5ö 1b†]¼§NÍä{ETù÷Ü”qõµ. ™vº¥Ç&°Il‹ÙÇ3Ø*Nä…Ýc8¯‘ŸTÕö`ý\ðcª„r©Ö?r§î¸_÷crkÇŒ€‹Û„d-QpìÌ“5Óšˆ©aÃAOdŠ??‹Pl]‹œL$„‡’u_“Ê—îÏêhÊåçuןÑÑ::çÅNß^;Þ4þÐ4Ïûˆ$ùë³æÄ•òÓ[>P4 •F/Â1Ë;¿ ËÔµº $Ý7…aPõò¶¡n·ÎûB‰¿ÂHóÌÒ€'02Ú]öGîí¾všmn…ÇG5ïçBjÜñꔀݕS™·~—Iî.>k#9\#*s“÷ºK?n+€Ž µ6<ëPÿÒf–’äŒÕ}aµE‡?0cf7 ‘ÍæÄŠÑmê˜*:n‰†µ08°Ç²t^+??‘^rz$¹£l²ßœ˜-K{†+ÓÔ +#Jgšø Æt’Š®†~Û¢1ÆÏ.¸±œ6‰tæ²2:\q¯-Ä`P_¸{8Zr¯Ô€³Ó•B&º¿ÎžèdYR»ûßÑ–É_j|6 1Öwß{—ÔJ€.b·EgÒŽpŽJ×ÒC÷>yJwÔAP !+o§8þ(hv˜ÉîqÉ=€QÄæScMš?rcc|ë‹A=+B¸]•ÀRü³aáZMa(ûÜ–¶ÜÕùžåÊ t-40¦…tZíi»Å}&Ð…ÏÒH„LM³#´´°šÓL‰—É?rÊÏ&¶sÕˆIÖG!×O‡}òßÿêÂøÁw kÚ®]~?nç•Ç‹fù:æú’=š—1ë°‘ÙqáïBû¤&™ ŽU Döfb1šØôËÆÄûùÁù»´Ãƒ] þpu“_¤žh¢)©Iú€ÎQ¢‹xeK‹¼íâr3R¼’¨ìu%ˆæ&!®;ÃX¸®ÛCŽß绦®TŸ›r 9µñxº^©yßL·áẰ7,È¡%5’7z·íFNµ3æ¶ƒÑÆësP*qÚ I9ÿo¸àáó×â5v4¥ âÚ?nÒ ÃþÜa³âڢϩˆ—6¨ã‡CH³"TÓÃvÚ1­îÑ??(úá±C,f4³‰dÂU¹ÀñT vÖsù\ÜM)~‡SªèûRÓ4¶#Ĉ%¯¢.²7×JíŒzþD=„0Ÿ¤àb‚)L%œgG³Ó*$ÄãlÑm!ˆBÀ´‹P5ŸéM¡=e𷜾ߖY ¥ãØÓÌ7¦±ØêX¶0a$•FÂÝN¤Ç=͈ôµd÷ü\ôLo*!èg;¨ºØùF²j:Å“õQ~àDpi§.>™ÒÈí䃫V]é¢n•vh ¦£vP^ß`—‡õ’²Jã„ê­5ÓÔÁcU‚>2}hÍÓÖ¼{½Pˆ¬åè©E:'ÒapR‡Ú˜š©-×ã¾æTJˆ›ìÙ,0Y¦¸??*¥ÓÈh×QrKåù²Ãþ¸HRî`m…Ðò—–¿‡Ç—üFû¼+\Ã"bZp$ó:?r=Jg©] ªý:KÅÏÛ1êÃÉ) 6À^ø´ù ¸ü,ì]Ôá»[t½2ÔÔTfäâu§³£ÕI‡¤ôŽ×fîB¹—5¡8ÕDŽ ¢+z”tl{²Ï »?05Fïø)à’«¸aÅa½Û*v€ylPš“<À©Xžø뙎=;/V#„[l5ƒDK| TŸpXŠèÂìã?r¤²Ó…”ÛŸlÚ†í‚f¼ñ¢EÀia§@²F#ÊŒÖ~ïÌߘ¸\zXð®´NýÂÓ¢$[вFË.tÈ®/ú,‰TÝ)(ÁiìYÉZϸ3÷þ+dô³éÖŸ-iáþCùã(s="k!ŒÆ24Žxˆè*oœå"” ¸ò—,D ÙßÉËÉ,Gø/£¸ºŸÞµ $@BÊc3Áz-\«Â½”Yž??Â×¾/ÏŠ¿??YIãâp³´Â¾mAïRÑ(ÃQ¿3âºÜKN`5‰'¤Ø¾±£tHI6m%a-!4~¼€ÈAÍh±Yå”0Cãl4„&²yÏ!­I+'œñf´½??Ù-®aÇ·ï/T¹ÿIÜ a-‘Kã?0vøSc"9uŸW¶eUƒ®^ÙÏD¨)´®?n·e™¹v5-]K²†õ_ñµ€ÂY¨¡¥ýÝhWÖSTBó3O“ã£x¤J½gà8EMÎë"åR"¨Ð\Ð"ÊÑ;D¾3ÛJ«V¥8ˆÅ?rÔJâ™r…+º’ãC¾Jå§tåöH”/R¼OQÍÅž~õΙM?n´8JԢ̪PȳzfçØ\¶??òÝéb“ý[ÊJJF¹Rꪂ?r–=û™­«¹¢¥ÄÒ£üTí ÏrÑ¿oWà¾%´Uˆ#’¡ÕÈ:¦ÿ ¦þ’ôÞÇG_ܸíªìšä`©’æ“afª«¾Lfu “¯¢QÉ 4h¤?0 ö£kϬï‚b†Ùå·??ªæ&HhÜm£‰@,{ú9¶Mˆ:Ó?0¯Wò@îq¶ ˜±k𷘕îèrB·’x‚jèœp, G(Ƴ¹å¸9èÉU‡»AY56ï•iN‡ܱø ??¥?0%Õ€1^Iyß“˜^”â0Éïe«5t”8lD”bÞÚ"QCØ?n“ÞjmŸ´ø«‡ÿERj›Çä…›|.qìár©íƒyÚ=}Dƒ•ó¼cЯ×µ^˜¡Öü7ÞØY#!ÕR+uBm5 –#qEµ‹ÒâJØæi¶¦ðp×`wR¯˜Ád²0'ÃA Ô&Zw.Nµµ±Ã‚ãÒÅU…ÙYFì°¥«…à$ŸÏ™Ü/B›í±}ÅÜÀCëÙ æ lŬ’£vŒ +ß"ÏÄ¥V…уopJ4.Ò/zĦÈ[æÁÐê×f¤ïp˜¸¸ß “†BÐg²ë?raâaž¾'´ä þ[œ"YŒ~øç/U¼Æm‰ä¨Ý¡ßc·Hy¶MÁYå–c¯cõN|??Q-pzP'›fÍB‚ws0ÔùÍ@;—ó´í°yù´³œY h˜¬Øt*¦}õ™CìÈo¸t<fûؼpçódu?réKÎó¸·ß§˜3ee=3ñÿÀï©Bn©bØ2!¢VÑÉXá/Ç¡ÜýþòEPQœ¼åuA˜àáŮޟª°ùMp¡Ž {!|%ˆ®"G{šLf¯’˜ ‡<«ñqÅgpoýŽ›@ŽáÅ0b¢—Æ~`‰DÊ,­fÍ],/•xrs®ì€èW뵊Ñ.‡’x»øêyk;ìιgÍwGû¼L¡šá»lÙƒ›ï¶¥4€Giɩӟ»s±+œwm¹ÀûESCæZƒ‘1XL¸æ½h´ëcî)óà¢Oñæœ;r¸¦ÇΤŒÙ\îî)Þ÷M,ý"á³ø•®bX<ë54•Ô6ÎYø§}5ú#A·Š½bܨ"2d'§¶`)¸`à^ßOoÎZ§{,.R L:2OLú{G\»\„ì°N $ûÊ¡+T'AiX‡YòƒÑ1v+‡"6oŸÓ,R«Á 0L¤Ág+žÿLÉUPV¶prvF€%CMEúyb”–êkt4‚\ $ªwür«\þ%¤¦ÄAi§šùØ´Å_?0F½ÈíÙ£Ì?rݸR‘rÞõÉo¬'æêY.3HùPk„R“;ïóŸn2µ)w‡Þa’+’¬Ù%¸Ê¨ÎW#\»x"¸ FYjǰ¨s·QŠ=\Éæ&öíÝ?r8Y“‚‘¿GéÂu3êã¤Këü$Y <Šj±¶=Ö??÷{†„w?nØ‹U¥<&ðX=--ïx%~—›ïI!¯+x³˜žÖ[ÇëÞ*9q›–fp¡H&I©¤¯?r¨Ž}“ºNR“š5 ›üæ¶§M΄µx[2¬\ÅÄ?rØ8ò,Ðq6¤öД¤ÍÕ,†í-??Êæç@›ôÓÎuò‘[u9)ñÜíœ9;)ãùâ‘„bËÇx±ïe©¿ˆ†Û]þIÉâǶwöŠèf•Ñm‹ù£Vá”E^Å%ˆ¬å®ÆœEE+’Õ’b}‹21™Dsº?n¤/àµ_˪2D‘ˆ¯ŒFû®!˜H°kPßÝ_}AüÒ4bû†è$ëþ|$éÏ9}ÝÎbDRç(3g©MA£’?0RBWÁHÅi@®4MÝC¹È~¤cÉAL-DÝm.ÞÞ·÷‘ž%¦|_zT©lOß»çƒ:œ<_æQ­,úáÂç¼€ C¨h”BX5àØ,>OèpÞÀ¶¥¡àw5¶Y›|²œóÚ?0k˜ Ø„!þàâ½yîOýézI¡aÕ½Šð‘2‰càoTÓç×.-c™™øýœmjä¾9+AÇUïê{ÆÞFn ‹+bzîM\ÚÃn,ê= x`Ó^7Ó”¡ÇžÞ}uºx¹ßáÐ$òî +#Úiý‚`Ûpo£v,Y UR¥³P(œô<Õmã}°Ò”Ÿ¨ ÒÏêuí 5šš‡ãd„Xœ\ÓdgУN¬4í{Åúµ9«ß¨Ä×ï6U'Ç뱓%|¹«›SgèÖî??Ö׸>Dà´KPæ·ÊLA>åoœÇèÉþ¹Çò?0}/•<þ£˜YBZhÏ'èÕ驵K"Å3w2Ñç÷$ùZZB \+âkä^éV©êO@µº`Aœqÿ-/ì Ó,yFߥyÐù{¯Å??Ý÷ÀK?r§ë¢ƒ:ü» ÿNP'w­·°‚ŽV'VÐ/AÈÕ‹Î],Ðí¤R~£Ç(ˆ¸­±%7² ¸ŸÉ‚F„2­êï|ò™ÞÖ]s{ kèeçöœã…‚ÎÔ?nQÕI??œ×‹ 4±·LΊ%@Ã2,î°r^k9U7,¡¤&G2?n¨‚)i ¥ï(OÓšýÝ’-Z”ܳËÎÄShL©}NÀNÃ0??p˜Àx¢õ7šüÙåMª{̨®øg/¿Œ6XÎj6£]ÃÈ?nÐ%áMCÁc’=ýoD¶P´X{u~%›é‰&MþSÇ!©;âÇpR ­Câ?0mÀô×,ÄÐ;!{EKÅb/ ¨p,ïÑóû]o©y¼”x;??År–·^Ba7tÍl‚=¤ÿªG:€ÒzåíþÓPçÀáS):~ÞdíŸäì÷(M§ ì²Åì­ìÙ·žxò/þŒn¢Éò`’Ç©ìAŽ·rVÛU[»f/€Eíñ&°“¯8!l¸ÛþúÇÑÙ˜ñÅ1RL–’K,Mhn}õïÞËm*uåò§Èõ çÃâ;iÏÚô”TŒ dßèbdŸErÑV³\Ê”cC[Èüm8²§Å¾ž6ÖƒâSÖnÔedh~Ô$?nxe¹›®ûÙÊv?n´ßpt3KÕ¹tL4­T ¶» ®0"\…¦ ¢äkƒú¿ï (½B\§(Îù\Ô”§¬±ïΟ$.¹¼½®'~4A-ˆÔ¿;’lG?n»ÔòKÚF“~CÁô}DD®¸*X¸Ã3ï !‹¯ÐÛÉcÝíHc¬ŽvXÔìài¬2Œk’'"ï„!?rª::w(¶l¼OÏ'¶þ|©Å!¹>HoŽ’Ø×FqÒ°Œ…föE°h`¯½4Þ6%Ï&ò–Á1Î79º-¬¹5Ô%ÙÂ=ò[fIªtÞEiÍç‹%‰÷atýŒ1fjZSްZÏÒ“%Xd²h±~²ÕlÑ­×Nu¨²X?0IÛÕÕ¤ò(Xeoå ÐU’S./V_X1< ݶª?n}[l6HÕVœÀ/VÅ*ÔNIo”ÉãÏ_ÓÕA¦xK·'l_ÝTœ‰\9ŽŸéš×·ß¦/¬Z¯‡€uël„DGOYkZFÓqE)ˆVLœLïkîœsT¥&&´rPO @䱡Ë߃s´e DRàBÞŒ—>h|GþåÝÄÉÍWÛxê‹5—¬‰¬påææógU=Ó"ñ¶=+G“åäê^¾o}ø¾JÅ’kœ„ÖP °ÃubÈ5€˜w?0D-¨ ˆ¹#ZCvçt¾w·Kbn|~îo »š¦®” 55d^ð6ÌÔéŸЉT¡t†–€„T>1P|,†×Õ'™×;fj&TóSÁíë‚y‰šß°['òMu…éº\qÀ´<¦¼ œH4HUŽJDp‹†M¼é*ae¿#Q{î_k“™RåóTKXðžo¡¶IFüÃ#Y›K­B4»nà6×°‡Å¨îÔ„«Ÿ°ý„,ûY–rZâel¤€¢$¦èÊ2ùpÇ J…?níMIš‰4E6N³À©T?rÇë«MÃ傆”+'Z_[À>âfòúÚñZX Ç©-“!E9püŽK—­¶'D{Kñ !z…¤À #›±XxHNìƒx4SLÁ–Q†²Ä¬½»á0Ÿ‡Éäªz%^WW‘wä3µ•:Ä0ð ~U¶3ÖVTŸ79A9VBÓÝä-ô8ô_¶(9w±3‡}ζšÅÙ©hî_X[RÒkê4+BŸà´#êý&g Ó¦§ó-–AŽnÊRkèk29“ëìó½âœ”%?nÌd‚ù#Ovo–QðXæ¥nüvr!ê­ðLœ>}Ÿ^q•`8ÎmÅB!b•Þ!õc´]é¦57ë“„ïgÜÛb•Æ 9¢¾7ϼù9õaY0$ëÅŠÃ>ØåÈËÜ^2[JR|Ë»ƒT¬Q‰0âë?n´Š(ÍgL7šKQO»D§éâÐ"UŒM>͇3‚äFÙ9ÈT²”7„%NÌ[½52ú9ÍlC.—èUOBÜ™`–êYê»|2¤Y]q(–B,Oˆ†ûñÑGŒ™*ð2??xß-¾›õ‰°£ Ã÷ø]+??ɰ¦³=é#/î‚Ðö‡²žS×µ[?0JþaE0Ú“©Nl¯äW¶Æ™,4rS¸œNpZ… %]BŒ?0ücøÚtŒHë«ÓÅù&F7ãÿ®?rDÌÓQ™Âåv–´;Ý<ôûÅV_¯!Ê%\‘“&¯Ù“…ÿ…F¡²Ù[M_J뺋óœkæ¹E‰½l·Üðת‹‹ù޵+D‚«Þ"ʼú ð覂.'d¿ß·ÂBÙT}µ¯\??FH—¦Afß²½;'[Ë+Öh‚6ÂlºÁ]±´Âl¹8Üü‰4–eßÓ+MžŒ??RyÓSTA Ò‹ ?r©ü`Â9ãªÇÊ+y¯ûõ¨qÏ”‘«lRGiòh¡>¹Tþ ¼?rü¤©Ml pÛ&Uª¡igSVév—¶mÙöž'ÚX܆Ëj¯©Ñ†S5—?0ô ~toHOgÅQh¬ÊŒÙP??”|‘ÈÈÄÇHÑÈ›1˜¿?reF>ªì–sOd¦÷\YÑë„î&†:hç*MõalI^jgÀ‡ª¸Eµ}ú—ÿ<#Ù¥—({õùãíUÝä­À‰^ò¥ØéM{® Cv¢h3>û?nÛ??ܦo*k—YuŸ E±S•ÙÅ“O¯=HºÑú¯{€°??ÂRǦ;X·•cøâ¹ÄEâ—@\à.Ž&Z­ò8ýݵE„*?0õvÍÀz?nq ,ØîÚ×`M¥¹qͳÚrI¥X¨¥iºõŠÐ •ý…qt¢‡Ðé;ÎqR+I1ɸF¼Î¶L|ÊÚ|Í$ìÒ*ºÉÓØ›Íâúð{nå̤ƒ3 ’I9³Ñ ó´]èïÞÍèÂ?ru„ý䤋~$cæ•Eæ‘Ꮂ¸ê¤ûG„^Ùþ‚Io|þ ÑŸC½¸Ú³áZôX>esÞž‚ÌÜ<•Õñø•¶¥´ÓS%FuótZ™J°DmUf¢# ©©¼¢–š¸ñØ‹¼wÉÙÐ8ÕÝ%ÆP‹~ñYVÑç1ìn€tžäõªƒÎaGÚT¨añuT×ºš‘£º×Šà¹ /¦‘U?0c«]kÿâºGjáýd -=4ÚìýÜ"U2äD…õKÀÁÙZµ7’Ð1–ýãð¶#ØØ#âu¨^)LŽ?r^’ýzgƒÇFìgfû??cúó¡,&÷§ÖÊ… ½@¼yw><û;H¬¿Ã3|Y(0s)¤5è%×t'Z)ÅIžÝWÍZÿÐs ‚‹Þñª??“Ú³“ó=6æ]Ù8¸ú1ý½Ò‹žÝ *«¶€ÐD€$‹B³?n–Æbgw]Æ+²rs’#TG²"/söö?nr(®*|£§5|?nðýéÎZ%ªGv’?rÙ%"LÔöµ¾—Ñ(l¸le à˜m`.©Mpa8ý¬—mŽëð†:?0-MS§Ô Ä(¢P{Ø$7‚¼ÅŽÊª³>ƒw‹bfV£ªÄt¾)IÚh9`??K-°¼°eÅš…·?r¬Ïb®†Ög±ñ“r¾¶&(WÓÀex¶¢ÿ6c‚Ï}#B™H¹Ï¡s%Ö™é²Ájc·™éÌ’ä*\Is}¹NUM€ÂCTŽxÈ…ØŽv=œ¦58ì˜,az¾w&IÜ(ƒãfð2h£m*Æ&r˜KìS¤b±¤î$Ø#L+t,5·˜9'tû¸ª’M.˜ÁQÌùáÚr™—ÿBså]T=“—Å‚ÊÒ\¿¤SuõÒ7fÌã"l×ÒF+#œ §…s??Ë0»GgÊ{F_q#_³Í?n]ÝIœuO3Ò%4•߇×*ÌîdË}^ VuÏàì¹+6¿ëG V·çW$Ì lg’y\ʨ7Ä&Âÿ0—ÙÅYÒ—Ÿ+‚ b¯*DÿœgƼnht´2Ád#Šá÷Ö{"Ø”>“+Â5ÖkÞ-P6,À¿ˆå_°8Bô?r¡VÏòâ"œc¾}Mcg&UØ\ßœàÝüÙúv7‚xÚ¸ÔµœNÐÜ¢'@ì-8ž’ÝlÀ¡Þ£Ñ½²¹Sz¶Ó 3:Ÿò%½Ä4ëÔh5ë«NIïœ"ô<¨6I8SÃÑ¡f3˜ô`.±Çâݳy{:c$qFÀx%ˆmVzQÓ%íÞÕq¬Éï/ø]?n„Ô¶#W*’Òâ–íÆ<>B×NКüf›–ðl±Omn—;nû‹ãž.úZŸ[¼ìæÏq÷Ïs_¢ìCö½;7ʆêB·@-ÝÛ7ì¢5o«¼w2MCŸL_ÛݽýìÎk?n(ÅM”’œY÷„;¹d[ò¯tïΙS~!D0£äw>¿òÔòø~Ïø>räîx±¦ÜIußu|XÆ—¬‰ì®o ±ˆP’©ãâ"âެƒéðÕCzÃho{²å¡ÂÒåw€FPïy¼¼Äp-l.7¯i³žíš87Œt^¥(7rrÛ·œí¯ýîX"ÓýJ {âb@ò «5«ÇÐXÀQ6<ó+ض+¹l5‘f¾ñò5<8™JN•i.¤€âŸMþ?rVZß]ÖìõVï4Ì5š"Œ©Ò PZô›€ .5èäø$Kn ÔŸ³.2æÏ…¾_æÎ­´9`À°&H½é>ܨTÝÍý¤ô=ÉÅw»*¢Ïe?0psÄšÄxo0 *аÍd§VÏÿ{Ñγø??é_Já^,W(j{¢KÃÈ«@þ?0w‚kÈ¢ýd3"}+ÔÉXŸïl`“g98MZtb=­Ê~Jà&TÌÆóÞ3€{•—;5ßaÒV¦pûDkL˜g<޵AwÙJÝêd)ŽTq •÷ûmó͹0XmùôÞ.ÓÑê?n™8 ý§x_2ù\™–Iyéi˜7šü4ú,]..:O#å`•hì¬É€ôc??úH[³”Œ¸O`ÅÙ’]ÔúúŽ´}Øà•4úýþë5èã#;²B™õgÿÝÀ—l%ÇõpvÜÉUe¨äb!Sö}?nR^ÎaÊ™O‡H¶¶²]*,kP–ÉÊ˹·°ü4ªvûvN@ê¬[}MÅU"2Qc“µQêÊμml[ cçŸù÷¯¸<Ù-;¬Ýzj»î#~*?nšäDÍ*ç·IcrÒH/æÊU;ã!KJ…}º˜cs/¤„C^ÈïiPvò¯¯ûºT‚Ò.8æ×ã?rUšœ[½/þµúcþ¡[åÝc€¯Û¾7mˆX/¦)KêúÂ5;ï·¡Ÿ·dTÑçIšR—x57A7¨T>Ì??1+Ÿï¨öÕ ”U·Û¿Õ¼iŒVMs¥V*žu§‡Æ—ªÐe4­³G²ƒS6¹œŒ?0"öþçÈM+¼){Ó“kÃüÈhõ"2[H?0çÙD9èuJ7ÆÛÖ_\j2¨0ù{"óÓáLNDñà\89™Sʧ¹ò›d’:z‹26/]Ä´´ îlAº!~ ‰›–“Prµÿú¯Wåö<>Ï£ÄøØ¥ãÌS0éùàjsN|˜ úæX¨â6=ФÍ’4:x™SZbOǼÌc›•:©¾qñ>G-æ[ÎÂÆ¯ µßKw,)^8þIÊ•¹•›ÔÂô˱:ù³\?róËAóÍ{vßW’º%]ÂÍ=•;ƒ7$(J¥˜‘s .Ì~´FÉg;´œ¤TþO~5Û½ÚªHž­´Ïâ¸ÔöùѬd?n5λZ{Ê7% *ÿ“‰Ê×]º~nÊÈ쌴•ب&MжHv›Û@1??}Ã?n8ণã^)àJ•½¢gÂÝ~Ч»$Qv®øÁødôpC9ûloä» 2Â)$EĨ°Ô«Í ŸÓMú^ààä¶Ÿ…º¶ u¦‹žƒ3Ѭaõiir‡£‡Òd©ž¬dzÇ:dA5&67¥øÑªûV]} HS™E‡wý÷bµ¶™Xòwìioë‘{±¿ŽƒO½öù*ë²?0;£ÓS¹+Jò3CÏeè™øœµi???nÂ&v@½1ºÖíB!YäªQ¡À¡']V²+ ªêòe¿©|]0¬Yê7R?r:A?nÝ'ÂN»_­{ë&“HÙÄ•rƒ??OçU?0 ?nÒ \®?rÑ…£cªhtð=J¸sçý×ü‰-¦¿HÒ¹ºÍ†Î^§ ™ÝÿÛë®tJå'ƒ‹s”¤иww÷oô*qâÌpQU¬ñù¼Íõ=»Ó0Ë4ö£¤û#')UÕí#kØGêNôøÌ®ð¯/1øc/>7VRöbfrÑB œ¢„¯™ÕöÂÛZó¨[‹šZ™U›ó*^cû#X¹¿…¡¡êU¿Ãù~”¢8˜ï7k8¨]Dˆm×ð ø\?n%ÇÑ}¬ü¸‰„ò%Ž :˜ãûÝ®sóç]Ϭ¹Eiz•ɭÞÀ•Àyªd‚ÆžÆæ8[Rn‰‹–?nû|ð¶p?nØ+6ÃÞuu^ÿÈs§µÃëÔ,*uÿ“¢’¦SÉÉ7gµ'A›®ÔõX·!6sÑ%³F D«+e +ˆÇ;Gû@–™+ðèámg-’ѹt|¤cåü•mÜÝÉòÑpîà–<§±7¢¡àT×eÙƒòO&Âý:ô/ðœf”cÈ?? W›?0ÜÃQRçgR•´}táç‘…¿‚²ÚTÊ2Ùô8‹-/#rgwz·òÊtÇpA^?r튤S’½øó©ÐójÝ|2çÉ‹ûI¸›–]mù¿ïìNÙí•פ¼sF8ħÅB{%é«|ÅžŒRS½‹½òáŠóLµXbV¥r³3ÄÆï»nɸ+@_pý¤Ø!-ø½ÑšK?rÒ™XiFŸÖwÕ?ny&ôÖ×µ??„9)«~·«ä¦‹[)÷r!·VÂ|f?r9§-ÄÌ_ƹ…á„øï–¯õÏ÷-ü‹†%k¾}œ/Ô¨š•ß\¥Èù&LJ£ß€z@55ÂrºU Ó©ò·î.ÉÚ?0'‡_“åÑkëXâÛ³ ém„H+0®P˸N~®ÂEp…õ¥ðJ«»ÍMO¬-ULëyñ=5TJ¬†mĘ—º•Øë5¢ÕJ Ç·GÝ(ÈÊ?0þÏdƒsQ·9?nA‡O¦¢`Ãõ»º¨¤6Ä0Âx(»¢¸Ë”ÌN¯Dù‚Sˆ¯åÍßÁZ='ÄBKIñÝ+®AÂÚæ&„KÈÌ@ÊͯÉãÀ ÞÞÆí­È0µÖ'ë·÷õ¦÷}¬L¢ ޽È5/åÙÿVFheÙU‰Î¥Íb##¾ع™SWžÌ0ìü5iç÷áÉ´˜‹ÆôïÉÔâíö“r鑯??Ÿ×IŠÜL ïÅ)?0´rñòÏJ/†íRiº©iÝ~ê%îezîÆÂoòR{™‡…9ÍgfÆ©|KN]#¿)DKs┬4„Ìëí-÷ {³ùÔ¼¨.zè=%šèʦҫÐñhª¦˜ŠG2*‡RìxÑS/ú‹Š·¿÷”™Z?0Š­BqŠ9¡…‹‰,a ½]3ÂÜ9Sû²Úk]o óFLåÖ1Ådm4š2cƪêæ> +#¨/vŸ3÷Ü™éã|z÷¼R£Óÿ#Œë×ìçÌ*Â2I¼2{Çd‘Gëâ3NÔ]åÝZÓg¡Ä¡â1šä5Û¢Ïýû~×È0—Ëø^?n7j'£Ju=< : §Œh¾´|D­(/#4áSuY/9ïëÒI9xÚi9F¨›I9ÙgŇ¼md‡2I?r®Ì¶ÿ™åI­ÏQ\jÜÚ±TXÿù¼´”²“r%ßO{Íu£³q6SE¬#öŒSÒA„æ7•r¶@R¢d;ғ骲Ö6ƒtÆÙRˆÿŒ –F¼ÌŽŠ9³Lpct“~¸œeß"$aã“+ë´ `‚Öy‡ðþjNS8Íál€[&¯ÂxtŒWá$¢‰üÈ ‰õäÅûtýz@n§IÉLÌo3im:tLF 7cå½£xê$˜WÛ!…Roè× Ó»¾ãÑquÏ®ü¿±¤Ÿ ‰—%õ ƒž +då¸Z|Ý>W÷›M?0þ (NÑyëOuȧ¸¨Õö‡ÑáÀ­úúv…û)­K6¨_¢Cªñò Ÿœ_ŽkŠ”ÒJˆ+J'gÖZà?rBv!â-j‹µÌUåÝÑäÅî®5¸…ä©w–£17™—‚ízMT±à¶:²)?r‰Bú‘à éBŸ¢+ YÝÏ0€y.ílVaµ–R^“Ñ:[´Ù¼bP¾ am¯cÕ¶ìÈ ú<šs¸}щÞBŸ¬“ÞÝBhNµ%¥¸h›êæWÚ¶2ç^%äe؉æ(Ñ‘L­Ó‹TR¡¾‹¾Yµ}BgwD-ÕED@æZ ,þ«#zÕ©Q¦.Piq†Z½Ÿw»[ùšB¦…åUiÊh–1ç¦Ç¯œ¯\Ðn^Ñu»4v'4\p¥Ú¨½/—X<ð–|± åÖ$-éO»òøeBqpîÈ­ª@ië*’=9\-FÕ °š<ï«’"ÞcÏÓúݮͺ¥+·:dÒˆü$¨ 6—«5v,_5:ê&5 âÜr%*³¢ƒ…\ó\N»hp=Î6©Á±;ÛœŒ×¨³ìÙÔ÷*§^Ÿ#êïýNRvׇÂßMKÀœhœôˆíÖÑñn‘&*ަi??ÅûY:9pÛÒõ9èšjR²Á0ÀÊ:Š{ô»ÌJ Œ¥’ýTV•b‘?nîº#)Ö-¼Üªæ ¦²M"tÇ<d ^ïõ†*aúüÇ —ö´@Ò’ø ¹®?n]î­ dTdÔ2:D$LB*xÂú—ÙT¶·Ãuw\ µ^¿/…¼òñÖ7ÚÛÛ·§©½u4-—F ÌšGõGó¤oÂ7ÌW„=f`}ÊÉÂñîc§Þ4S¦0n•4›_šU–ÜqÞm:‡n¿ÍÃðÎè-­…?0(©0pØŠå¶Šód?nOƒ®v’Ï ù/cM}˜†ª}Èýâò§Óã~åêÞWªK†+"=þ×»Kwrýÿ::«¬”øGØ´ÉÕšÌdB•È}yæ‹MàÙ£.î kïÁ&Õ†xZ‚´!ö@¹ÿr4æB½iR‰0 p—j*о#µ/4ž¸ˆðâ5àϩƧXp%ÌHÁ#iÉ^8ä&1Óó.?rîŸ #??¼R(ágxòqh*ž5Nîët‹çc'~¯IxPoþmYõK«4b6ú¾‡vAOjÎ lì·‡tdeÄ£bLå ®Õàë±yæoòÁh@:ÙË&0ÀÌ_@üøÿÓÄ:B/•z˜M):O:°cÕŸYÉ=(gÜ0??õÈÑV%د¾{úÈÙ(ÕI¨¡Ã4þe¨RÊÄãÀ®ZÎZ§1(¥Øº¢µ¤³ÉÍSÃ~(dUo¸‹ùŠÐ`Z"§\õ¢?rØy±Ü1žm\2¹|ÕŽ:7ïÝZ·úœZ{× paÉ…q#ÓÈpF|}‰Õ×-°ø?nbŽŒAÐÃl}nÏ]BÙÚòÒþÉî0ÌìÔdBAK?r]äM‚j8üj¦À¸ˆåÈǧ}X%ïìì?nc;¥rá{óá³Ú¢Œ??áÅ;½<\”ü#‡’áð½»8sûª“Ÿ9ÉÇÕ^-³eió99*MªµB^±gv,Ó¥C«”]è¬À¦ý88{¹r·»¤å«™P€ý¼ˆ3¡O`àóAy*[sbÉ µü—d0géðºÌ~uWÝÅÐþK¥lÿ„낉.4XyÎ*÷Õ|À [v±[fY?0刻C›Òdå÷òåQ?0Ot#pß©ÛÀµ…ýüoáÝÔr¾6Hìj_€h±Ùäþ¦ãîuWx@ñ–|Þ.ã]1ÍÍŠEØ?0‰&²°Y²pŽ\IJ2Gœ>âb¥àoÊxWÍÒ¹¢´v,í5vº,#p‘¶†åŽR©·ÈŠv´šqÆ]ñ'g£+À´dٽȲ '¡=¢Öño/, ñAN€qvEúT8s Æ»vÒ³µ|Enf ;dÿãõ–@“mDö¾ô$0x™•¡tïOePøGBˆžq ynaw=ï`ùÝb±DüÏ>…”YÇ??†x”Ö+¹âŸÛËß&ÏÏa”\_»ßC&”‘³Íþâ߃ٯñ ßï´^Õ¹žäç7×®Xv:IÔØþœÃñ«þ“-äã4Dý¸÷’¬*°Y[^GŒÙ9г(ž×¬ö}Þoܶ½Þ³¬EZ`Ú×Ã[ŸOÿÚfÓÜÏ«÷??(~²@'K¥·ÚíBÇ‘ËSèê¹ÄÊh(4:ÑÀ²q5 d¥¬ù);í–‚?nÚe"áÎ6Ùß1¾_²ÈÜX|éº4Y*—ÅÔÆóôH1Cy5+ŠÉ¼œó DCkˆá‡XPx]˜¹ËÙ”‚Yû=޶M¹Ú ³uàˆáE6à„§¶’$]÷]þ–]6­+ÙêÖÉ©¾ÚÌA cî=š›J†—B#8QãÈΨ%ÇKayƒŒ¥[??CÝ „r«g-@%Ϥ׋®<Þî®®ì‹÷ü?0|hûÙiM’ö\fS»SP¤÷€¾œ1žÁÄaâqåÊû€k¿“ChÜ!)!ׄÖ?r½ŸfS–FÀ"y¨!•¡µ7Ó Ë²À¸àнŸ®eh ³Ôn&Mo‘·äíñΡ/èÄ‚0ex*è¡=·aKû³ § b3•.¥‰µ¼·iÊß«bõ²Ž*.¥ƒqdL1ÕÇ(cj;ŠRÌ$œçœ…ìE€eaŽfùÛU‘¡ºlµ¢èжMOØ€¼ƒùòRæ°§³²í’`Ñf´I¢(ílbÏS`î-Ôhh??AÆqÖ\Óñ³‰ÑÂÓ?rƒòT Š2ÁÃ.ÉÈ{ËéŸ5iù$ý)P„Á/œLÓÄ…hP+M ZûŠJ¡ÐB,x ±Ç–Œ¦¡jÕ[K¥Álß8‘‘k¬½=ÓÁúÙF–;?rÐ@?nˆp„­ ô’L‡…’¥(‡ÎÈ**×ùÝ%í{ù‚­û‰PP¬h×݃¹Ž#ýլʡñ-~TLìj•“Èþ$À§?n¦ hl›&ÇnŠ%ɇ—ÙZñ€}Èu´5¨èœ¶O±Pwù|Z¼|iUôý£j 9ÁÌàæH´ÎÛ¤Eý+‘×BL4©_Ni÷ýe7p?n%ŽšÁŠ‘hx’£¤¸M´ˆ00ã/Sot‰jeýHH*¹yJ?riâvq¾Õ–-wÙôìm…:ŒØñ𹈦Ûmƽ¡&¨–iB~Ïë.p‹Ð®4HQfû¨€¥ÙEI” æ›à÷UaRæ2ïr«‘Œ£†ò%0˜‚"„á3ZbAÿ`œ›ÀÕ¶?0”ýê—_:ÎCo€ ?r£„fÉVzå‰ ÓWòuПÐè–ìn+ÆÛ8<À–Ù­ÄÛ¤‰®Þ°Øˆ/Æ U×>¯??T±¾ü³¶ÆÝn©JÅàl|#j AðSiµva2Õìòÿæó—Ôóè¢>Ìå†ÚF¶£u¥È)3é*ìƒÈ%/¡n­ËËtt‚sɪ£$ÐG¸—?r^ ¸9RÙëö;·åNcڼ̊M• ³Ž‘åÊbÉ2€:åŸí=5e›*ẫ¦,ú‹8Qfy°2|Ñ璺ˆŠ@]‚?n8\ü‡£Âþ„Ëœs2,’³ÚL?nT÷?? 玡Wï~?rjv>(ÊÞ7{Ę$ð¬¤PÈC.•*ÖR^.V臸å¼H±K`éŒm5.œO°6óv£* ÷YìÑ”¼…ÆæiÐJÅÒpoí'¯M”Ì\…åˆ??·ZºÃà\̼‹A~ü´.Î|%D‚a[®9oÙÿ$œ¥Â„F¦LJ{"¡LÁäÁø§î2â) 2Ùï †A?0ñwGZ®o^€hopì-\Ï 6XUNc„µ…ß­Ñ$&>.yeü2“G×Jv`q@'×B?nu Òs’›ÿ‚v°ÃèÒÃ{QÌkwåf€–ìvÀƒãqHÿÕ®/Ðcš§«µà;^ß!#QuÅ?0ÏŽò\2âh>z£´$v _gVÇBÞ··¹\SÎÌÐ÷¡j¯/ìQë<>tËVËQ??š£µ˜Äá †?? XPUqigöÅvC*mÛó˜:Œf/¬Ç¯”J˜¢ÏŠRmþÓce«âk¨öŠ?0v5‡*›Þnl<ã‡æëáûl™øm±ÚR¹étfá„ càpŸo¤ã$¤Y•¾@a/K)v?0“Úk†exßTY¾°»š™9|5H{K¢…•ÁLÖùž_ó wÑ›sJpÒÜŠŽ·v±yŘ‚½Ä²ÂS¥Wª±BΛmÛ3%Îú²Œ9ýîÈÐÞú±‡xøË$‘ˆÊó6ųzEEÀ,ª€óS2ñk-Zè+‰¹„CƾªÂaL+™¦ù, ܘ?0£¸F6?rÙ"N*3*Õ3ÿ~?0ÞDÙ¾»(OV—{ks¨;`%âîÎÑvè©`&K4^'—ø´´’f¼‡‘°›M]èÈìÏ‘óÌQ£·î TðÜûùƒ´M_3o,¥o>-Wˆ Ðƒ?0aÒèg.\\a^¯š @jQΫ_¨ÓÓ:îpú ‹Ãû¹×`ùÚ-ó,¾ÉÜ|üào/_).ñ{9ËÛ=áÜßtüÛØé'mÊTuË“D¼Éú’Q¿Lä0kxÍE3v¼×Iÿìüˆ€Ó':x5Ä­iÒD\‘Ë~X åÑ???0…E·tæ¿‘Ø·¼V-Û¼ÎpeyåòÏ%³?0ÃeÞþºr9ûðÉä·?r“,,«hå½/u??XúFaö;ßhfË^O¹j8ÕD±k¹¾Vr”í&msEx\”ãäg³ùÇ6üþà¥ËÃ+B÷Îòe§5UJ¡bйÛqæð?0ƒ<1?nðƒÙù2Œ…pS3µË÷ DÑ)‡.æB8ý@©²$î<±Õ(³¼p«%ï÷Û^Õãο'b)Ϭ5×ÈuÄMãSLжØ¤²/Í_7ª©»ÓÍnQl'ÄZ<ºz«®‹¶õ˜ß½ínrAŒ¶K'…C5ÕÆÑá–½–Ç´eoÐûÎÒ.¦¥†~'?rSä¹–€l’Í-µ¥)Ý:Ÿ$OJ°«Jà!rÖvªªÃ¿Mš_1eÁã iÃe;`ÃR¹ÄWå Ÿ«05¬T289®ç‘ ¹ Æ‹É$=ò@-Y‡‡Rˆô\8?r¾/Ö{”ƒ]†×&¯y;žž??z¸à-÷ûeRïv.!×ËÈÜ&¤Þ??pã§tŽßŽœ/×míIxzå˜àS©w$壆”ƒjò®TC!º)û§Oñ {–æÄbû£DÙÞûϪaÒ³û»ÓõéÍpè?nlû¶ŽõÛŒ¾¿J1桺ÈÄ· ½_ï©’[p:oöLSN£’§‹‡—Ä fGK÷?nÀGưîðo—$àmó$•¾Pa²‹÷¦.×,ƈˆV9ä­ô¢dã¥[|Éœ\Œ>a ÒšÛ™+ÕR6zQ]]b†Ÿg9´I}à•+ÆteGSÖ4ÔoV̰mª,-€Ä‰@°ùì‡èN@èF 6f²6¼©×òË“Ô9úîËt¿Óß-EN×JíBn¢lÕ/«N0M†onš&©Ø‚ìé)Ãvv¾­ã‹ÑD;=úüKçɰ]ªøÞÚ§Ë€dóv?rÜK3·W #¸±‡ ùñSHbŒ‘i´ìÃtBe*kt±äà€”CjÕlä펨QÌŸD?nžÿ N}$̈??X~X´F'XGæF`?0u›DŽZ7ç&¸Ñ`R…œ(ØV??{Æß4¡ŽÜ2àˆåü^y"ìÞïûÖO²ÙPEQñL'0??sÚ«´ZD® °ÑÐ/x¸€³?0!· o»çë|¡'ÌÍï›ëÅ‘‰«Ì™ª,kÆèøœ}±˜¿–#éw~È;|Da \„«D.¡à†"©!FbxXx]% ÂÚDî[09’›I+òÄe“î¾äRZ??s•à6äa…¨¬šÏÊI#Á+}ûJ¯û=¦ÍϦ4X­”Ï@}K N’o¥UsÝz§¬µùC^êø-??†G?0‚ÝŸdtÊô«[@”ñäG1Ú5Myhhek‰tq –ß%˜gü0.ŸÛQ‚vž|Ñ#_ãðjÐ’ç þ‚Ù•6Ó<´Á>“žÄBSHþE¯mosïrD/Ãàc`ÃqîF³?0wÏaÖ{'oÒm·,6”L¼(ª—`tZLÉVÿ`$¨è¿dº„¶*‰—Qcž¨”ût=5„G ×®áßšN–¦ššó678×Ú„+û-ßÒߦ¶ú1n’ó8«ó?r›âõØ1_¿Ù2úÓò¤0OîÄ:QW¥¹'Uä×QÅ|ÒeC‰æŠÖ•Ê0KAò?rQ*Ž?0dX—®ü&û€U³Ã>åëU6ë•G÷‰>»;!Æü‡éƒ›FGôiYp¹£!järsRãÈë!¥QBpÛ09¼ZÂY=|¸êÒr{?r"*çXXho·eÒÚüèYHFWгpbPlêkæ!mO1LIZ²Õ’âQwá1kåÍ©èa=&ªjkbõÛÁŸµÑºT8 ꣷ>W3 Ö_®£åTDêÑkž?0y›b@[co¹ ùd¼v$É…b&å?nã=—R2—ÙGP»A~+g¨ZÌߘ³ˆº +#Y†›‰v™ÕZÛ`pèòæ9!‘RƒâOx¤4ð/¸ð禑éϰìÚ*ÒÜvÙEº1cé64‰Ø&_–xªxi÷Wi¸j|ô ß),õª°.û>~œ:?nrŠ %ÒS¥î üË”“6³o¶hX)?rïÃAa](ÌS„Z ‰¾Á®'³³+ÒÚ&~OÜ鶨š,lº ·ƒÝd„¼ÎÜ%&mI±–#‘”)Q¥BÎO åµê§?r–§µð.o¯ôÈã!??ÉÁn§i-Y!üZ ÌwË×–3tý.C†Ñ ^¶ìùìŸIÜòeüO®o&5xžÊõúè8^Kxâ§·ƒI)L¸5Ù^¥r-™¯ÎÚJ=+Ò;í÷),=5°äÙ¬ŽžtvFif´^Y¶»ëáÞÍN¦ös˜0‚+Ü”+•Ÿb/ü‡íÆm?0ŽF™ì×iµO¼KâªÁû¼×lÏ¢¹»ô…6Æ Ãêãp?rð\áÉDâ)ƒ—÷Nž9-Æ|w²»c„??†¯?nî’(ÕngBñ„L?nRfûŠçKô}ˆ®ëíhÓ`?rõëì7Žá“£Q!!8Òž®†a¢PTFkï—õ3€Âetqj"]…çKMÈwú¾¬«²#öÿ6’Ïýy¬¨V=]ÂÍ´ω1ôWbLýyIxžQrÄà«hnì¢j5ÃñŒ2HS¸²0ŸA_~̦C^%—´*ŸòNõB™Æó¹>Âú¸TTµ*>žuÊoWîvðK­ÝÉÉTkŸ{ Léž¹ÏøôæúÎsãÛ+›"¬òº‚{ígq8å~X6óå‰ÓX¨Ü‰b¶O~ÿFø'ô†…êÄ0E²¹ŸÅyˆ1Цe^Ž¥Õ~L›Œ~­ó"ýp?0ÁBVû.£dlól‘¤‹@ÜN0p•²H1Ž0oçþ"å™¶§%íUš«L"ÊZ?rPf/X3rÚ3£¶ÎÏð¿{@Œ¥;NŠnß1d•©† ËÁ’†w]EIqZ¯2î¡=²¼Šj¥P2ôM/?n »?r®"¡ª^5€.?rº--ëûPÌcÛ0TÆf=àƒºï'òò˜\‚$ѯȿé-È÷¸ÉÞ×OLuQK3²:<:?nâù½‰ eýó•WÉPø3?nÍÊÃö-#Ùl´™¹á€.D¾¹¿û÷` ]ÏØ_Í„H‹²&ÈÝVÁ4+¯'T‰¯½Ç|Å•ÖVOa€R¤ÀóM7õ}¦âì6_qbi~–dÔd²Z8¹]Iå™ÆÏ„ãßËé×ö ?rdþÈñM£÷ËMÁ<·˜õá??ô”šùÒÔÔè°~2…¢HdC>KD€»"Vý²<•óÃK$ŒpŸ»pV>É œµ †p*ö¿¸ß´$™.zvˆØÙÑTÕP"•}†£ÑŒ¼àyPÎeV˜pzÖæWœS#ÙKâ?nŒÐõj(”âôB)Ìq góÑ5(Z˜Ž®Zv¨zŽÝñ&íYîMÐÆ¢æ¨ìÂ¥gób{hk½%bQæÁNåpyTrôÒú22Jc·gö»™‡ÏÃmçÔ©Ž•0¼óN.»¾]¡ør?nI JËã³—±æ*K?0PBðßq¿zc{¼oDì>‰¯â›âН‚Q‡Fc5ÚŒ{_Ÿ¼m†o Çh“ëð?06‚ƒ~P$¶×(vÃOÌx?r²úeù??޵ Ì²…p~Eë›t÷³xô´‹}ažPZJ(SNÒÔ1:T`ðêØ{ÐÉn$0{ãήtá9ÂG~.ý¾??øÄ¬Ç×®}Vô`3Ûe˜®p36&#gÏ%|§@&î§«Ð!ܓپL»3ê†Õ›ˆˆõ|¢ð_=¹`”Ú»—ßÝë ½üæjšðˆíUi°†î‘³ªQ¢cÆ´#šü2J©˜2S’¦Ën–þËxÓÍâøy“|ŠvG¤Þ³O?rŽœ?n‘Й*H½Xרv€J‚’ó!8W3“«‰-eyÒh òð2ßá‚´*…r??(¹¤ÞHßL!¥E§¥X_Ñ’nÓÎ1ê8UßÂü’ïù$­ÁÕklÙÈP°tË@3?rBó-·À`mFJíÀäæ(Ì(âßCo¹—b]) ñù²Ĥ™#Üå̈́ƺmÞqúNµ«Zý¥ÙK|0?n®DÞ“9wúÏ}ÚÁvgS²áo=ÁHä•q4N«ÃS,[æ 9B“³8q•%Ï¡T3‡WæO»6 /ô©0_•^unuÄÈIj>v{xˆÊªÁïT€Èpݟɶە|úêC™Y2¨¸/ïrÿ>(6v³ï°ë…«º¬Ý­êEºò| ÈÃ2Û¤ÕJ??AÓLÍdçd(ÈmÝëùp)ÁÞÞES9¨‚(Ñõtï{Ì#q|7XŠ}'’Q[º§øH:Ö >ÑðÁçÍò–<@‘»ý;ßMºüoÇ?rÁ­C$;IÃY§åoAl­ƒPÄ‘ÍÄe²©•¿r]—­›¤þ\¹òª»;b\¬F½Tô®”‚œÌ›zW^(©hÙe+% ­5ÿvÑØ’”ˆFÐ×??aáˆTn(´ŒV=Œ‡0sCq¦?0´è{ïó§=ÖLÛU!zÊ*À3K÷ì*Aúj嘖ƒyІ¢^ÊYSÀS%a÷€KR»ÆÌ Õv®§I™†&¸DI­[J Hœ‘Ñn1Hu¢3pZœ ‡WÍfaKö!„f9"$[ðq{RÙð k©rÇ#'(/„j›õ;wð Gà&‡Ò1DØw•æjÞÒžÌË?r¹¸"A†Ž¶ydí¬öÍÓÎ2>¬ÂðŒßêižj}£m6ƒ#¼ „aÐE<¼Mzáè,ýç-]L,A{Oˆ(Te¤©–Y?nˆµZÔnñv\ƒ[åá+=˜Þ¡hE€[ÝÞc>Š[FB2Ú4ÄĘÝi79†ôpý·?0»Ð©ŽÓ#ž7 :FŸþdS§#‡äl“©§ 4 ø™>ÿ¸(ƒHfüÓc»È­ìR1Ö";£@2ÙšF3?rfgï9±.>†¯ó@7>oŠW¡QQ&áúuE~ÊTg%úT'.­;E›W„·Ÿz™³öq˜?rŽ·Í'%8‡º‹`"ˆ! X?ný6ÊŽ¹}s­{Ñ`ï«=É…ŸË*ÑUÓèõÀ ž+‹éÖúÎà@H˜I`¡Ý„Mकä‘Ti”ܹÄÖºuO»çr ]­¢ä9¡¡ ¿wžY=Û³°m¶DCœu±aŒu·ƒ†ûQa£ƒ!¶ÆØçc~…Y–µ^Ctì6½¼®ˆ¾ÄOÿpQG2“ ønPF7#²G.ÿÞVC/¾LvÇ?n9b`é©;<å‚Óü]ù¸:ï‘3jŒd”O{„›ÓXWÊÑkdd¥f÷äƒÊì??oÖÌGqkLË??óQ|€ûR­³äYjÜñØ$jý4®?0/¥Ÿ‡»Û ìâȱº‹aH¦¯ÞÉWœ}×~`6 tïÜÓÂá$—Íà=ÕÎm(䦬vi¸º%°ý¤‡4Zm£ÝiØÅ«„Üq›Z<}É-¤Îo þZr‹Li$_ä˜ôAÀÉoè¯ù÷œPÒ‹˜mmÎ=Ãz]srµ8v!Çì‡N||X„\'ú}›[Wo .¿S³²–²|Ø‚Çnôc¦'•Ø]_à\ýNšgÏúÖa7p«©õNcõ«º].‰]­C™¬6ÎS£¾Š¤ 4ª¥YÙ¤ÐZ´^–õûúã?nÖàcÑÉ´î¦ÂJñìV¹¬üOgRü_£)[5dlÀÔ&­µ9éK:Që”§Ï‘lT<¿ìŽ'K8vIŸ˜‹ñ@×é\/â:L’¢áhž¤>ˆ0¨2L½H–GQn½‚ZË~;2ñ×u§­VGèiv¸”6¦¸qpjìB›(?n]üpjô_2 y?nµl<'à„Õ¿Åö¯~Þ~vûïO^sy{Bع;Té˜< ®¡û²úuoEýŽoÇiñg>[¢Ží|íBíåÐgàIáäx§Ð~èP"óK0àý”­Gäíg'M^‘ïéì&ð¦aÁ±ÞÜ&BL%YŽuIX7ûã²'H3gzôJ=zÚÒS»»?nbŸˆ›|]S¤??[Õ²¯ê¨ß‰’®ed·¬¸À_øÞÎ1‡zÚê]†Dd‘ØHV$î@–úÆ_&-•ÇÑ÷w€.DaåÂjì€B¶«mâöË7¿Œ6z«·ë–Õ}“:“0p)êös|UÊÆÍÊ«TÔÿ2LäëÊe|Ù@œI1TàöDWî*Ñü\ÈW \úýߣÿ¶ËRJòvÆ£?r+Äi©KðïVàäwçØê5ï{MP¼y³ïÀX{’AµVŸß¹ÿE•w]¥ÔþÐKÉÃgïn´°ˆkï#¦á\°2Q©h5áY®Ì‘h³c1ÜBÔ}Õ¤ 1Û¯!zò-¨y+r­ÂÒ®˜Õœ+-pÀ‚¡®"îîKÞÀzË?ru¾®Hš`A41«—û]“„ÚÞ *ÛÍ+‘O½eÍ¥B˜â>ø¡Þæèý°¡=I98¸}µ¹pAsut!Pgeîª ¥bl¨_4¤ôt1ð~?0°ú’Ñ‘!½#b;op?0Ñ]¬ƒ“{Ò$‘¼1èçãû;ˆæú&=C|O0š¸lŸ¹~#üe°½‡¶¦/<írØŽE²6' ‹ºÄ’ÛÿÏÞU&¸±#á«(Lž±ÃÉ233…™™Ñvxx–y-ƒ9æÀ º¯“ìWŸÊ’[†ç‡¿^À “ªe«ÍóœwðåEÞ7ÿ??œ%½è/q>ÎÆÙ¨Õ£†2“Ç3"%œY°«¹ÒØ“2ZŸXú¬†,gñ‚óÐg4ºÌVMЗ,îØ‚oBRýéýå4ó4TYÖÉPe 9û=ùõDX6$ÈÒ?0I ±¡Á"u©Saúˆ+fó”ÚްTf2°ÁsûÁÄÕÎá»*§ô¹vˆ4vÁI(HéŒG!ÇOë"» êõ4GòÀ!?n7¹K|?0ø¥…¿šXG¾’þšpž8RsÖZ^ôï§¿™0dÛ®¦””ÎÛœÿÐx_á ×ÅJ‚-^Š*žbfzz÷x’™5?0ɬñ(ß"<Œ?0xÄ@†ÚÂÇE ½"0œ 1B}ñÝ0í‚?n 1®{¨Ð'¬€†nDªM£­R‘ dMKa£ßY)œEœå¡^ƒoC¯ah8Ù¾ŸÝÁ·m{«KÏZb²= MBë ~’’®£Z½Öq޵Ñìñ¿ãjR€!c;6Îu??«nÁêÕd]I‡–gWK¯þV1Þƒ1'íÖÔ}Ÿ2î[鯦¿‹ê;2&<ôg]è!èÍšZDI žqíÄ`DTa–Ñ\r¡þº³z¢8 ,iœ=äOå§àÌÂSOom^2žÑ<ÏH4œ4+Ãj?? žp |‡«þׇ\3 B}žPP(:ô¦k| ‡p{ð°GÏë¹ÕÞ¶Îp$þf\¥`@ Šû’<}„“ËI_YÔÃØï8rQçÿý&úC´Èç_Úyùb??~TH/=ƒ¶WºÏdWm$Ž_Ö£çýÏYÜNÆ3nò 1yÂ*A<Ú’œêÃ:N,[Aüà??èaõœ;$ââî‡ ÍÒZwƒzƒkéêT¹cçx5œX÷¡ÂŒk¿¶ÌìëÍ/K…Qv]Ÿè¬S,~&¦Ií¶{èÌ’^˜??ìáž%n2ÄC,N;~§ÏãeñŒ6{V‡aÜÚ ¢»MÐÖNžsw7?n3E×wí æ8îW®_±ÁA†Ä3D42Ì½Š¢?r¨Â_ëumÛÚÃm)‰¬K·Ò¶e¢,˜F;I…¨È¥%ª°í(Û7¼Ç ¢ª×¶qi0a™@©!p‡V?nM‚7pÔ4q[܃×à&¼áÞq­Y ¡$¸ÖUXkj‡×ÿêk”Æßà%(¼n¥)O½Ò)C?0 Ý,öÞCÓò3W«C6´Ðck)Pϱ7³ $H"x]#³ÜÈú‹\…ôëV’«½‹ôå‹7ð‘v`íÛ€}R-¤…èÂÔá’Ä®dT¢ 7Æ-MKªãNw:œ¹/¡.ÇC¦ä†4\yJ¦ñý¤8X·{jó–©­›“Rkô%A·d¦m¶lýÈÖÌG2™ŽËûŸ??Xþ3°ó9a^ú“3%keêçºë&Ý‚ q«щE@[Až é0ªNœaþxúˆa6”%ˆ%h#ÁŠÁÑL¬È)­Þq"5ãƒ× -¾fï(µm:ãXŸ9¥'Šøâ#ô>xG…êÁ%¥GfÝÆÍ—§ÎJáÏàôÎdŸb÷?nZ²ržÒ%.µxú-èJ=mHÒ̾§.±@Rõ²L²ßácÁ“¤M™L¬$R‡¤~2Gŧ:°v€ÇÌÈ2ì˰ iÕ}máQ 9bò‘dXÔÃÚ»ž^mù$ÖO”~Ô@’Ñ}ÌúW`Ç\¾— gjBׯƒ–ÉÓXE%+ƨäŸþƒ™é:NJ2K©×Øns%]¨³©|l—.£ÎŸai×,Ð¥:®thüÐ^ %Dè]­<+‰gµ–Æsd—£÷Ø–V Õƒ tÑs(Æp/Úø wÍIF··©³)Nh=åkÌóOöônH3q"n°_1¶bDWE£ÐìT¤Ø5Ö]3b¤ð!QbGÏhå2.ëÑ0FݵI£.Ά‚[áCœî¶ž!3·gr0'XKú(û+fœ@d'Áè3öâr1Õ60‡×+ê=<ÐÜçp©6Î>Œ3|/þ;ì??ƒT51¯ŸB«ÐûÆþGØøîw#ãFE?nà²×ýh5Q73223222,¹ùÿâBœŸ•„.(ýu¶·ÆTÚ³ù–¸J²íî´‚Eª® èé), wå`½ŽÍb™¹(8k¡Ôƒ¦¦î&î<hÁJƒ03z0Kͦ”åpb)ki°[yz‚9 ûŽ,l±‡öòÉÆt‡E.Ì“Mä[6æ™m‹“ñ¯yIäò«—üôq:|ù9ŠZÿÕ…“aüÑ„qbm$­ºMÛüDÖ³R¥„}ÔY ™o(2È:Ëâ‰yûÎ}ÐØ‡…wÞÆiÅBMlAñ«qäÑ‹Ì'¾ü¬c,Ôñê°81˜ý}êþ’Whäú¡ZÄ«=Á~kvù'>ƒO@€û®Ž‚@ @f’Õñ [»§†ÈË| +‡Fæý‚™¹)ßN£w{~sêaÁ:Û«{¼.î$Óq­n?04ð¬–zßàyôTvõÚÎ#…·ÔV¡½`W½À¿ÇèXf·—ÅÂSëˆÚ¤äZÿúDtYý·?0oe õ§®Z~Äcµ„ºÝïà•6Åîç½& `pò?0çD1s÷??©#j.ѬV8íÂLxŸ1x¬–îÔô‚Ÿ)êgÂúR‚¥)bœÖ锘}N3­ì¢ ©ê),ææ)”Qh&aªêD‰—qµÓX71*ÝÑu´>L' ƒ) þQ`–ùÊoªå?n³QÍJDšãk°?0jNBbí†L(à_€à,$ u×ÒmðÌÈCXó ÿöÖOß‘(¢?r|† >w=L7Y:°wªL¤òÌ&ă—®&M*^h®š ¿ ùÍ×dØ0-t?r¯-ʨX¾Ü#…7r æ0½ ä Á@ÎÆ¯0¾üæþáÇ`ÎÕU©WÞ1 lj‡­”´­ØsènPg–x^[H[J??9oÓy¿§4O‡I {:ãÝ‚§!0¯ˆóÇ4¨ò¬‹jBUç˜ +#ãÎQòkãb?r2ï±CÂçñº©À?rÇt1[P(×E?0ërñ1´*låøcô0¥²6ÀßÌéh’Ð…R7¾ú=T2ÐäìœtÀnÐ(÷‚é£x’À?rõ­›Þï M_SÛ‰üiø'K!nt×KCpÔZPìMò®I¾Yå¼ñçxªå”qpÈ¢À!©!sǼ˜¯CçÛõfÙäE§&<ü$Ó¦Ê뀾=F·Ê¡I+«~Q —g‘Ü„ÈðjÁ[ähw[Ï€,Çœu:iÊC+ºQzU™1’ê!à'MãÍJͲf ³ºžÙèÄäœsñ•"úê1˜xÍ:4(Õs¹äBÛví¹LêÂ,ýž­Ôœ¯Ò,ƒëèÐNZ¨Ã•ü°¸÷JqïõâÞÛŽ÷?nüù£â޻Ž¿®7å5'Þ)îý´EÕD•ÉROq"o…G¿,UjÀ” ðÖE^séc§µË<³¤G1Xr]ÐG‘R±ÆJlÈÜ@|ž¯>Açt=žlKÈ??É;y8kiöéêfö’qPª‹ºÁŒ£†ÖèthM²žÐ0"TŸz™¿áûGEi¨9¬Œþ¥²ò.6PBZ¶Ù¡«¥ß…í´äÌ+z??ÁG´ÃCpKpð?0²ä'‰¦žÁ|ºÛ‚??uéä°×&=¸ÁÆzë¡sŒZ¿úÉg¨—Û²›ê`Akb“] »– UŸ8µÏÍýéjk†d†BÐBï#ûÒök³µ??"£±ñXƒ”û †­~CÖäYÉÇÀ¨he랥c~[»íÍ]¯ñf$†ôj%£Ç7¿õ¿úC¤[ZÇmUNâvРÁDÀ4Dêq«†^t.ã5¯Ø4äûer8Ž(wuíð-(¸4prüÕ)çÖnÎ=åÂuGk¨ä÷x,¼D®Î×d+òßë‹*t¬?0HŠ;,-y†õx‘ÙdÝÞ¶jRÔ[jгoL™½ií:ÕÑä`^¶BlØ ¼ªñn?0‚Iõ|¸‰Å¸8½&Û½??6;dRŠ‘}8‹ÆB®,›??ɤ¦ì.æáµ9öUn¦Ê>&‹Í-Ú$fÔR3ôޝ§–A¤VZ­)Ç·7dµôóÁNVM‡’?0‡Jz|‚Î(}BØ¿\§÷‹~Öoøl§3ißÔÛÜQë0RŒŒ–ÊÃlI&©fÉ?nÌdc¨ gVe-á6-ÛÔm*;ZÓæ7@ÍÐc.§oˆ(%Æ`«/r¦Ž8Ux,;U‡Aí´$=Í/æ(9D d„鼄 Î:´}s ~•P§¶Þ 7x™óàXÿ^§É³íõiø“§Y°ækÓwhТ¦_O8˜ný&OÌòüÁÔ ƒf™LÏÏlÂuPóöPÞO?nLz·¹}ãÀ4kB¡ü,ø½¾ÿÃß(þá‡ïòÿR*ìû· …Gˆª[?n`Öog´W¢@ÙÊÈL£B“ä‡)qLÎy@?0ʳõ¾_Ã÷G‚¸¾œpn²¦Õ\Ò.p;‘\yÏW??õéO|­ &'hÛƒg*®ÂÐ÷ðMÃwvèha¦&qJ2úžsOÌMæLÝ5O<žùŒ'-F›æá~aûþöXi0Gêò¾$z»ç±dJ¶¾¶ÿ¡Åýo‚4›fªƒ82ìW>þá}þãË{-?nž|.pú>~ ö3Б¬vÐNnëêÎô1)ôq9_‚¡Tzºº¼3ÜÛÅ€.’î–?r‡÷¡Ù„ój° $!1ÿÍÁ±üdgŒªÄpà^P†ôoý¾Š%ÐÝ‹ ƒ[%(gU¬ö4U9E?rÙbQóÙß§ê??kG× €áS˜U%ÌAQ&L~¸UfFMK%~Ú št¹˜1ë5…²ieJ÷Ûjì{§|¨Ý{Wþû¶üïâzÏ¿ÿúÞßn@åÆU3'˜š5æÐ”pú¨¢Î“‹Å?0ëøÖ1!šþK4ô ZlúÁ¤ë渼òh³ÖW?0³â&ãe¤vß#<›¨¯ GåÒS«è¨~L²:êÔê@Ï™ëÜq¥?r½Õô!$Û®!î|6?ndÎ¥qZ-ëœF¥¬¤¦‡4ʪÚð£ A—9y‹Œ×u&—º¨–è©ë]Ë¢B7gZÈÒ•»þïP²Â\0DxöªC¤ù­p*Ó¦˜÷­S_sƒ“j݆éT,”påX9u‚ž&IÌÒnî<“’4ÜÀ̹4þnWËÊM]ú,“,Æa5'Œ—kü2£Ï–ôIo¡1q5\ á¤Ò3XKAñízÉkRƒÜÛþ–3r;Y!Ë?0M‘??FÌT®w?nó;ŒKšLÒFV'E½ký ôlÜ:Ê ¢ƒxº’WtÕžãìSPK×bÔµ×Î2Ù5ï9L™úî§Q’ÆeøÓdDöƈœ¼6Ýåj3jÄ7ZgÉNÇæ1ÀÏdÐWÈÔdŽÄ*ØAËŸSþŠñy¿šÈ)nÙÝæ¯k*~ÌæVymâ2áÚУT7øóŠÃ‰"ס×x"+™ä›zAäí=hKê@ä‹eó ·¾f àm¦ f$}›ÚRBó2­q?rŒáÅ;\ÅŽ` ù#S£½ƒÒªå‚ãÁd·´T †©'Êa¿ÈJ“?n?rˆÜ,¼s=èÙx ??p˜ŒêfÊ8?0ÐÏ&Ûää縂’£aÓ[ªyl¾¯ž??j\¾Ë×°€Ò$ù°-,®]ůÛNõ§±KªKØx$Pd?0”d¶-?0Uo.}?nEàͶ®“IC?n~P/䃤?r!sWãLLÈÀ{2ž œ:ù9>™°”^läaæ³}¼PÛ˜.)»îp¢ p˜€ûù;"QS .= æs—pw*ì¨ò삱"<غ?n„5B'ïÿJtß½a§M|˜áx¯ÌQø‰RrÛ¼!¤õžÿf{³ø&Š{÷ç˜ÎÃ^?rÑ»o73æ\ÌcM‹skëzq` Cˆ—³)“€Ï®þÖbùw±B|dñ\ÅÈpgI¶§Úâ\i öf,¸ oŒâ›þêç'“7“%뢛3±!N¬ÄÔ³Kw¾“òN,0Ø_:´Ï󀞭Ê[¬‡ßå×PÙßÜõ@@ÑofYžWô]¢ƒéøyÌD]þ½ˆÎÝØ`??˜]´’ZÃ3¸†pØäËPe^bF?njþ½M‡q«ÁÁ’w°»œ4<"]K×…°ýJ?r[òøT_Áäªô_Ó«‚ qFÆ5gDIÒ|´lyÙ,(0œÜ8ö/u9ŠÖ9äK|œ×W¬Ñ¦oØúØŠ –¤.V?0tÇ€.áXæÝzÇI††®eÓMš¦¥B§¥Vgø˜˜<¿YeäÉT}câÀHiwBÅ–>¬¥fbÝŒ ßdH!,Ç„)@ctˆ †z ê"ÀÀ„å"OŒÍ-»?0÷¦%;¬ šQD¬ðÁÁ^¥X©¾7<8é‰cvöº$” è,`Ü;Ò«mƸ\Ã{°b?0E‰§ªm†âÞÑöÛPúLK³3ù̶5Hþ¦jV›ƒ˜…æp¹3P‹›W듞=—6ì–7ÌÖ–â:Û ûÞ-Ô¨HÝåxÂ{V~Ž™³üF¬×Ú§­M?røn…U65ˆÎfèÔì.û“mÔ³]ÊÓêa>ˆ= îJ¿AùS;Ãh·³_•©#h’ˆ’7_/.îÅ2(vlòeµd`VP×É›ÓåKcâHSÊ+†q~.Ë òä¨åÿÖXù”*³í‰¿‹èŒI<6à6!ÕÔØ«yÍÝ&†Lü‘>›,å0ÜI5’o¿žë°,ø[³÷Ë©R»áÔ.úˆÊ*ïèÔ¤ÕoÖXz8t!äÊÚ§Ï„÷ô¾fR/þÒ%ý?rø±¯ºÒèÃÓéTMű€ò¯Å‹¬5±nþà ï%pƒÑ–£Â#Èp9°xYÝ1£ ·Œï["ø\‚ØÐ0‡ü9!4F@„Qf³}UÚ¿??ý1S;å§R¸¿‹°[X2ÍoËèë\g}ä¸Þ"3ì >ËØQšæ/KÔø‡üR—%»û?rfÁŽûÇû1Au#›ž! .ªTƒ$Ž`H˜Žir6wÊŽª†P™c¹Ž5Ë’µ@Ñoø›ñÀ¯†zP;ƒð¢±ÇS¯l5¸?róp)ðͨ1mCô‘̘œÑæ¹%ðe®÷µ;`h?0Í}È??üìÅ$FCæI`Tœ¢V;‚;[^7^??Ÿ2pkB%‚6ª)<Ä1)“U æµU «-©ˆÔ%ÃPþ-m¸ô.3†f HÌ‹)‘‡].?0Ê»„î«,&ýӃ蹘§ùH¯0ÞhÄg5ælÓt??2 XîþáGo?nSL¿Ò¸Üh&k N©W”Þ²Tj»ð¾+€t‰ÆmÛK¡‡ßøâÄ??(“>og;Šÿ5áÔíií ˆútà!Ø ”8n†•nOÇC?0ߥæ}½_Ì6¯CRº á‘á|¢"ghÖv6‡»ÀPíÞXrÝ!°cJ‹ù*¢ž5e©.MB8?rÙ¡ˆ3Sú‘”htXÇ"álÛ¦êõŸX9å~œ*è&Þ5S§9¤Xgx ??î>¾C‘”ÍCIÝš)(¦Ø@ÇÙùÒŽH IqkT ¶dãÏw–†|bCüX[š&ƒ)sG??ðZé"ªõ¿C¤®Rn(x°—ñ,)ý´ãª?rÇ X.ÊœGý¶>,éìÚß%˜Íý=I¯8¬ uT[jz‹”z¬ä­k©ã¨Ûl4wTX3Å?nç )« Dm¾OþFC+¯Oû`Š?r9ÅL@;~ŸŽ„{æÓš?0#4ÞÒËÕ Æ Ž‘Up½º¤Y F1¦Q«Ñxýpô‹½ÞÙ…Ú(¡—mœuüŒ‘j&HZ_±¼XXÓ/ꉛF[æqäGô2ÓÊ2,†©z¥AñëÒ•öfcQÇèHáåµÇ–Ϫ¦—Fwio½›­Ô[þí;¯-­=&=ÐÆzÈ"˜üS7q馲U„®ˆa9”ÑL—+ywÖZ2ƒŒ˜Ò=!0¸àâSXøE÷WéNÔv—½êþ(·èÊWGjÛâ'!Ÿ¿HY+ùì+øè;Ó“¹q–HW˜h³Z“‰ìšD"ÿÆ??4Yðuwï\!Pç€Í6÷RSåóÅs\-Dq›ÀË?0㥶kîÝí¶c¹8UÒ!!Fu)®‘“é'Àôˆìæθ¥Jòb~uimuuá´²'­]”ܸ[–›-…ªöiÔ­"tn§{õV›š“ŒM=ªR0øæ>J•Õ5Ы1»£O¾¨1;NòÆ €w¥Óî8xkÕå; Ö2j€-rŠ4wr`V—ÏG¸õܽÍWUb$Žt¡ðW뽸gçV+;ê4äH¬ù%\Z]~Üõ?0™•™\êà-‹¬¤$8nüÓ Eú­ìþ*!*½2¬`,GŠT5™‡ÎÔÊ&oVÉú|ü•w‰S'ÞóI0ÁéÆ|ªVÝÕÈåzªX[0Õ¨FmãåG°¨t9œ!bèAQýa@8§P<ï4Y®G~åM%„ß©eà¸×ü/V€ÇË‘ð¡Ý*Ip‰&WhÑ??ím…ГË$ƒlÌ3H÷>àšéæ0ÝóÑYªõH.C”!:Ÿ<±@‹Õ—~Cj‘ª±I:™—?r¨m0#š"hü\ŒÈx|Ä„8©,­??"ƒrÖA|d®tå"øt¡ªF8fùåâÑâå?nCÊ ë‚<î,AR;¢V ‡žÎÙgǧÔ"ۅʺÒ#ÑË)érÖð7Ê/…ü ÕTeæ«|š(Þ\ô¢¿;Àrã'`÷„œ·Y®Ršˆo‘ÿÞ&Å»Š4#’5 ?nÍqt7(yØ<ß%8$À„žž0Ý­\–®U4qdµ@ü} ž§¥;I>”ZxG&KÈfÌd¦Š½2€6Èq?r¾#M‚ŠZ ìsQC«“ F«…W®|ñ³UŒ¤÷Ñ­aùt7vgtåJ=½|L.ç½›zïÒfŸ‚‚s¢áÓU“/>¾þð¢úgðÙ;ðYø?r‹ïs¡íز†,-øçý²T¹ç¥¯<)©ªZyMÚ¼Q¸ýÎ[ÀmàŠ¶tC¦˜«5ü/×¹zõA"âDZ3Þݧ1Ø9--;¯©@/vÕTèsÔ/ª±Ê¢3¸fJL<Ït=A ã PÚÕée©†¥z–)¥ÎkOv nFµÛÍ}h‹ÚöŽöÇvÓw·Lãc¶2¿üŸ¦Ó„Î$˜|¢ÓݺÔÞë?r!̪O” ¾ Gã&åSã;É‘kæòçKRwA®ÔÿñÎ74»q‚¹éØ8}Üq1-¾–6ÙBªͯIùìU¦T#›MJ?rëZÂe_9ã.æ2ìã¨i¬p¥…xs¤áeê1¡•—d)u`Ujh\¬n‘cÞv1É9ï{ŒRSêMT±XŒ‘$š"0z?r™½ÖE‹Axv Y\:“­ÍÖ¥½ý~I?nO¶:—Ëd›»ÿø,LC¶µ´eÉ5-W]Ó_;Øùb2“†x!PÎZ *f:"‹oìG‰]Ÿ\‘)\¨ˆ‰íÝ»ü¤öÐþ°Tt:,8¼ ìFT¹,¸þÝi³.ûÇÇ¿f›YTÉÿU-h„XÁ;1•Öî?0+èœy\zPži;¢>ÞYS ç0+%™°tñpš3xl‚ñœ"# õ†mÈ–æ .þ‰aéznP|dó§6~²Ò …hN\f’(17ˆ Í'`)Œ_†ø{oªÍ+Ô£"îQRßò—>ü•¯}úß«H0¯’ŽÕÞuzœä½çßv¦Ò ^_(ÄQ#Ấº;²èã͹Èa[d^&K;&æž¶a ʤ‚ÛB»ÔûNéëÆW+ÝXµÃC—ôqYÉ9Ãv.39Î;n§J2¾Õ¿Ôîµûež$ $¾©Iõ7äìjò$M+ nËðÿðÜÏöįôžû¹´æZûÃV±•Öñ˜XÆýÓŽÎÀù™ã7ß²¾ÿšæß›9¾íû)ÇG™€pÁÅtó@æøÔðî€N§™3ºÓf G?n¼ù€€ûý6ð½…üó¹t>;¹ÿÁ¥¹Hq@sQ›–´ãmbì>˜Ú=ºNeÊ'@ÿ~4®µtò^ßD*k³ªðÜSÀô߆%nØTžäb¡,1;´¯¯Ì÷O,3`ȪI¼C‰1m”’Š‹'Žå²X.˜Î1µÚÞGxXZþë:(µö9!mhpÁ£Æ¡Ð»Ò²Œè\¯ô­}µ*r@q9ý>Üët­è¤ý¼Ó/·ÚJÖÚ§VãÙV’©ºíÊí÷"†›Ÿ‹óŽ>/†?nF …©©P]ŠvgFåm51ÖPœX!«ùh*ãò—2©Â,c9r·ye½˜rù í…ÔóÊãEu®9" ê€M÷Ÿ¹%xd?r3Õá-3¯å@ùâ—ÊOúâJ›®š_î'8÷açlcqd­ÉsÔ(HSe!Êÿy^ŒRºú°cÈÆ1êïb‰fM{WKX^, Z  Ã8r ëŽK=<®^º]Ø6¸òIÞ‘-Í;a<±Òú²Æ™Ås¡gSÂz»ÏV?n¬7ï½-ÿCñÛ7‘GÔ¯ O’Ö­ËÓ‹Y6»œÓX½¢yª(]ºL…°\&ö»ƒÃ#˜ÂZ ‰"?rÝl Úä-‡¥*‡¤h¦ ¬4ðÂ:k1Ò¢ßÞn_Å–¿-^ ûv‚{…œ øZœ"&V\!?r¤`Õ ¶¿çCúê4ÀŸ?0®LðôÓÿùâ¿K¯àë´?030„$jþ|ÄŒkW <±„z?rÊ€¬">Ø­riOAfEG6Ht œ"»Y¨á7u‰q<`P½½V??@ågÜU݃E'›*Þ:î|¸c Ž-•î’»ˆŒ'—îÎxAyéÆqq-¡à|–‚_wH[ãüFæ×‚w‰û¾¾«e=¹—¦õÛb|¿Œ6þ¢SwÝ)¤8­$3“"ª«âŒïv†»m??P>+T|F×\K=ÔïÎCW0.ˆŽð{ù!¨êUŸZš­¿Ë”!ßKÙ^|Ž}õ&}Iwñ"öˆüæn¯ÑšŸv×W=N/W‘<õÎÔG¨ë…æTÆàéöbé§+¡üÏ¥Åïe ªÙg)”ŠÏõš„h’t£ÚÒ*?n%&·AíÜp*iݱ1-WD%³øøD>³;©ÍîþnË2pS)íäH´©æ£cÆÃÆvJ¬‹Óx\ˆƒ»S)½Ó· e‹z))Äq(=^ØHˆlŽÏN»j4;PP>ˆôMŒ¨?0M¿°Õèì[p„j§_ž¶©w·¹³ÎHÇ–‰£n~ËÝÍ4 .Xäê Ååcê䃥4¸wDæ?rFoJÕŠžü”8põ½T=ýI-?rÝÔTX']T‰¬•M©ïÝáîØó9×]S[D¶ïj$'¾¼¬ŽjNÉ@}l´b²™Å˜¤óN‹V,ßÄ8ƒßüÕÍÖÂEE%Q•x6µ$Xªße‰÷î½!RÑOïý’Qµ¨ÂE×q“öšbyˆño“ü k‚ãX/G—Ïc¹ñk¬*ma“[èª6fç–@ êž™=°øàm:2ZÌ8— *¸^Ò1蓞î–L–¼Þ¡ˆ_‘Œ¦›¢eÌ!gºQŠ sºLºNá•“Ÿ{ЈHZ¼ûóÿÓ‚öìýprÆs&é›#}aYæÝƒ ýÒ·w\íhkŒ¯Æi…p°?rxL*çÜ(‚6b²b+‚¦ë˜¯ÛBJð6o"¡?0}Ï-ŠYƒh¶¼F@[gò?næã›$aë­©jyÄK¤‘¢ä5JMœ½5·|bî1Ÿ KZÔ‰â áz2¢óY“¨?rqCÏØ©)Oÿß/T]#¬!t.Ò„@Œé…1Jå–Õ)ã†2:`":‘¡9¥Kžyê0Q<¼N&ø5­»!ß k/+àx$¢˜ÓÌw‹´nL<3·:ý½0Ñ´ÑÓDŒDÙ,.î©€õPò‰e êóQêäëÁ2®”)-ñÓKÒŒÃ}–¬P<óÑ›ÕYŠgFz-p—â%TZ¶Þ0ê’0B«ÐBÃpAs;Ðá^=¦PË[I‡UƺÀ1h?rŽAÙz-¾8?rý;<-€Zó|'KXÿÇçʩάWóVîŒÍ¨]E{·Ä³Ç«vßãÒ¹þ<¬ô´Ü 7õ]>í”üO/Ÿ»È–°u„ÐX٣ݜ6zš9ìò%õ>W\í¼º‘ù”™(ù¿Æ+tSñßß*æ±ÿôâ9«jÁ‘ÖŽ~Õêþ‹ut›(%b+Qqœ®6ÆzUˆÌ*IDŸÐÔ|7[›•üK6J»K êî>Ý)Ìó§À›}·I²³s¤}¾˜W–Asæ€`Q reÈÍKEŸ]\¼ŒTÍ»Ù8Vu:z~Å2Ô‘xfŽßÁ4ÿú¥Ífp~Ø»ÕÞç°Ÿ-‡}™°Ü°ê½: PE<ó¦zsïs\¼+Q®ÐN³ÁŸDÂ)fâýUÁCÕ1n·:f‚RJ‰(gÍ‘Ó2óìL$>ŽÅûÙu&ª%…{ºªF/”³uçó S>ã|‘ËÊ1Õ7t¶«)³1¼2i¾3óß•&ÀCkÞpóû Un˜L¦&æÀŸ¼^ã(?npªÑ“YG5e:áóˆ}íY“šR²mâªM ¸V_Àè¾®2·„Ll²¾Æ¡zÍ,¤?rX³?r˜Sù> Íå -ø¢«xÆRw¿×NÀ‡kõŒùôüï‚7+hWwúUîìàÚòQ÷ÿ`l-°]ÕMq1«8ó2m|Ñà—\#¶XLO<©ïC<Ú¾ŸêЛ€^‚?0|}|h¢HçN]@*mJ“f¿yv]VXqøüW—>&éu¾ôÙï|úKÅåõåÕG¶‹yœI„UAVäÖžxâ¬hSç-åt„?n=Š;ÎîBxA5]£î¥=2“à¢âY‰Ê& ua‚I‹ÅuÔ&# ¦Ô·”?rö•÷׈=Y÷6}ßc*•!sÑ^²ªÚܰAv;ƒ¬ÎÞZ4÷{ÞÞσè[míJåqI??[—KŠ w°»ÚëµTªÁZ!¿>Ls·ŸË‡ìà§Ö-zÌÍò¨œKàÏ¡95BKø¥ƒOW dÉó|ù(?r7*Ç;SɯcUX1;&oÖAíUh£<ë<Å;/">º„jÔ@ò:ÐFc.yj²žµÚ”Ÿ\;—’\å_𓜩ÈâdyÓ*$‰Kí´»CpBKVàŒ7®sÁìâ°ÉPŒÔ Bç«ÝHÔÛØt]‚›ý®,oó™§Îlï÷矞Ûõžž[8saª/?rÌ·h$g犔\’(r«Jyî¬$Šl2Qä¹Ç|¢Èµ*Q$̳—Ö5Dh:¬µÕsì±5ì‘eæ$f-Z‰æb.Èx™Z.y,(÷5±¿ÞøOô!úÄ6ŠÂåþ\&y{›??„gó©±[”>ùKæ8œtYFýò’‚õ‡Še@\þ¾Ïܪ…£‚«$£1o`º7j’b{‰€‰¹Â&aÉív··_ž“|µÃGX'î ªBÔŒ2—Juù}1P²{TZ}\Â?r‚Û‚îm«}??1‹§v6:]ßÛ3’<#0ë6Q»(d??ùẕò7È×½ôŽ[ q^ ¾„rT‰ä!ŒÌdX†$õ.Ô´ˆj¤©Q.ȉ¡î5@×Z­±•¾[@²îûºBñWyŽky–-Yhþe•ÿa„è]P"ežË"ø‚ª­-»[Äò-}û|1ê­??3ááîs¥ˆXw¡byëkÅÊŠqóø«ÝÚnK6küSL>ýFõïAc«Ñïç–×ÁÁh¼zîtÆ”h£õgÛ3€7h6]€ÆúF½¯5WÔ+ÅÏû]Í×ü<ì`J*½ÓíÉŠAI uŽÒh—‰|0lHý£*WžÌ‘úLºîè+-)ŠÄ~»ÑšÐyÓ¹õýL¡qÚÝï?0¦Ù‹½øæ?nà ÐYÑÔñåè4³éat“¥5’ÿd8¬Oð}¾F‡zÒ\©Mv/w·ˆm«œâdz3Þëq#¿Çüq‹!KØb d Óp+6ãR‹àûM7–Ë—ï¢j}NÞ•Ü¥'DÙ‚_Ïkê½Z¥ä¨J¸X 1hí”]?0Ø¿T¿'S"õÏ3IO¾øÙõ‰)OÞ«Á/ä¿ï‰Ç÷»U(.ôÅ)ÀêIçVÌ??>ÿ3¥òs´p°Žù%LõuÁæ=ù—áQß!&T² r¯[¤¥à7äªA?05 (à%åÜ»B*xkKT?0]êë¼ {ªÜŸ«ÌŸo4E]ÑJ‹¸88*®˜já„Ñ•õ>}¦|vl¬¬lŽÄ¨°»ÛXÞÛçÿï÷·W`[¿$_–›Û‹Â¢×{bUr??}¦€Í¢=”þ¤vñ[‚Ý®íÒŠžŠÞ« T“¼??þ!Õ2°$7‡EÖ|ç>÷ßt¸y™•ùã–“|Ú“2ò[í”>ü`Rì8¨m»çDÑ0õt×Dv9Ó6P ?ròƒeP¡Â|©œsĈ'ϺCvÑt#v{ü~<(?rÍIúx-b&?rÈ¿½õgç h[EbÓmœ²•TE×±AÖúŒIØGºñM)ù"‹¿¨KR%¦žCè FM!p&ó‰"@[ËSj05ŸÑ¨f vaºc5¸à†±ùJ­$ÞqHP&³‰ú•#·ÌòâÖÍÝ`ð¢L7ñ°Ñbf9¸;ªãy1Jï/Ôº‰[Ýæµð*zW^¾c¦–˜þ@Ñ?nW¤ÈÃLý#l?nWBõ}õÎî¶õMøÍሠ!,rã”ÒVš Ä6wŠÜ}>S±'K\€~òÜ÷”Š[bÖioQàÆvIKYVQô;ä¬ZÀÍq-¤í+ñ­rb?n͡ڗƒ†œÇÃyЇ[´EÝ pR³ õ¶ÎððDš$%ó˜äÚ??ÖkÑP­¤×»Îk«s•Û#²Ámª\lqP´N1–ê¼—ÌÅ$‘uÅ$㘀IΙÆG¸Ÿ“†»Ví£Y¤˜ŸZõÎB=ñÿœè,šü±‰óO?nùÃçȆÄmv/<¹Û¹Íþù‰sld..ÁlâÓž\x€™²œ9wUaY±®UŠBމLrÕ¨z…Þt?r蛜­—Qõ3Æž9°ÝY9{¯ŒvåéŽA܃ée•Óƒ›½%4ÜcÔ|Y¨çGν‚ç‹ú0€Çf‘¿Ð°ñ¥¯"_šj.¯­>fTU{Ë–ËÑ^ùí­èŸ+Á`Ô¬´Ú°?r•Ú³çy|íÑô¦­ž´ÈÈIœ]FÐMUŽÂ¶þ¯¥°›Sé‹cI„ÙŸ¼¡£ÍÏ4n,ð^py-ƒµú¤TfDÅU?r´né}©'âNšrØ\™èvµ;&iy,];V5À˜b7©¡Z§+VH*27Fb…W{ŒÞ³b‚Þb÷IeR•š«Y´4i Nðk¦‹0C !&. ›+–ø(O¤.u.Ë\F½nñ­A= tÌÛM)‚Ù,‰½`iÐî_NÊBÓaHRÇÑp£Ìز]}ÔééµÔ+š’ãuðÔ™2Û²Çý¼ÌÍûJùÿonÔâ ±èõàGòç?rÈ÷OznÅ,tHÑÛÝoQ·xz¿¦ìÃÒ¢+]m(þä­“†ô“+%ΰþ(­?r¿®5™êf©eM˜žzú×¼£þæþ¥Òû5;·ùBEÎ8HÛ&ç›=Ó­U&ÜT{?rÓ„‡÷²*‘„#:¶bIUA–i9h)/5·JU ?0£CôZ3íã“®ØÄJóºî)zOyTXàÊ…*lUU¥“Mwʦg]ecË]•áÒÐú{bu)i,?rsE¸ñPþ¹ßlìÎĘ^XZZª6Ø$Ì/_ÌÛ0e©ãºÄ Ï«hkúRFÍ…U?rñ¸š³u²w;Û_Nâ›nY¼?nHûRß +#D :J‡$½±4`~já“„C%;I‚=*ÌÂø?räkÙfãBÃÑ €b–ˆl"n2G+?? Ó+Ädqžxö:³D7-ö\«¿5ôëè9¸ùní9"˜Cmº™Û„µ¨‡ºÎÆGMŸ·Àq4sÀ0˜o|úá7~H,ÙÆ£ó%6çØlÏY‰—ñlÀÍ6Ç@tÒ¸©—ó(Q‡¦ÛØß÷or#ŒgÊøÞzŠKpÅ8ñ¾=a594,qÞ–Ï,ö‘fAp»ɱ Q‘ñq`ýãÓ‘Ûº¯ÇoЧ X©Yž\&Á>ÚÌÉöL^` cÀCµßx_*؛ƦÍ`ÔŸ  +#éVŽ© cw9´ÎˆÈƒqãõ‰d>1}j§õzcü—§ÏÖƒ¢Œ¼ýÐ3nuá§T! ?rzxéÒ—X`‡·$Êá-?rñ\ºÜ«&ÝÔBô”~ɺ™\ýI3©%—0ðà‹AÓkHž[KÊ„ùÚ86]´”5=d·º˜£F??tÐ$Åû÷R¼ÿ`‡§d‰§?rú˜épú°·Nvè`ºú@…„ÛÂ>’A@EG㦳jr/d7™?0ެôi<) U**ã¤üh(¤—ßåʦֶßÚåÝ5ì?n>{´<7}rúb]ä¶c¢áêÚž,Ò%CÅú‰ØH¶Yä?n7’×KE…x(ôMvÆÞ¦kèÌrtLÒ$/AåB–å쌘€§¡Ý“O\Ÿn}ûâÃY!!C»¸kö‚íYpÀJŬlÚW¡ýbýy»Þï¨ÞÅæ~޶Þ逈zNÄò*©™F½ú x¤H#Ä,zÞÏÓÔRzËMÁ„¹˜Ëy8aäÕ˜Ëõ…«¼ä0a†Ou5Jj6RêR¿n~fü³ccØ Üøéew'Q3\oh\ºEz‚]¿ÆÎÒ»¾~m²-äàSK±è Óf‡¬wÎ@»dlô7oÊÃÎJ<™¸ ˜•BÏ¢®î"±Ä{ýÚXlyÜapg *–›òóꇞÐÑ´'y=WšÜ‡ŠÕMyüùHéÍÝinBþ6ãÑÓÏÄ¿|pûÑÁåÛÇC6Fd ;º|ÇåÃ;Gð>T¢¶Æç èsßÁjÓÏBôŒH¸ÒÄytK)ŒkìÙPcËÉAÆ km©—@!¡¥îñ|ƒ…•ÿ‰¶s†ŠŒG’ô³¨;ò, ²xÏK–£Ë×¥ä çN†$gŸ2–º³ÜÐuF”ê˜Ò9%·?0–‹í¼LÑ)}ÀÜ™?r ¤ÐTÐ&ß#³‰5·o £?07aÑCòTn—,í¨’vgd‰IÔˆ8Ú1LJÚ0v˜d\xöàs/³¤}-÷;ßz{}ÿ@T[´_¾ÝZþ^¿x(: ÌjÚ&ñè–Í/óÙ¿K‰.‘PÓìJ-$QºëÌÜ¡77 ü~²Ëï$ªÆ//Vw”îvKtŽÒ¦vù"oË Ñ|p}ÐIß?r\QŒàî“ÓŸ¦ˆ!OI¶GúžŒþß¼¨ÄÜm$À1,M ˜ú?r6]ÿ"$½{X̱–j˜µÔÏ9ˆÄ‘Øæ!Τ-ysÎ9çœsÎÑo2ñß}„©_U±Rtš<÷^‘@¡?n•]êŒæÍ7üi6ßð¬¤fù¢º|C²Fà°©øšhD£…÷AHÑ{< ˜€@Û±t²oîÜQ-)ï|¥–—ÐÒOg˜¸È±6kÝÈò­íeP6`«ÙÉBHŽïÏù—¶®ëÀŸ³òß…Fš‘Ærõ0iŸÌc&â:IJåªè{ÀbI:åÄR´t¯Ð·‹Ìl†nŸPRøé: Ë 'îò:\¸Œª)΋ÐÙ&¸i홢ڀU¹hâV25??÷¾Ù2dtª™Õvc¨al-B`¤Å:9–{luò@{]¬¬2?rà6 ÝŠ5µ–Íiè·r¢Dá=üÏý[³ ”‹ÕM©öK]Ÿ´,ê˜;ã@Á vÏÀðÞ³ã­BO²;ÎîeG雈7_Ó›'Ùíì¿Ó—½¿ÿZè뢗ù‚=åF4%ö3<-‹Êïɱñßê¬j6ß'_BÑô<Ú@ƒ¬×aF†»04ß1¹bAr|‹-2ÁñŠ ŠÃ²¬]šŽÐ½sA/àƒà·sðàmh]õ°xÞª²7‹"½þ§ç%ÌÚH?r–KWUDZ¤p7MÀáÕàTx…N°DÑÔ‹s¦zÚ?n?nD¨™€¢Ú£Ø]gOØ™;æ~™lm_§PÙk]ÖÌåt©ºû´~W[ÙõÓ£À˜G5ëä£=yž‡T.^TeýÒÇ,/Bt8T˜øg\Þç™Ý?nIÅ"úÁí"ȹ–õ)µLGã>j‡¨xÝÆÔ‚FoäE{æWž_Ax´Çˆ=â§eQ†á9Ô®öbíS½»[Ç´fÝt<¦={M¦2ç- ¿j{CuÔ’1Ø0bv?n*L>ë¯êùоv½yî/züêÐ*®ºh!-ܽ,¨—**U·ª]Ķ ’‰iÏ-äÅ0Bã5$«jC¯u™óNhC)K…—4;”(G‚ÂF#n› Hð'¤g `söV:¶¹ œ4p…ë—ÜT«+¿ôþMªL]Yèû–,?r~ö/ÿ…ìgÿòßÍè_ºîüW$Ƶ!Tx??´>£h´öõ¬Ž¶ÈL…“ø™,$Q5¦8=…¸ú—þ†ÞÚ yôÆ`›×Ó£ût`=ùæÞ÷žÊl͸uÞæ0‚ãÈWº<Ÿ•«2uÿø*»ì_°R[9:¦£Wc»¯Tñ2žÇ 7Nö_ýk¡¢·­$:CQIKÍÏ);¬B¶Gm={œ¶6°¿­ð•"nÆ­Á9§+ÄPÁöÉÿÕÆ?rtÚÉHœ^—ŰøÔkOrt™Ø)ªø½?rêãïíÙ¾Á¨± ?rDVóbVê¥+¸fê!¯ã)DÕc*éÛ©ÿNßÝ wm.¤à³}z[(`PÄÉyƒÞ\YPµ•vB\Ìʧ«pt÷ÖwwVSJˆbI(BLØ–|àßȪ×ÿÔ¯ü@2ÅùY?nª¬ ¾sÖ®*À-ç›B¤f(Ò1<"?rú»킾˜öÜßþcWtÛBÿ'~.žÐÔ€uáTcš(L Å`ùnSÁø8Æ”ò:硬!Ûò?nµ)i(›g'¦j!ŽÜ&+Uò?0´˜;ƒ}g1稠?r@‹ ê!¨X>Ê0 ؤêÏU8€âEW‰¢†ë¢‰¤Lä8ÀþZ‹µ4=“¾ÕóG6^UgqæV§èܸW„Ô?rùĘ?rv?0Ó°itS@a&„¢ ¦_1³@cUxm¦Y€Æ`XFV¬}†²Oâøüÿ[ýœ¼¤•x82‹}²Ï˜Z`¤·Œr‘”C|3uóR+^fë£Ç³Å?n@ß11L72\ðÇålTüÃl‰é[pa`òsÖWSxµ ½׊Ut€OH”^lÜ`ZIh§p-4[EêéŒ-!ÀEçë Q?0þ$ÔÉ×ŸÙø¢Ë¹áÒAª`Ÿ D`B"°‘Ñ¡,fçOæöÍž¤Éêô;‰Õàý`§r`é[ÿúß7e.¶vYÃ~þé}j+t·rA¦Î¥ÄkÌú¦éÖ­ëÝYuP«CìbK[³?rB„¹oä+]tƒÄ„OÐ0#9#‚"˜ywãwØ‘9H9Õ¾Üz×Sí&}÷ƒòë-á„ ˜&¼?nƒÙIèqG7¡.{Ñ!^*c‹U†>î?r*4ÜV¯ÿ.-Iv²Ç¼Öò¦_?r8,c®º Úoy U|%nª¬E±Ñ!7B†vNš?n¤tcìʼnùìÌUÛÛ¬.Ek3 åÃÂÄ^{Ý:6…ÜQr [—?r£1ö¶ó‹?0y($(;žÛ±+]ä´uÕÒ¼þ§˜›Eze•ºG{ª±??¸5râRqRcc”UɨԨáeì\Å«X‚o†,ºŠÞ•VúWJÍ¥ì³<õм`VùZ]å ­Å.Liɇ×îTå÷ÉÉõäÑe¦}UèÉ5†™Vn8ø’ÑÜf]¶IKd%«XÓ‘ãò\?rõMë`/ +#ûuÒ¾vժī턲‡ ¿¤©¬¼Š«j(K9;Š 2¿ô'Û(î~zlŽ<éG·è~wh¯ïícŸ’@AÍt¬˜Véjº]í‡x¡Tj ~•Qeïóô9 À«6a/]\Ù·à„.{Ž=$Ƶ0–ŒM‡ÂÜØfíkk!Û>ºB7>;Å…âÔ¨‹®¦“V¦wœV«¼l/2£P_³s#gK-Æ51)5ÿJÑÔ?0 F1&–, ^m^%]så˜,LI??]K’‹ÔÇ„e*TÎ6 ÿø”Q‘!¼˜Ë†õm¡Šá¬VêÀå©j´¬Óè®7ŸŸ6:7Z†º0¦C&’åðž¢â¨/›9Ρ÷Œ|J™Š±z|N”ÿBýØFYU±ÃÙUbý¤@³”U©Û$eà©ÉºAX$l3r‚ðA÷Ï;œ?rcuZÉZŠ.£a%i\×`7EsmelŸ)›^SÿÑ£û &ÞéÌÝN\·/Oë ¯êMƒ+\µˆƒ#«Ír?0Ž9á/3/«¯* †hÛcx˜<2tºgƒá³Í‘.!öã¿K'97Ll(Cd?0¥è…Ÿ ‘&Ì´§“@3©’Lƒ?r¤± Îú-ûaÌD`(Îe?nXر¡ðcj]X cëoÑ¥>Z|©B¢:s.ëJMŸ[`Zom‡I¹¨…¼ºÇŽ>°—6•òaéx “dõS"B×h R¥¿C]Ï,éõ¿¬ý0/gA‹•7žw¾îÑ#žU9IïqQNƵìõÝÌ:…€ƒÙ,ݱhLBˆ†4iÊ‹H9*| ö Ñë+>+hnìÖ|2Ÿ“îÈ˦Ûã$kmÀVær²l@{Ñæ?nƒ«Ž¹*£Íøýþå™𔲠ø×ÛSÏ^Ÿ‹ê[$¹Ö¤èã*àŽÒ‚9E(dýúï®`‚Ô%ÆÄ>´êãã°øÝÆÓ§–,@[v¬á÷^YhO-ó ¾Ü˜ÌŒ±Æ}öÈæõ³¿ÿï[·xÖ-Ñ/ HÐ?0ÅQÛa-3Ê'ž+lö‰ßcÐâ¦à@µ®U}ƒÏ%u܃·ÒpSR¥ðùWæÐž…A©0’ûÂäqÌãTs4Vé_”-sJ€«ø«ÒКìm%‡!õö‹RP‹)n*ù$ ®–(웺„Àj´—mX(pe?rò7Cˆ™lÉ(ƒ"¶æhvŸ“ê~ö/ý'›Z¥?nRÞÓ~Cô†J<ðåŒ"sœÑºmÙ®ž" ?rüÕ0Ñ 2µ/Í‹¥LG2¦iQ:¿fôLÀ$Lg÷5ë5j(?0£0¹ iúyXþ¯O]:³JÇÔÁ\vUEˆ2’ Ò5X›œ”Äp>RÄÆßøƒ½}g¬½¡zwY«M=µ ÖóOàù@`x>1¥PD·À„ö>ÕWשùºõdè[ÂF½P?nÕùSòUÚÄò?r‹Ìñöµ°ÅŽXZ)b*%g›¢_åš X³[33%Çr `ýÙ¿ò‰iؼ¤¥b3"ùãàäßà¸5;ämÕ“ëi3X@ [n¶e£†øs¨¦žÜ¡• ^Û1 P¢÷ªƒí…óAFÌ|¡lîÒ0¬Å‚gÂ;…ày±D†8ŽÇÙ,Ÿá;·—,fëÂ3éŒ7Œ(tóé|çàÉmÇj>˜ê¤ƒ®†o…#¢õæ U^¢¯…¿T΢?n5˜¡?0ÖQ¼è3þ0ûê_8lJ¢ÀÓ|"3}" áÊÚ"¼,??óhKJ Óä"Ò̦Úçxzb9'å®)ÈDákíôƒ»Zi+Cáô|îE®º$™KÎZýñø÷2ƒ?n ‰ldtIAD‰¡›^™9C¬ÿÅ`ˆf?nþ}h3€ˆ­Œ\x^”)þÙ??ý_x35ÃÌ/+—ë¼kP-žWÉàˆïhþñhèâ¦=wX}⬃KÛ‡ö,mŸFž•¼Í:d«õµ-ëO]†þûü ëíÄ`áøÕ¯Ø °`Þ€ó½ÂN.y p …÷Òžn“mDʦì³ëÙE’‰Fm“ž¹[ZOúvÜËõ&–à–Iú*3}L‚ë`–T‹hœZ¦(²2{ã‚È„¾/KB­•F™D¯FÞqþÌqH–v’¯ 8j»µÍ‰Q¦ðÙ(0nŸÕ Æç^s7Dºó—GVÈ,f±?n±Â[õÖqóhƒË¢l$jÆ_cjy b§AL›Êh£Ù’bÑÀþás˜­ÇI´@“[–ØãaZëŒf É6¯nï]½º5=§ª”$š;Xq·\úF9Q¹V¡C²aDMáíS…å”bSWƒ¹Cœý£ô´«Ót¹¡‚åKÍ‹'"“ñn3–µyþ¶ð°³0¶§Œt?n“ÕÃÆ‡èÞ´«;×§€®&{šãÒûZævá›ÙîóÈРuxãtl_Ýù|Üá"›ùÎK³–z,Ù^e??ÃÓd¢éh©°Þ*d‡¨¦‰?0ŸEïjKoWuËÞ.O]ñúŸ~ç*óŸ4‡Ê9r—ƒ|a¾·%ÖÅq^>oK£5Õwb{‹æK°ö‘jÎFH»d¢–—®ª|³uhªœ ¹€ïþˆpX˜+ØðeÂâÚv¾-“ï]>/'9”O@è¿í?nÜÙðqu ¦½;KBž|¡h",oxÄçŠÔûô›£»O¾ytŸtÛ¯™@_†yöé½GôHü_!i´‡7 '3ÄÓÕ?0¬BÝŽœTeÛûŸÀ0ëZÂdoçs-‘Óè¯áñæÝ‹¡î-ËÎ7l"ûé³ÌÜÌ-RÑ|1¡F—á/² fÆHÃï|΄tëé÷iYÅ??ºÿ„æ†X£éø‹«™¸*–ËÐ,¿iUÛCiÙÜ»ÃÑÍñž˜Þy{l"ÿñeù¼\C<^`¥P§—¿Vö ·‘Nfò.”^WŠÜ¿þÿÛð1cöf3ëÜ´~$֣˺«¢Ë’h½]Mô×Xâ5‚Øg<×ü…\òžm2ÞMö½ë²z Î"ë“Z¢ç¨9Ò¨Ú®¯·0 ƒ*âï 5> ]IÊ"§öZfi'í$ÐÏÏßz?nc Ç:,ûDùìØVP=O¶²b¿³Îf‰“€clb3ét?r»ÍÔ6¦ñtÏ¢§ÐI¿žnÞ²t`]xCU_ÑDI‘æÆ˜Q„ÏqbÑìjÎ,1±wxÐú‘7ojÑ3v\/EËH´â 8¯5ðLyz™´Ôz??³í–Çd±h™¹§™™º)_¸\K’ eŽh[m$Rl}€+Õ€ ®øÿþ;¨–y\ÔOMÜ‘˜ m®ö5sÁ¿©`ÀÇšB™ž:èÈ|~r„pà|r•°µ?n|ªŠ›êæÖ¾p"àÆ®¼'Õ\FÜM3šÏHZ Ô µÕ`„Ø;‹Ì[†m ?0š ÎGMEöŠæIq¼?nb´ý²(_píé2-]lܰô¤ r=/ݪg;çòå´:F±Á•K|]@˜kêªÊZ‚°5Ò}—°Ó¢×Kmõë€ó"rì`¹Ãóâ‚D4膱$n‘òB=Î妓wȜڌL}X¨ë^"°(ê•k-Y‰É6YQE “ú˜Ý8:zj”Ê"€Ñ>ê?r5â<ÔÆ8l¸JKWqeפœ¥ÐƒQäW—D¬ªEM÷»ÅTÝH©Œ“¦'Û$Cˆ”†«µ!–üiDø¾æãH—zçñ­£§÷nÝ×õÞÉ’R?n"ÿݾõðöÝû·Ž²Ý•"*©J[9Ë‘xëöÝÇO¹-‹˜‰óŸ™š+0ƒÐA“^Ûᛓj±WÍc Z"r],{ÕPç–£a 7x׆hÂLÜé—?n‰àÁ–ë5ªœMæ_6Çvl;!Uˆ´2 týJ|û`,üÏþž¿Á¥ö÷üM,[Êår1Yõï·Ö&oÏÂÖê`¿r”×ó`1™oƒªIdz€m–•í&2W©á¨«m™„múm.??¨¿M²Mׇuçy´Îëì=ïÄé‰4GóÌ»fl9W–…MÀœ´OœôIãpS>k]äæâÝzÕÈZš>nž?0ŸÊ6C‰}•Wi?nÇ…¾æ3uR??ÃFë•8-ÝI:Ñj?0;äß)#3w°V§mà"å:.Ê“íõ8«r˜±û¤S??`³~ÉЬÞl¯HeçW®è8ÈÎ^´D1i­9{~ÖÄ¡´œ=-ÜEJÉ–ýü*[£ÏÆý r‘~éa!¼3£Î{ÄÃ?rápúå ?rÃa—© (\žCýü\ÃÀ»1äí}xþþÖ‡Û¶'ûà=-RÞ®ß4M{¬Z®p$×pèYcÄÜijÆLêMÚ)??°#¶_xŒA`j‡t¶®q¼±f`˜ý—‹Òp#P„Ð??û‡þ[–¤Ä|Ûº%°—øÃ„I¾€Z;?0 m£ö?0ë}—¶³qëñý{·é$>ºûðу¯nÝydŒ•Ná´nP!–Á*´>Âäá[‡ãRJåÒJÕŒ¯{áj˜Ó ?nÍŸ0§þõEÅ›ÉÑø¤î!¥ ýä/ëõ²¬¦ÍŒ§®lzW„—ñм·ý’à ?r[ô¤M<{ö;nþ]z>õsê·?0¿sk ÌK¡f`•!C#&±ÒÔþéh»)è®÷/Àk°˜äh$•8D¬]/ܨVó!ø€gL#„Ö•„o½‰Šš‘l€S`úd¬5ò‰FUÓ–¾‘z¦*²Ù„Î6Xª$&elößÜÛšúF¯÷§¯Åö;ù^ˆµÈ>^˯—u\— 0©ÇƒêTWð*ÅZŒ@·e[ù øè*?0y"åâõ +#¿l;¶¤¥1‹1´ŸÞ‹íV¶}; : …pˆhüi7X"ÛAëã·Î`L7n:í«µ+67Ûzk±¿¸:¨ñU%.ʬ¶C.Û·ä>›¶õ¤*×ð¢ö/ñ‚ýôR,$ØK;(Ö?r"ktµR“ù65©¨ñÌeÝ™ÏøÂXNºÇ«PÃvU¯lòhÙkÁ^¶N2¢-BG0y³¢wzº–¢” Moh½ÇÕ8X]„±à¾³ÃJ ©˜ÖÇ/]Y?rÖXWvkFƒ»‰¢ŠÏ²Íiš*Çæjbä tBeà1=¹ØU-[xƒ>fÿïëñ'“2)bÛËÃÚx7û®Ð£5uϱŠãóT£hšô«SeÂÚÔ¡ö&Ák ²]Ö°œ™MÓJðv|Gé‚Íó“bë¦?rÀ f¨¾s½ÐWÑí©‘úþ½÷Þ:Ò‰õ¼9êÀ¶¤/O¨Öàë¿&¡}ìŠïjˆmq0\oõ¢¸³»“Ñ :/6½¹UÁž˜Žkj†}s3|ôï±mj8Å‘ùB©þT¡ÿ&4à.­˜’8! ]o©_9ðá-CÀ]^FlF×Ð^ø ÷î÷›øëAÖ领ÒÚv´S­TØ›ˆx™² —É‚3儲¡l =e-ß*qhª^k39H,{4É}OÁ€\ϹÇSœ(ÒµŒ,…90ñ˜§TT°<çz'»ã*??åzܹÌ" ª#TÚÎÎ7mŠ3²ÄúôL7/lM‘ýÍF7QÀazŸ2 _@(¦ï2Zy\8C,g[¹Á’D+¡¦åžË›œt¯MyðÚØ øw3¿ÜzÀw÷XOÙ¼ÍÀãóË-4š -‹ L—Ìq\YÜ›xÜ¡¾»0&Õg„På1à$޵ðo+»cÀzÒLRÝx,ü$ºK¨o’º9 íÙPõÆ#Sòªx¿¹âÍؘ̾‰ OÌ<1sÅoP¨‡ýÖi[›ë_1vªXÿº¢§Ø½Gž«wWçÁðÜ̾ÿ½GàïýÀ!د÷“)*Iµ*ó#‰:b)AC–™ÈCv°Vøí‹ã>Âávoî¶ÚÂ’6%ß¡¦›œÔh?rðö·_[޾…åâÔs¿—Ú`‹ mšÁ˜>/þߤÿ¾øZXûi•4Å??)C–&:xêÔ7N.0÷cR~Æ[3´SÎ-»¶z‡Øt Ü´„lꄊÅ>ã"ÿ+³vÀ?n ö^+›üËÅPKàð/þ^X­tjý´A2?0ßg?0m?0\²gþ¾Ž¾’vjQÖÊÂøa¢;ý¤ Û²žÐ@€kƒø¢TRS6©/Bs9cð.Pýu†Ð$±Võ¦U¨K-Ç*ußC{ŸͳŒ3Ý<®S·*/Οÿ»âªµqÞœþǶ²ÿ8´s=Bª4s}æCN³§?0é¯ýù„6{H…é8Õ×+57+†²‰ õÊS«ä'˜«c¹Åßò'þHB=HÂÍ `eļŽäàœ“‹Ö';ƲÒ?r˜Èýp…\JÒ<Ìa-›Ér7€…õ¨{Aͬ[|c??Í^Ù°W}Uë9©±zqz ”ÕdUåPÒ‘ !ì?0â…?nˆ»’ís(þý*dg0¼Yô¬‹:+§”[ë:åwJtzßP\@™yáÅúSÿ˜`ˆ•nE?r ˜¿E-ŠóO­‰\¡¼º«gõ?r.µB’ÏN;¬°ŽÎ€1•CbËV!) àÉFçt›Wûœ\«ù5^9ÇP0ŒzõO­ù5ŽÚX×ävCêGÍú¯B`ɃÿµQÏÆ£Ú‹>í¼¯—µön꘱.iu{*…ñ¸–Š=S&ŒÛ$LöøQÓÕ¹ºªÞdSÝT’oÜÖ?0aß èËw‚À8¶YãJ!Èn²àEycÄpÅ®²ªZfž†C!?nÖÜm¡æ>vLÑ!že?n½šÞÕó¦¬ýYÙF;ÆLÁzYÃUBÁIíþk§ë%‹Ö–«ÔL!]kO–‰ Öº‘µ°Ü9+~kÿP$¨±K"êÕQVtr”«áž½?r†,÷@ë܆ƒËpì÷AûK¯º½¥4ƒÙÞsW2þ™(h3±§e??1vyüZ÷^#µqÏ#*ؤqgÓ/w¯ý©Z·ë`ÝZ3f6·Öµ¶¾??ó—û?n œ_ý õv{½e¹ô…+d˜‹HzŠLÃÑû#‹lŽñ,¼œ‰(¼ˆ?0Î?rÃr‰f??rr(ø$å¸ÒÓ·£ÚJ„éOÿì_û«$Ÿ|‹ð2;,´üŸ}âŠÆ«Mè{½ZI¸çI³ž£·ÎÝËq~Öh˜è\?0(޼rÿù®ˆOËážI¤k¶9NÉÅù¦ >@Òâxñk\.(¤ê#p}Ž}kÕMÔ¨?0$ô0uجZÑ;ÙÇÙKwFÿ»ž?0žô×ÿ\õ÷ÊZdþp7OÔ;ZB¤8šl$OÉJ@þ?0›5èR¸l"™©?0UMÕ­Ö AÛÇ´,ìe5¦JÔ˜ƒì¶´©1½Mæ]#…ø† °Kƒ.ßÐbNKk¼ÕÎ{ãÅtˆùûáf«™Qõ²_õ—P¥cZ¿_ù Ž™X›\3’úëûh/^F¤áAŽ“9íÔ¬ö¯Ž[9°œ6;­»m)__Ûµ$䟨ʺ;‡a×ixt];Ѝ¹9LéÑ´«Õäâífƒžòi¨ÃQ{í£—íR%`¹kV„*v¾òøtäÎγáÏß:üù+|ï×~pú2–zè_Œ;'.uíòó>¯¿à¢ÌOtCuûgŸe&Ýø¸•Â/Þ??_rí³ ”€3ŒÝ嚃ò%†O³JSÈ9 sw™³&¿¶OÄZ!|¡øÃÆÕN05›ñÁ“í;”·òøû??uïqöbçê'§ÙfÈÛ§òÞ_\#Í|³…§VÄP_Û!4-Aa&?n¹¥!ÓŠ½DÙÕzÅ¥:Ì0{g!ÑRM.ÍÛšŠ–_Àw2·Vµ±7+ÎÁÙ|YŸ½§ÐØéƒi§%ëíù²Ù±¬Ú&X ÑŠ(Ve¸¡1éšW¼ëûÔsa¯‹‰ñÛxÏvê5`×ÑVJ÷y®D–FßörÀ0kY°CnøLÃåî$ÔÏ7‰å·"ei…—M>«¿CèƒÂ¬‹€³3p‰çí£­1?rAšQRP–VO¾3dêЩoÔ¡Šó{èL’(°áí³r÷Lèºp3õ4ÇÈV?rçYBIÏ6TÓ]–Õ\ò˸TVÝ>9lh öÆïzR+Îíj] l ú®é¾…Ù'ICéǘ!N%¹ ÜÐWJV[ƒ4²RKËÌ~4¿˜m\¹?n¡ï6—I3]¶Ç}S¡U-PKZZ$<ƵO8H¦[üè—Áä}”ÕeMÒš‘ÓZ,.²ÿ6 3…‘}ô:_ï7€êôÆâ°f]c×Å›¸Ù$vΞOãjl~¯¥¹†ó\Õ”ø®žc0¼Ò¦fK£÷–+`.õù@ßî˜KJòjYa?0´W¼Ã¥›x6nÀߣõ“´(ô.õ[öM]Íß6MˆädA‘Bíl©äÔ„ò`ÚŽa—Ý+¡KxN†6l^9éÖW`$e=ý‘ÈæØBu@Î7å^¿–ä^ÿÌòF÷4o6×ãýPo°wõzšyºg¨DçǸcnNŒ;ÚT`†íÖÎÆõä¶Ç?? mQ6²s|Š›>ñäÝwHQÖ—N¹éÇ‹gŠA×”F3=¬’¿zö¸h¯F™NãFXè¥!¬“öUR[ܪíèVêñÔ¹Óïˆxnüè=-} Êè¥Å·_Ìe7ƒþÝÀ}åOAÕ)x®s]¾&`Ç”á£,£m u'†ìu[® ×`©ÓõÝ”èb/>»•QÙ”­ºßr‘ræFù/ÖµEßÔ¹òùÛºÖ¡TC£™~SŸÆT‚ú?0¹ °gîB¬­P|6êq˜¥¼8ÉÿI{°$ ±)𮎠›À3¾7 ݉~·?0š§Vˆæ±4kšÅc&÷/Ts×=²­Á¨\ýüƒPÐÕC(Ò®¤é†xHf?n>*\+z¬ð¿7Ó1Àæ??ẖݰê"ˆ‘0ù8É”ÐÓÝ4Dˆ÷у{OïME,«)<Ó'[ìZlˆ@Áåþœ /póÌ~M } C¥÷t‚%¹ßú¬þ#”d”Ë*I@pìëÍ÷hõp–KÝkèêÅ€9ÝW{ΗÓÁ¾›Ý’W p,Ý#sw^ ŧƒÝÝ“ŽL5UåvVAþ?rÍé.ìÙÇôf'??-oÛØûì‹«”˜O<†ßR! b$°c¯CñH®,0??µÞFýØ [I}d…«iüD»Äáª0ܱ’‹­·vÞt\•K¯d²¸dé6+ÆÆn,1¯…j‘iv#Îø¼*êûËê.e Zõº¬¦Ì¥! ©ÚÚÅ>íóÿÿíü¯e}ˆ­€»¥5¶SËS?rƒÆ+q3¦ª¦‚úØÔ¥Ë1¬D*páR3ÞÊ}6ÊÍ~ ¾ŒÁ]üðÀqFÒÇÕ@±ír?rÅÎJ Ón&õ¤Ù°ÞºH‘*À1<«;§îXèÜ¡±;ZW³° ‚®¿ïL—MTB£0<·’¶ëƳx»‰k*ÈZK”™éÁöôMû¶]¨oúFö8ò‰åOCÎRR2OoZ_Ì]̧œtHz> ?r۲჉ÜCí÷|kóÝ­í®@ú]qŠfÈŒlDû)}OËêÙÐFéy˦€‘I#¶ sÄ…OÕPóO›¢ä]%Ú|¢`Aª1 t"KïZ4°q²òÜ¢<úþG*hÝ’<µ"Ðfø³²ošwS –nËJ/?r‘Mµ§C`|J”uXú:¢¦©Íµ8´t&ã7ŠÍ„kèí™pPÙ¶ÝœŽ¼%¥r¯Ž…à ‡-ÔOÇ ýF£g±]1‰N )hl°³)#”ô$a÷­8ZX”t\.:|Ï¡?ri„kÀ#áýÐoÀM?0{+ü².ۀϮÚ÷ýö­ìI™jQy@??)޲_Mè{/ ÅÐáãʽµ"¶$‘^ +#x#ÔÜŒ+“k4ÓIÁå7/\¦’ªh¼uˆ\}kRLÅ/»êÆ—Uy#ÍßýÞõTˆœ3‚“ÜT•ÜïͲ@€´˜¯pK¸j¢<Ô;$FM$v ¯c®Ímo6û-e²—´Ú¥/¹ïÌ<("¬Ûrƒ’ùíz)T'?r;Ð÷'NøÊõ5/K™¨ß9è?rS/ö®~6/‘ÆnRv+G×»ýŸ»eŒ»…‡ŒEÓk×??ù|ïÓYÑ”ÕH& ñÂ:ËB@Rš¬ìò×{eyYµ;çýÿÐØ«ŠM¯U×øÐµã‰6Û¯ƒØ“w”R…„NAÔqy¢µ~õëÿ6õ‰G-y6rj£ùŒŽ?n‚h«•í994¨gWÓ;ÑÆ¥ 3á­«õò­ìS!Y›Ö÷mZ}ÅŠkËèBaO+ê¸M?r· ·\|ß7s'¡kĤ0QÚŽK­3_mpÎÙ[??vïÉ£½YMHÌõ˸vµîíä*<1àÕa¸ îÂbS•Š®pY’ýE­Y1¾(…GQëJdü"ù¶??uôÄþf'V>7~~ÌœW0±X ¥÷´è,å­??N_¬I\f„‚tY¾(+ZjÞ¾ͤڎIÐZ„T‡be•jkc¬+¸sÂÔÁôèþ²¬ÉäÆ5¦Ð~µ‘­¿Úøôê‰Úµ†.þ°á))ôj®|ˆgN2à7?rø-êã=)áVû!U#N»–Zr¬îÆÑhTc"6G_”Ÿ–¦tÛ֊܆ÜøLvVeï×?nˆ÷&w»#DÍMUfTe† 5eóâ#ñ¬rèäZFà½ãÄ¿/»Ó„EH¯Ð??\á#lùYð~³¨i?r¨j1ɉ¿£]ȯì=¼›~{ËšÕÜkw™Jè‘ð{½H»ƒW}ã3H|õ{G™¤>!)ÝKþ”tÒ/“zºl¤éO’NR/¾??°*`~ 07’Íd ¿W\ÒJç“UÀr Ÿ?0é$h­$¤ž¬¥ó 7™þ$@œöãçÀ-\w#.YÄ¢´“Õt2?0Þ)Võ´æ9lº¤ž-ÀtÄs‚h`”—û@ܧ£\Ò©dÔ×Al’ì¶‘N’&ÒN¿ì²än EÚÄ ~¨ºƒ¸5Òp&SWÁðLÒ€÷Ôþ*±<ô2)?n'ÒØ3Ò(ô¹å0“z²?n°Z|³¨O¯LµïL¶R¤S‡?? 'ã$[¶‰ø%}XžÒ—àÝÁïÉ'@h?r5)8ëžP“v™å`º)­D'FA?noÐ|^9Á±ß>T}‹Ø¤¬ C{%Í?n¸É&Ås˜ö?n¦Ó¡n<×e Ï€ü"‹¡u1’üˤ‚¤ç?n_#q^¿÷£®®¸«IÀ“[!9'@?nì<‹ÙtþêWAUT‚ñ17ê~ÕÁ«Ú$?n©–ÌAü~Oz‹×0ºÎxMkb0‘lq?n Ñ‘Í`iP#!??S† ~²1ŽÒ³š$æåõLFä«Ù¿¹;Gî!©`ŒuºJ„aø'Õ&‚Ÿ]KÆ_¹<гP^è¢ÝÕ…Ò‚USƒÒÚÄŒ°¤|?n¾¹Î0¼çÄÍ»DÝs%Ö¦.¹š® QågéFFøÅÌ?0â`i—©ß7ž³Òª\!­‘D ¢œÈÂÖ’ "Ó¥5ó?rºC—·þö—{D‘pÍ f.Œ|5sÉÐc?r9ò‚á&˜5þiD¤w·GJˆf4ª¼WöP2‰Â­ˆ·¼—®Ž0+øäQÃÐÃ.›iäwö#Ù`„e ²©ã —W+.ä 2ór7CZ9—Ùá®òžö&\¤À.ä’iŒˆ—ìÏ4è?nÔlŽ(ôbœ‹P7ùë†þ¨G^ä¬Ù;УÔEäà'uÚ×%ËJýÙÈ×'ÙY^2ÂÏ•MÛÈrž¾˜w”¨9EÓ8mЫ†ùÕÐ'e?rˆlF´²BÔ5ˆ~¹'5??4òÈàŠ¦ƒsÌ|väñH#Ô[p6"úrÛ‘¤L*­‘¤a¬ŒÔè¾}??È'Ë€£×apô:~‚j?0ÕPÝVŽÛò^¦y)ŠÏyKŽ0Ó ?0.÷Øôé’Ø3°©Ÿ,M|l›^<Ǻ’ÎRºMqgåÞOz?rTA’8$q±¯¥.Ç–jÈåmö?0¶KGî=ñŽ);KÚ#˜¬ªÈ‰îBã¾L”FÈB4²Ò»A=ePdc±Bë0äXx€O?n4íx64nì•xQ*´|YJs‹QÂ??Ë(/ä)¯ìâåó/7þn˜æÖàÆQy…Ãꉑ‡GžfÑ b KýUJMB6»<‰0??ùà&mPH›$LàE©šº·l*6غÜË|Õž{5×ïl`Ë÷q“œ¿ÖyÎØ±~‡QÞfáÊu%W—”§½WèG‹Ívļñ^"Іšê°C/ñô ?nå°E]¸ÞîUæÍuŽv±ÐîÙàPWáøñjx˜9ìÑ&—’:™sEž^[°É|rž°Y]-Ôý'ø m Ð{}E$pnryˆß$??K.àÿ…äwøùËä¼ øÁ)}G$Ð|h/*K®Tl¶9,j7¹Â³ñ?n@ˆºASˆÇãÁlè¸ð¦©_¶ô³¡COY-Â:D,2œw„Ó²j¢Ë‚—¯6²Ëá„pö©š&ž9Ђ6×°u¨–^+˜°z!h¿hp4&{N¨á6ËU7¼÷ˆ·kŸÌ§HÞ]ÌÄ,ï‰ÓÒv0Ž7m\dlxÏ—ø~¿Ê!ï]‚þN)ª—úø›gŽâÉì>ÅFÈ“F«êk ~Ý$±[ô*º€í¨id(¯™–λpü¥d/#ì•I’žØÓ›¥pGAÈ+©[#jغìBÀë„–†»ûÖS\}jƒU+Ps«ñÃÌÒ|ä&WÒ,÷,Œoô;À??„ÇPÙ?rº¡£ {r² ?n‰is«óØÏ7éYfË?0,#_»eæ´Vqä™,JìE³âø— +¹Hjgš³Ö´Íb%â¦{vN õò°X犔Xb)'È„cŠÁ´ÎTBÿ»#¨0‡iX”&¹¤)Ô™EÉCZ’Ÿê°$Kßì‡?r£ÂfQ[ÕŒLw÷Ï:_Í—øíá³s¯vϹ84‡ôå^ýT”†”'t†??l3—*&¤óU¬EÑ¥!¿J¡QýMÚ™ŽñÍÌ??؃#"J%ª¢9kerÞh]»?nèK­5K5•i-NÂÅ›@S>ÂyÌŠAm‘û2áh€!zÓ×qE=uí÷äµJŽ©QůR¢Y7<6âFºO à¯{ñ\(û^ò-+(q9ÈÖüƒ—?nb©T\¡*á‰~„[øýþûo’œÒ?0^àøþûøxÑL”ð¶¾7và»tã6g (Ž# üÚ1=G_q¡Ajø0©ßäxB¬ôzþÖÍ` {B“pº-—›…ïѪÇqóÞ¿±CÑÌNßE7rHÉ»6¸o²¡ &ˬ^‘QDðLpb‹úA?rP\"ãt•Ï¢ðéB:½Ó ‘ùîLæSjP \$«2e¼sÑFpö]ë·áý\ÆOÒé?r»ÞO„îîFŠDp`*ÄÓ µ8C½Üë]ÍÔ¤·ÛÃEw½2œÆ3FŒ²b7ƒrféȱór^îi…¬±¥ëh¼sl$¨mzîE³/füNà59o÷wo¹¢-Áf8èþOÞwïÇq$i¾J c?0ÜÀcdЀ';FÞœ§¿Fwè#L/º›¤tVÒʬ“åP^\o΂ 4äˆ"Ï<÷Öþ;pñÕW‘QQ™U?rp¨së(²+Mdfddø k÷AþÏ>ãyÆX62˵ÞîÈ^AÇûà§Ó©Pg¼EM?r4;ec°|)4ÆÜ²Æ™##²7€Aàþþõò›xºù} oY!ƒ«tš\þ×ê¸ú;4r8©ßTêÿ2ßÚIá½Ìaãÿì=¼Ü™,ÁÚ+''â?0S”Ñ.¶õÛOþåG×¹"ÆÏÁÇSš’'5­™zÔèm“ÂæÓËŠÊ6‰×dÎ7ºÜ@dZÉŽ'@¢ü ±í3™HîJ‹á†P^¡’àÇ??œ+Žp™¡iFßÀ•?0çtmÚ†ˆöK?rp*ò0dÕ&¢RM¤t¡òµvIy~ýÿ<©³27?n‰] Sí7ÉΚ›\í˜ÄH(°åò“?n581hšE'‰³»,„9CšœzœÀ7íT)‰ñJ{¥Aà­"¨ÊxF°¸IGxô®Å!–“}ûÉH“¸SÝòV”+Sþ& ÷ÅUTÿò%)ÓÚÉNërŒÚÒ”:eÂZ»ë3™·LAêÙõA&C× j¾w“qÛ«ÆŸ"yZ ÍÇÛ)Д‡œ‹2Δ-R?0d}8?n—=êŰ;¼"JÇ_¾VC“ô"½Gû²×¨KNÌèŸÚty’Q>‹3@#^??lš®Ë?rþð½À¡ÙW ÕçÉ8•#×"9¶Ît»ýl ±JÃíQD©â??:ŠÖÇ…Ïo}zëÃ[ŸÀ}Á>ï´??oiýûÖjsD‚µ6aÒ‹V ±}ÙïÊ‹¿†6ÿì©ÇŸ²_S¤ÎHèæNkkc‡HmTÒ!ÿBq–Ê%ü#6¢kxÐpÃj74Œ‚“Ép¹ Ä7­±#åJ wWôï¢ÐŨқ7&¤ ø€™ŒŒ–r»7ü廦 /%ÔÀÐíQázÓæ0tuO('¸Ì·˜7°gxA±“•©Í§¤W)n~e0ì¢tDHëó!l¼{›vIí_ö?rãG;zuV¹œgóêi“¨ž&?rƒOÏLñN•ª©=‚øs«‹vëÌ?0¨^,æÈ`F‡éTU5Á|"‹÷ê=?n&ÀCüxvã5$¢ÕV±UhûÄ·òdv¡?0ÚÄšAˆ¸ót;!Ëk XС° ´åúö)JÇgœ&?r·»»T¬"ðaø„(+?0öȾ@Ý7|±ßmd2lñ¡Û¨ ¢ü¦…¦’ C²þ¡-ÿ¶§_Bª0x?0űMdµãé¶µŽÍ·èï?rѡٙî‹\ é©®-Û9-‰Ýët$L͸vøÐpp—ñÏÿ†;YîÔMt‘¶´„ötÍ5âšê¯lŒ~ùªÌå$ÜbÙŠ›Tq` „_3AGpP‹Ní!ÉÆ59°??Ç®$¬`¯‡ûÍš©³Gh¥„¼tw“wáPUuÒVp™Ÿ‡[??À÷¿üàBö—ü~&ÿ•Š`¿ñ Ñ;èeƒ¤qëµ^Û£Ýü²ÉzÌÏóá ÚBÕ£Ô!?nÿ§#~¢öůbÔ×€l³”ÕdPÏ ÄÁ'=÷ãŸ<ú¼ú%”#F¼z£~Åk¦@ô¶éSPÇtÏœž7ò#_"‹M!§z•O°SשVËÏÇsà§2¶¶õ—ÿå¿ ;ûÃApäés¯â™ôûp/%y…«`/?0<ö×Ç `üz‡ÈÖæ¾¤ÆPëÄ»w&¢œ®O_½‰5“äk4çÛ¶I½ƒE&PðD°Dbª¤?0.Ýüdª?n/’‰>ôå0Á¨¬­Ÿš¹œõOQ]uú?rzàªÃ@U‘EeLH2öo`õø§Nãt¶?r£a06šÚÿÑóÙÅÊ/Ìk¦s¦w¶{ èêº&Ý&g$±ín1r¿îv“ˆùì#<üÄ#s»o&-0´Ù¼¡pþ4ã8vE;ç·“ƒJ Žîù¹íáîNæµÆ.¢­ü@®Iã0î@|“×Hºƒ¯\¹Öä//ý–òÐÛïîÓ×9Ò'S³fjµ )•Ú 0ÜüëoSÙïµcsR Ûµ)ÀHa¼¢*";ÁØ…ëq™+Ä‚ˆkO/wü{ñœÎ+ùÑ” »Æü->¦\Æ$D=óL¯‚àX8œCÓ-·D•Š`Ž77J¨å±€×^«Ñ¤òA(˜-LŠ€£øŠì²))üe¦©HPH.M0SjcaRöX\ÍÂÉÒÝ÷œN¶P­^ô‹OGÔq¥šRªP?0‡ÍÂ~„ k³WÿTÁy‰ŒpÖBêXºëTõ:¦”ewåßt¢òÆÅÚ%LD4OG;“™Xi禾„±±)傹^Qð$Ñu› 6 ðxb$Æ|Ò¢¢ù‡¦°#­µõfWæÊÓf|~kpi»Jjbþ–îÎrœcS…vÍ·¹•ÈÕåMµ›Ûi?r%Éã3BðÀe?0±b0@év¹ëÍäLìudá%ØU§«$'6 \ÛW8ÅuÅT«Øqâ´qD4PýðŽéÁc¤ÒN9?0 ÄÇE`Ì?n@gtb¬Ãê뮕/IÜfcŠ}µiKš/Ühcd’ø!Â5>ÌÞµ_U;|(pa•8šé#ºî¤Fhh'„¿ë Tü?0>Ì!JOâ«lº+>%x&Õ,§3ŠÚsCÞæJp~:¬½C«™—|cCQÉŠ•âÀ•èæÑ T:êѪ{ì$~žË$8Š”õrx2Њ¦˜Ô´DÊ/¤5ž{ÅgæGáIÅÖ¦¿(??†P敵< ¬àMö<1'?r?n9ùo¼o|±Ùø&±ƒ­ov6(öü(×2 æh®ºÒ¼ø?nY É—VcP;Y}Œ¸óyà??èZÉ´´Y­µêMŸÞ¶™ßüÂà™Žƒce ?0 a$_J%Fc„ë?r <²aQÁÊGt…ÊNÀvSE̹Ä|üi¥’A¸x(IR¯;q£7¬ ƒôkáe³\)$mœcÉ`…ò<þÀ“¥½ß#ž‚½fWW†n\ã±eYIƒLƒ»Ä …mˆ×1«FÂ!‘ž_ `ÝàÕ ›ç´FºZÐ&ìW®ñà?n9>šõ#?r! ö²P¶SŸfÙ˜ÜKH±ÁeDùò,}s@GÑ‚îø^z€w½)š)˜ÁÞ„„|,t÷â^‰Ìüñ4¬|€”qœ€‰®Ë ßÉÊ|íLþ¦TKRûÚ5×Uɇ¥Í>×ìc»Ñ¬*nÊG2wªß#½àÆR"‹Œ%`\Ìó»“´}¾uÄk‚%ãÜaÆ+ñ2àÜ(t¦QÞ:9›?rþË‚Û/^†—ñž³ñ) ;ÅŽÁùÃdó$ñwä mɳfÃ5ü2H6öŸB”PžÖŸGyëPÎ祧¼)rgá+h>¢Y¡gϦH¼™ú6^¬çz“øJ¨ñC7T®®NÃX.?npÌoÒéþÈŠ¼j=7ª=á¸Ê&\Dg(fTbªœÖ?0ªmÖôqÒ©®Á´@2æJqjûŽzfQË]Õ…HKš•;zUÂGýÅh†Œ8è8†S’û±çM~2iêà›Ö­°}jôqåÎ s“‹ÛœÎœ…‹ˆZŠã›•ÚžsâÃÛE,™ï"º-8ÆJ…AOÀ3cÀ??’àÏ”@#‹äAËb ;‡Õp3¢0š1YCqá$„]†"ssf¡l?nG+0}Û/TÆÀjk*ƒƒ¥ y?nà:ÝpÍ)‘Ž=¸ÔöT#[J¡|w±ÔÙ†QÇÙ4VÛ óÓœÐhÌþq˜»/¡Ng’ûg?0p&Dâ·Z6¤ ]ÐI }:¤«|dw_µƒy‘åYzNK±Ø=LDe" a&åE°¡=¹èð&Luî ålè?0 lq°À‘š„öÛû(öæ¶Ä@0â¬yvx¡T>†•ßqÇ<áÿ”éç‹w!R;}ˆËV&çŒçƒJŒ¢˜yåÖ—/Ì3GÃUagƒªKÞ¥³²Ž»â%³+$ŒÜ ¦„¤÷º;èP[b×Üõè??‹SƼáóäo&q4°Ùú¶KÍpkßÊX0?0 ÅÞ…Ý_¯dlÄõ n÷‚EhÝ9hÛBpò•[M#'â#鲯c B¸gvW^Ù}úi0×#Ó„L>–ò†??QÌö)¬rú ðMâ€êй¹ÙCGAÖˆl¤‹2Ò¢XSÿòÕ?? ƒ?r¶G›ž¾”GÔWá+ãiJÏ<ÁÓ͓Ǟ‘vÓˆ ™¡[úÉÃ~²øÝ¹HUXÞ+`.`4ÇSõY¸0tj¼½’šÂæž\÷ÚºÇ[‡£L7¦¥«¡À¯ÚÕîëÝç“[ˆ`SÃ’¯‘¿âÁä95íô[ŽÙÅîÖf8âmãF”TX”¹(Kƒ ?r—ÃoýåÅŸëá#Æu'•DbyMö„8•Ã׌ô›à*ôv8èËÐȹ¤z-“û˜ÌØ,x–¬³ü¾&šn–6®ÔÒñ¼)Wò†‘æ8#†zCQ2®Ê??¯*3‹}öÞfxQ,ÎSæÑ'Á0—¬nÄË*ìÍæhgGöG6œÒ¿z7«bI2hßä3£m«üò)üeTèr¾8(n˜ÒƒÂõ0¥ ‘Ñ>ûôªÆ˜°G]EÛt€6ûƒY×&t»”4b£Ï©è +#Õ‹fI€÷–‚§6ìk˜Í´= d{jâ­²©ÞÓÛÂrMϤMÜvfÀÓ¦×LÖu–VÝ>›D\BîÙMMÍÌåÀû`/ñü &íïì0¼RŸŽâWÇ¢ª4 Q£Ï(‘äHñÖ?ró ²F«=,óÈÔ>§“Äyè@9.½Æ“" -© ­8?0t´Îof”p¦ìª÷Ã?0gSÕòÁ55ôѱ5TÖn×ÍßðWœ´Ä§÷Ó¶N½HüÖê3 ¬×eBP/ã¨g ËfUÈzÛX½\óòzÌtãQÅ;¼’ad›g0$Ì‘øQËLüåÛ×2ô2ôC¥ X© h ÔBž :½É8øÑÆh¢•°ûýViô1n7@63oNåïnyº4¢ b#.áGŒI³Ÿ*åiÑ >£JØwåQo_õqñ€Ø<ÿŠ¿µXÖ³bžÒ´çZ˜¿Îž“zu3:EOÊA/Þ ËÅÅ|DbL‡A¤—Ñnm†1äï~ˆÅüÁ‘¡¤Z7Ùtë(åÛÞ.Ñ\@‡m†]ñi¡§ ÔhŽå¸í-õbT?r§ÇÙ‘ª5Øì,:‚?0æ.Óƒî†÷zóp¿eTÎÈîæW‚ŒQaͼ#lV%ªLøùò0Õ8à\sò\r7/?0HÇF[m >_ ˜Ì¨féä0kãwÍI†,±%ÐH‘ ¾Õ‰Lrºõµ“ÇmeôÛ¹|ÒŒP‚ÁjG,Ëê2ØêU»”Ï@ ƒsãÂìâÂÂ41œ|/ìi?rŒ‹Šf n UyË¢¹¹uJª+ÄS@UËeŽŒ’z.ƒj©™²ÂEÝdÀ8Å??»”• ÕÜšáà€À€’ٯŮŠ(+pϺÓ$¨*Õ“ˆÃLÜ¥ÃiLN.5nnÚÅ‚??»ÅÍÆI¿œn˜raîîêlwcUªwxZB^TÚ$mËTá:L27›¦ªYÀÆ]:ƹÌ.ÌÝç»·µ+âúN2öÀÅOêçûj¶òkÓÔ EÚ³ç°hïXB­?rh)ýkî’Š-o¹šN×ø2§žRúö ÞcÕ.84f:)‰œ±ÃQF­"^ªâѼ¼‰×d£§+ì§4QO‡CK|¡·Úåøšþ?nªfMÓhaÃÈr¾îõ¾øZ\)U?0â)M¯–µœ~·²Gž1ESïä0<xà*ç™e—^šËöÀ@LPÚÛÉs½Îp{2·tV¼«Ë¯?0@snÌòp"TˆDäÆt-wï{þÇÏ>òÜŸzüa¬ñÝÕ°RïZøìÏÿ䩼¡ÎµQ¥¾2Û"??ó¦Âö!A–RþCT¥Êf—¾ú|Ð’³™x±X¼„¾©e!î†K{Ãm1o; ÍÉÉÒÚž¨–Ü™q‰Ð(S¦ÆÖ6}¦Œô4ßxrB‰4ÓµOï»8wnáÏ??£öÅÈC>‡ðÙÇŸ£òâ ð¬ tŸ Þe)é«|Wó.eŸ$ddB?rËe“p‡É©Ùn! æ2ì/Œ˜Tq€Ï³œ²ØŠ·©Êäá·ëaÌ¥4X×6êSX™)l7OrÑÔ0°€†Þjë=¥ƒ@åTòXŠñê1?rÔÕ{éöÒã0Öwþ¡<ª±2’U@ˆCHwûëd]ÔÓ ÆwL§‡šÝ·ô=Å¡ÂsèÞeý…2ÊÝß «Ý&ˆ«åܨ45#¦ôž³±\èu¬G(9“f=1cÖõ!*ùŠY5“z?n^`SÑFÊ„H¸5»¾TŒŒ¥¶×*´f?0‰\Kü0Þ €å4yAÚ8årZ|)í´k·µA_ó€-HŽ¢-‡qpë¡?r¸×ÕJ?0LºSªrä6ÎÂCK>ƒp-?r©ÄââKHMÉߌÜå)[:e£ Fý¾A–"÷¡§Þϯ¦cò2ºúéÀÎ?0ûgPyÙ)YÈ0tRMjŸEû¦Éü˜X¦¬Xó0£òlUV0 Ôk&²n»üÕ3Qz¦Š·#…?nïèMg‡áö±g2î¶Ûrv{ï¸fÑÔ§÷x±Õc“ãÄãëTÇDL*„}1ÊöZÌìòçÜ‘X®n¬jAÕu4ÇbÒâHœ—‘{OÃø[¥_j fŵ’\Jìd‹?r"¦Î=ýÀ³ÏÿäÇ tËÒUb!ò¡ž|è‘ÇõÖ=’ÿ™ÉÒÊrP6²‚ŸÙ8§CHµf™ƒ|°›T<—ìcƒhs4\ͺ½wmãi¶jw¾FS=˜tlc*ö¢²1–S€ÃJ‘çôÞ Kê`ØM%åç}.ú ’îo¬wû݃<¿XI†ÅBk«G¬œÞ³œcèDtqæÒà/ÿý§fJ)‡?nÊé­ó÷º kZˆ|‘‰Àa= C»«AñÍïE¿Ä“ÁQNKÝ4;qï¬ÕcÀ?nxå þOãd°–ªÇc¤Rߦ*–zôwtBê6MûOc(á§éß©yNàP¦Tc°>0_5°¹×­L—ñú´~¶à  °þVŵa•}R%ä_ŽÃÅ+ßëÏ0cÄ‹8‰Æ¹É[úiyT4;*ýáÓaáQÈ Æ3ÁÍr‹] àªj nªqí>mòÿ‹×•ŒZ*0W¼Æ¡åÖ>ÚÁ‡6*_‹‹¥âʹÄë?0Õœ­v:´RñEÁ}ǾöÏu¬¼-Èá[÷|ߨ #_%S#ª+4îöö´|ªý¸}0ˆ«£òS§õâ ®„?nð»g-3ªæa00uuÀ <ÆxÎe•Èsç“þ'ؽU ¯³#°œZfò‚ØCÓ.«LDÖ;iGe*?r¿`+ÒO/ˆçœ:•s÷Ñ/7ÒÀŽ›j޲æ„v„œÛ°+¸ôyÈÓ¡dë`¸®^ž¡ED†ø9’,Â×j2¥¤“®Á³O8!™N3:ÿÁ ‘†2ó’F]5R™pî¯5ܰñÅ|×á^÷úW“^êÔÊÄ•ý¯OýE~âËÛ4F[ø¥†Þ,9(`ršAÕ«Z€”?nñ`§È¢_ǼŸÔ†rÂ#Os‚K$ý6>Au4Þ%îиsÙ|€|O\5Ð,:U’0;WSc…Ôz†æ+–¾ô«\Íé‹ÉæÀ§>.êÞ–cŽÖÞ©­”,6ßbç¨3µ8í=u¤»û¾}׬/Za;ìíæîÐȹ,€ò3/†”w–ĆR½xAb4ÜÃÞp§ë'ñNñf)ÅÈDïOíGüSЗ †ÓÙlöBrxJãLtWxÌü ¸¼ ÄWHbíMö驎?n¥i55ƒ‡ãÛ;ýVgjj¸7=³4³àA ÄFÇ2z J¥ºaD¥%Ͷi„¡_SáÐHUââ^V5pP[+chH.iü2.&öSÕ«fN«Œq¦eÞÄÝNüh shµ³¿“7Q÷]›Øšè28iÜ`ØÚ²lÙW,Î%Š_qï¡ËŽ£];g© LQn! Ôp£×ôJ€ac4à[Öð¦ÔèD„‚ÒI4£¦øØ%yóp±¹ü´Qà½ööºSéªÇå(5•1û`0¤Ö ìJ¯±3óÇÉ{û™Rs¬Atd5¹¾ ó~>þ¤ðñö៧”ýê(£hØnš9ÒâF?r›…>{ý,Ëz547­´Ö9oÛu)ý-ÙNT Û*ïW?r¢Sç7:Ó§(Ã'??ó °¨#¡s{‹ô%B„wúDþ Ö?n@$Æáv[ç%tFèãçæ<Íô3oÒ'ä.A£ÀíGBE6G§ìhÛz´^]q³f¥AU¡:f??lz–¸»/+„‘kqi°"Ÿ.s¤A®’H×uzõ]¾ ÓPk¬ûy¨ýbzR7Fw&®`ڰΔæ…ÃÙ–ýŠþ¿·•}7šÓhñnšê΢ÖodgŠ‹ÿ’BÚ«›•—Ó5°ìõ²?rùŠM’jZE:16mGΗfÁ¤Ü?0{4[m”Š1:MÛ&e'jpÉ)‹5Ùh˜W‹Ž˜ï+­vBšÍÚêÄ`*¡7Œ—ù€Ù£z—Æë¶»÷ËœyôPÍ)Ü3Iž üÚLLÎçS¯è¥ü•¶,¾Ç/6ÍÔ׈®”/–>º`ô˜À0-pY QÉG N(vÂÄ$ÈUcN¤­?r#=•kÖç;x·¾Íâ O6L_ù§šFªmãÙÓÝ8xÆŠ;în4¥z-§pü”ᆋ‰ï‰ë£VÜ[±ïñSe…J U¦±ã,n;†nkšÒ€Zƒ°³ô6‡?rºk Â@?nÀk·b¸'¡¦V¿©jâmŒÞå†2N—R'ˆýŽ?nJˆÖ"°’õœ*%¬t˜¥x˜ÆMP{é_çï{Ôú¿a•\ä¸2Véj.iª}ÜŸ;±k·??3ãÎ@vÓ{sÄôgÒîܼ³??Y)0«ñCÔÄ>’®¼Ê·“'²)s𦹢bÓ0h:;?r9*.žcæýg§óÊϱ떟ÓêÜGþw©Â÷~¢‡ÿ§×KcÙÄš"š8ªhâi˧}#Œd“L¯ifvC¶"3áT½—C6Ÿ¶Î{EŽþ(|P6.Éçs­dv;½R’@Ã6¦??|Œy`Z??ààl×§oKj¤(òÉã?nükÕJ „p¹?0}Ey«Ò³š%ÓO÷o>¸Ín·Ìðñû™9?0Ç0;IfnŠüÒºµtmQM‚ç·ôþžQ=Lðñû%J£€†EÙ!ñÍýÌÊÞE?0óTkoÐ˺¹—!›£\u¾'¿óféVé4X&Ë|ȃeÀmŒª;eSUmÈeòxØ)ïíSõDÔÅ4¯9ˆüQ­N‡oˆ¹ü›¶F^JÃDš'ÅÙSIÖ''´¡no(M¹•jç"ÎbØèQ¬µŠ çÄn.D{ ÒôLʾzk”–H˜È2n•¥Œ–!-øbÄÄs|F5ì¢ÆŸ§Âøx¨Å‰nIÂàNîËôíæ pú0CGräJls7ý©Ìĉv ‚m‘?0|ž½ÆÎ™:éé àyWVï—‡Qª±Í­µÂÝ!\ÙJ X).€ŠuóΗ}¹-Ù¡£í¥šà€aì«òˆ;‹¾¶aóÚZsWærº¹¨>%ñ–s#x?rX??œov™[‰1Ž”ƒxª1(_®Xh¿Œ,Cèh/íîòýMAÚ¶Ók?r,z\ïðªA]õ6ëQÙù›}ª+×!k…–Tÿ9Zz1rûŒ÷ò›JÞ'vDƒi›eéD³Øé9J½»…+–y1ÐÁŠß”y¤N7 #dz.ÎI@«>&‹¨Ÿ.ÎQd…,+¦Ú1N½â4vê0ÂvGi‹7ÓDblZè»»ß:HóæÑÖéÎ%ðªn½3,¡Ô¡þzСïH®-MÝÜ‹üˆ•Òúë(ÿuÔÿ‡ú%Hu6â±°Ew™r@DÁð}ÉZˆ[tJ#Ìe½Áþ}÷,,†®{ôyľI©³”:4ÏG3ó0Ÿø°g‹å=ËLû€7-°óeJTb¼ÉsÓ"£ºã‹¥¯ð+ÛËÜžÅøa°ábù¤Âhmy´_û¡~€œ3.zî‡ßõUÜÅ'æR§­#Æ«ð1Pbë¼Õè« Œ™˜´ÛLÀèÌ?0ÕäªW‰ä¸Šì¶¿¹‰NÿuKQŽ¥Kó?np/$ª„0?rê¿øË?? ãF zŽÄÅU*“U€¹Ñêti^K LÓ¤Dðd›½=¼øüöA?r~¼Hn°x2б¹«ÚÇŒÏ&T z".”~ªšâ×%Àæq^«níJ-'îƒì"l´×„Š.e·ˆË˜‡ –½!‰“¤/YN›=üðDö=IÎ÷œLa9Pp°4”óÊ?05=¯ÌîÊ WÊ$h6>—ÍˆË 2 øhŒlîT±¿g*hŸ¬.rã=A$~UìSAt§Amfƒ^”€¦WjДí”4eˆ¾7ÿìHWfVæ“CäC{­‡ló¶ ë4@ PnŸ@C éxhÞË€P;{ »MPÒ¿OW‰± lJ¡4¼6ÇxtŠ[§Fíõ÷È„6,`dðMÊq*øè¼´>$sEU#¦©¶öF³Ò„ã) òGJ™VÄŠƒô[ç‘€ÅÒhÖÜWY‚-ªh\6aÛÊ9#O—h3X¶lë¥^??ÓÌf²ËË®Ð0dyx`–[ÔM.Ûbfõ1›ÖÙœ#’‹:ÌE©¼¤Åÿk—${Ÿ *ò_àìh¸y_Å߯â£é_èt?nLîÁþ^3äƒÕô´Óe :% ø>CQÇ`Ãà•ØîùÞ&ð4£mœ„–Ëmý ,)ªž~ìŸÿäéììÒÜÂ÷·²)µ_¨r[üÁ–E­UJ÷¢,·¦s1V­Z‰Â¸_¦¬]V¹t´·¾Kê–ðDò¾€4rj!|a¦ÆÚ9‚Å&HI•+·_o@Â7"*øZzáë. q¢dT¡!œÑuˆD6çŠMºÓ$­*Ö¢½ß½9|›Íᘨ-ŽÓ_¡ÃÂ+>ŸIO~‚öÎA*º6´îÐd’/ÒìqOi¥‘жG•k ¼×x)zb ùb6¤€3²ÏÔW–Oä1C­^2C¬ª™KžÍ˜ÄÊëÝ€{ä€Õò‰?rRˬ”îÑbt|x—KnPSÿ>®¥mmi쥌ÍÛ)V˜áÕ¸1Øjí¼aC8Pæ`Gš¬q1öˆŠŸß†¨ÒФ!ȉç¾Üè5‡É?0‡©:çàQ~yånjºDO¹pi×ûºt £uÔ‹¦6˜ÉY‚m]ZX—îK5{Æ©CÎ[ºnÜ=²~!£ìM3@ gk5ÕWêÐÛ“‚¬ ©5¹G˜ý‰åË 2Ø“ÙÙ8ŒÆ¯Ð%¢Úß“}mŸùáÄÖþpêô䯍zrzbmŒ??¿4«—LÌm$÷íÜþÁ™Æœ±w/WsÆÞ}¯Ï»X䌅…} ÁmMã-.Ü%¡]ôJ•pR!ìÐZ˜Üîèv›]Ñ•A»í3çÖ\Û!ÊϺê¦å¸ ú†…¬ÇäëšáN®nÛè ¸•;q#™¥:™¬ðªKð,•ÏuäÆ[·’h²‹ÞŽ +xZ|ñ§:²¦+ë?n£Âë×|ñË÷h?rñu”^˜VãsÿÑOp¬ž›«Ï•àëÅ@EN÷6 Ä4´bäø®{éíC'Ô¤<6Uà¶Í.—íWèl0)ù¬…i&¿E¸Yä`ð^ûu‡x"šÌ$ìFlÞYæ}¸K—,ëæ¡}ý5Ó¯¨¡KØc$ #±f7gÊ­ÙÊ v¾Å[8QÁ·X.Z©Áà*ò ö×Å‘ŒÅ¨Îe¬%È·qò:*XÍFý¥3Y¬øð™È‰®„wi1›Ÿ7ʼŒu;[]ɘ¿îK¶«Vñ÷Ak³uÐËîž[T(£¥3@¡“@§Y-½ØmM¬øÓËåžôìFý‘UÐò!D{I´›JÅÕDe[@3h„À+†[Ò=wÐ2Çj]2/Kï¢0”zÑ×Éål cÃ6y¡} ¶Œ¥\?n£ïí÷0¸¹Œ×N(›ÙL´Ôêm´õsohãéarY<¾"6# S͘U ìf¾Ì??©ðÞðÔÄ›€çJ9Å´Århì]œÁºdÓóum6-Èéì¦4”M*¨;’8ùö€uc«Q½©ˆ°ZèØ©2ˆíÞq×âêe²¹9X–@2Ê:ôÔcó.šDy’K’èæsùó‘‡AÚ±De Î4LÁ¤Fómb“HŸŠG‚ÌàýÈSð0»ÎÈŸ #s-×où™&?r[(°|êÑ„)‚_vµ5ì]¬Ö`±€*‡Í1¤Ó•ëÜ››¢ÙËE³h¬H?0éǸxn/ŒRO´ÚÂ\Ïg°?0Þ •]Ò\W{˜™¯­òžn(ñ>=‘ R+óó#1.íì´æv÷ùßýƒ­yxo¬Ë—¹öVï”ùÅ{° ÙOOd0Zu‡ÒŸB<îà§¡Ì?r<5ô?0*??Ê÷é.¼fK×ð@G@{2½)´eÜ,Ó¬7éŒÊÍé¹Àc÷×wz›]@hRTªv2Ê©1ó˜’·‘±Íž/ñ„°58“ë6k#4Rº P³()rxr÷úô#Nsàc tʆ?r©©,§±yéOÿ+úr±’Eª´¤+F¾o+"Ó “Iw9ÿH­7ð…uÅ(—ëB5M#Ë'ã”îSï D¿é}â’ªÞDûˆ|9ŽÚd¤„¤ ¨Z'û-;Ç¢…lÄ88ˆV3Mt¤ØÜ<nzoxÇOĵØIçkfWÝ#)zÀðò[dÁ‚ßÇŒ|³)uÁ[â–<]êÖ¿)t›*\&®ho¬—à•`PÏG§m!+ÒÚnÇL|¹£¤bO÷ øM¡²’ØY€Çv¥ [šò§ôœš¯~´ ¹ÒªÕ,*”$ÎkÿûÍ;b¬ënRÈõR—±Ä K¬X6`^é“åŸÉõîli©g¯Z"pY\\€™=¢òìæýõŠ*½ÔjV´!!‡Zq­k öç}±®a³wž‡ñÔcwQâ’,Ø®Û#4æÝßÕ>ííx'ñHAV*Ü;½*¥\ïà…’ 2!áið9€ M!&¡±H*Ç}ŠÊ1?0qÔãqÈÓ«Î…Pਘ“"°Ò¾6¼áÛƒá.s—ú!í½ñÛnKá¸KƒnööÆ/ßü´‰9©W³ÁB#É/j‘™†H"ÒíƒàR¨qç>nÒÞ^oxòIéB‡ß?n ÞΪQ4j{@¿iÞPtdø°??~óÂÞ”Zt=9ý@ëœ4j—ªEð/ª‰ÚÉ× @´Õ\¢ÅÊXò7Túð…†Ê,'ßî]?rOºJÇŽxûµ)/nµŠ)MMiç&µ\¥‚àŒ¶Qƒ\*,sRR·]ÔÞ‰ÈT`üì‚‹LÅ1,,ýÅ]?rþÉ{ÌKübƒœ‚H›r®„-Ø•”‡­3…':œ¶Ñf&0sGYg=Og<``–h]×X©ÃÜÛÿ^Më»+³ƒDOš–KhìÄZõqS²çLTÙTµ°v¹.Åt¹Ê¤ŸrSVlãÇ”Ðã??ø¤Ü??ÚY»§·V›7øÑ»}¾`ó¹^ëo|ÿ¼ŒˆQ©á¢©â‚Ï)¼òŸO6gݳê;~ä>Óm??Yˆ(Wº¾Vô€@›ïG³‹‰ÝW3/[U—øÀÕªrž‘'¥¬Ò¹e¿æ]’˜A¶÷b÷vú +#È.ûš´ùu}ú¹5¨]\¸×p°$«æò½Ñn.¨÷çõŸó½Á`ÔÌwº°âåBûòÝß¿oñž ³"ûÄu”._áÑ”XbV©?nnnþ_ˆ›¦?rõHØç_Ð)¦¼AD®%°Æº“xÇVe½îmhbâCÕ×ïˆXðBûÚÔ¶‡~S•”Õ Â’ÖWL9^g‰—+×ó}ºD„J±Î%B’iMÌÓÓìaŽäSù{è(©”Cé󽙺ÛNó+ó_PoV@v*m?0N·-Õ‡÷³bF™tΚT `3É;®äIœ¶Šº¦!ÝÕúY[rK~8‘ç1D>›ÏC–ð ¢µü³ƒ¯dÃÑÁÆ~B«??“Ý??è·ö?n2?nê »ŽÅ??´åÃ÷¶yø1ÆûbLüØì©šàï¹aÔË€ÔÜY£þ¸–9œ°„) ßÄŒ?nM»ba4K‘†Þê8}çŒFë¹Ó4×âßÌÛi&ܸ[×ÅA‹ç%,S§÷(¯ó9“)>?0(‰44^sÅì6e,NeŠÔõöf®—Áöâ{løq™z{{bÌmˆè¼;‘õ8q¶ùµ 1T§[m¸7\¾gM­LtKKw@“ã‰uºÄ´0Ý9¡µBgÕ.,¤F&Öfgg 4pÓÓå1® pJYñåäåL,¤ÕW aÁ­Wè¹eš]äé3·å,öÑäSõÚÛ²^´¬Ø&Dc»ëûæ¼+"SVÌl|–jð;a†½͈몼™ÓbJ6:àac§•Ãw§OrÎÒ‡~ƒƒD??ÌC™¶jG["Uô8éÀFjR¨«ÎÎܬe4šyéÂ[Œ&"ã(Ôüo G&\ÿ ûgb™ûX¬iþ¥F/fŒ –æÝËæ?ró;Aigs®éŒ¬Ì¥ãó¶6R%I)J6VÔª÷8C_€ñâ8FÎCjOI¤ ®u‚7@c÷š3x¥‰‘ú{/?nL©­obàú8˜£oÔ¸s°¾8Ú§·¬'#à˜!=%ÍÞÿ‹¹ïînÜXöü{æSÀ¼à3E…qzÒÏû͹ã·N×3Þ¤«åiM’&¥ÑÕlÎ9çœsÎûök¼úu5?nEéÅs$¨®þUuuuuh(ªðWI‰Íý¯{—›*í??‚…FåÝ ïx•îz?r_çÑ“±×n‹µZFTê=æ.xжï¸d<±ú‰ûŠºÖy?0Ò*a u›­äô”ÞT;¦ôã½ ½”^S”ýâTõ¬Öõb•.¦¥6ÐìŽ'zêMyeš–Ó3hÏDeˆg(ùÛñã·g¯Uá}þüËŸ¾òžzçMC‡±7H³_Á2ÂW¨WøšG)¾ŠU‰¯2WøZ]ƒ ‚‰æžÏ8N y’…£ÇÜ•??"†yVx>„)dNb’f–gDùØ;:#~€¼óyyn“ÏÔÅQ1w™ÇQ }5öŽG•ÞEì%Žhùƒ Ïit€0þÈ»½õ@¸ ‹Ç§œDP銊讨”]ýŽ ÔOºj¾ûîÈûÅãGÂ9#Nå½ëŸÑe—{æ¸3:&Ö¯fßéÀL.õ›Ò—B¢ˆ.Ya+ˬÁòŒžz³1Š(I J˜Q‚² Ò${t|qÁä“ùäõö¨.Î¥¥ž9†Å¹&ÌÉùåâÑ£ KË,Ö“8[øƒ^•eDÆ|ý9™[ÇÞ%Õ?rØm^’gŽéÛ’Þ>¦¿·ì)ï,UéÛ\c[C#ªb]ŸF­å*Ïí¯³Ð|$zƒ!Wô5xe®ƒhéúòlÝSÅ¢¤ÖšÇZ•Ú[Ñ–ÒÿÜ3K]êSV\Ǿŕ?n¢Ø3Ó¡I“eñLõ,ò‡Y^G“(Muñòõp2ÿñ£·Çf¹š^¼Puê’¦<°¢&™{3(TÆÜ+/”Òƒƒý*…ß-}:€G 6Í‚¢°-ªÔ?nYÚ™È1”™þR­5V!7Ù&I-xž:tà™Èð0<Ùv8ê-Ø*oáâð(e¦óÝö˜q>1Ÿïl"£­Q*ÝþâŸÞ?rŸ\†wÀGTtXZ%â¯ýÑ;D„õémŠIõ8;rBòîR.Zp¸ocEÀþÑn0µ?nZ`y¬Þè?rÔ6þ¿»á‚y[7Ü·±ˆÂ`¼AP{ëg/¬ŸÊì<Í"=øXòáy6ÞvÁ0Z;?rHj0¨~øýqÕÎFÊã¢qc¿ŠÂ…6[Ú»`]™l±ˆYÐE¯«y ¸ß’–PšbrÛ:órÙ2ë•™Eªé_óX¹T…–u'Õ‡Uè´ƒEgÖ¿ÿ*UÉHˆm$¢8¤??ÿï‰(“»‘‚•é ¥Òé2Qö?0Êßt€ˆâ€þÕŸa <Œ~7R^vU"Š˜üÏ1”ý½ð#9B£jo¢ü¸¯r‘Ö‘’ˆú—,Šhû ê“SêXÅÝ?nc"Ë¢ë Ÿê!„ôâD麃ÃÄ.‘&; nµþw­ÀéŒI;Âx'NèŠÆ»Œ—öÚ.Í£"èÀZZ×!ˆÔïZf®M ´.‘ö¨òäɪOmJRA ‰œÿÇrTîVš¡º?0© …¶Õ£³?0??j_™E…húÿÿõÿ  ¾¼QQËþ;ÿ®!Œãw;¸sÈŽŽþHòÀİ^»°³¼Ð]É’¿ô!n3S¬ºÁ$—éoþÏ­™R½á U’þƒÍ$E‚êJתdí×ù=Î,ŸN>úhà-u´XØ“ÐÏ0ë¶œ3UäYy'Ïl5ïðô˜–£t^Û7JBO¨??”q\JZ±t‡ÛÒjN±ÞÉ¢âÙ*ÙÉaÒÉüÚ»"s‡°å®ôpUtÒ;à ~ÂkUÓ‰)î·?r1æY‘¸‡«p9ðm–Ýóð[§y“kj¯«ØD9îlVá‘ãr5K"ƒÑ¥Y©7Wq©Ï ¸hÊAŒQ³X;9ØÁ¸4Ìõ?rÝ…Uà?r6ç#s??À®½Š§¤ÃLØ‹QdW°9tC(1ñV—.Xêàr–]£¹²U^ÂÏšYb5Ó±GEkñÔmEAL¢î_Â~<24 ~ƒÍ3£LÖ«¥0´U$"ëˆã?n¿é:®NðÓú½:?nC[Gdœÿ_ ”Ì®v¨ˆäŽ‚Ù«÷ uO½vÏ®¢÷œ'ܽª GGû²3Kßüå[Ú:ÎL:?r®Ðú’;x²‘•–ýš}¤Ë1c¡©Y‚{JQl@ûý¡*—g¨O¶xâ‘Ëg®³½dxk¯¨f:&v¡ipH}téUTHÊ¿ %8Ôñ)„`‡èQÖÝKoj›f†j§šv9¤òLÃpí´GàQê맃£—ôK–OzNb M¹¢Çm¿*Á3Øš ‚´¥#8sf¢›•½T¥uôSõ”lÝ·_ÂXƒ8âBq•´ðK†[ñW›ø«{ዽ¸.^-µ8᜿£ƒÙÔÁÜ¥C§zvT’*²v„V+S‰ÍÚ]c@1ó’¥Ó7îZÓj㤋.Nº`ú¾¬ÕE±pu´ÙMkv$i×?n '¾ïƒú}ô{ÖîûÕåúZRQ¾SE-GÆeœÚ–-µj‡ñ¢U)UžÛ_&€ªîØ­ß—Ä«<ŽÒËrPçá÷ü!ÊrØÍlËé ËÑ [>çÃ&ìW²tþ¿›+/²Å&W·/:l›‡0fYø†ª…¿»1¹Q ¦×[Wî8æ±ZÌâ,¸Äx·ì2ù’uËEDÇ*ðÄy–™Îr$/??*·3ƒRæM£í'—iúCb޽a¬nÞ ét‡e×y4UÛ_×ÞS¦Îfåըד\:5_f¡Þ™®âx õv É(aàÁ¯ÜõÇÍa?r(âSܱYx e¯m ýù*µ'+=ßn#A錔nhƒí¢¬;1jX着Zº$ºu^§óô&äþÈn1xNÁЊP<·d×…UÝe??œq…1W23Wós}1 íP°¡1wé vÑH2ìҺÀûè}C!ZìRü]‹Å÷Ï?n83_oÉ.séGp0 V yôíz¢ò\§á§Ë(ýŒ’ߎdæzdË´›vzö¡cùÄY(ˆ§YnJ6Q…¥}-Êb² “òRBºYÄÆjI½ u&Ì ®6ѰF¦Ó=°:+jR/ºY®’Ùþš‚»+(u¼/x{¢e–ï Þ^ 03ó(Ö{—øû+âûpÿâ·_«¨8.;PÌÝ ––Y±7˜û¡V†Br¸/q÷W ÿlǾPÌÝ^/÷…o/P9SÅÞ¾`™»Pýs¥ÝáÃ,u¢{ˆU¿nA8 ±KÒ÷Ãg°5ó¬îz??nJC"Æ"OĨ"˜ÎÓQ!v˦ìÎr¬îpUy9]í10Ý"îþ&pbÊ}qÀÛ”d{¶$Bs/¦ÊW{C%;"EV®f&Ö{c]©2Ùe)j „iŠ­±(“5ñ»À–Qê4h,r!Éÿâ«O>ûüŶSË ‹Ëå … e(ëzrl¬'oÑÈ[èRK[Fx¥màT­1!åù¨)´ÞuÌ éËž%]¤‡Íj3fÊÄ*êþ…8™LZn¹m÷>RËî¹Êfo胱_f†jèÔûì„ìu `Ñ¢?rô\vwö‚È:*)\tÊÊDÙ—üO»Ñ¸- "&ˆ[ì7M޽ü±{Aæª@ívó–ñÏß 1|“nÑTT÷BÃÒâ8Ž~¾ü^€Ël­‹-ˆ K½üíí «ØaUÇžYÅ­4'” ‚ì¼7ï&l=õºžÎç·¥×h©AŒGM”Ö\1+òƒ6A§®õ–Ú<7¦ˆf+£‰ß“53Tì‹lI¼ˆÌŸf©¡4¼ŸõìñÏ^ùÃd +#¹;Llù9r>Ì1¸ œ6‚Û2¼oÎ,hˆ,ûd_ïâm^×Ï^=·ˆåÇÊ®ßnytœN<""=ð0qïÊ'æ,·W„aá× 1Ï¢eh9G yütÂzD"Ü–ò°~?n¯\Ó[Y”r8i¾–ò?0’ò™eT!?0 pø‰?nˆš8¤áÏT”ö¤7– ɵôpÌü··vå½ÂÃŒûÚfæó;È©×D"û+ÈE7¬*j Æ$Ëב¾‚t¦Ÿ¦™ñ]µrW9¸?rG,ˆsþøÇÞ;¯¿úöÓ—\ÆLÊË(Gá‹•†Q-ÛdNM³ôGlÀ¦ñjÄfß±3´Úðð9Wð¼cû¸»ÉÁ?nÏ혨"©é1š(;}íáÈ0Š”Ä%N/iYÔy}p½YÝÔTì“4)š2uœFÎA°LJO²>}ÉS<¸±4è?r±Xr~as@5?0T¬(ÆÖö•vÚøÉ£5b &*úˆk/Ûdbmn8‚š-²õc¦ûìáäâ|˜?r MÈFm4’Ûc©p‘åZtW ó”ÊÇ‘áosQµúyÊI‹>vˆ²OP]¼ˆ5¾Æ´ÉrºV(€x˜pî’âRY¾RéÕן}ùå‹oª?n[/pÄ駯^5Yì3jØÒOÎ.\?rœK'NÐÏø«O~Ϙ>^žyxq; Ÿ½ŒntÝòøÌìªÃ:ÞÜDÓñÄñ'AY¾Ö×6¡_žÇÙÕ)CŸ¹9ìÑQ~}ÆgKÝMž•dªY™ÅäËg&ËOÎb=7§GÃ1ÚF4*?nI$%²ù¼Ôæ÷@’w?0Å\2Áò½lò½´Z4™²!±Ð •¦)‘rd xp@ÆŽaM4Òüz¸‘¼´É/%™ª7K ?r;“&* ýÊÊcGUÙÕÑ+=C¤äç ‡BP·—„kÕyÞ¡ö‚˜ßòÀÅ‘ZJÓWÞg‰ZhëÎD#Äsi”ÔÁ}ÔTŒ¶.ñà$'\¡ ”tA’?r[Éo8†¾Yº(²b·0>UÚ Ù,‹€2 O#ëlg3UêÞ{ÿô›eöüùóO~ï7ßþêÏ~wüü½×Ïèþù/ž??{ñÍO??‹¾~ïåóçOŸÎ6»pªÕ©Ýë™.MûøVaéÕ–¸½±Ê™â-“¥´¦_È}&5ÃùQ?n*ÊKÏ×u‘+؉1êMv䦻–wZf‰Ý–ºU“æ]›Ê“O(H_rŽaèäµ÷ö.€pèTR€áÓîè€ÈìgkÙßòÔ¯˜õfäð—¥7SÁå¢ÈVièÙÚöù)=ª‘(+mCyþ©Áy„w>ûúåW_¾ §”矯Â(³ák;rdrËÈ4VŸúIÝÒ^ÛHFC®cJÄ"ùzÞ,°«ˆ^ô!M‰žfђȇçÏ~|±þùìp¢¯uà[nÈ* ¢¤;?r‡—°Ïù]óå/ÞÚ˱—äðÁÆÎÙb:Oá¢Ññ]G%6¹È8Ô¬¹³X¿êu®ÌÒ¨b= /×TUÂw¡xD›×SÊ.IÔña´³d)ñší¸ã’xo©??K‘.‹ iÑ"Atò£òËUâS!ŸÀ©ˆ†þ ç‹<€‹|¶?n¬UF®¿<#ëá¢÷§ „~âl†Fº³Ñíóȇów§`â8?r{À<önè ̘ H×M• ƒ‰›‰}'tù{"³¤~b8b¨GÈxSuåü<3 Žqù¶ÊŒ±Ð ¼s·=œùÕÜNì¢??F?rQj®©MòU¹ôÏoHGêgà?rÀøä"“$J}pqéªê¨ —Ö%?rDæï¸•ÌÁ5“Ådrô«¯¾ú’?nWDé"š¿¦+ÎIQ·å:L„Ùdñï;vBP£æìŠI9$Ý®PºÏ$0HÚ‡/_é²$ž¡¥X‹ÊdÂ…k[_€é mz¸D5¤ä‡6»+h²cÙŽÉHë]Kqâ,k=ZœKb8u ,ÕmPîS.WóH÷R°`‰i÷‡Ê aJÌQ‡Wòwá˜z¸9EtÊÜsÔÀ©U?n”ûÃ\QS(Û@L»??TŠ…Œ®“2ñþ`ÉêeÛXL{€Ç—Sjê]§'Òƒ J­/»X =,œë?r¬`ý?0gÈbšPu ˜x° Kr˜š£vá; m |‰=í¿wàkjB‘[Ën“qÔi‚¥¯-›??ùlŠ„Á>V¢ÇI¶ãä~Ñ(ÝÇ2ÚýPJ³© hw›‡g„*€Eø”Í6‹¤o«¼Í,¨?0g ‚®š]Gh×¥îFR{efy±t€ºÛe“}Ís’ÑHë]˜I´Ùu¥î‚ÏHšß…ž=’üÉ]HàÙik¢È9­žVl¢Ôl'={ÐùÌf‘Mèõ'“ÎV}b[v¡÷ˆ7¼/5?r‹` JãnËY5L4™± ¸µäúû JÙ?n1”¥ú“G"I°;õ|7£uŒÖ‡ˆct1D$Âwièþ“]G«fP˜š`RHîb q&óCéx DÆÛvÍVîuh?nº-J—ÃSïÈ’¨Ÿ7t3üÁ‹ßûú‹'ßò«:±šcÑñÄ™_` 5…ÆV®xIhܘm®1àç©Í"ÔÊNìեĺÀ?nöó‡QWˆ˜Ã?rÁ&f½f‡ËmØ M`v!0+B]ø#^˜?r³th<€m·ch‡Îײ†ØátdÇ+wýÜÿ?n7ßõq»á­ã–;.èvµr¬|Å')PGn§kw6NšåjUÚåJ©pg¾™‹°–g¸r .Hk½RÌz¿]l¾·,??©¶ÍôvüìÊÁ×lž§y‡®ó™*&«Ü}¨nìépå®¶ŒJӌ𷘕Gš’7¢R. Ü‘G†ö½ÎßÚgáñŽuŠ<¶qËØZ2´wÍåÆý÷'É5BÄ«¤Ô1öÂèËU»=:ð`Ù)!7N‹Oj2q3§õ ëËÞÖÀ£š¡\õ7¶ åJª‹[ç4É){O=г‹V§¹´½Vr œzåœs³ï°iaÔf+¹­›0*??°V’"ÒžY1ŽK?0yß‹1däN²­¾± (ÛÊnyÕÿe4µel)ýÈ{ïƒÞÿ`41Ù+»ä??ù`T/÷Ë"ìØ£¼6A ÃMÜdª4ÔRçþûctI¥ï ˆªÒKí§i1avôrÝìÓˆ‰«²ìv‚»©w$*Îi¬‹ÂiœOg…þ^ÔÂB]M]í׋o˜cÉδKæÝéîV-µy|#ƒkn S5V.]CïSgh¡|Û91H#ï)|̉å[ñŠYÑŒ*Å9šâ¯]œVIü!¶”H†-o‘NGÉŽp(Ô‘‘_Ãʸm-ƒ»¡%!Õ ?nê”}Œßq#͉we¾.ðëØæ?r%Ë>묩5õå­yÑ‹ÅLùÇ'Œ??x2þðý±=|h5<ô°xdÏކ¶\Àuw.Ï‹Õê²µè‰aœw¶ƒŽZЫ"n,»cëhp[J®†•Ïì°8›ñ?rìqœ­ËüúrÔª??at}Ïñ‘°¦áH`-®SîRy fY?0péPP’馓ŒbH2ÝŒÂñC'þq­hºéÜ<èô@O*½¯‹œ[ûÀ²››^ÜÉ&"¼æse}Źñ@®8mkäÔḋHîD|XºÞÈ,;“#Ed.rØ‘¥Ÿ\ôŽxZÇûd b0îln¸h¸æóC²k,̶Dv:M,{šµ§??ë BIµÇu+Z×-2Ï·´q+1\ÑPSåY9öP\e'žáV¸rӦЦҪx%:[QÍL52ŽºG­øMmv`"¢öC‘Ä»ºP§’??¡k}Gu:•ô`Uà´”±,6Çì¤Û…DÜ8ñZž$¥Åª‘‘\÷ô˜ìN𛄓ê Á+£ õrb¼gÞÀ](?0?rØrƒî?0®9›pµáN—ùÅ„É@¢nQx¸0@W>x±¦2|•T] .üŒÝ)­“ÚÆðþ– ¦?n2éƒü®àš\÷sð¾=¼i¡±±ã»å|ã…Ø„ž;˜È¯¿ÃŸXø}{¨³úæ›Ù±=k$`qî°ùT„/|PÓ'ÁõkÊ)+H)fgáöÅz¬_R>d˳ÜgŽ?0Òì&=gP {γì§üå½oañ£Šu"¥ã ღ8—"œ|KŒUаž”7â',Yç??à;S|lÂÓÔ,{¦çdDgB£ðbÀå‚`)Ø=€íù³J‡À·p·ä??¡.F4âù.×Ï·yº¸¥y;ä1º4>à„šílTA‰NÂÕM`™†½Ìköð5y/>\WÁª8LćSx”ó’j¹ÊŠKôç¿ðeßÄrÌX¦–ÅgÁ¨TÔ\ßÊÛà­T‰ ì‡Ý "?noÂéêF[³Û2Ø„M~“na>Nˆ~±ÉÍ>¶™cÿVÑ’™í…³ ›Ò)*oW [´¥èÿªˆ‚,™†:hCòÞ•?0Ò™¸ø$k\­@%‹Ç ¨Ê‡cæ•îy3‡×N¶Ñ®èŸ¸S î dëdˆףܽ™¡¢?rÁ/Uƺð‡ni6Á÷=s¢AV{q¿¼X;DFãİZ›?0Í{Ö*x bNRèØ??8¦9®÷–üæþ)ÝKÞ¯CÞ¤e«ÒTHˆûi™¥‹ûÁ`ã‚Àµ@Øq•[1 ëª??ETr—n–e²-ïݣ¬íãÅüÖI<vöÈrD›­û¹%âÿÞèH¿,‰jÓ³I^Ó[¼FMz-3{µµªCvØõŒr—Ý1îþõ²ä1°s‹úµ;Cn ë7&ª«”VæºØ¾J!ãÓžÎS“&…=RlW-ªžÎ¥Ø„6ÇDሠ+#C6Áá!M­štüðG{Þxæˆ'n[!Uj]U5w*ÎZgßnð›+ÕëJåH+êc˜P»ÐC:?n[´¾FDzûøFŸ·u:"»X­c´ƒ[VmØž P­²Ò"5Whì™èê£gjUi²<„átc‘©¹žòÌ«–µæ1¯mÝÒ"ÇmsÅê–Ë€ënçuÉä!,SÅävª‚'·*šãŸ>>®nURܪ\ߪÕm8ϯ’ÛД »(“ÛÈÐßâ†>®ñqòB¢$á5>è??{Bÿ!ip-è?? “Ü&†+ÈB\.H¡nË' ýPùä7·F]Þ£nm¬ ^%êöj}{Ð1^'×ø ÿüòöÉ"¿U%d‘¶kÒ‹þ/é??£ÿü=ú× ûqb??MI_ Ð}º2¤MBÿëÛ"¹¥¤u6»…’DkA÷̵¾F`4^™ñ+rTÚi#yr¼JRw^5³óŽZ?r¸Ïãú¤q”õÍ\Ø09‘öœ]õa×­ŸÙå9DŸzŸÅÒd#T­Üý‰N˜”%`œÖî©¥ |½·{Õ¼š†Û œr_‚à'Gª§¶=­v_Þ,ŒÝ~ùr퀧­€9)dÏ£š#á¹Ï<†Û:ÿ1ÅŽÙ˜MX2››¯¸§¶ÊOÞ¼V‹/U‚glC7·Å{&ÁbY©‡YYe„_çøh<%HFbÙˆï̲u¦ä¦Û;WÌ+³òò<5¦•&?n·ÃGn÷¡ª9^J#vK•ú='ÊK·t*7‰î<5â^Œ„E??ï“7W4‰rR?rAÊ̦ÿüîçᙌ۫üÍÝ:½¶YÎêU2®:»—+Ïø½m¯³#ÈÞÛ@¿“ŠŒÑ?0?n!;iheë ÞC2’ÜŸËQÉ ¼?nJ?rêONw‰F´sÅ6€¾Î£³x/k´G“÷½_òpDÕ]῵«ÐÍO÷ŒÐRd‘?0^ˆ¡w,KªNUd•ÇÏsjŸ7ÔÅÅeuŠ.?0»JY*ý)!D­%õbÝ7G¼˶‘øªÀrÛ\ÇawëZJ÷Y_÷¨os-¼z’bgYáÛK¼Ì>Kü™ÏWöÉŒj§F&æÆ'·´Y;m†´ r†³F¡Ýž\ãQÏzp±%²u¢ëÇü"Ûî:Ôñˆ7šûvšÔr»íM·§íqmõö9êç[Ôü`ˆ'Hx˜Oñ˜(ÅönÛ!M¢‘ÀPwWöä¦ýÄæaTp†vçƒc¾#àÖ]"kqL!/¦UñmûmíóK )?r™“£48ëÚðšO_~óÕ¶=SC<åv$ƒ‘kµ@›Ðå™4„(ó‹ÜÝ@Ï×âIN÷le6•mY¯–ƤòvODß­?nŠ?n,^‚@KüR!TèlKT¦kX>ºnWÊ&X,`­#ï| H”´ŽÆxÛÖÈñÞpÙݽKYØ4‡žï®~ÂÊ2ÝYSQú{-#Aa9b=Ûn‚Ûî7©Tib0sgóÄ; ÇúHÚï´÷l~—YS×¾¡àÝíWE˹ @ÙtYˆc3ÉthG0B]Èg¡Kmªj·UH tr}¡›1wç÷cºãÆ`ï¨5"v,•èöZec¸u8ñîA{üÅ–&¾ji5`‡´ÈŒ½0{þèé/ôá§Ø’—£K›ó{¨H.€âKÏ^{¦pdÕ&XZÝÜžt4aFKá>¦F˜B~|8®^(õcJȱ»Ô©_ÛäºZW¿ægÚ)ô~£¹EØ>ÃùÓÛº÷…vͳ¢E/6÷Q›BhG¼¸±I–“`Î,r–«D¥ØðòS´¡Æy1t äÊæhăš£‘-ña9”í„&8^ÿ,ºq™®©’râ[=»Ò×Ëâ r¬y”FåR‡c&g…a£$›i\ºŽ°’’"0´“š ]ß@äq‹Q)6ò~‰Š|òžû’R=êU\Íø‡l•æÍ¡ ìÊ Øyû]Ñ'uÙ˜„þTöYÛNEˆ¥–@ǾKA;ì0°??±— FÍê¡ílÝžqsM¨ü†º%Ð6±±6ZÑÙ¡†”Pù¶É.EÖóŠærÞªh·Fw;ÎöªŒšgH>t§Át‰½ÝŸ§ú¢L¾>26iUãÀiŠÝf°[VŸˆ¹6ûéö \N#«˜åîŠ=‡ï¾+¹åHtÏ› xc9Æõ–‘¶{¬ßd«`9SÅA¢‚²Sü!ÄÔDZµ¹?nùêÛì'A^†:/ñcŠƒÏ8â¾~%ùp~»›–aQû˜”^o)ŸC p‘’ƒÙ)­LÐÍ™¤É‹,\nš©øQ ò’’ìñoF̳²ÅÒ8–±Š&UŽé*'wÖ5Z×ý…Øuÿú°<±õ2¥¹9 £Àø†zR¼†I–̲󎆉y©5çìÊÎ…©ÑÇå6\Ëü¡XÖ—ª¾4¼BD¶…ºÁ<Äî£õÕ‚ÍòŽõH5«ÑÙ‰æ-}š3‘ ©Wµ…À‘³Üœj A?rG›]½É>Ï®tñ©B#° ë5ÇvVÅžzF’D©[šÆxSN†®QV”ùÜ\âgä;3ŸP±æµÆb×rä¦,—쵪}k?0ìü[vÎ 3&?0ú_Ò¿ºhW²œÝº-Ü;¦ämhaŒØö‹_àMKÓ(ž’-¦ßºN>Œï~ÄØãÃIJ¦»ìy¿°î?0{?n[ë÷‘bSpÃtüž©d°7=*¡ã†éøYÒŠŒk¡?rªãµQ(ÉB ZETÉ!4”°a#g®·NëØx6„ðò…Lhø7Øïȱ’Ç’Z9¢r?nÁÍëHðswÙ¥É8ûΗS¥òrª?nçL²qÍS—£{ðÝ¥7ÔjñBåõLU§:l¬Þ~äFëMÚÛÚJ0©X‡½oÛaf®ßž¸cË3”%oÙG?0¥žk@&կݘ©Oá‹zAoÆí«^A'úWŠMôý—þ÷ЕX€CN×k¸FºG[¯LãÀ³6(—Ù•=e¡f…@œpäV\xno;ž†àH&->&5`MºÕ¦Ü,°¶ÅîókìýézI’.ßÌ¿s!VvH‘ ?0j%%êÉÊÌê®ÓY™uRYg£X|?0 Ä&D"•­Ù÷}æïlÿf½ƒÙg.åÌ?rÌ%Ìk¯[,<" HÝ={V‰w777ßÍÍÍͬƒ¸P„)]£,#^g:—fâCHª÷Owž°všrëSžº”9h"ˆ©õÖNÛM£íkˆû5BÔÀיŻ.ÝuÏ…ïo"Eõ7uW]BTˆomŽuzÓÞœäèªv }@Ï%†ä·¨1GZXFãÌ ©ûXV ´¬‡U¦Ši`ç;:Nº°9,z>·+|®@®pPÒ««†<)?rÓY×7®Í;…_Žšñ(•W¦ÁþœóUÛ!TÖ[&™¢H–]]dî\0ÒL¬°:äå †?0› ƒW *.!=Ü¢‘âd#µû«Î¤?r1rJDV$ÄÈê*B°ì¹,>ºw›–žêlâ??U·Ô·ºøT¡3»¡zÔªŠ™‰?0UÄZØ?0xPJ¦¿U¸Ù"??Ê›f™|Ôü»ï{Œú1jˇ;R|ÓBp½’œµ‰Âæ£Ìæ[o¿0&rž÷;!@‰ÊåmqÉÜÏš>K+š¦6A/wš ¶*h¥·.÷•ÉU³<Øî§¯Õ·h±r?0ñ‚À¨°˜ié/¦óüJðƒÊ +Th KðJ..»šø¬`[á=Š,9¿Þ—œÌ¿¦ÅÈG˜uü™™Ñöpt‹»‡??LïF³eY¥¹ÀG¾Ô XPd•'VŠA©…‰?r` Q ñ?nÄ„‰&%Ò}D(ÆóŒZµn×lô°æÉmˆ#°‰#ÿ¾ ñ#°.¦÷ðï…Æý‡?r03k¢+-J°^¢ã²ÑwÓõŽÒ>bÏ+üŸ`K­d5—Èö£çò÷1¾åj¼Ú˜ó*ÞS¯Ädæ×ËÙrýC¨¹?nwÀ“tÆýrbS-E‚þŒ¡YjI;X…¯Nr£ƒAOì0·Fš€+åÁf…kö¸\Bb:ñ"œZ}.µ3LÑ@•jq°,Á¢¨<,¶ûD0‡¥Á'¼ÄÈC¤ÙØäº+Î0Œç¥Ã |ãÉHàpã¾ðQÅ' ° „ ìýñrÁ?n·æI?n îA:ZOÇ?rCU„z¾?r?nš†ËëÑkauídïÑþã'û}Œ—–‚K‹¡øÀè›dÅÝè(ƾ¼'b“ÏFªÆŒŸ^$RÊÜ«u1ñmJµ†T·[R%]Šêq‘×9,‰ˆ=7ŸÒñ(/ÆD•V µ.—ìé7ôN½Ð€ç2J›£âó¡¼=Gèì,ƒ¤Ne|›7$üÚöÀ§êmµÖIõ«+òÒߨšéý‡É3·EkVRuñ½áx»“†T]c‹ÓäéÚió2––Æ£éƒo!`÷ˆ¥¦ÈØŒÄS5 Á7#·â¶n«G] @ÊÀ*X¯æmÀR‹K@¢¦0-›}5jƒ Q?0;h‚ÀÂà€›€dº3¦/I:ÚªW³hG›·Â&Üeã;þ´ ϧT¨‹FIäÍ.¾‰é£<•4àëøhîf‘Þ¬VÔke@â·Y‘âˆÊjØ`.ÈÖºº@;ÏÑ»_HÂ<¨†yä¾o„øû(þ¾amÄ?rGV¤«k!F"Nò Bž!\î'( ª ½øÍgQ?n‹AÔ™íû|‘rvðBGOñïñ¹‡ì!º÷ÿzøGèÙ$=B®ÇOð¯ƒÜ‰èQÁ.‚@ô¨w®CRGÜY¾k‰5JéÚ¢aÇ?rkVw]d@o|ƒÓpk³¯šzò9µö•î>ï±ÒÏŸ a×*??ùëUùÉÇ«|t¤UöM𸢠,BÊ“m#KMãWqGj??¼gèùóç?r[1J?rõ¼º¡ºÍf²‰©ŠËƒ&rW·ç£Vh)à<Ò‰^j±^Ô ]ÎŒ¸|Û­…¨î£GõÉ7Œü¯ëZEZ$ÿ×ý??eôn|Ægq«ÀQ¬6…¦žÕñù‹è#å2KeÅ£Y¾F¯ªXÑU=ëQËôß&_bǯPs HVYp;òidµË²5)Tº(™°2·ˆoÚKÐîø+¼Ì¡“ÚȧwÒEe Ýgå5ò©Ñ8¶ÚI´Jpo‹Ö÷??’ºŒþ`«ñ.'Žx*`ÿyüô¨%°°è5ïÐyïÐæ=éEòz>t ^%ªáørª€ÆPä8f·ý•öÚkpÇã†0æ24õÓ›õ( ônè÷ywP,êKÇÏží)X¯ªRï3¦RL‰møQQ –Qy€Þ煮J}šëŽãðãÝ1|É3¢!‡ùÚ4wŠ?nè}ä= "{Œ³¦2©’Ö°¬>@,~X׸Þv8ÃG˜mx,ݺ¨>·¥ú¼6oÝiE;”‘ÛvºŸøqÍ(̾lÏ-9"2I“7¬›cÅ ú»&$½ÝAH 5eu»µÐ?rÅÔ¤¬­À~•¬ÝðÓû|I(°ÆSš‘BNQIVg?0  ü“(PfQøf”n>- 7-ýï÷×Óþ"ûº¿J4ãÉÛÁ%B„zghßIpbÁÉF(eìB°þµK•ÌíJPÊœã&Ë­üo@f0‰e$—Án$4`u¼Ü?0ñ^n`’?0?0%E€yðèY0¨’€£]E?ný¤“ç¬)öÞÇhrz0î¬Ûfÿ‡eWU  HŒGwî±0 dv{L2 ÏoÿËTPÀŸ “²‘†D;€Nœ$6®5Ì/d«ÎÒ­Îd˜!’׆Ö-Ǿ. ‘Æq?0çi¹£,‡ Üxû¸ÍZº’+âÊÅoYíŸé³%$ïm,*Ì ¹NÒÁˆã¥dyˆ–9±<fSÛòËœmòJ7Ëä½n§á›FiÄr¼Ÿð,âz­²Ñ¬°˜±UÃgÕÊÌá¡+ñ 1J1¡9àmʓĕãÂ4¡?0°õ¶’t™@Ù"!´O²å¢÷Iq5þ$?nA#:•ŒãTŽæîàѨ%(ܲGJüÅ4)-wè»Æfàç_hÂsõóg-ËÁK[Û«¢;‚ùJBJÖ–_ ™iËuµ3ÊkV“ùq†ðs‹µ‡2 …d9³ƒOömº?r—£{M!?rF—T?07Ãý“,[¥Ç‡P•¾ÍÄ©¸PgËùál:X÷×÷‡ý5ùÝŽs-A2Ö‡T·þûp^¾<39Ze7ýÙáS ¨ Qi{|Aˆ‹?0ò?r/½^¯F—Óñô9Rį‰*ÝžÚ–¯IبBo¦ÃßþðÓäùãÞÑÁ×ñøàõ??èv÷`1¶_îáØ½ú½SfÃŽ{Ÿâ(F—a%Ä £ÛÆ’N–}7ˆúÆ0Ú@ËÒ¾¥87uZû7«ªƒëÐRæ1oº9,õ(™°QrRL¯ úÃ6ųÓ3¿á g ø9@:ºŒÉÂ%[ Hc|j÷YvÁçäXÊ_Fi9äÛpšÐÅuaÑê; ¹SÉÐz! £Š™#ôeò^QntÛVDëëа@Têú«ûWW½%æšh’7…ó˜®McßžÓºåƒ%ò-ÊËÄk%¯4M2›žÂrŒ—H˜ÎG±¦N…è`ºy}´p)e[Í??O5·%­b’‘ÔùÅb9F%£/!¾»g/å6#rᆭù” !œ"rmÀð]G nTÜaòèœÆ3r²§–ɦÈZ=%Z˜I(´ªE" i¥ÅîñµùXý\‡w7ÎZk>ïÇ"¶c©æIx†cŒál‡\`.¢FÔÔ•±¾‡ª:¨Ç*ÓïÛ‹r«ÝÛk÷ö8è*/ô9­A¢ê[ãào­9Ì(YÔZJ;¬Q^^N“#[Šë^‡ø*¬8‡³@É(‰§‰€ÈÚ?nÍO6jM|-KZÏK([Evúd7Êý4?nÚãq‹/†³Tϸ$Ç=¶ÕO?n×??´E-…ì&²«ŸŽ|ýYÑz¥]6ÏÒo?0¹Kw” LRŒÔE^®§WÓÅE¶n.-ÿn¦¥]J`/ëp€0eÓj`Û’‘~=?0»<]D³Æú§jg‰¶e¬}¼ð lóÚM‡c߯–wîØ<2ÿPàæ„d5¶ æâ&ꪷÌ\ã=>{ÎxšxÙ8Ï‘oTþ§öŸ,—×ÉÍJäù;&=Ä.¶ýÓo©ÅОf}Ä–‘Úø[G~ª´î$ ß}¸á†LÔ¡î±PDªYÐ}ä…ÛçkgqÐy.²êfâHRîXÛPÑw“ÑhVUQÝhÓ,7YN¯À|ãzh–õÿQ‹â»m @ºô¸`åpÕ¨ß/‡ªo®HsPÖ"Pǔ•^ôTIb<›+ظaF›þž3þß²ŽúRíløÃ°Šbªy«h½ì€·Ç/]ÆuºÄƒôÒëàâ[®J˜a¬&× ÕgΣ¡ÓËÇ‘Ü5KËaY¢ŠEZwÈR5LZ ¡´F3^L±"Ú4‹ì¯GZº¢ô´D„•Õ2}ÍšYV.]õì)bNæz^ ²U'FYõƒï5Ä+ÃÀžâ©ÙoñME@¯¹‰>)Þh*h€ªÊsþ|º@'ƒ¦S÷4“à5ž¤X¥Âªnb,#üáÃ…OOž³AX@sÓ!öÔõr>ÊŸ÷Ó•¬o<§bÁG®›…tEÜNG¸6Á9:È­R Òå&‡åX*çAõGý8òƒ—‚YM6´S¸¯mš˜Zú40x6W bx•ME·¼¼ì§À€û†û€;×TÃмBBšã”êü”uTáQ"‹íĪ—[—fƶPa³ó§pÔO¾§t×ün¹›ÉeCYí‘vŽTL5Ëò€áãª@ˆfUl‚k¥káSÏ$Î<["M|Ô‘T?r<窩%ÁЈÙ:¯›·®*'Ž {éßmzÂb"zÆõMD̻˅ºh^]R€0´Œ?n×÷Š3ðÃXbË’?0´xªö@(fðR†‚U`:u¨ìáÐò?? hr£^š@¦ú¡ôé&Þ@*~,I7Sh^ŒDމ{;ÏY¯ÎGéËÛ׆°ø "6’wÉžFËk´jUÙr–,ƒÜ‡¤ÌÅ­e®>Òž&fí¥Þêˆ&×_}Ù¯Ñ#5ØñPu¸ÖðǧnÛô @˜]=´XOk>4Gn“Òä7Ëéù¹Úwò†e¼9_FTÚTÁ|œJ‡í'?0´ìøWw¶ªÝ>??Ô\󗌱áp[Ó£DX!³eÒçË=¬ã!NÀ–Z»M¾Õ4ÐîÞŽ{Ç3{pWÅ ÀN¥ˆ9;ê¶{¸»~ÒkË?r¶|öxA¯—Þ„ù÷ÿ>ë0þ ~Ήç?nŒñà’û°HÔËqùè1òÑyNÄ­@ôV?0 ÝWÉ?0 h"@ ýAwš×‹ÑH~É;ÈG??»fx–6,ÏuÈ3‘LpR“/Ãf”¼¯IG›ì µËÌpP–g¥Î}Äp›¸ÕìæÊôç’Öá÷åä¾»ü¦¨ÒÌ>ÐË4Cø—ñ©"˜‹hâŸ÷î©®¢Å¬M_ØÇï±¥–,G8PÔf;¬`/ hJ"Ð)œƒf$ªÊ„¤[(œ¹C(ÜÄSW1r’煔ܻ"B›ÒŒt‚!Ô_`!ÁVr0† M8—½Äb»\¦YÈÓ\bt÷±È®¹Ò./1  (2ûþž,ÍI°Ò2÷5ÕšU5=·ÄY€ú›‡¯¿™¾½éË~§XDþÊR]ˆ~‹ý';ê}µ^c©îæxޱ5}5Á¨«†@ëX¸VPWfœòÓ¶xÀ¡œzÄ;—%X{©Ù:d|æ3þ=‹‡ÙH‹•i»É°‰e¿Íñý8JWXŸFMÒ´Ÿ rû k@b­£%S{VÄA:O“«år¸/ÇOŽ ùË-€†£³9Fµ*x8Q—ž 7|¡W7çÖ‘è%iW¿`¡Lâ€DÊ™Ð5y¿Ÿ¼ÝOxÖ?nKµŸ= +UKé‡uÀÙØ±F²@=•??ÏŸ(œtÁdÝÔd5/rÆE¸71XQ™›Mr¤‘Xa-Ò²v*"{…ÈÎcæ‚Ì:H¼EKç²;OHà_î´aqðX~’«xh†ç„Ü?0>Q@WYŸd±èžeé5« WEVF狇‹dmêò5[Óf«‹œ<”ÐÚ^¼£bŒ,ÎãÙ•@›ÿ kKbÒ&GúÄÞ +#Ü© CQš0ŸÏy¬”Þ÷Žü]ŸðF@´v¬(‘ɇޛhHé¤B³ à›„¿ÀiÔÙ°ZËovF°ó8Œ|žï¢k6µj÷zyƒfm’t¥ÝõDõèF)êo¦]”ͬ]—š6ÜüTÇTiU,ùìc/t ô,’‡6âÀ<]Pdꧪå)I^Ƨ×#®(´qM‹@^?nxÇŽ¥I|leW=ÐÈ([K!* ^){?0Â"ÚÜ£oÂÿJ0vÛÁX]XëzŽ0¥~“XµÜµAŽÆBqnwžæ˜'#km‰¸Y@X2Κy»Õ3ò¯NÚ*íõ€?ro­¸Ûé…ÎÛ‰;pݱᒽè–Ð;·ÙÃí`a?0lNéóÆAŽ™peKÞÇMÆúz±ùHN~åÎ8n8k[ާñ…еŸ\F+4Ä:ÍDG½ @Ä-óç}½/U2°™ùÂ)e‚É}𠓍V;ä1XÜ’ $HÒ‡‰¨{L3ÜrŽ1%¸‚^¥õ]‡éÅÒ`t%ŽGØ pP¥$[²šxR,>"ÕÖlÝa´ËY)Û_\í¾¹‡-Üéeúõr.ÂÜt´Aj†ÙÐh.û“h¢Ðš'@+gDIÔÑ9‰« R§Wu·¸¨ãýßôRl–GôE±A~Ô Ò+@0Öí¢7;Tc1š?nâè˜Ëf?nåZùí]í@ó7þW˜Å®é×¢KÑ$û)Ï‹_þإٺbo}=écŸ½–™¨=• ¸«Q%èï‘â?0gWÝŠÑm‰½úÄõ¶œëm9o™S,÷7ñGOò¦1Oîi2?01Tba)Ï’&ô¹E¨p¼©Õm_Ȫ^œ¾ ºÁòC;ÄQV’ã4"HRcêízM¢•QNìjÎÚDI帰t`Hã”^M?n(³²â”ž¥xŽ„ã»|ªZm‰)Æ{„¡ø5F|ýʤFw?0»œffýÖó8²‰AJ³ é ÚB¬ïÔsš|zé Ä Î<ž¢´ª j´ÜKËåÿý9be²7‰›ùF y)éÅœÜQ…??((BkEˆÏ!ãv@è$ <ßžç¶H¾Ú¼ÚÏŽú÷³}fØðmxMï¶xZãK¦ºÆ¯o} H5s˜FYì[uØïµGni×à–e‘ ³ò.“rÑoÅ辜÷8!-†%îÀš¼ÝÞØµÝgý÷èð÷ÞKVÓ:Ðõ gIYÊÎâAÎ À_äÛ@2ÖNü­ؘš»õû¢zÎ]ç}¾¼Ýl¤‹sënÍô·ÚÝ·¾¯o¥o–·e+F›41{Àdce!›gVì¨'ß9—³p\Îí§wÍ4­é[ý?0WZülÒª|×£{÷Ðcµ‘Æ‘ûÕíøÇŒPó?n-9ŽÓ¾I=ÐäNÎRÕÕ?nŠé‘ÄIÊþx•5kU“ã—YXóÍ??‘¾)™“÷ÆÚ9m°Ø7,Š»–éŒÓℜë/2QÐ8}‘­ño˜ÈÒª¿x¹÷hï”/œÍ©£HxÌqcv…u$[˜wG<9Ãs3uä˜RFgîÙq´"ŠŽ9&½`·:™åLŠÇ'³©w–”Z·Nð`[!ô€ÅæE´¸ ê¨%bäaˆ"¯ù‚Û’$jã?n£mÞøou;??ëï£0„>W9´AP?n óó¼|ÛS 2¡ „rW€Pß— ˆêªßÖÔz NhP"¥›C ?rôî)úR;ÌzRY‰=ŽÏ—{x¼Ç"ôæ ôãÃMǵˆ¶çNËìÒ‚¬‡{ôI…T—þëÅÚ'1?rG7EÙ‰”181|Ò/{¼åúþ‡¯¾œ¥2®Vˆ±ºôÎC¡•É>ÕyÃvýy ^ÌO·´ÚkN£Æ‰‚=\o'¨£ UBqþ0¾Û6Ï?nJ° &rŠvë?nЋW?0n».?0ýè?0‚êÑ“h„—G30+¨Ú.®¡õ£™/n£±qjц×ÿöFFMo¢ØêîÔ?rl•_1õÛ:£@TÛg@WºB›! •s/' Ú~—UÝÈPîDD†RˆÀa;??ñIºM‡D?r„V.ÑÀÜ™‚ªò+JUæO*λ‰ÁTD”¼¥7»ïº??‰²¡ "‚Ë€ïŠv=Ù¬³%C-¹”:´Õpfç¨zbËV°YY|BÕ,ÝÒŒp±dï~½† Iy1èšç‡¦â\åp±Ì ž>HâÞ­nØã½Ë5Öíø…QìÊÏnÆJχ#·Ç6 áQ®’ÜD8lÑæè€^p¥æ(??à5<‚â+=º{×_/6Êœ“ÅXY(ÍT§"Ëd¯­K9Úó÷w??5€³%?0…Å”v‰Z+Ó‰.ìb,ªÌ,Û_¾Ö<}5¡ƒkb«'ÙÓWñpøó)Ž_§Ö=O5’18?rgÙ‚;MôPÐé‡6OSüÚÙ:)ôK¢'õ|r™5Üû@Ã]tÉîUÅ«ƒ=íU‡:l"f@˜oÁ¦M„J'«€t>êký­úöP—Žfm½ü²–Îõëá³9/tf°ÙµCÈPh‚Œ ¾ó˜}fæTP¾1À¼*w»¿Œ›0ÛòèÆÜûÊPl‰¢Úõõ¶ž5¨ÐKdª¼ân:\­Ðû¦–º’„è¥Á¸Î5S72Ž­>>d Ü,£}ÜkʲV”µ²±m«©ÎˆÈ°%”4X’y+F‚ËÍ+7€¨{=ó¡½%®eT¥†QÑÙ€àÚ"Ç£o–Y#ÎßP/—Y´–NY=û´ØÏ‘¢.ƒ6Þ7_Y¬?0€¶§{§z8ñª§€ø-ò»÷Õ:8ÐÖOÞ•›ã¦|¥/g¸õ#›v¹ÆwObÊ„Q~µ¨³‡eóã^ßÙœà"(eã!×À—063œSк—?0â4V`ÄXèåõhl„xÙ.ct‰í’D–&%Š„Vî‚ö:=ï9î ËèMý”:ËÙäîâç”-ªk+¿m{Aƒ!ƒñ;>(ßlA H£–o¿Ô«Û¼äŸZAÌ}Æ':ªÞþ,½ò=Ým9ŠuÖ'|ŒñVËjsf™«3^FÓ!¯7'+g(­Š›ÐTr°µ>ÔL5Is¼fÑØ—ñ"\©:ñ›÷x°ƒ4ìPak>ÐI#Ã6ÿý“ ¶ÑÐU&˜+»"`_×v@?r <,8Þ5«•=1æÜÖ=Á×iäYÜ0~óÉf)E;)'ö|¥nÛ‰&݇ÒãÔVðåàgpK˜`ÔÏäáˆ>åŒhç^«Ô›H[]ÿ±o°ð¢îÁxY0"EasSÝ6±tIõaÙßT½H«"Ò¦ËMëÝ”ª)¤¢-ÜJn  «C!í÷ß~ó»¯.¾ýñÇ‹¯~óÃ??}ûÍñF`Ìa8·œ ¬çÉG°|ÿíOÿà‡ÿò¸dZL3 0D^ªGžÌEƒ«Â…ÕÎǸ~Ó¥É7€yt·1Œïìâ=0}ñØÑóÕb¹šé¯¡‹u‚+oøDƒuª$pM˜TÍÀž­Žf|¬ëÊ`KƒŠ€Œ‰l^\ý,ö4ÆS}¥`Wú6€5·÷bô ÍýÍ·_ÿðÍ·¥>ƒÆÂî8^ÿøõÅ÷??ütñúøCõ3Ò¶¦¾i7ûýË¿š??ê·šo^ýÕ­Ãi°–­øù>”ú÷ÐÚNÿA¥.LÅAAˆê{•Ù°z VJ®:2ʺözßÀÖäñjÃxßBÃ8þYVªûÍ‚ÉøAçóœÓ9yXlU,Ò)t¬E©€Þ,Þ,~‹ïãäù¯#_{š?rVâj¹1ý/ÿmX0°^AøfÜ£Î#‹Ã·Æ¹¸£œ«Íemäf¢ôðù&=xIÿu³œXÄ[š‰«öQ öQ?r,(`#°¾??‡-ÐíyÜÙšÜüü¼­«!@ÇüwßþþÛï’±ÿÃÇ9ŸƒŸOó§é„¯uîrsŽ“µ$ÿá_üÆ ¢Â¡±ñß~õ\žYGÚÃÆ?0eî·B€£¤Æo¯øþî&UCëñå¡¶›ÄÚ)½þФfæKéBú£êtô‚¤ãGÓu Ý‰¥kK>­³0Ëò–?0ôRcói;±+]ǯð±n_o¦²ÈÎû‹š`7oŒ7,Ç8ÉÀ ùµ.͛ݹ’uŽÌØÙ¦:¥ã¢NÖÃ7‡‡:[Ñ4ô“{¼p¼çÃCÔ£‘l®·^ñz‹ÁÈõg¢÷Mº9äè“'¸³„_fÛPn”7 ®"hw)ÁV‰xDÙx%Ï»h¦{±d‚ÖÂ6†ÖÊh©BZCk&ù ù†£ÕˆÆuüG·#ý’!"M0zóy…ËŠë‚ìa‰¯4nâQNŠ}³n›‰úP¢ßÎ?n†ó^a!ËÎŽžÕV¡ç|°Ç¬.•ûíÆ?0ÙmËò[ÌQÄŸ~…ÛÇ«óVs|pÖ9xÞ??Ÿÿòl¿û²ã_·¶^©¿ˆÛ 5䥪™ÎÕ§ótÈ@,µTÆwcÍöÈI]¿>û){Ù9ÿ²)d??<ûýüø¼õªÂoÚç[g¯ÓóW_(s(nd±s¢Ó"MK&²„í´r¯1^OIèdªi‚ĤXÑ›ÏànÊÂ¥’œ¸QÛòŒ?r¸Ÿp`¥?0ˆÏ³Ð»›ÎU.z²Ìpõ^Pïr’{O¯It*fw¦pu¡N ’±èKíD%«!Ô9¶P×qÜýòêb¼pžgÞ³4B¢=ŸÖ)† R™_°‚Тt–dá§U Í-I9¯6§ö1Q²cœÖúå„–?0¦ú"—%ϯ`m*ô¶Ö ‰Wšê$¾S¼ `Z_ÓÔûshœ—ÀDÌw?0ãÛr˜çø\s Ì‚8þÚô)Ík›òN`~Pp&YOØLetîÉz:´Ì_¿ŒáxÕá­™)"sÛüƒ,õ{Xze(P­²2Aƹηò+ܟ̧?r.1£…ç£sÉŽǼ­1obQgŒu0XÞ¯oÒÜk‘!ÉZ:`úW7£ ýË»6D1¢]ÓÔzä^KƒÙC“œñ†[z!¹û¥±G#cÊuËÝÑ4¿;²<9å)ï¶khó??—Á¯1ÎD@¦©ÆÑom¬á²ôÃl+Ä—??·nõWo_æÌ)œ¿/›\0g>_ó”·¸Õ ñSÓó“^Ínµü&9ݬfÍ_’>˜ZßJ'6왵tÝèªÆ^—{ïWɾÜÓüÜ¥a8Ç8¸´WY‹X­±ÒÒܺ„ZûÝ®œ·ç«ƒlyp4´ª¯ª{¶L:Ä}'îäü¾^,åᫎ¿a$U yþª&Zпš Ê«ôé +#?n¬¨Èø“ãðMî†À¶é –Ã{dLGëì7#ThÔG4ZŠz›F§»§&EZvÐη©Étˆ5‚nHOÿÖ·P?r±:?r¡¸„{’áféÞD Žs3lf8GÛÌ#R›á‚Ãx8©¦Øt1xW|!üœ²ƒÜ¼“//!XWã%®*£Ù߉{l8½•vä…@±åwnJ-W?0¸ÎLàJµC®–3K¡8¶…Mþšm£e‹:Ocߌe4œ"{®–+¯¼ƒ`®§sX½)‚Þl]A9¨ÔÈj°r>£h|Á›|°\‹Ýr3Ý|,ÓÍJ5?røj\Ït‡‚#•CC®ž‹¤ƒ‚‡}ëÒðaÝ"÷DË€=ñ€ËD;t\ѼÚÓ?0ÏÔ†}ØaEÊ”LZ¸Ž?r,ª+¥”‰Rú"¥ýËùª@+[%Ü5ó¼‚t”H›#°ÛíB-ý‚B5_”8Ó~!9Z‚~ZE¤«ì.ζ7óT{ðfu£ö`ixÐõ¡Ö‹{öru!™©Ýs&çÿ—{(Û½iWI±ÓOsȇT¨ÂÔ¿˜¸ònC,p¸ ê=zRâûí÷_ýôbK¤ém»¥Án¡"Yל-ú¯ðw‡„\ÊìÍ1‚(“WÊ󞆋‚O@¡«Å~Þ$nnv¶??ÜÇiŸÈJl{Ô ¤5>¡‹DðÞZÌ>5ß…ÄÏÞž‰cßv׃=c߉Àìm…õy‘˜•Ô†@Zd/}¿‡Š ƒ©ö¶y9¦¾>X*„´§S…Ú÷ ?rd×ŠŠØ´†Ê ¿ óUö𰵍I’œ©ŒÃš›’Žóc]Þ›÷,ß»ÀÞ8??WÎÁ?r$þ-3»4T£ÌîhÒå*s66Æ"’ßRø+cö†Yã–X§wiï†'…æóHZÆŸ€"Šl÷J*Þ$œj?0¿OT8¡ØY–Ú9wæÓð @€Ž[pÛÖ(Ýœ“T2œð‚ѦYÙ4ÒhðÐ¥%_} #ÜNãSšïçË•uZqâÎ<ìønÒNQñ³ÆzÕ8×Y °ž‘·îˆüúzŸ å¢ñ›¿*?r¸çíàAÉ‚ë•ÓVWI`¹Œ€Ð×ÓPÒ™|´œôRWåÐ?ni‰ÔÓ,^NæuÀ{†ÛÁ[Vd™&¸?r”}Œ[ÕE—.J…0:D€¡•’ƒP0·Â¬˜å§-¿[¾­¿îíˆ<¯¤±‚YªÕ å4°°4Š`›u(ë_¥‡E-ÃBavr?0‰ŸÕ½¾/Ò[ß¿R6w¶óÈlê/ŸS–·oê‹ÑœMÐæm¬h žÏÚ@ª‚â6`õògìJ¢oð=Q{2Ûn-wtDÔn*µÅ-cõÂÌôAd•÷Wî$Å6]8îÀà…Ž 2¡èëõž^³]º‚µ+…ïkkõ$¦'1îX/p"ÙBûàzU%y–Ø«IÔõLé·Ö]taÈ¿ÙóVA-¯¸kùR¦Œ].Öª ž ¶ÑHRšœ:DàuË÷ǽÜe°18HÓ¯6¹¼õ ûò¶‹;1Ø}­éHó¢ëFû·3ù-Åy­ý̹?0+›#ÈJý³…F—nÒj€nÖ‚ 6·Ì­»úÌiÝãÇg¯üM+R¦PQRª+LìÀCé>|ŒB[ä,³¶^>kÖ|?r&© ÈÌkîÅ,9^]'{ÊÅ™Ö#½·I¸I.-ìÚÌ“q°¨‚=YR1㹸y]`8A(=Ü—ðF*“­UÃÀq5E%f»WäµP§ºï&rcÞ/«öŠÌ ?0Ì$6½€ê³O4Îõð¡³ó réýF¥¤ÆT9àh7ã´Žò·¨'®sÈkægÇ™\Zâ»LÆYÃ/vœl'šY÷d«·ijô"¾oCs³€‚ m(ÛAZhkÔו$@ªŒoZ¢ëÔă4ØúF)‡4]输ã²ûëR×ì -,QÓÐ"’"‡ ·: ‚î?ná–’&æÃ3İlf¢ä‹²Èc=•µÖ1µ®¶´z-ä?nÏ¥W¬¤äÀæ‹ u «Á)ãš–??9àÅ—ÑÌŽ®nƒ2ûæÜv²½¡Ø³·±ðlqõða}Oa=úmÝßÜ­ûK;ê^±??ÎÔlé ˜Ž>>?? ‘†Ñ°} +ٕ٪??Ü+ðK)sp®4ö×qVúk/'–ÓR”•E>j3$|Ð=Ê` p)ÅÊËŠóÏ¡?0]~d±ó¤OµV¶¨màÐæøÈ¡Y/ÒøPêÈ\62£ˆ½o§õÿ«Úé!j‡&Râ6y™%b+BYÌö”D^15¢©&ˆY”îÌåUÝ[è¶Ô 4´ö#T¼§°JçFw3Ëær&‡’FìvÝüÊgŒïpè€7²X|kìåMf±øÎcW÷‹o]¥X|kl:Y[,¾Cl†ËŠ4ÉçÚn_ÖÔÈ·UÞšF<6å&‡³Æ]Ï%òª5 æžVö0{õöDQOȃê ajÁºÒ†#Ç$RŠD5öÁ?n¶¼LŒÀ—29ƒe!"??’ÃíNÃXSæ #S³Ñ*Zä¹*ù[|ü¨¢$YKcìhzWR5‚ë#ا>‚Ýé#BOjX%|9“P̃¦˜®¾˜ß?n”ýõ &ÓE£GÃ4>b[…f(ˆ·nà ;*±5*°u?n,#Ô€hÜûdôÙñeƒ‚`Ÿ[†…Á•8áÏ&{C?0Ù ü`ëó4¶Z®-µ=´žR“7Ù€Ì]”Å'ªDŽÒ,?rù7iø4 ݤ!àÓÐý›4|Ú*uùðihª<­Û*)×0HšTQi‰UdZ"B%Z,±ŠKD‰ÑÍÉxÞ°ÇDA]?0QI7‹ÇXÚ›mÉÜÒÃÌno‚848cC£‚E'ð;:¥URA3ᙥ`“)Å÷âœãtÚv;6#™­^?0ëbÝ%g’ o”ºö¶qŽËÙÑÏm­kvUfÖô•àÀ¡ÒUýº4ß`¤¸°È"‡3•²Y¬üÓÝFÃÚj6vO³ô²MñrXï$©ÃÏô½}›’}/pÿ«ÛÄëQ¶Åx)ÉðšNš9¸©NSI—º!Èöø¼?rÑÊ21 Öòé¿U%ôR]f: µ'/Ϻƒ«?0’÷¶Œ)`”±¾Îâdªq0ã9¢¤Â˭Ÿ¦7ôtýKçËÁæ~é~x‘ΡËuúۇᣑ£É?r¶G^0¦ï1ÄÓÖA”(Ø5DðPÝ›§L!˜—™åê „O~0E„”ÈÛUQéÞ;ê_·ƒŽÍRêÎã„™‚PƹX¾½žåz¡ã*f~/;ãéúlQ"ýÁRÍ[74Á!5ŒºZê ¥QgE€2^ke{(Æk4Îay`Òn5ܘZ|76–“%¨#œP±Æ„Ǫ'^ÑA€lU“@ÉAGƒŠÕ6Íþ5ü^ ¬Ó;Q­¡XH¢\.gÞ(?n“Ôo‘è:¤“ yDc'lƒ¾C?0«Éµ†—K‚;䢰Íe»ZVe¢uüYG•Ùä“£´ÙM©1”¢E^GFR¨A>¡BÊ˽$1”+â4û(Û) ,uGnWÂ6áýé˜aŒ»¿z7Ü­x@îÚ?0À¹;x,í 0£Ew©³Zšy^$?nEnÉp‘"“0M¶aš¬ÓÝ1?r·aöï?rUåÐ^,GwndSÚ÷Iƒ›üsÔžœ3Ö¤µZÌZo€ü¬Ñ¢Mùáà|Ÿ9h4›Þ>QÉØ•Lbí,§éDmÖöŠS?0ÑoÇNÌ??Yápº|Öjøƒ5?0º^zf16adk¦®W¶KÐÌZêh‰!h‰aͲT?r[2ûÝRCP1Ê]B??)ÄO,~Xˆw^»„k‹ÆÒbñ°LzK€6…WªÂümöqg“û<+Zoì#CÁv#bJ¯§ìâµ{·d0¿"cV@·î¾M šLbü±•X¸hcS®ñºjSó¾Ä« £í!™ìˆ¤÷È!FH†E¢›´oÄ!Xm@mÒ©ù›±&?0ñ@Ó{ÔâéP±hæÐ8h³ê«÷)^gmS°$`?r9vÏ'tl2wÏh`n#—z¥ÈM"\hèBe??[†ÕN~@ió¨NÉÑ5°™Rv¤ f7k£ÄC hWY[›ž¶eòá¡Ú® œR.¦Î·÷õ%ߤó½8%mä ÞÕFôu}éøSêŠëši+ÓçÀ¬úÆÒ•Ñy} ¾c–SþèZ¸Ù…³©à4ÇÜkmžãÍøoÑØ Í21Æ®e™¿,õk*dšÛaƒKúµn…˜«¼¹1Ö}c¿¤«q´Ð!J§hd¤uÉí0sÆ5$¡hŽCt³/5XìîZ9¥y¡y¸º¬†->wÇrwýùð÷¤t´Ò?n€M4ù— *>]oƒÌŒK?0ßõlE¼<¿þ¯ÿÜ¿%HôœK ˆÛô«¸EÙ8Ž|0_ÛZT7“n«ì?r÷Ëf žý‰ ßuðÎ9/?nöƒ¹Ò÷PhB$)ŒÎúâ’ÒöF—b1ÎCe*‚+âZ«w¼ŸT‘"~ÌëÊ^§ËkŸÃ??©sÒ„'ûc}Ê'øLÛtëQЮçé¢o`}j²3É@y¾/0[IÐ?nÈO.ìZ¸ÌáL'ŽR¥\ž¤óöäÑ^BÓ¯/÷`‡65E}=Ó?0Ž4<á° ÉúêíZެBôNW?nRÞyy½)Hs2MW€!åç­Vëåu³'gy_†¹™{%Ä{?r»W°E¶~ùÓþ­A¶l–€a¤¦¢üI˯¤gx­ù­=1v¸j !?ru‡Ž=Awõ|”Ë<Þ=sôó ÉKmÓ=q¤º©ú~ˆ¼]!Ö­*¯!è™›áç?n‹Óç±n˜vrÄÿ§±<; ž5e÷’«ÞlÀ~xýÇ×??Ê ;|Åy¥ T¹??Š››4û œÀG°3®MwðŽƒÜМ‡«™y¹Ûbr@WBÙsü¿ûú‡ïÑøkh@LÇ÷MiË–Û¸ÝK¹Ð$ä“Ú||ÀˆÀ"šàoM+?0õ’¿ú‹±“:As,ø:âX_€??XS–èÇ¡¿¿h}$¿ôÝfks¨Ë†”«,·?n;á¡l'¤ÏùæwGÉ#kïÄ&£¶E÷vU%ð#0’LÇÂÞ†UºÚPÒ iÀ$[#;T#S?r˜¹QuzfÃ@ !ÍìÊÃ??rÇYo8½Ì¼°4¶ä†\ÖFÈ!qžk»1Ø;w˜ç +#ê­??רò‘\x…:ŠDþ‡w‹??¬—è¿ì¾yÍöŠÅð `xv}.Ü,??‚2­Ûzß¾yö܃†×¼. ½®cC&¸1ò"²ÏÛ<[¾ÄL3rq@’-î?r†»ù F8gþò0.#¤£Q ußÞ)·bXC£›Žwè{€a9³KßuUXÑZ»`˜`õȸ)˜*în³9<ß;-‘bdX_ά/K)¨¦¥9uBèð]…˜f¯´ µQ²Û.µñjpœÚ`4ô»îL•|–w&uãm|¼?r¥õgc·Ü[–»fŒ™@ȵq<×êgH<”t­1n i:]·ŸÀ?0_go3¨HÁfYÙj^?? ÈŸn~8[–¼Ú~‹éÚDÝY˜­.9F€mÌLåÅê×7œ1Yë]%í2Ô#'9yô­q‡®§É@0xB• &¨2¼Å0µÎÓg›¿Ê‡G»]€ÿ„ìëfߣw”lL¸í_>)sÖ´×aúËfñó¿µÝ"‡·ÉQÕ|žíëŠæŠñ9…çy›ýµØÞ°ùlP–™c Æ·uà©Te¤Ý"®Ñ* z4™LdEòºlj[Ê„B$&òþ!Øñ8`gý?0!C_¦?0±bº,©òËÆUF†SùíAÙQ…?nQÅòÚ¦à…¨“ìŸÑì÷Â!Û|¶=X:cÞ†Þúiá±GSsQšš š>ƒ³Æâcü59:÷#,DPD•ÒÓ1¿ËŸŸ´…ê=¬H°€' ¥ËI;0éõt]{7äÏ:Ö QÁ‰ÅžœÎ㌑ÆN©È¶™_zc=íâÊV… «M§¥¯,¯¦øUUYZMÁ _G!ÏaÄûJk¿·ßiµ“zN½AÊAµö«=Å¿\­.´4XÀP@¤¤]‡qT©Ñ%ÄiøLè<§ƒ+ I\ª©áç§ýO¼jhtÓÉÌTŒô­n·%ÔÌ3còpp RŸŸö€ß±[/W,@7d9Ž„˜ÓF-ƒPâtÝ\n¤#Ä¡;mÙz= CsènoPE÷š*‰J¹ª,õ@ç?n§Výªn3Æn*Vk¼Κz*êϱ nzÜ_IHZAÐÆˆKk&BíÐê#Õ=ÜŒÉËÑ`£Ð+u5——6Gܘ`òf,lšÆªàã§×NmcO‰srµ‘¶2#̨åÇØ.TS$›?nïg¯dTɨÍVKpÏA5Q©0â<½¶0x6йvP ØÂ=™b?0¯„¿…IE\M”E4œÚ/ô-—¹Šïªeº>h]|³ûú×Öc•±Ò6.Y—r??1´`{Îd¬*@b"{ 0(Áæ^ÿü8ºúön%)ô5™?n;3U“-Öñ;±?n±½ÚZ¶'w”Nmìá®øö1;ŸÌàíz2s+ž9×CfÚ ÿÉ­'o[u˜•`ûÝXM…!½x7äs<>¼E6ÅT4¢ø¡ôÈ×”Úíõù|??œ&òè mÖ°xFa¾zZ UÍ‚7.ç>i.¿­`Èš;ºˆ·¡¯<¦læöâ}Œ%Û…ýêé=ƒ%•i¶n“åôH£]æa÷î×ìô µÅ~þhóVà´¿h.áïŽtìþÍ'ð<Æ¼×æ³ší[Uäa„Ùb T¯õ¬ŠWhͨªvºBñ¾EYSY©×@Ñä²þHI…¤EÚ$zÙ+Ö3@%²W;ˆ(öFqˆ6šhï¤Õˆ71†fç»¶“êп’?r]Æ¡Uíð¸õ>_[0¾Ò×ÑkWòÊí”vÄíOToWŸñÀ™‚ÐÊ.¹A˧Ýëݽ>Îø†ß??BlmÇänø­î¡5ÖMÀ¡jºkßßÀ§Ssc¸‚}³» cÕ%†{ÌGf¬¢NÒaa’‚DÓ–# ®%+…ÊKã?0qúFwÇÁFèúÁ·°ÍëºiÝaêî³ÚOjmRºš¨×³‚=6g#:L:ª`Øì/àînkN¹PAʳ!ºS¼ÆGoÂûéuJ¹Ö[¤«Ë^~??”Hg¡‰Àb#Á¦‘Dùao¥¤ìH"›ãUÕr}ü«ç“™ek\;·/La5åœ×-0~N_ÜÌ?0lË~2JÓÁ¤b膂 ì~µšZh»+ת¬vgôaj)œW’³ï|ÄSäO]ý0Rç+ï™±°(2õüSW¶Ë®l t§•í2Ö]Ÿ¬Æ®‚™w6¦—OvÖSª]µ¯69½‹9t"P €IäàÝÆÄû©3ñ®…l,½·œýºÅÇ”Ž¼3òPQ?r_Ô{Ò™¹Í"¼U°º…<ߌõd¨¦œR¢ÃŒË›A?n;™9 ô޳]aÐmhFÚ|ˆ´Þ“€SÄéô%_òG(6k.}09þâ?rg¶u;»krT?rÅgJ›ÝöcµËk]P}Þ¯â‰ÿÿ̉ûÿM\›fÖÖGÆ›VˆÍnl”u“»÷¿Éý70»wšÞÞmaÔÍô»ñˆªØºƒ%îœ0·û8•«Çb¼)B2€øLXDÁò”·éÌtÓ¤¯ô´lxê.#S9s ZoL??+ùW{ î”FSŲÄì>ñmÖ zÃÖ䂃WÎ?0Òq®klQ¦?rMÞ]-"!OXñËlr1fFòP«ÒlÉ~B—y¾IªXY”"2&V7b%ËUK” ܯ$kP kÐ2VÑðø1¥8ÍÙ ’{×'öWËåP—ðÅt¦_ƒþpû(¨?0îL:†§Yåb¤â)ð¤,vhêË?n¯k3YuBfƒÁ#Qj×]î'DÍ;›Í”þ,§ž¯¦¹V<³QÄ??W¬1g›Ý5 Ò×¶²Ã8ÂÒÈí›Ë6Žhp[ˆ´R^1v‰h˜†Þ‚™¶ÆjSŽ$ýƒ¯Æ+’VFÇÊëBAlÚ"ã캩Ɩiº— í6P‘»‹ZŠa!¤”®È™ëðFEÅ}áÍÝ:ñ£bX¹¡sÙ Âé½´Àjð’Aétû¦Xº\]ð^´øie"Û?r݉™©dý6B›lü+c\jw*¦éùuì¬^²?r‹OF†¶Ž—?n“–À ±X¾Ð]K¢ëþIœ£uýŠh–êŠN%m?ryÜqDÔ‘P¿¦–®fØ–#ð ¥sÝ*Ýí\·²s]ÙpŠW“ÿ멚ï¤k¾?nq•™“_/éêDÖíã$Wn³}Á??o<<øEð±ÞÊ >,TbE?n.óLîäWHØMqÔà²õg«‹*’0o¦ù { faºÞQi†ë÷Cñ9jO†‹nކâPÉ|)…Äu[ÑÔò¤ªdÌHi2›FÄA“˜¿[l`x¼eö¶®±¼n„§%Úüh D„Ü(Fc± Ö‘e?0e³\ödΞÁpúÔ8P`­«(dmîd/ŒܶËÇv|¹ÈÞf=Lú_Rý¤ÍßÌ‹ë¿|è-®ö6®6§Ÿ¢hkƒ’`Ó?0ÝQ«{7t[•r½¨–«Þºu¶»“2çUwQEÖjy­l¾ê®¦ìxÁI´Ejû TÇ¥­'»ÄŠY>HÊ•2~:öBs??\édß,€Õßô/ÑZÖ?nW—ËÕýK »ÌÙw–7mæoË´çêDõ¤ð(ë“®†ÐZ»ë?rn»¬¾û×’ÕG‚Xíùlm•à[§Çt/¯¯–¤ÙíÈwzûyöìÍNBdEwE€ø‚k÷Lmˆ©3ª¢³T‡—ãèܸ36âS8JÃó’ÉK_®XÑž»ôF{J:û‹+¬¤¶ÞúÙ·Š=»UÛíz l•"?ncÃÄF×užzW†n¯²OLG©N`sÒPc;„?nKÈÆIÁgÛø¨´´®¨.kM­»i(Œüh{#„œ86QI¬0Ä>$¹”e³¹ãµ??­U|1ÂÞÂéည}`éØÊ­̰¼ãsÉÚyµV‰“eû›,è›ÞQ2?rÞK&~`XCQ.í_*vèOQ´Ä“‘NA’?n%²;Ÿ‹å:¡„du&2%ÎÊ_-Óì÷M*QÖ41HñM‹¨"46¿¹j($,Ïo/jùã)EElU@b?n„#…„½º/D#,Ñ«ÔC«Œ 4qíh;Å{;Üx•Ñ)§9†×øTXëù¶±ñÐ8‹ÔìÛµx5ËÛýÉ5Òàa5[¯ÿ}ÆCýMâñ3½´¨ŒQ"kը˻;D‰R§F §ï‰n‘jĈ?0£Ùônº¶¨´7^÷¯$òj–Î4jBÌy?0%?nw%ø1ø˜xŠ*ÒÂØ&á^9Y.ŒÄ/ +#\뮑&¬XI¤ §ãq—v%f%?0ÓÉh6³øyMŒѣu±fÅ×7©ƒ.ÖFžäð3q­Ä¦÷ó¡&Ü$îv°ey #Fë)ê©QeQ+ÞNÊ`÷svßÇo>&nP\Îû×#±ü“b®!cBnpÉ×£µ <$h(`1)72Áà"Et Z??¹ä39$èä1išHË&B)„ÈA$8]L“Ÿû·ýäçÿ_.ðç.¹Îðÿ4™õ!5Hf#de!ßô!Š3`>„?0|žà~ãëNøë#JaéÛY’Þ^%é;ˆ?0A•-Q"ßN†³äßÒÉûé•èCJ-îr§·‰:½­bSîb6…³ël¯-Éîä5,`åW?rˆ—ÌÑœ÷ "yÉg¯~}Ž~yA—I™~M#í\ÜtqÖ›¿åÕÅx±¹) =™ã{ÓFMÌ›'ózí lŸÓ:ôýE.ðŒŽøWB‘€u¾I(?rXOÓ¹Œ#™h‹›þL%kI>>F’ÊË÷ðˆ¨o8— p¦MÕ·ÒP˜}i©ýÄb&˜5ûÀç_¡YL»ÞTþ­›gúõùÖZy‹P[‹¨~ÿÖÛéJ,¿^H9§ùšÄߘ•Q¶Í^Qk­ÌŸøn\,/VÒŠñMìÛtVcÒ†´4›ñóv¨Ѩìn¶®5Á$?0‹–M‘*RÔbv©õáp´J‰‚ò™’³çÃpoù« „Ÿ^ *;>OÀ0ŽÏ#¯¶ÖG½oËM`ãÿÁ믺Ž2\š3D?nÈò)0&¸ˆ=¶ÚÜ`ª^ŽU\XÔH?nsDfGx—°àxÁ9ê4ªx¼\«¶»=×ÒÖôupÔ¬}×Nàj6Ór™º ªÇ ªr@´Ñ¬Ñ?0^?rX¢Ê> ¬.j× }„­`IÓ!ÆœLØ‚„œ}ñÁÉû¹¹eͤäd<OŠÇ?n¡M=ß§¿¹ÿ©õ=Ð#i–??£ùÜPerÌ*ayXu׋.uó!ëdC”^wªÑÒÕ½YmJ¸³Ü…VY¦Ž]ù`Ã÷Õö~f/÷*×#¤,–c8ý_ Ì¶ÖZÿ±Ò«›©ÏVŠžnª+q[Á1Lƒ1~-ÇÍÎ0Ò…P·‰ /?r¯å–??€ [ó¼šT8ƒ?0@ãçvœsmÄY˜3´(‰PFX>ä¶O¥Ô_áa&;eöÈ8°y‰6ªÞNS‰²Òψ®Ñ ëtÃk‹?0wâ@ÄÅtXÖ’{76£ÞøÌMƒKŸi,>5¶ðZ,óÄšð¶^ Áµ´Å ÌöxÌ?0òK#^-¸[†?n·CøC«+ð…m@ÍuºòÎ`—(QAb¹š®F??áRýæJm¿H“‰ôúZjY¿Fÿö¡=}“ž5¤Ðq??c™<æÐÙ¸…–+±ÛØ×Ã¥1v]¸Ù —mœã?nÜÄÖú2Åìe®——«¾Œ&fqý#§JÈS¿üòË«=Å.FD-úä®R‚ì>€ìV·^¦Â"ŠnSï:F,È›í Ú@E‘™#"(7 Å·yý\ÍŒ-·,áoÑÛ“0xs^ôQ¨&1ö‚›N”0%«X*ö›;Œ,ÐÏbyKgEŸhÄXÏAŽW@jù=‡WŒÈ¡_yB–úuðþ²ñÏ¿•»F Ö-ÊRqÉ·¼ÂýždÆ=£è1­öâ‘Pzïh׎ÖÖ}Š+Âtmg¢‹…é«ê«½ÇOªt,ç´»–Ý^Ž\œ/p2U)te›©›¨Þˆr4¤FlŽ´$9u”Y¼þD ÌV†hÙm"‹ç­’yœ÷Q‰^b˜þ©Ñ>~?n?r yñº‡©ƒè†c6î?rÖÍyI2¬Íî´@LéQ¸ÑŒ^ºœ\«y6ìݨk_½VÔÆ2½ñ?r¥‹às³!¬h09JÓøma‰ TŽo‚Ùýa†ä3ÙMíf1ш!¢"l*E»š÷ÏÊ3ÏÆUé\à’ ¢H“¶žS>1Û4<ÔYAºÚš:ë¿¿¯KÃ4­K­¦Hª7¯M áŒ,Z5œ/iÝpšÕÃ:fŽV9g™˜f÷Ñ&ÛçŽj×¢­È,”9m4§µƒ?rãÉaÄ.ãWn]gD÷¡Æ`f\Mx‚×c‚!ŒoJF3Ïð!ù2~|Ì>ÆéƦóã‡È¯@'ŽÒv$صWMÐee?r‰dHXÖü¥DuE(W0p”æ©JÎ??,]Fê?r.D?0 Ôz°•8|s×¼9S¥'ç¿tö»O>Ìu»Êêñ¨óü :Dk€lÍò|‘ßú ÂTÓ¯§« ã‘^B‰ Îó•§n¯È7ÝMí¤ÊÞƒ˜ÍOV»¾û`$iÛjÑQ0?nÓ°ôÑ,v¸Ùy¨N£ä“tÝÇIà64ÏÀR.LBÒÔûIÐÒuEM|‡¯È¨2è)3:sËX¡‰O :#!i£ò ÙhÑ#j[ÝDrÍd1gë\VÓâÂL{ÖþWÉÁ—r©7:¦`Y‚úØR«Bx›~«Q39ÀÇè`q3à–!)l¨ƒÜõ$WÊ1#£•wý͵býp&¾Ç÷sÚ¬–¶Nã'¯I¸+¤ Ç+»q`ó‚U†F«²†46@ß­KbCÆ–—yæ“€ù˜Ô­½K£aMns¼€xsE¯G°nRQ溔رd寒'2ÇŒåIJìåâÉ1ƒÆ?np½¬\ÈA%Ðý†ZK#K†×lj\öŒ°°÷!»AîJ†rõ±†i2¾.ÌYAêàç¬8”ÔIëây#i¶a uV¹¥ÃYc54)] 6ʦ*Ä”‹ÐfÙÒ.ŠóѯKáÔhòøK:Ä5‘2ÍÆò¿dxÔé=>J=úüìàù“çøétžŒƒ¤ÅÏŸ&½gÏzOŸ$½¡ü/y>–ÿ%Ï?r??NŽú%ßÓñ£1ó=î<&Ož=}þ¡GGŽð#ÿ©½ÛÆ£Žü/wŽž —0¹Òµžœ5I÷¥<}IÅ–—n>­m=ÅK+e&ñɸ>ú;J,Ÿvj•l@@d™çÉ??5Ãù°•ïŽÈÀ[6Ç +fŸ“œt«†œÔçÌX¼GcòÔÈÔÈ%%ä›{¯Ã“Ð,Æ$ºY  S|­W¶^¯S°.äUL.Ó³Wó@âK'Õ ¡Ò ë<,©ÚÚÚ'NYð‚Zv¢Áv£RŸ¶4{œÐëE9;1€eõ)"_:ê`iF_à@‚¡ÄG€6€?08ÈÅ€Ï7€ÏŸúv"<’ Óõ-a8ºC‚ï?r–¼TDšG?r1ÆËºËÐW6`Ñ9V¤JGá8@Ê6߉†bJ&ã³s12>ž Û™ yÈ.ù2yæLàíÕ/RõZzí:ô/¯¯èàÐ#É B"]\È<Ƽ9xG1Ù±$’‹ð“ÌÙÖVQ߇ÍÀL»ÖäQzPˆi³®Án¤z¹ÄIYôlðéju¨íÅsŠÉ±–ͦæ±jAêTì¡Æ¿éÕ[bµ42q³)V¿º 2½•ªYÈTCFx=Š?0r»´É÷k¶³7ý„Ì|+È]Ýîkˆw5Ì ­=Jã ¼±)ŠA„Æk½Æ°u‹íÊuý2^äWê|8'Í~,×™áEŽvÀlºy¼Ã²Ñ‹I¿aîÈÓJûýK ¸Gˆv¯J_SžÏ Ôð¤þÙÏÇÞ…bP.ö¦–|GEýÒ.Õ¨~F³ñy„eFøòC™P,Át-7vŠ=òÌM”ÅHãK½6+?0`ðw³??Z,kLÉ_XøÒ8*üQ?n‰<…ÎÁ?r8?nAĶñ@DÆúý”ydÒ?0›>ÔÚHutP•=«J#Sv€ƒ?rÿn¦ÍbÅêEòvz+]jD­W§#”©jhÒê|ü ´zÇó”Ü÷d©Õ~Q!=*çäÈž\9ÂÄänÏÆQK‚¼8©òQ<ùS^C!ŠªáÇ»}™ Ù.—¯Dcão£Îá33Þ!£e™Ýfì??]½vÃ2œU Î> ¤·X$ú“ð,0D*ðHô'áá\,¡Aì'aQ!aŒF¢wÄ“`¨\:œc\ãâȰLÀ’îí^Ž ìŠr$úshæe`Œ¬/ Ÿƒ•¥[ÆVïh>\ÜÚƒÃÈ«2xô§yüh4/#-Н¥ {låš÷™Úœèwœèµ.ã»>5™“_ÖØè·•Îª—?0[OuíÙ&Ǻʫ/jKÉ:ë©N(?nXÚW~‰Ã¸kVË©û_œ1ßò?0⫯$ýeݹÖjÕ%UùÜ‹²én¸o-®˜C´á¿+Ká6±˜ï!Ôh™¡Nse®I@Ô=Âù¨gÝdYêß›˜yÃáÊu¯@¼b)o”èÍ ?0@¯Ó"B·ó[;¶œsdŽgMØð:jõÙu¸×¨³Ïr5:wßDõJ¢g}!𼆂Ô]Yzý¾Ê­4ÞXë+sãÆ¼¨ÓmÿÆ/ WÙbò…*ùD˜¢9¢>dw«•D}ª£»ÑÑ]„FÏ„ð~¹œ Èqò‘½óý˽ƒn»ðÿß''|Æ}ðÕÞé¯Ã>„¯VÐvD5x¾Ù;}(ô¼šD¤^N–«H ©q‹v%뎄š–±l%SüDÔ’ Üò½gà´RßÕÌ‘¾wðéû@g©ƒÎÒ@cgpàŽ%Sp­¤î +C£m{L´í5¬þÉét!ª+¿¡m•f˜àûºˆ´Tˆò…’¢×3©¼gAÍa…É–aE‚<ÀŠm‰gøèÙ §è™,ŠG?0†ÂÓ?0;M‡Shª9ÓÁ`yצèýY!‹öÏZ÷ÃùJ~UØÙS=忯N~¯É³êä™óiöÞküˆ6ûûæ{µˆ³†ßW¬Þ'Çáó0i¼om²Î¢¬³EÓd­³?0g– +#´7éº?ro©h3à)ð,­;»f•§Ö GѨ6<óv:5Û€2ïáŒàHym2À ¸Ï¾"€õKÑ"?nw…`ûdG=n Î5·7œŽõ69åý?n=ä—N&¦ŠõÍ–w–NØòÕ½]\ÅVÍá5UÁ®yi…?0UÁØ,v·æ ÿVmÊL½*$\ù”šIÇeU½VÄzBßWoâÌÂKÆÑ¢ìNk¶5σŠLÆÖ4ð²)’W’V)ÙâÙEyå3Ôa„_¾™mKŔГíºÔÉêY¡Ú}Ö+¾Ô!¨÷„g¥;Ã\械Ò#›§Îí›)# ÕÇÝàBZÜêܺÓl¹Î¯mº©Yyš?n)Æ{ª(K­ÊaGŠlvnS®¬Ý§«Ö­M=ý”<³µ…MîDZX—È—]TXÒiòÔ’n(+èSŒÂçϽ³ Û9Ú?nIÞäW6•KÜ}˜2‘D{]ÛG7HîŸ]?0B%Z¾bº’ûŠñºÂÌ›Y5Sl!º½Ýæû^D°nŒûy[vá FsŒ²ê1¼¤;ªËÏ®×¼ÌÖ3„9åóáy ¶™Òß~o2 °†a[Œµ©6¡y~ ¥²{UeãVé3‹P]IM$îv}™ål*6rð_¹Á2}òeµZ5B¹8O”yšß#ôý’Ów CM’9o«‘&™ö`Æ4°áñ1K¿8lg"#*ØA¶›©"S Œ$þB C3úä`ÁL¢?nÏ€,]žVŠÆÕRx³ïÉòôÅ…Þ±(&P“–±ëÑ(NŒî´Äºy³öoG?rò:ÆŒ¤±ßµ”L›×IÇѲm‘•¼žÀ\0©H6t íF³U+Bí£&q³=1ø«#ŽC®÷æ(ÁúËѽ¤ç°­œi@‘Ë……ØâÌ7Ï]tbE-ãT²xK&ö§É@£­¢º—àÖögò†l¹ŽÙ°h.;üB•«ÅÜ?0¨ÎÉ,¥¡?rqç ?rC,ddzuV ¥yk¯Gog)‡¦Mß(;&ƒfO›g?ryäͧìú3U?0ÕjÙ̪¦Eilž}nAEx¥¤q1ÀŒºn8Æ(bô0[¼4íð Ö_dÙ£WKò&D¸&Y%?0kç—\}h:¯z›97»Ý?rSP7ãâJX1è_M³|&ª6©"ÑÙÙ??›ÏÞ?0,ˆP¦^Ú²öF`¹ÑœÞ°ò)dµ¸V][—–ƒ®ÄÛätTóö¥øãfE½oFY6úÍòN5ãcÒ¹ZwŦLSÕ…l’7ŒõÀ6K?0ÿHekÛ­*‰m_#ê•#‡œ»*3wž¬„YŠTÄ­ÊW¡3ªQ$¨®æT* +y‚‰±”6}mçé:‡©íZÝdØÙŒÑ’m›7mâÒ¯@–¤wIË\Ûê“"KsaË™È>å×jî™G³Òx8¹v»‘߸zéÇ£Ëô½tÕÂ@ø†|1 †¯å¤ÒLeQÒ\Oë·Ê¢©Õ–kR}0Fx•-Á¬‚a¨x*Œ9)H†Ë­Ž¡âák§Õß÷±ÃP‡§©öõõM[ JK(¹yÈ8Õድ:_8ÝÔæC}/ÀFœÍ-Rا¬Sˆ¥¨”ûJZ%šhY&q½I½š6 F7 ¨A’dTJó?nEå`Â(\ô\X3¨¡¸bžœ$3ŸÇ±VýøJRÜbÆñÝhàÌ;!¥ÂšRžWÍ'ÈŸuÜÚÒXÃù(Eëbhréš3n„€:"ÎÅïºñâ>7SMó"¹‹hÜ7dégÂfoh¹"³ÉÑÝÖ")¦¯)),îîÈF”qaö„Ô&Œª¤úñõ(Ë_#öï.4áë¿øñ‡ß‹q?rÞäXþv"JÎá¼€†Í&¢ìEŠ ò"›è&]„ À ?n¬Žƒ‘m<²K|J»Ä§°™&ÖÝžuù| öýeh6–²Ûè»Úþ²r·×I¬€ÌPd?0BúH_’[¨cA{êU6,Øh 9À± ÐYwˆÃa¨ö¬¤JH$7Œ Ú V,¾2n÷Õ®•Ž"ÞAðï7;®d¾ Ú~gë¯ÝKÝåçýÕ1wÓ£µšõ°ÆnrMu¦ñ¸îΧ 1õkµœ2°!šbFÇÓ@‡ ‡²Þ’¿¯4öi3¦#‹*#ce×’è14nÀ˜›Ä9?0¹ÁÏ“??Í?nÉ&ˆ}Q ·©‡5ÇÊî‘O¬ ½1 —ØŠ„Ý’ŠñŽ£áçFËu^.E„áFkÜØÀÈZS8r?0ƒ+›©Á,8$QZ™1!ìáCR6º“·zUæÐ¶úÖ †Ñ?r(˜5¢ìΙ¸,qrú¤ƒúÂàaŠº­ÈÃ|?01?rmË\‘{!{{nØl­??²(‘u½wÙ¿œŒ^Nìõ|õõohµgs°«4(ô=²÷ë5MƒŽHgÖ®ógP…±8Æ[ƒáþ¥?0$à46¦½M;wyr‚U•›IÐhnhþ¦Þ&u۽DŽ]}gýrO¤å{ ßl‹JÏä"|Öž<`y¾^j˜œŒªQvÊÏeüxLo!½ò.™íZ»í+ÛjU`U%®ºJ7f\U3 ÁŒL) ø‰é•ÊùMU—CUeI&$;l??ü}ÞÙ©áÎ R‡ü䢲ӓBz1±kŒ Iö/9;듺@›gÏjnêÅÓ,Šhc‡0h²²Nª;rÇÇ5 9Ð8âh»^Þ»†ÈÖèAÖ7 ¹o±|ÍcÂ?0§çµ`x#´þb*ÅpÁ_Lè1…_C¬rœTÈoHÕÅr•É3wü-JsÂCÖ•äJ=‚«‚ÞáM]¨/m²´ÐzÅJÕh³äÒéØ7‰çxÖ¨ôhÃ4_EC™}–`ì¸,L¹üÏ­P“°˜‚~™ùJˆ‹Òa‚¸"=Œ??8Pí5ÂฮIÙRÅFÏßµÜrñ5uû–£-rÿ ºqµ)ÿlZ¬r•HÀˆMiÄYVægÀ"H˜ åƒ\'‡”®YÔ‘Rõ² 7k›aãXÔ6…^L4ÎíE¸JêE~æ.—p¨³2IÞ¹_ÙDµ¶Cùá5SôÑ€‚z™&•8*c+£{yôá!V¹;Ð8›áÜ>‚”¤¿šóöÕ÷¿û½m¾÷D+?rV¼L;®ˆz{]c;Ty??yBZ U;×¹€Öy³»Ä¬_ÎFbIµÙ??ñHUx»‚Uݱ‚üiá–ÈÊ#±??GU,8f#³º#Ê}¨úöC³Ù?r  'òÏþofи˜]#‹¯ÏÝÑ–ÚØi{÷Ú3îP;[=$°{-¼ö¦à‹µ72hðŽ‰â øe'ÛСg–'†-x™ž?n^ƒÞ?r18Á´JÛÔŒ€T-¾“ªú-¬-Ù„%BÕwWl.ÚÒíËŒ­“KÚ¿œ*?rû­QÆ¥ÒH!??9+àtdûQþ ž"sM63±ŒäîÙlp•G¢åÇÈrÛžñ™¦ÇlüU9 }Óü*]+yb»E§9Áx—’ºûjЗmj=’ó”ZÁ*E}0Uèw™à*(3^I±øjÃÚF %r³(ÆT‡ŠªÕ|ðyНâ)7á{!m‰]1(ó$ý6^–•앞¦×Ø·¦%?r°°ë¾å6Šü/÷Jþª\ú”«¼uV1äým3×Çúk¯\¶æÑ¼(Z/ÉêQ’ø£Xd¯Ñ9Ì’ÉJî(ø‰*µ7³MP æ|?0(°Øe¡K8š3ºgÑ¢NK×»OÚÒšˆ³æpš?0­Ô¬­â÷ñl¹\7"·\ÆÀüG0?0¢ÜEö¶{G¬1XÒÛ£¼ÖkO¥èµÍ[ÍÓ>m:lñPN`Ú}^»ÅÙe4–(XЪâ¦<‰‚Èå™Þ YÂhÁ9¶¬9ëxlªé»Oõ¼‹AÚûË\ÄôæF…Ò83²Ì¬L–âMáÇZ´8Äb|[>hbFR"2e 9¸Óbªå¡­…µLS÷DŒ]Øê¸sM,¿WË ~±¯^ð û3lÉ_-Ù0µ¥ê*Wýœ(Z7âuI‡ÒwUž²¿¡qe-ß–IÓåMªÚ2EK'ÁL/騀umj¥$"Xº`9òêÏzó»z&O7äBÚzzE583y½žç8c#¼dIZbœ1â¾Èy¬a‹0£€æ¢(ïä??KS.LÀ‰:4û»¿ýíÿP®sQ±žªÒ„¾kFwúN¸?0?nþ\¦‘ýà~&-k±h“õg¹Ú"^÷¤Âs.S ?0¿§Pnc"ë8Xâ~P´ø°ßa´Œx'®ž–è3³ÄñÊi¡Ú?rØ~‚â‚”3Y€/ÝÖ˜!™L?nõñ#²01…#ÇDA9²ñ¼VæšÃéÛBØmÐY„ntyíÁìF-¢g%s¨ÂÃÛ"âEñú£ðAÀ Íæá+­Ænü|†8M÷º(s:¡Sç8«Oñ­›¡ßx;½CóGy9–«ô´_‘SÍ1CsRÔ#}6M¡Î™e¨a«¶ñU–ÓøO4Ù°XVÌYÒn×=åjYR¹f9-…’¢ó¯å‘EC=è]ŒÕ.› [:FXs°Yjfªç•*Y´"Ù,ÚC‘¹yè†A¸JN"‡å!¬äMGÝ®d°ðÕÁþ¸rS?0V($çó'å,ßà:ÀebP³ÉeùDïÄûKŒ9¯Fû ª—ò­å"âÊùSŠš=Ö[&1­½bi¯Wàx´B†8¤QØIçö 央ü….¦šŽt#~&v×Úœæ”v,ß_9ÒûaLéë}rfKŒ› ¯LÓ$}’Eò³32Å»²×kƒÑ­€¢á$ %QiNåV„ÈÊtæXO~êóHJY\;¾è +#/`t;ç77ðû¬{ËÁl6ÕÄf9DH cks}O²+ËÝFó¯Ÿßeh¿^9W¡yë¯û ‚;ðŠÁ¨v䀳S”O“¼íTòÕ,ÑmšÝ¢Xp"ËG[áºìÛ“ÍÂÆA)&CäR=4ÀL Ã ?rûbI¾Ò"·éÓë?nH?0ü<`›úP¶.‡>È〳ò%ˆl ûë•Kµó¤Éü6lhܩ֧ƲûtUt¨é/<؇¼ùÛü’e›Íú«t4TcX2sáƒÍD6üþ¡~hk×£²&Àɘ¸[d×–Fî×.«°KJJ%ŽÕ½Ça(þ¾Cq[Yúð‚+sÿ¥Ë}]g™­m€zˆØ”ôÁo\Ð'}S16®7VŠ¿Á"†Íº±{Ãõ&0¯ú'˜ÿN»›#À”±¹á•ŽF×ý›‹ùÍL?0JÃÞZåïºVùÙPÙmaÉ?0¯¾ê3ç2Ï|fÙt??–ù.óÊg-™UwÅÌX°\î??ºÜ7h·W¨‡Æd70??8˜%a„Ü76[§¢ÕÖØ{õ”ÿ#GmqkÓ%™íÇf[·%þU¡Í‡ÆIw¦?rYnƒß¹úMµ?r˜«nг ¶¶@$†Ž*þ{‡mîÁ$ å,¿qYQÇ‹ƒ°HüˆúÇ\yûŸ;ìW†½žË޲ÿä²gQv»,g¯_|vô1É®Cð۞Ϣkº¸5ØøÌ½Ðï À¿¸2ž¡–eˆ0Ù»é•'1ÎþË>¬Ê¾ÜãO|–©5‡ãíð£±ý Q3SDí&ߺ\êÉa«ÛÓX@Z&|¾ŒóU»Ê_m.JÃÊ:Ÿ2è”õ]íÔñ8NãóúÎì?rbœ½??§zûguwQ÷¼ÐväÒª8áD,‚$ý„2E%©”Žh!GGAgRØwínȤžÁìÍlo??ÁÏ®ñÅÄ.bº$~ïÜànr¸þ]×sp?0#¬1Ì4ë0“"ðãŠnŠè>þô óì ûx??ét;-S2Þä­Ìá¦Ì^§×©(w=ÔìbåCàô—iRìQGbC—&ÉAp<|Ô«À5^(.Y1$—þ2mƒK´2¾¨Èžõ5;0©?0ê/Ó6Ùÿ4]ßL«ò÷óªô‡·?0Ô¦lrƒ÷=}Ùý*Iðñâeï7IÒ¬æ§Ý'+0ÞäÝ„N¹ñrÓumÛA§å§³79ºULŒ9Ù@`¥ìŒÆÅvMXy?nzëõ¾˜­gQFó¦®oÃh\VzR!èÂOà ½¼[iÈx;Å]™Nr|„Uå¹?nìø÷¦q…Ýuå-`!¤Ÿ÷¡¢í,kr‘£}FÖ¼Í3S)Óáæü­ü~?0ÏvU?0—ñIÀâ–_fa"›7]µMW?0µ¹$Žh†³y+‰Rö?nNÍöX)ô¦Ó&°¹ ¹;»x{3Z«ò+ÝôܾE Þ¼ÐMh¿üްÐÆ~‡Êzžp¸ÔfWô…|@£ °ÔÛÄo"›·±]ò?0õ¶|T®Æ&) I£#.F](×pnîË¡aT1èNÅS=<ê%îöF‹ÓcÀÇÅ-fô‡?rùVN"·axó÷??üæwßÉkÞGØ‹‚xCßõêЈ'´v¾U¸\Ç*c?0oÝ â¤çk´üLº}ž‹f”f!N[WTqµ‰¼°¼õÖ|íy›çFÆ›Àü?n‰ˆn4¿l5Z¥¾ô£ÜÍû·fkmw¾¬’{²Í»ÎU?rærÅ,D[w¦·¦Þ¦Q»F€·Öš!C³Š ÷wÁeCx›¶ÑèófËr“‡‡X|2Ï??ï&è"ÔÒÞ˜F9 n~Óʧ¶\X­Í¯CA/¿Á° «PŠ:4L{Íö—­CKPZÙ³aRǘ.ßÏZ‘V7ýß¼±Bå??+±\•TZɰÛ(À8´SŠ+lÅ£j*ºéª7oöá)GÚî#µÿ`cB­J˜n”?rZÖ’;ßF´+½Ï|??äUyMt×€ÖF;×¼Öè€3j®YaY¾4U˜·|ULŽ«¡qN£^™,ÐùŒFÙcî=7°›g/œ¾8ùªu(É_t‘ª$Y%­¹\™î,s{½Á>ûB¦h—6\™7!öE1öOé{‰?røBô0“èa??+Fß0úf“ûdœ³Û]ÈfS‰¼À‰h„#vAaW~Ö‘‹õ•¨27ê1轪&ÿÓ.¶™jA‘É›Û1ÿi;f›IÑ2èÆ—?rk¢ÚòŒíú¢¶4NñJ|È"ëJ³ ›D=HÆÔîj¹’DXt±Kt‘g « f:ŽüWÞuQÒ[­ÖãxGô\©+oó±r`iOæüîÓ¶31•~Ð#ßßøáµ8ÿÖçò¯¤°Fä¼É#ìá)?r?r ¥‰ƒŸp?0tC‡˜ASž¶:G(gQÛ›Y–~ÄE9Ûv;›Sð6þw_ÿð=6;yå+NgIp(=N “ÈâûC™a)S׌âþ똗;>r½B$Ð1Då„![‰üÅÙ??t2‚á1ìKDð7·UeŸn`ØÔmÊ”¹6‡t ”mÒÙK±Ýk?0Ù®, ³ÞÂ:ûFòÂÌÀ’½äV• ÉBï,c¿-’=y?nªä£YÍûíØÜœ-£ÊIøÕ²Ö#™ÚÜè­î>bvƒà‘á?r‰âñ¯ a‰=Ù¶ØKHSVÞñJˉ-{ªP›–"€§Ê?n°\iÎþÄiÁ¬õ%ì[gD3¼èG÷Z'[âÎU•c 5ý2) 0ƒFu’M䥞Sc,dzÚÏò6…(?r®?rdKsŒÍf‘Ò/-|„V’m¦ˆýŦöùj=£«æÑ8ã¨ML¼Ë”²ÖqãrÄ+cá¸Sy¾u0eïp†evUcÍÚb?0’>•IÊø¤¶å4&ÉèËmâ`âÒÄhqš«MåÓY‰-I9îk:-y«Á†6~—ßvs $‡[lá=lë¬A ¯3ôLû®hd…À0Œ{Å)GZ#3–kyúßý Iõ²á˜b{çÜ }P^8nEß\ã²–ÔέnDó"¸ú~Ïì5b}ˆ,pO9FÍR˜Ô@L®é'ö–S´÷äÒö„+ÕA?rvjm¾ñôn4ÄC Ç}Úã±b‡Î›¸¸9G¨à³(ÁÏU#voÈ:C|ÒŸŽÙPW,Ïú¿ýþ«Ÿ^ÿðãOHÒ/°4ê&&¸–Œ¹CÙÂì?nÄõÍSé_²+Ë6¹Ä:¡î³Î2ÌCN›_ùÉf&eèáý¢ÁvÙÓATeЋ3íwx…ß?n´Kðá7œ")\½è?n;Zë¶ndõw#ññÁ®F01®_?nÞ`áùV–¨{Ynš¨xnøP&ø&IÆÿâ«D¬±T’£(†¦é29¶Ï 9…ÍäÞGÊC©¦t.îcÈ}å ˜@]rïöŪú hñ׌2˜hUâU4%šïyQ~»ÊØ2K\;6ó–²-´ÊK¯aʇ‚ö®u6±æ‰½ò¨°$ÍèW;ÙÄmöàÛöššBĵ.—å–L¼Ü•#›O…À–õbåêRέ%‹¾”wAr ½SE1±¤úº™8’ï(âWz&FÄâÿ"/YSni;±[Ü6mŒøhû6]÷<'ÌÜ+½ˆƒ#¤›¾Öº?r˜`fòÇBŸªäŸbÅšM›9G{奨ŠÖZzñ¶xó×9vœï™l*;ÛˆZo¾Ã(Ͷâi.1=É%q@¥ ¬–/ú·ÁKý»V5Sorç¸ùw=ÏN¹ó¤ø/0v…×YßPg°ù!ZÖ,ƒ×úfê×®¤Ôîc)Í*KÙÞÑì°FÙcVª&È&´¥øòÚ0¤ÈèÚ~{éû™Ê—š«Qu@Gš??—«÷혂’žá6/¦"*L­…cQ‡úµÌýO4¯_žýé׿ºJP??€P¯H&믱£ó“vx͘k‘„  §•}Øbp¢À…üÍ#ÖŒÀßZ•ò²†îßX+ŽM–̇SÂ^R$åÔkpl×u—6¾¼´‘ü†‡òt%¼DÖ°¡lÐå£tµZÉLÔJì«×ª÷3´»–wÙ½MdÓ•kaŽì?rPãò¾ïô•ýâß(žNÞÀÌÔ?nǬ‘??§K˜´ÅÐçuóñ›Å›Åž´.‘ W•Jˆ©”sü™Úy;³åJ¿ØƒüB÷{f÷²£ò’ôÙÉÊ CõdDÌL´‘ïRbUöͨ_e õ??Ž®¾½[Iª¢w“Óxñ©[t?0[+kµe†Iàhí 6Û4Þs‡¥ÈbÕàÍÌ킽ë?0{NËZÃwAY³Ÿ=±\uª†Kê-%e…{;Ðu0a¨N£?0Ùa:HòV÷mñ®ð¿´·ýŒ—pÚñ… ~ˆ¯C†š@y q­V,", 4br¥Æ]ŽÅiJð ÞKÖ²*³QZ±ßÚJÏ´€¬pißk™ïµ-|î™Ú›Cy9º·@T+’·™+’Ñ[ØIl/ƒçÊ©ç‹Rvqˆ·ëå2;—Ø7 «Ð€’Ùxô8·©R7—ýs\"úLe{ (ÊŸÚJ\¦Õ¢¶æÒr”²Õ‹fäX“{›:†åI)Ø#{‘|<­mN–¶¤ùðØAT«ãÔ—ZNìôÜ=]îØ/ø¯l<ÿ¤ôôMmògÙ71¿áTÍÕÈE#Œ­ÆN>Õd÷X,É‘Ü2mìµ¹×0)G,ñoƒØVÁ€~ž;_«E—ñz??j½ÆÏA´)¢qÌþpÏœÂiF¬–'Ö­£•º¡(¸Y?r±èˆoÌC¿®H¶?0X??d™.м[“vˆj´lÙµ Up7ÈfÞeÌ7Òç?r|ž‘ÔïÖñ¯olômSpMw­À,šýr¦çñ|ñ^KëK64÷Q²®×/•eeizì2"Ó2‘ÊKÜdÎĶZ›­$óª¬íhµ??1JéʧÂ%³äõ'RGd–Ó??Ïiý_` ¨râÓœLh­®uâô9är#ж¤ƒæU2gŒyɪȂ'ª·£BÆXV¹*Ù¥ãñ›LpMyÿYC­¾ç†Th°D™J*{¬vN_×hl2<;`u¢Ãn~hõmp³ˆNe†&µGæZ(‡»<½Ÿ‰¯u~d#ªÞ+O‘ÑkÓ8G°M¼Äz^hoÝÖl¹Š•s9ïÅá]‘Y¾?0QA9*µ³°YhñRæH¼‘>º× )1îR†²Þ6f¨P—ཪ=¼'I"HÖÁK)¸ÂЋÎ¯÷±RïvN¯L$iU/ îúÂîQˆÈ{[CÈy€8»i‘P1Ø èǽ¸PÛ„íÌB BZ¶Yb­ðÒ_m?nl4Jtµ`ƒÕc‹qNňLÖM\±ãcTBjWƒÚKX(ì=Oi1‘Á^%ŒDûèÆÀ­Îy@ ¦ú­®âè¨×óŸ,öW?rìòÖz,ZßV02»šïþü÷Éh/èÝ.69ì¹’“‰;º-´.¸Á\U^0[¥žv¦ò u}`ôþHv@âüŽûúÇ|¿u¯Ÿ¨\š‰Ù$Ù„FžÃ,ê¬Ö½+·«qþ4fCïŠUK)‹'É÷Ãu??sýö»oY??`ÄÒÏË0Þ*ûެ|ç…žýár~¢ðÐ7oo“Æm¤=Ô¿ýMÐM¤ÿ ÄŠßHœXzüp¢ÉÒdH—a["Š¿9˜¶)¢õëDJÍÍR>ë|»£`‚ÂY Œ”ý4¨ýÅ“©D”Œ‰ÌW³6¢ªË+}pÜØ]g…ï˜0îI•ò¢Q·wD Ͻ¹oÿ,|-£³Ø®œ×sÏ0yaÙzîÎ~eÒ’ÆÃÓí©[ƒ¶o('^zÁlq»é8Ø""›ˆx\-"–)ãÇir‹X™l#ÅöT=“2ª|&UÙÞ`J†ôJ1àv9 µEÞ7K÷ªšZK,Kò ågŠÊûÃ@Jnc;ƒþ—ÊÜP4ëDé&”ÆÚŸK?rºŽ$îÓŘþpøÌw±{?0ìt’WXÝýÖd¢rÚPì ÝŸ¥ó²}“Ïc´^ðÉ`xžÓÊ–ŒGk­?rïõ¨ÈøX*¸} Ïsƒqo˜›]+©e$íxµ#AÉÌäPB,_¬Ž+8¿)^Ä5’ߌ¼Îc®ÕìÍ-:ZãBà¤f_SXºQÔOl‹jrCÞ[¾à²'Ü'ÃWËvÀ’¥ØÛ Yö6œÏ’å81`  l,’¢Õœ8£ ¢_ʹ’ñ¨v˜ìB`¸^ÆîàU; hÅÏ»CÕ%vìý€.Í®0ŒŽ¹Æ6ר¶mÛ¶mÛ¶mÛ¶mÛöÜwïóýºæ¾:ɃêTw%TÒª®tìÔ̵ –5ˆ$ô…Øò1pŒÒc›†CÞŽó$+lÖ?nî´pS³LiìɈñ‹‘a™´ÝYúõ xª~wÊ=ÃÝôXÕ✛ÙZ*±º_MPu0?nf8–•ÛFœ{* "~†Õ7ò3w^˜IJ÷bøÏ:.ln.'×Ík9·ö¹JÞ=êWÜíK'–­¯i–™‡)òõéºß¥¸P¬Û+õS·üâƒ7LQ„·64E÷C¤¾StÚ7 àŽR‘KÑ zÎ%·«,õ_LוÒöêåk[Ë€3øÑMpiHO¦'ÆiÆ »¼Ü 3Ÿ~m´60Q£I[ôÒPŠcw±¸¥Ù€J©Î²ÉH_0RÊ7%«À$¢»;#'æ­M­ñs8P„?rt­nŒGqN£íSFªk8ÊÄ¡“çS$òœïôa\å•ÎFf‚‘??ÍHäûªOL­R¸ËôÐ??¶ã2P{9®êÈìl|B—ªj?n²TY»lŠß­²Ø@R§,Žëfê÷-S[@ ×þOÙ/ÈÑ. i÷‡oF¬Ò¹ÿò†¼b§™¬sËõDؘClÒÛ°e¯¿A€åmé·äE™0;¨¦ÈDßÙ“f9ÂoÞc”í™´,gÒ²??žÏUÆ.’¹ßä2Vgùò_Ò>˜¸üøh®P_?04W£:šÀ?066?0*±´~FY¯Òf®E°rÚï_뉴„!í?rßet÷S³ëý¹økåüêIâi…fo–Õ@Ûj8tÓ­nÝ&Ìj”a¢e>G|Æiºh"me¨OŸë>PºÈV3o^ ?0åÔÕܱV¼¶&{«n †\z×#ÎÆVÞ -]{y©#ÆüüF2Ó”i&Éðã4…Ρ*JËN³;Pm¦}R^¯z?0eraf’ºu´‰&W¯\ŠŸT|GÅ—Öàa2³£jAiyúÑ,øƒ®«eéÅ Áq‰ÕÑ“?rNaZmAŠ‘Šb?0v3áR0îGlfÉ?nä̂ȼYÕ Ï2¥,DîÖŒ#ŒY‡ñíÅ ¤I×áýðuAÑîûóÈ(¶´!É´9c‚ZŒo]SF¹4%EH¹L¯3 ¾9ÿ[=¨º+ywªL7emmºÃk /fƒU_`ÎXõFÁýN=C¾Ð5?05Ëø ÁØÏ5$n,8<10É=FLwšzûâ€T*D‰"eD(E™n™Ï/…!µ¨Âéaõ–‚ý‡¾x ʸÒc·òFý÷Û;ñÓŸCöO (J/ÝN/c裙¸AFÑ¡íwó­>"¡Á¸lZ–¥Ì¹Tx“îéÉj‰¿²1QcU‰2˱ÚûÆ"$ÅG*(ä;˜¹»SÕ,1k%î ö §ôpªdRù!lyã¹ñ¬0\ÈДÀ<´†8²@“ù´yYí øcMò ­Ùý~RPßHKBtÆF¶¼ðîï5½Û“h¦FØ“7g?0A8ô~]Ôúã•Ǻ.]3¹/5㵉e0«×éµ2Q•‘Ak'*̰xíTykÿµÂÊÅÏ´]Ç&5´Çã¶Ýoåû³uåû°n·»1)mØõN:R }ök­HtôQT«?0&VCå"ÕøÓ¾M`#­Ê’ .µÕÇ-H|–åjj•˜ú7‚oÕBM¹ñScr 3%äsÇ6êÈÇ7 ?nÖŸW,Œ+ªëµAå”·0ûðÜÅÔ×2_/˳!pÃy %LJšýÇI+ß'V`JÅÖ?0Å|T% +#À»ù{Ì¥úöÝŽLÀdüHZЫ5+ø¡z̧Õ냿[ÖìJÚ"«³Ùj†Œ¼h;FÚ¡Ãã×i½—_v<Ð7FBÙo(5¢izW¿0^?0*R‰ÓbÐ¥¢Z—äga¢-¹–K»Œ‹ç5Ú£Ò??Öº&C AEhïų›GTM}=œâbÔ0]NÒ68ÐçÃ`.â°ã\Š RüRŸJðĈ$N¾³æêÝ’«Iì@ñJy+Åÿ1Uxíl¼\žVÛ §æÌdaïK4\ù¿-ŠØ ÖcÚšÌ(×1ºx—+¢ÓD=­"OÕå…»pKø¦s58sÜ“Ïň‘×?nÅQ•ÄÜyWí:Ò¿¨|‡;êú½"-5ZÃ?0Ã7²üp(3´lêÛ¥þ´Ï£T„Gq˜Èpñˆr•ÅT)Y!»²C­æÀ„??­¯›÷:ñƒ—ŽÓnÃE·£™9ÖÎÔÇÉQƒ«ÂCf†Ó!,–Fïß˳7õ4] QêëXŠÐ½µ¢½z„j&ôQGèŽje¬¸Ö_ °¸…þ¸ÿ9ƒÃ]S·K'%“ÊÞ«²—öf”æ!e±Ðå&PÊÉw¾í‰?rN?0(GÌóò'ˆ'“y! •[T“’ÿæh¥O×LmÚ²¾`Rë ÷rß,pðƒÚ»PÖÅÄîâ¿S­šÜTÞŽY#fe†6-¬EÉl¶Æù Œö;·P¹÷x‰Än÷ðm\ƒøª­UvpàVr‚zÉ®#«ƒÀjuªÁQ¿MýÁ†ÒºðÆ¡Ádˆ¶bLî ƒêx¡Zh/>B¿Ï°f˜æ^5ƒîÒ??j†ŸXï# cØîëã f†%Œ­ÁïB}hV PaíU´WÓÑ•„ŒÎiµh¨½·äì§Z÷+þº‘ç#Äœ‘kè,eÓ°òîÂ8+Täõ„½ZÉ´dFø“žŒ??•ð êƘõcNpàÊÒë´aÀšJi`þ¯t¦mÕŽhXõUjˆËû—(4?r Jg=9DÃø.„@*ÐsJų:ÅÚ­ŸØhl²{ÊjÁ?0T8q”ŹÇw8”o#ËIÄEû82ư— csb!‡lÌN p@ ¸#ÆzÒj?nH{•B£ÈÚQ(|DY€îæx°²; ¤-”F¥¬Ð¸·e¦±FÓ:í•0ð%eG¤RjÊU§.N‡Ú¨0C«'Rl%txSÉ_ý³Õôýt\¥”¥bFå͸M™zǦŒÙ×£âžF?nèYð…*%MŒm%‚Ý,,ÿç®ó¶÷÷E”Àøl×à|Ú‹ï–ý Ũû2Ä`•?nM·×‡ZøvQÉI,~Bƒ÷p~Ø?0 6bCЙ¿©«¶&¥ìÈ,Ò¾=BUÚ?0ZÈÉEºÚ]R†{|-¡ö.C*Èý?r^вÍìQŒ‚??¨ Ëià”ËúÈNKÈâfpÚ}4·~dy´»Æ¾ßÄáùñœ—Mîƒôƒóù ˾3÷7J«Ú<]‡’¥¢Ó›KtÁÝÀ@Ìøýk„£j¦ÓÑD¥Pá®lÐg…]àáÇAÖîefºVx_„`n¶³ÎÀ‡<ä?0á¼ÀFþZǵÛ|M>’~æqñI¾è¹üü¼œõñ^A jÇCÁ­†¯­á¹ÒºÇDîk¼ÁdGˆVsr»¸äjñÔ+•«&hùðC]ðE£P‡§¨óDW¢§d. `_î¥k†~·/vï?r+úB¡yÀ~+ÏDµP2X _ ­ça l 7s´µ¢£%?nJ-m´ºEýæšµüžB·–ÃÉ«CÊê[¿¹í<Ä?0y¦rãºR¹VMÓÒUg›¹̆±ó$h?0j??ö‚O^QZ–ðâ|}qÄÅÆ©4©@x_A/Ç5ï•Fš¸/ÎVqŠYHÄLVh*[•x2EŽ’B¼ö4(¯–‰órvtŒZgæ~SîV?rŒÜΨ|?næÞ›ý•JC7q>Èj–òÖÖ ›qÂL"°íÛ­|1_{óø6,þc¨gÄIQAѱŸ2­èAÕ„ã} PI½(P‰™~Qe‘Ü5ÔÖ„¤´ÝóG¯¯¬è€þoXRÿ§~ÞÈ@“·Ê@uíZ“=x™O¼Ñf§9Cp¿ÞËü°œÁ’ññ"Þº«õñkQÈ8?rÑn8•Tñ%X¤ÈŠÏÕ`š5?0e‚¤%’ÿQÍ´ÿ3VÅ%Ba~Ñxº3YßöÓù) ˆSU݃ˆzgLÉünì?0^4q8ABN!²¾Þ§ÒÊô0$SA"0ô! ÝÈâð?0¹än-jµ~àNm¡?rì}Ä×þvšOö?rMH1$ÁC#!K¾2??Ù¬53Â~L`hl¸™Ÿ6Œˆ‚—æšÙŠ'vl‚…*ÔÙH~ÛijÏyòEÖ¬ý–Ž»f‡?0?r[SÚ> ŒíášZÌÍ9k%þö2Í z~ÐT:·r…,$L'cí!:½h¾1³’wd/[®/f4dUÙðY´ºß×—NcÛ­YÛ–4[´Vµ6wÊ>I‡ÓŸ?0N ™÷¬÷µ¹hšÞ-z2ö&%]ÐÑi°}øM¦<΄¶-« ¬Ñl?nÛ$?0ÄÈræ¿Üâe¨o rt%6ŸÖY¼÷´·êM¼Ú^Íæeô DXÎÎ"‹qcÇè7¦±‘)E ƈ6aÈf §”ö¤0`¨p?nŽÁohOåðìá@ÃCèl{!”ÒWøº’žÓ«û°d^_#–u-m.T „P‰±ª-'Ž^þñ?rÓ1âò;삟‹GhLÕoûr †+?rúÈ8ƒü3?rÛ$Ò¢8?0s!3@¿#žB<î¸3â4MëåSkß\Å¥.7b|ÜÔ­SȦç9. Óä ç8o®Õ·Sç»Úªˆ‘jXmêÀ‡·1Xå²ÂƒpWC­¬ÿ*ýZ#­¤ž•EUtFt?rFö8éÍÞÈÓ’sM®æ»c??‡8É¡èÙmšœGh™þâ’Ù:ÍèQ”Ö#qÕ¶ßç¤áq÷T²›ˆR§ü-ÖÅüç?nIè3Š·r>žnݧƒS– &üÔÁQÖ°2] ?ns¦‡&§£³Ù]?r‡i<+‚è铸~XFùxº²òÍY}ùkjÛ.}ºs5gÔÏ‹!˜˜2W\™'[û`»6£j§òšk± ±aSu}åê"ûú³‘W@Ëѽš"rCÆ ‚z†ù¶È 2žÉÔ ˜õWßÏÙe€aΰ¶(¤RéPX9Ô> ãeþ³›pÀ–!ù8À'öî¨+z¼d0T¥iÌ»hRyÛ-öá^oØWg/…ì9G¨1âÏùò3»¥ Ø £sƒÖ?n¦¸3Öb§HÖˆn4›(¿ñ¡uxEéÇdïSŽI°îöZóSrzÓ¥Àò??åTƒµ™Rhl¼ZH§e¬£™òÈë \?0;íGZþ·ÁØ9ò.#”£Ãê£dÃF„rü5[¯ÔžÊZüÓ!!®¥7wžp÷'i—è =Qn›CR‘`ã2€åˆt åWiR KAÊa~þŠMÙÊðDÄlXXÒyáÌŽbÝx‰†«uò±Ã|˜2?n—£`Iü"´\§‹õU¨¬~l ‹Ÿñ˜*pM‚ö-ûÒ\‹Û(±ïVé×dæpQFtéž•*IÊÅšÛ ØrP©÷ ¢>-~Ž"Î6 ²TýK-y_š¯Âc˜…tXܧlÜ(b±ñG;ÎKí3nöU?0u?0G}ê§;(]`3Ï‚G¹X[è%£ß(ÄCƒ…¯mýÀj\7xÿÂmù¹]żAµGñijcF «]ãRýÄ&{ cè› —²l/Þ¬??óÉp@§;f¡)QéKb€€‡ß´{qu©0°Ä½Töû1ã·Ú¥.=ì<æ3l¹`e‡¢ÞÛµ (’žÜ9‡˜=“}Ö…»Yœw½?0¦xdT>CLVZ>;M‡š)´íí4ìáOeýé,íÔh±÷ªà ~ߺÉEÔÅÛ8, ’à>ü*3q³è¦·±’’ÖÆ¬¥bA\§-‡ø.74,Ó‘µ”'¡z|Œ¦Ð•œ|YVˆ’ æðÜ5j8Äz¸'Ç˽¿gÇÓ½»ÿ‹§{ÏŒçÙ¹{fK€+ôböþ–;ûö–Kûö–[ûö¦Ç÷/2zÏ9?0eȲfþÀ²—Vëa¦ãÏ‘÷—SQ,«×WT¬­Á‘ìÛ›ðÈ F£)|8~è??’jˆƒ?nôTáúâÿUN@{3Q_¶× îÆ9æà?r¤ùps÷79Šl#èEÿþ]_©è&ÈT âÙlú3ö¯Æ˜¹¼ºöɯÀV?0à†ïàà ,“ÍBµ$+5„&FU½¼¸ËÜ?rÇÀ{aþ‡ìZ¸BÄçiÂ]Ôóµ©ñyÞ¬Ûþ‹‚?ngê»`R)@=49›(è?0¢eX2> Î¢ ©v6)¢3,q §Zi»z–ïì¾È +#9˜€pËê]J›k2Ç%§{šè®z®§•°Á†ÏûÈyñæ5EÜ8òwE˜gà/¤hBºx%@º|??7Eܾ)«£=d6ÉùO  yd"?r™•‡zõ*nòÈ?rl#ɳé ýµµ¢gÄBRZ!;Åç|mV=Þ¦ôΚk¶4åAfå£c9OÀ?0fZ´¦Oɺ*Xé¤^@–•=Òþ¤Ÿq¾UiMhh™wë…Që)[YèÏ7Yã­}¯šï6¥õ—‰Ç¢säÒ=—qÔ…CXnü¾÷-¹$4ŸõÞ9ð@IJn½¨c;mñÂ5¤¹ä%ù¸Õ–ÑÂzêÅ0óœOøÉ¯1£Àû…°,tå´†hSÿûÑÓ>õ3½¾~&û>ãÈDÏðÖ,Ÿ!^rnâÅ\TÔÎÌl&£qŸ^ÞH÷“·Áá̓“jÈŠí1u=ŒôØ ƒñ“,= E…õ9v­ £˜žCªgW#^¤_á½@m«¿·@°·”n€·5·Wc3}¯I×ýÇÍ‘ÓÏkã°ƒrV¯¦íDädåå_+"ȉïW¦ï‡.Edÿ°<´”Op·rmƒ®?0ª>$9Ì…ÏÊ6/2ño4«eg®êío%ç§á}å««þvç÷±S¹¯ŽŸíÞ™•ob¡¦ÃTì§òmÚkî賂N ^u±®?n]Á%Ì0tß;×(.-ôœQ)Úbµ³üÄ&Zgây8Áüï«opˆ‰¤Þïâ9⮽+ÏïÕ"må7ûgëŽôzJédÍ•6l¼žkïTkM,¬íž²Ë]†üzO~ÚY,fy\ÃOYÔ”KCöhl›Ó7£ý"påò9ËÊ>ÆKjé¢òØ%Ú+:~;Eê‘r>ÖVæsÓ\¢—÷ `µ‡ïÓàŽžÛ¦/ç†|nŒwètŽd‚ú(·Vj0>øÅæÉuþ¢ß8G/¦öœu`ißB,áêâQ7¹„vc¢áü™“µÜs¹ó+üíÞ7ið{:rK?rpÜ 2Fæü°`®ÈÛi¬U¾GFU)ÐäA§×¢s÷€®k`E˦ÚS½›ŸU%¬Þ !˜BÍNõIþ”q¤¼%xËÖ¡VúaK Y#ÆAOægíí@‰Ç+Ê0ï Ÿ‡¢ŠX\JCœ—`8Ì-°1cžxüyXyž ¾ËKœxEk¯Öq©èeO¹ì­Ø?nÿÒ¡—3>Î"0¢þ]%äjwƒ]ää58ó4é§ÒöôkBÕÀ­46ÃS:‘g…Q\¸^ZµïómFøÄà|hï ¡/Ô°Œýó4“*žò1c@Òk@âb8Ë âÜÚU„‹˜íõ¬º“Å–ÍSU2¡‘zSøöâγƒ ¼`Ój®ožc‹õø› c'Ø?nò´ñ"ÅëCV> …??drx‰¹‰¬6ϼd& °ÎôÀÌq!4??†ûˆ²Â$·§ýPFæ[D/ÑpƒMæ>LÓJêG,Ôk(Â!Lå:tw¦ÚJúhk}þb»²ÙÞ4—t¿f'×ÅÚeø—³‘tÂÛJÆ¡T¹À˜=L…Ô%°q®>bú¢Æìƒi3cõr?n!H“¹ÃHXq'“ûeâ}[9:…@8#^uL0xy¦Ö¢¶?rÄm÷X¼Ñ4*ºyœÉç3éO.'¡ÎÕÇ+ÕÇ2:¨P+ÃÑòŽ“ŽDšØûˆu×9ÈÒØõõ:à΀ô8ɰœo[öN¡ªA¤_Ž·v2-¸†Us‹…¾ûºÜµÓ|o—ÝÆÌ†+œõ13=Ï` K“Ð1>Í #›3`Í­fẽ/çðDZ |Œ …ãÙuåž~Tßèë'‡W19ÀÞØÝ¾óh.÷ÞE?0FÖ³÷jˆJr!D;3è”5s‹ê.ë­Ö‚mƒqƒ‰ºŠ„∊֔ޏäi~XËqE,ƒëó¶\<ûFÀ;±÷ 5ò/m…3úâçá€{Ö…sqi¢wÚÕ˜sX²J…ë8", @#£6&dÙ\ 7œu,­ã?0¤3.ÕÃ?rr”:H_¦‹)-o?nL,ª*wЊG¿5¾k÷Q=çê*•W¸“‡\\œ“Z¶¡›±ôñmŸÁR ºd•¨›¡!(:Ö„ÖfP쪵G¦„®×¥LZ’§ñ¼’ÇRÈáÄ`Ì?0öz•ŠÝÛ¬`ËNíŠVïš_î¶Ó¿¢ŸY¤ú× ^˜RýÔÈÍÜi¼ÏjʇF3ñè:;â—êœBŽÏÛ—V¼o~11g0†”K×D3Âëþ¼à‡,¾7àRÍ3xº[½3ô-rv̯íìÉ‚2,ï›ìzâ4NŒá¬ü¹]Ñô â}ICÂÄA <òI{ÇwïqdÑg!qOÜj-`'.VÑ©v³†h6¿®ÎV º­O¼$qy™ê‹ª†{/¦ŒH7íìû4ñÝ8Vd6 k+€æ¦è˜Ìë!¥,_Yrèî?n£#Ü“¶eÆ;B»&HqµLð‰|kDiPW×ÝlG¿èT½I14ݔܛ;;*<³õ™JÀX;²°×ç Àí‚à5HÞG{0~é(–á@E×cÁe¾zMðŠèT'›¼_÷²ÚýXZ: —Î!|5 º-¹­œÔ”‹Ç¡Eªã$¢EQüB}G¨pþ~‡È@ÍM‘ô¥dýͧð‚<@ðšŸ2ŸA$¤çx/S“¬úF.œ=táÅçs&L¦¬ÇóÓXÊ#IøX¥ƒ"¥š±§uÛvmë\Õ€»Îù–òÖÓ­ªãïÂ"ìÍ­ˆ’F}ÄE[4†–縗X|½­8ðïåÙîµþöñh~^|Ïp£`ˆg6?rÇà}Œ=Œ¨‚°±?0y¦`Vg'.pÞn¹ëa?nÜÌ"^ÇâeNÃÚùÅ{le„&ð¼ƒj}2*é$NÙLpD¼šy^Y^^NjbƒfË[HD;T|Žñ¹;åýöº¤,HZv‰bÉy‰^??\á±Çã¾f0±Ch!ž¨x±…äI⢱Ìô¶‡ÄÞëìͤm2-‡3"à¿_†‘ã«Vv}üPAbô›NÀöãÕTóÓ¦˜åºGA ý•m>²$êtàuçY„)­‘ºj!µIuhÉ¿F>7öm¹9;\6sü!ä$_hT?n¶³ Ò¤×ëá}²,s)DÙa‘á‘E&Ób­ÖÉç:çd9ÇqßfóJ’šQÍìH??xÄ“-]k¶ö+¦ŸQd圻¿ó•JB]ao+#À[te&Áš?rÒy»1?0Ü\†Ïçú’º¾ûÌæ°X\aI”'ËìËQÅ9©ÜŒšØé«Þyq)¹kƒ(,,l«÷9ü ÐÕUŸŠ¶‘é•ëí°vŽÛ?nŒÄÉŽÔe™Îó‰=ça¤BÃoÁòÄtUó0/rçóër:¸ÿúý]xúik¿g–ÖîIÒÅ\¹RÞˆD?r>‹{ŸV(CDKùÁ‚ÒDT'† Å:P ‡vÿ;•z?no`æ±{õªíL`ž}/:¥¿½/ÿÍá4,ÿ¡…½†‰¬a|íÊ/ƒ0OùÄüá¶V?n$®)³`ñˆJªWÃ@1’‚'??UeoˆDÕYnCM??e Ï¨Þºq“zA™Œ‡²êëF+VMÃÂêŸ)>¢X??±yøø<#̾yò\OÃîûQÈ—þÊ?rÁ#7i´V»Ø?0Û>?rMϽ3ëmDoÁÔŸ(??#˜éOGHa›¸sذxåÛÊ"‘Qzõ[IZúª² ,Šî¼à©ZqtÉ  z·ÏIÄÄG;§)†Á[áÒkë®K¢$Á» +#ö!Sš€ÀÜà6õiàQyØ)ðà «M‘ýþq2"Èln}ƒcŠc'@Æ¥Éø·K{1ÊÒuhk<¢Ó~[¸‘7ˆñۙãq]*µp8ºŽTCÞ^DBÏ›¶B¼ƒœfEŇ5Ó@Ùhx€›d_¶ðº²ºZ{/æ·ç<ø”»nýÞ?0ñ'ìÃ8RÛRD}óqx8|Ãg Ë猺“B[ÞivµéT·dÃ=ŽþÉŸ\C_K‹vz8;Ï'rØÕ'ºF]¦%ÈïÌÒ¹±–‰ê–´ñ¯nR€x¤–‰Z Ûle𹀨rm{—cÃ((1iÊhÈ$,)f¹ =®ƒY¹õa™¿ES: N_è‰ïâq»Œåì×"ï׿ØLc¦uÝ„;¹¾OE,ÍƒŠ”’å@O¢(leÚq¸ukÁ™›Ù˜Ò¨2nÃ~2¦Òg¿n(‡y/Aø dÑ&¨É¥Ô{èøB”yDj¾Ž@àj=„6d4§ß«ŽÀðŠá¼³gŸŽêÖåƒhª«¶‹y§frc»æ ¾aËDù“ÁdÎ?0YJ©LyŒ©M?n¯T>ï#ÌÓÍô“Hà§öô‹5Wˆ$D†Ê "ACDïT©j÷?nÂtÜ·à»3Öãë@¨V¬¾˜íÆtÀJ˜T%w´+ìŠàŠJë}C¹O_ª"Ãrϱ¸:~yx¥ž3%ˆxsÈzŒ¥ rÿ6‚!ÑfÈÞoÖ» JÍ_¯×FVÞïíÙ:KöÝK¾‹Yi‹”­-Îz—/ƒxïÁ”4' 2Q¡gCÝ!½„áäé£iª'ùŽob£nÇEOi{àüý+ì‘Ö«Vj™@ó”µê×Év ê׫×é²LK5å:œQ´0TÙatþõ A‚x¨°hêvq{@ñÞ mõqÌEgc*B1ÎU:§ »TÍ,ŸJI‰BÃÆx‹Êx>Í›×3X Ü௤¨ÆýËQ<à‘Ñ„°?0,öcG?rj¾ŒÈ)в£Dàî(˜>›Ÿ¡1¿ò7jÞ«˜]¯Ôq9ôçA´< B°Nø+ÕîaÊþ—AˆU_Ù•³ûñQE¡±@_X³žíínïöB;°˜­þ…K$c˜B(°œu<§˜rÝdm8è¤Cõ—sµïâþ*°ÝZs¤ð?rV,†¨žn½ Û 5´Ù¼‰äÀ6tßCþP,Vjç9c¹sÿàžÅ[™שfÃ_ádF=ÖnãßЉ§0ï¼…™Û=*|^@J\'žó'[jCƒæËgß9Ëöã=W;^n??òí+ü7?rc|/žÕÉfÉ«jj#B}ßO§?nÖ`ðD‹þ¯¿»Õ„ê¶>W?0ÇÞ_õ S¾±¯>¿àå}_OÁM•Œ¤1WDGñ鵫öœ³§¥V‰†??ØÍ¾áa -F ÕØøï zEÞàoѶeN0Å%>[תn6ºSÎϰÃÉŽI®`V®ŠÌT&;×cPÑ.;Ùõ_äV,¨ÙÞò‰>Fg|¢‰H¥c<‹S}ß?04Ÿ¬££?0ÈeÑSP_îGàYøýH.Çå#%!eV×µx‹r8Þ¹óâ`a—á‡*Ídááî????øÎ*ßq„Õ‰šdV“åvZñ¸­!ª¥ýa,ÛôãRãØW»{¦­ìþ죸A¹-WÇö¯úô ΈÞtÇ ¸·§-t?0›ð‡’ŠŸV¨‰§øSÜ+±ÁÓ§‰ûßBvä”õ¼®Ç!+Hx—DjÇög(X7UÇPù/!¬;Zo훪?0* Ÿ+_MgîqJ:äÎàóŒ9èÎwVzΉý/ó6žØ4Ð_nÓœ&v¥rP¸å2æ,R»¯QékQXÐz`Ð}M†£ç{b¤?n²0,°¢Üt¤‡Éï¯â÷Vlr@ð¬95Íû `Ž2A#kTO­hL=øQ(1ã09˜ï¡þ{ÈÚ›$úú…„uÒ¿>åŤÛ4*PÔëy"aÍ,À¸lâšÀ´íÞXº]Þ½î?nË}7?r—‰þp±»7ui2ö¤²p D{—??t:6¯#©??™>©ôì‚åA¯Ò.f1ñD/ž1vs™Pì„ißYÌ«è)’OAê³Þ }çL~¾™Þÿºk*:ËøsÌлuK??Ê1¡Âî'¬a.JÜhmÙ±Ò(°½£²oäK”\sè®õçv`Ü´$ôÕ–u󭋉[®\Âä7¦ö…¬Ç j¢½]ÛH#…Ë«ýÝЇ¢&ý®·‹FÚ©0tÃá;¯¶õ•ú²¥ìOî~ÍÜUÙiõ…u7@?rU¾½¨µrgJ®Cj,“S³6 ð©‹#¦Ïi¼¢Óvéý‡{.Ÿ;½“çäÏ]Ë“ßÒÍ?n‹Jx¼ìØ¥e&è#•ÌÎ÷C˜ôõÙu”à>ÖîUðîÖD<žîkÞÛnŸ—…ÛI®DgDˆêtâ¯Ã‹GÅ?02z¥ÝOûà¥|?0᨟‡­*õ³)È'³›x58<úÓ6ÿØäAŒé ûw9!]â©}Š'M7çøt6±nÝ,Û©Ú=ŽÄ{T ”$XËbÙ:>÷W²Ë{³å˜k‘;Lç:g}§™4Þ¿†"§kŠ£ò  6/†-?nMqŽÕ´µº#°d¯o{[ÎàÁlwN0úv7øLõºŽSÎb†ë!¯£Úâ›E·`›8Ñ7ÉÐ_°#«À™'–aá ¬ú‘˜Îþ„Úõxožè5àòCvH–?r^™Ø¡ E¨Ñ„ú^bN‚Ñ<Y?nqÏ(ƒ,ß2ôVÑõëÃé WâÏQ2sÛ”>¦9\> cÊv¾–,¢ßÙYY?0ã $²GwK y?0»`R½·o†BìÆ=Ÿmv,Õ.×[êø÷DéÍÒ??$ ÇÖ[Ȳb+‹xðÇ^˜X²•©H@a¯ëJ¹§g^젳Й÷‘ËXŠÄ³èlIA:Ò&SÐË7Ûè”8¸R¹Í…9ñÜìW¿ló…rS·ÑeFrì¶ç??×¾æ°Å¢'??ix7úÛ=… ˆþ¶Ãì<¢IÊn83ùz*>i{±^:\üU¹ç3&€°3æÉu­|çúÜ*w\žÊ?r¶¹ç¿Dgˆ?0à,º—˜e¯êãÜLfÒ¢ãìò¼üòÿFÿÀ•è,>T²£±tçA׮߾y¡•UfÍ3Ý›.äi¬Ìžš3n¡)1t«Í¿© ¸ž¡eS¼÷ZO»V1|GÆ×Ík‘^|¥œ¶Ðõs3Ž“w^ aT˜Y6¸hN•@lÃ@¡Ó•Z5Ž¥€üãxÃ.‘å¿S¢ô¸ÉÞ'4”ýHgò°]ô7Q™µ%t@2¦0’WZÚ?r­~™á^ÅZˆúŽD¹P\n‚ŒR¼J7äEj;•~rwUˉ—DßçO¡-Jb×é8»ª$‡|ó^ ÜéÆŽ?01ýë¾-_µEQÐŽQWŸ±)§ûr•» "ö¤Â3PÈç{M²(³¶`âÓØç>¦¶ÁTëùnp–dµe¡WÍ´'q‹]N›z¡³õ.ÑüÇ7–¨„'Vwý.¥9%õËØ}3DÂÀ_,r­Æ–_‘kÿð«H£ ÆŽ49ýî÷„ˆé1üž3ÿŠ$Î1·ìXâêöË«E]¡ÁSG*÷¥J¼FNxÞeîx²ô&G̨ûíER˜síúÀ6Ú™ær½bæS"ÉQ$·›¿,ö´«Ð]êeï)Ç[„ஊâ~ÐÊ5h/ž@Ù¤ª>æ??PJ;\ìa°úîÆôã&_jèVõçÇ3¾ªOn–©…nz®^‚­Rú U~±ÑJÎÌü:‡Èþu™ì'Àú[Sñ}9‹€¼(#Þ‚ö^3(耡£&4§ž$묓hÔpßè?nÞ¬ µµÅmÞEãUz‹ÏÇÎø”©mãT™%t|$nÁ·C®™í??V<Ía"üdèÏþ¢"›Ä?roÆë¸íO·æIÓšó¤|©v4Ú«n1«w8ÞïQ¹­Ü¡„pœsµŒ8]îô#:³Yüš³ß «ö¥E?0|ûEqç¾M!S,¸žµ¸2ÖϰÑYaL³š7dªušö-G2x„ÉÑåi€•¢ æ¼Y¦&Š*ÆJ;î´XC{qŒŠÙ`u×=º|n²î~Ÿ«Úúlz~~ Æ^qdYÌoØ1Bµg–''?nÛ§ha?rŽ-?0±Í¢…3S$у€HËv³r…0µj]zw˜ýi‰•‘’qÆK/è`ª>Fz–?r“¼¹ˆM(1B'îûz×ôº×Vm!xòÃù+ƒcv¡ˆ¦ð— †©rZÞvÇÄ‘=kjÔ˜Ö¶‚ãéÉù±èO«ùêÖ¶‘¨2Ϊ)?0ë›ú $^Ú´ý²Q‰G´ÛFaÔüW‘Óò‹Ò§Ðd(: # +#l»bì³å?0‡®Cœ™PGøé?0°S’ú3ý.nµÕI©êÚšÅá9cr¤mÕÍB{-„¤ñ÷äãõW€r?? Å× lÚK †ÆB –mZ^?nKcãd§Dx›㣦´åÏÅ7 Ó²tìkíƒ5`›!Þ´•??BŠÕ©®éJ“[ì³ôbŽáEèWåLAJõ²Çy±ø­e5ñøSL…“í˜ôõ^¶Ø4¿ õÕ©-=…—üíRÙûQùæÜF6½”“o@ÆiÃØØºcìÉš“€–‘Ì!Öá)ýàB‡x*ï€ü|,Oˆ]*ûp@ïÞ é°§.·;ª¹®??(%i¡vŽóYDjĶœõð@A“^Š«†ÏHù£gá%­,Œ?nÊ6›°*L]chò}®9pö–·²ŸÕ¯¡~ß!éPÏ·(Ñ™¡JŒ¿¥@1\±œÞ†±£mþ?0f*—ÀL0$N݉¥9qf¼(šÌ÷x€¥FPΪo𦯮^ÜšIvާ‰íB¢÷Ø1Ã+úÃFÜŒ¸ÏU<kBlw’ ã{{Ïq‹Í½Q²‚~dT%øùúæ®ùPØ‹;i*TS»”tèˆ Úù+0¬h®«ÚÑê‹/ŸL?nä!ãßxŇÎÓ ?0þïC[;w;}'wZWcZG3}cGCGGS€ÿgÝ??ÀÂÄô¯zVfºÿåÿ?0+ +?0=+ã??+1ÒÑ1?0ÐÑ3202àÓüÀÙÑIßàÿO/HÄÐø±=€³¢iÍ!>üO®¢Ú†ŽX­ÚFg`èü  ÏôÄ6ÚÖâæ’í†é9´2caT±.K¦t '»¦¢¹LïQ蛌c!÷x>ß3??Ž™hV]šnºnÿqQ=²ßÇê®x+J"Hšh¡9 椿M#­-êD€§ˆôjXÞ µ¿ã’i‡”J‚ð—,ŠQ_ã“7Ü?nÍw.%ÿ+ú$q¡› ZŠÑˆ§âœò{[a©®Lé¢qIÝlØß”TŒ'N5¸UαAƒm$š´ú¹??ùѨç™B´bd3BtC29«\+œÔvUØÙ¡æ¡ü… S¯ý Qôt÷\þú}fWžÑÝÒžÜÖ©Þé4„œC ”Ê­1-¯ôåË›fK?njO‡??‹ lV$3÷ÚwMXV=;Þa[àö $FK†Þ ÅHMh"T”ïºÂ¬T÷%E0Q©ÎIâj[è×\?r©±àMØ­­HFÜTN‹ÔMÜ8d\–˜X>vͧ³…²ßQ'"™º¾‡¶À,À9E짉e!¸ïKj»v„UÍû¶È$tP7òÈ\€æ›¹vŸon³¼—ç+—ήw´Qõ<Äþ¦U7°¬UØzb¨]ŽÆ¹Üz)÷”}ËÍ:÷W¸Žéßà?0=(© ÿ§ýßÎJßÑì??ìÿ t ÿò&:f:V&ºû??ëûÿÊÿóbþñÿÿÅö?0Þʧ Õdð??¼²úËÑ¥*ÔHõ—{  )œ´†ÄWº>‡ì¤˜¾}³5–ë!¡Iˆ¿RŒÕå ¿Jã?rŽP¾Tr§w Pµ„™/È>ý€™àÅ/³ßPÆwåþâ|ÁÛ<%¡ÛcT•íÕ f½ žÜ=??\Kø\Ìy>‰P.`ÇÕœÅY‘…’Ÿ1QÈ??•UÙâC RO<ý?nYÒOˆ/!n4A‘ԧ̬´Fw_ž®JfJÇ¡ÄË“'!âH}¼êÁG©ô³FÝ£ÂØÛøT§yb/´—ín¥C¯¼<œ1a‰O]Ö¢a€¾»ÏšU¿ãßRT@ .4äUÛ)t5+Å›!hè‘çpxó÷&•^w½®ã¼51@ÆV?01¹§çOz–wo¸»Oûƒ§1O4úÀrD¥›?r••d@å11¹¥  ÷MWDƒà÷ÒGÍŠ-¢°B럡r4&¼š™ÉÿÝ9ѳ"÷š¬‡LÈ Þý`àsÌÁ)´ä»¹š’ßA_Ha¥{6ÈcÁaö&U¦oÄ-U´,RIQ©ÞJMœS/¸?0=°­¥bŸ“·ÝZ??¾ÜLD¹A,k™•ã…??ãeʯ?r€ñ¯x/ÌNI2å9niƒ/„ ¹wnÞð/º\5˜ÆAËì£\¨¹—i¯pìá´ÜJ1UO‘¬Â%l|ß÷²4;9Ê#ÜRP‘ƺ­V¯~W‰€T²°/«³å ýD»þÃ=&§¯¸?r#IÒ ŽÜ¾šÅئò ÒÝ«TЪ„öê“"µÇû-€`»Ë$€˜ëeÀ…|Yÿ~n¹Ía&5Kò¨ñfäýÁ‹‡6õlÝ`ôñ:ާð8=÷sG‚Ê5GëÆ•ÏCþФ4;·??š¥âÑôsOËÈrú—f½\ò¥õ5îÁG‰‡Ì©ë6®“ÐEã3cÚ¥óåÍný"hV™~NéÐO8´Î"Q…ÎÚaÃlMÅÎô¹¢Š>Z$‰pÖ.Ü¡ú$"L¶}ö‚˜Dd¶Ô3¥WÑ£C²_ îKëšJÔ«*ûa‘—|Ò<›X.0Ë:À\$8ÿ¼R•\D7 X²† ‡Ôo2ºò0­¦ö·C3"4À^ùøÖXlê÷RîÙS’E ×Z.º(¿^~g{üøÚØ.fJ`sC‘’×½…_7 >S8!.w«Z&5Ùod°Lܬ mb8à¾ÇÈËSè¯Á‘¬0R­.‹ía}yÞÈ2t¸|?rCo¨FùóÄ{òœz Ùa;ÒBØñ÷Õ·ÔÙ¿W"nRqžˆó-Ö?nÛ±Gùä5Õüíû¹2UíÐ8‰…sóÄ*'²¸C´ïŠ­|æÂaº þ·cZ?rhy,Da[c,¯+ï(äѸiÅ-êL1'Ðë}ÿ™òˆYiôêÂm{·7éŠlí?rƒˆ­edé ÔÜ1+¡??Ÿ?0 ê±1½V&ª¿*Ø`œ_’@òaL`¨D³‡4jàƒŒ燆fiÅ¿f›ÁYi¬Fhb¤ÝJå2ß½Dô:ì¯%¯ܽ/¥ÈõI±1¹o²À.Žb¯dšÚÀÈËŸÍF7eÅt®ƒQdyPöi>kŒôó?nÄ4¼¼íÛØÓ}w·—ß#®.7/ÜŒý–AŽURg'vžÂÏÜØ×ß®¶÷›•/kË­“çºêˆ“Š+ñ·%K×zϺø~a“e«¿c€&è™Ì¤n]ÚGbáHËßͤÛ—¾©N?r¥xNÛ§Ûeå2Fi];}ÉÔžºgrDQ7·çW´v=~¯t?r*G©ËúQ’ÝO7}f}—ó77`O£K¥ÒQ|MòŸ@ RIKñê6­ÑDN}zeCŽ:oïÎÙ N°Â.I0Ø,M???n&4j¶·£VÑQ¬Ë.YFvœ¸È$’]¼`¬GmX4B_žÕ$:þ”(áP˜²,(±¾«ÅÄ ohêš8 þ õ*QdP\ì&j]ôXT>²4|ÀÁ´±¨·m"{s8æ‹u«eåî~~°ôÚ” uùrój˜qUû3NxÝàç½–~ÕD8=`Vz¾kž/ )4™*ŽOaš©MÂÆêie1\cÒ¿$„µ-ª©9I?0^vºqPj—\«8ÁF|½SaÆ€€!Y¡ ¦b¬[§»Zâí=±ÿá Á|Üà%zP.Ù&3íß&*1îÔˆ½†ûÑ??IÅjëR…õ¹Œ-5yuT??I&3Õ*Ÿ¿;A;µ\˜=¹=²³‚'ábX'U¦aéÚ™ÖèÓ(íUW§g' •1[?r‰LQeƒ,׈güﺊˆ™!×Ë['ñmá?0î}ÝNQ6â#`É:Ó¶îòâ…K")Ÿ!N’´×ÅÅö§¾r»²Yèüo¨½%…C·í€ DO°UPaZ½×^ºss.Pf±‡€º¥ õÁßò ó¶ç:a!›xÌÖ‹ÐJü>ãRN?re³?0îêpÐŒ¡5T¥‹¿ý)"yËúb¤`ª‡¬É~;É÷YEøù¢A[6K46•(» PçIÊŸ º˜ &ÜOÈ0S«Å…P"aÎé¨\üРiŽîö‘i‹H@Á+æìÇò5?n ã*•̰ãÙ»‹™r)rÉ''Øp<C“¤5„ˆ^òF«'È,- —¦Ód&Ë&9³ÂÙ•-¶"4¦|[?n\ ¾ùaëÛej·q¡D‚,ÉコÝsBg?r :[HCçïïè¢H†ÊéшRŽªù£2EÈž¹ó~ç^Ù[Ç1(I5§˜jʹÃçì:+€7cñ>Ï·±òmcm}è??´Fë=äÓÏv¨$,"68ɘÐÛd\ÕX¶!ÍjÉÛ{àï~6›µ”?r`]¡o¥½$/$ð«ÔÁ¿}V—´{ ¬•*aŠM€:£ÝïŬ®ZŸ!_¡IF›œúþŒªÜžKb?nn&w½JÊ×[âvÛ$6Çda =Hâ¦mF:ßX²gîYÓ@?rÍñ6¥¿×¹ÕöæOýÚUqbo4KÍûvç k[Ì3 Âe“õ ù£g̵u?r é©?rê&¤fò%Ê[n¯j”PÓ©Ö’+­éš7[mÒªÕdj‰D³}áZ{cÊ&\õE&„⯔Ôè`(888‘*È_q††á£$“(ÚЈ ]£ÅøZúÏ~Ù¸h¨ â4åPo??«çݳǞêÒPÞçÞïóPydÆáC»‰ sù´ äL<ÄH…Ǩ ÂÁTÊ”3§°Ìýß(­a‹Ýa £¤ÈJêf;)úcûQ9é1¾•næÏ‹šú—?nð÷“¸€[MÊÝÞ„&tÄ3¯_‰±#à!ª¬P5¦oû%?0Giº»f7??Ÿ:l5?rœ ©#¦kÚ³èËî\>*âf²B`üi|ÕEöèO×¬È ¡}š‡Ú3YpÖveÜ"õæ½}•DãwRLß`]ä!« GÌú{´??ËûÛ4|NüÍÁ·]Û~Ä} ÃND‰WiÀõ݉+ÂÚóÐkm[w­QJ>gd˜]Qýì¿Ü[ÿ]*.xÓí`0 §Û&;ª¯– fD°Ã?n;ðìOÙ‰rbç_Ti¢äCaê7†aÂY°Þ’îYȈäù„ñŠ^E#ÂjÒm‹N1¾ïÀS{¿.$"3ò²OüÞf` f!ÐrÛ¡Ï¿=¹":îÌÚñV ;‘ßõ5ù'ÏvØŽæ*8#]Ç1CºßȉcöYjSÛYE4}äÃy¨OƺT[Ãè¢g›úÑÖ²°ëT*@@FV_B²†Û2qáÜ&Œœ:ÂUÙÉDÑU»†ƒÐ-Á8Ž®B¿5LVdY̧¸+è£â!´~>ÊënÑv²˜_ä(…©¢.¬’+Ä{Ø8.|6Ê‚š`÷×?rÓ@A~ß>ø·?0ÚSRåìŽ[Š_?0Wð- 2ìa²âB5Ÿ{c\è{ĩô´ôB¨)ÕC¯ŽP5ìŠØÖK]žÏ8ëåêxñoh×&›Rí½Žå««‰s59ÒçUÇöìäj{ÿé}m›ùeOk¯,…jvHÇèYQ?r¯ñߨÏö¼M— R1®>Ö~ò~ßçD®îûPõ¥ ~Y .ùåëªÖˆË •,æK™QWöÓŽ’goÑòöÓnåx;Oêéûð¡ouû¶5ÛÙXž £%€Òñ¾ †æ‡ î}‡Æ·2ðÈ*÷VúÇíÚðéoÄ/TûâoùdãÖH2s«@%XB‹ Üjòaùõnù„OéÕ'I1EdB™ÔzY¢‡wÅÎ[íé¾î,ôÎÓVwÒß+â ?0|ÕgÏ=6ç_@>|?0 ¾<éS¢yۜ޷ Y¡[&;ƒŽ)¨—F=åº?0Zš:lÓþç† ¶Ñƒ~ôRðiÓ½0ê\0@ñµ(Y!aÄ4‚oâªqŒ®à†TDôú$F$ ]໿ýnð½¶È†µ>ŽðSÒœyø–‡!DóF??Áïc燯t¾H.0–DˆT—ê‘,˜òH3‰@ÙD&­/µínΖ>Èö<¨H4¦ö‘bHº¿®]wÓ–G­úÇbªÈ®þvÏ)„!­??k1¡¯b—pŒæG:ÑáÃXŽ6 Aî,Ú“\^cªæËתºª3†F+©a%‘;M½¢u;jV™ÉRÁùDU—@1R<1#[Ák‘7 cŸ–Yö6J[??þ±VÐE™n-üQKå%” ¬ÿKØ«*C³Ó"T¼B€ßÁœÒ¨®©á·ãÐGß º_ôFX}þÞíjq4ÖÙ”+ä°+£Hºéå ÄH¹;ó$1ÐBÞðGÆø½*!ÄóÕ(Fý”àÎæ°CZÙíªîNÍLÛj{õ&‡Mü62ô»??Pt¥ÅJ8shR5óz2æ²É36‡¶Èµ>Y:ÆgXõV‡Du–ί ®Në#Kº"›^·|×C©BW‘‹©n¼aÞ4¹.?0‘O˜‡ùØ®´ ¿gC·ÿb („U“–Ñf̉paÚîº$á°…°#§4;çj£¥.]§êÙÊÉDõ½•¿ØøøÏ¢³xÕ-jë%KlC??íuŒƒÒ— n †¿“«rf¸g=Žv«Nå¢ù0OÚ¾\ÿÖsàÜÇ]Zd½6p» _Ó??·Ým;T,±Ä–é´¸zÓ)k+›œ2ÄXìbÖ\ÉÖIÏ9tÒ¿·½ßŸ*Wþl•ø??…heo­:a* ÷©:a‘‘PG½ïü>?nø¿Û²—ÃîÁǼïü7*ø¿m(›#ñ Ž ¼HZ(ßhÔ4ä[lÑÙ{ë©´º\¿7Æœo&Ô›He怡“÷?rŸÄš4çÄl˜Ñ›ý±r°z9¸÷ߤõ«©n²“FÙ—2¢3¶àY³6 ¦pñHî‚hš,%špXµ;ö?rjå¯èyŒ´é"JúOåèº ~®Wy/î??}î¼ýž†úãßçÍ0@Ö ™=³”wO˜úØ"‚§!§Úx£êܱõÆ’]Áì£!¡—3@5?r:7‚­Rö!}°Ó?r¾ÂzO/‰}þ¾N2h Ñoìöz8mñyAÑsÓ^n3TÆè€Ç^,fË~Or9× 2_yä(b|q*ìhïà5°J¨ÐÍ@àݧiCYß*ôM.»–±kÖŸúÁý0ò*qß™Üú‡tñ­›ãž'š??}‹Úke{ºà¶m ñL•“Õ÷ŸT,m kÖס??*92¦-‚È ˜Ñh\ëÉ•ªŒÉ´Ít_׿Mè:ž`—lëÝ™êr ÔþÜT'—,ýÅGSØ?0X–ÂÄÆ¡mÜ6F±„;ÍØ¥‚ÒYò„}ª‚@û™0ª§!TRãÜ5ŽKŠ—k_ äì+¡ÜASWΡ"_VHAHæð?n*"¦ý¶£[LS ±ÀÕX,ÍÀÝ-VÕݵыí%»ÑÑ’*cõõu†º®¬±êöæ®S¦–Q^Ì??&*Þ kG´VYÁ.Æ ,Ú˜ôXq@Ž4ŠúÜ1xenºìë(âõf¼ ÉûŠé‚S Ü]à$!áDøŠ©9}šO³)ú›¿-??Ã[] 5ÍJR?rÞŒ}ä‹}ºjëµåâÚådà÷ ¬ƒ#mæêKËC9Ÿf=Æ#b Ô•Þìð‹«msqM,NˆÂE†ŽyÕº$·emÓñ£Å%[¨SâÏ ?rµöonøµ°;»¶5Mý""XXŽ‘iLúÞjïºA ¯R߯„7®“½W*c½ãU$‡àÆ2ŸœÂÏ5‚ÑKÑ+a"ÏÅŽ$®WүʸäõÅ“¸°£u +#œ mâ8C´ÄÇdªÅhØj‚Ùá•Í»2‹fññÆŠë:±]3¶RÁlÀ$àh&¹#~þÒ3͘–ÈqdK}9c0ªõ]?0W‘¹ÖKÄÏÙ§—»Ž¡ŒÿZ½œ»9(°¤P·Î¥û˜1æ±_³œo“‹Wæ¨3¬¥v¿ 0¹ªGâ@MûÓap\ž?nµµN’†uQ<‚qç&s§²‘H½¡”æü$˸]—oe9ûPÉ:ÀìÑ5"ø¾A€~?0\n"‹jÉ?n•EÝ»}*]ùÍäuäEû0rgòlq?r?rYW –ÌäUÞ-¡ánIâDeë{”ô-™Õ3g¡?0¯:‹5vÏ»ˆ1'çØöŸç\ÒÊBgg¢Úº‰è6Aš À¹Z5s?r‰Ã”ή“ÏÕŒ-.±>;æE÷š¥.UòA ýäÈ«0Á%]Üà‘kìeÄsw¤‘40ŠÔ©OY¿%]€þ"|óCñÁGx•œ9‚… ;C±f`cmXÚ?n[Åó¦» \dÙev\au{›Ré‚qóä{~}ò×AS„°Ê§±åÍH/9??çÃ<Qcà¿@^f0Œ¹ó¶üN¡›ÓMø0R±üƒÖ@ Ux¼ avêàYð:@ÌaípžÄƒó;DMÌ*8³L,¦î+€i.q?0k î‹S©ž¯¨ô*ö.†ŽƒP“|•¯NˆãÁ>É]2ºy÷Õ­©î¦8¡pÓ÷à¶ŽQŒŠS8VNNŒqÕ–Ñ?0Ú¦ýp¤ËëLy¸Šq¨'-ÇX(?n§#²gZEë­ë[µƒåлÎTRß¾ä!Ó`_(ÄcHšôÀÚEóÊ–ìk¨"M³X3!N7˜Â}&]VŽ©3úe^xŒ­óhæÉ%=Çô°{(ÄY§Ñ,í®Ëûº ²± d9ÇÚ€HÁ#§4ßçj´šà~vj—‘ÇXõ¦¦êK ­I þ?0ÎþI|=«š ›f?ráMÓu¨<0 ÑãDÏàá‡mØÛ ‚ i }ò_»Š/Éè®?n™p"£-Y^kfe&µÚG%dÏD˲xöüÖ?0ÌûˆÙVfœHÆåò&îÊïWëñÏj7£k‹f¤ËVÙ¯¦ §©nÏâ??-YbKA5—A¶¦;, ÿ`ùaª²ÙK‰Z·áX|ñD€Ú58 …ÓDìR)Üœ*¾½??«lõS!ÂÊ…B¸ËºÑü-²lU‘µ¼S i=êâ}š‰Æ3ë°c@”ëh=‚~g5 H#ɹ¢›Óy£•Ùt«SpÖÁõŠ?n-mô ‡JÓU“¾ÿw§×M¿Ô9}g.íýpF¥3 "Úìœñ[mý¯p»ü‚úqC#jø.[—óËI‚?0víJ­Ô²kªÕ|ä±M} xè|´Ð|•ØUVHàÎKYC•…qþå#¨ÅQܬPï,}¯Ç}×C:]믭>´hõšÞEÇ5àð²¼rÄt˜féß•¼D;Êà¿Ï†¿²¼°w¾o¤ý±¯¿Gg£ào¿w²É’õN¿SŠd}¾CýúT]¦Þ™Õî~üÁöŒéekÒ¿Æ-ÓÐFmk¨–OZ†+‘È‹¾"ê3€ïvÊiÐ ð÷-h’€f‡³ä{tOe´pªÄ"1bEBÕ<Þ?0K çQà-™È³-”õÝÝõŸ¦ÕÔª²©oD• ãg`ÎUîàµ5XÛ?0ßõ8MïŸkH~!о²h—Âå\2L0ÏRûJ,x3myKU®ŸÃ:p …äÚË! ÊòÊr½³?r6'Gð[u·ÈÈòG‘—«ê‘=$×͈ÑÍO“Žþ¨£¾7G1†ƒDûìÁnásôçÍZ‡»ÂœÝ–x¨J;ÓÓæÒCY¶ä3ežV)ä#ÿ`BÌðm¸uîÔ$0³»‹"Ui%4rf] [¯€‰î«9>bý|Rÿ~ƒþzûúÞRfmnhn„7½~ž™±°Y[«›Û»!>AؽõP„Ïò'¦ârä@,õƒim,ðVÒÍ®u0W9Ú`<´ÒÖ)âc}îAÙ#)ªñl‡M#Vô/ñ÷Þçî‹2ÒV%ð]¬âzlwÊ·zýÜl_¬™“~ÚPÙn¦Ê6µ?rƒ‚åì/Ù-áA?0åc]R®÷!)]6E¤Áš¡¼AÄ‚~Ñ~éÏ,\Y‹3 Ô;1¶T¯GÅ3Cë_,·['»hç#~irGÐÜÝ.š©\õÊU›ßhÃ8§B‚m:¯È­ZÛƒªuç  3ÀKAz(É­ Á¢Y °Æ|Gñ•÷ÉE8˜bë9B8¬÷êÉM#Aæ_rÜ©tRˆ,I/ªÚÅŸÒNÈÊëñý‘ã±›¦‰—te°_,_<6ÃVcÙÙ)Û}7ø*ç‰i¡¬TáªøC¡?0Mez´cøǃB½_y²Ù¿ûæHs¨Ç¡yæUÈî³Ý@ëgrÊøRTÊ[Î-(ê.Ô<î†ðœ‰p#¡}€UʸYÞˆ~öädÇÆ9bp ôG§æušIcð º*¦`?0EÑÕÌ(pLÈj!Þ,o¨À  6ÑÕ+œR3c&åá¡Y¤Ü¿oÿÜ3°c¨/š4KÇÅ‚Ø '’½œ 6ÊSZnV[tÐᛥXÝ8Ñz ,¼¹)‚@ýïY”ñ?n½.¢UýümHC‰êâ4þI­…öý%Úl®³:®Ð—ò¶ŒÎÔ.½0âÛÞ©¢æ¥k¸pnV¹c¦k$ÏIa‘sBSvÝþ Ó`›#‚£K$B¿™-Ò›º]z¡_"šåÓ¶GeváêbÜ®±7l—?nzÅd.¦nÚýõ'|12V Û„k! eÛÐÍV»6-¼(Ìæ®E_ûõ‰ÀÌï¦y뢕k¢Ç¼Û(B©YÌ7­“ ì ýdãpU!ÞþVÿ¬?r0Ǭ÷kJ€1’;§??’3æe˜Ï´à>÷Aƒ@-­Ÿ'’??}͹0ïR^×»Ö^XA†4u!ÀÑò‚0ÍGFe??IµJè>¨qÿFŸl'¼DL¿)ÏκPJe¤??§:™‡-È#±H Ö"`¸ …¹-ð0d°6:ùçwutýWNdâ^€V³”«ÚÂ=lƒ–²ÃB™°¡YH>‡!ՆŠU]þ ‚"ñð E¼YP“uK[²%ßLM™òKõL#(íû®Öü/;`'챊™kÀšEÛÕÔò9ÛúPV`d:F°&¯ŒGPåè6غ ï³cx4,šÑ¦ú!½o-"û/;¡|ºÅ8N’ÖþVoûJ??R%ÃtÞòisQDœŽÝÈ^ýÚ•LëóÓÎt· ?0ÿt¢Ö:¥Ï̽lXÛQRˆÍà_‹çhHÀVêøÚAp‡ué{áNÜk©F?0GŽØ¿¤mIžþõû Œiò:*¤eTYH½—°Æ¾Ô|Z=:Ú‡ß󑈽£«¾°ÊÞé¨ÖX÷‰‚ðàñŠùt@Ïs|sîÙ`NÞ´òÒü™YNÃÌS*}«¬‘#ë¶ã扄nÜlÔV?rÊà*CüéfŠŒúá~ç ØöM*ÚèÆhàð‡™Öíó"‘¾‘mÖY85ufÐ$mñ‚yIнC6eYjÝÕøÅ)¡Š==Ì :?0ôGÑåÌâê¨b´æ?r 5}ã»[Nw½:kI7È8ÃkÆ­¡BåÄé.¤O@|}±¤s€k„úýûãNð?r'‰b^T!Aœ#?rR[ t“-$—åþ>Ætv˜‹¬ß Cç€]ÑyUÞ^@™Nøó[åá‹È4ÿXz¥”{‡´\£ëˆRÝNÿ>ðéycÿpe½ÅŒÇÓ í„³‚“OtþñcúY?n'Ƭë•l"Âg Ø­¹bm©Àš‡ØÝ?0Œ£Ö`Õ•¶ÞÓWWÿ ÍRØdD®'µKGU˜ôÔ×?rØ2køiy´Uê@˜sÃAjv©N¯.—öOÞ]çJ3ÊÜú¹?rßó‹Ó¸²ÈÓΚ`ÅRø¢?0Í?n=,‰?0C!À?nPLn{¸z¼õdYž^ç$/s¡~JRENöÕÅ5(7Ž½Þ±I‡;~a5eü\É¢ °hèÐä?n{/wp äñ¨>1d‚_ ¢sÚÎr0ŽØª9np…ÖZ‹üßV÷Æ”ÐGsOL“BÆ™îÝ€©N˜®âÏ3œÚƒÝÛa”à3*¯=hQz¿¨#ªŽžÙ<+Ýq+uØ%X˜%`B&…Rrv‹9ps²J—ŽUÜ„&¥{8ÄØÍªˆ…;µ`Zîašc/bƒ±À\ó±î \Nø&”œóìÑìvìZ›“7dÞ‚VZU~ñe‘«M2y°_ÛN9WÐH¯$ê +#ªðe?0{zÀÝHrÁ>Ñ !(ø­ Kòö¦„œ"Ú㟩0ðXU…)Ù^ÛÝLI! «;,0}[øÏJ+Í2t~¦ß3h?n´j\Tôñ&©¾ •;½ž+úiÝuÁÉ>y°úÇ$`Ü($µ} 0#t<üS½Hú”†I=¤5äçL6?0D@×wÖMáו½WÀà??v—2ƒ¡D†7«Ï˜Éƒ’^¬žÈ‡Ãèµ²?0E½ª .ƒ)ããP”9”ïP˜ =HÇüùŸqnŠ^Â<‹ÀtìGV™²Ow—4±Ü?në¿$’oW7þXqt>]éËðbM0$'—, ÝÑÙª¤¹ž—“`Îñ?0·-¯nÊ5n:þIP–‚ÿnYf¤þ} ÑÏÄÄW©Ç€\[ZI‹Îp®C„‰D9º_4½Z,Üa· ×X,–íÓƒRB<ñ oü¥Š£ì*… {h~¢¬ þ1fÈS‘b„y‡ÚøñiÚj‘F†ÚÚsÊÅóXi#P>À8=K??¨Åqö$Ç4«A„Wéd•ŽûèÌÀœmö¢£Âtz˜]ÛRÈÁ™›ÞÞѽFþÕ&ëÜ6å<ä*ÁïBwÄ-F?nô˜ Aà Á ƒ.ÏÞË.íX»—zó^=.°ý[=Íp4ñÖÞÎ1Ÿ¥,íý†–‡È?rç=C³l®Õ ³ŸG1ªÌ??‚–ÊA¡2Ôþ³YßͲ„Ó(˶•±ŠüÕɾ8x©€*Àc·ÿõ¼µR.3 Á‹ÿÂâÉëI«£y©ëDÆ~cë[Ãs`gOĽÓYFÚ¾¾@ÖPÉÞzP[¢-õ?nû3¬—Npefñ„øaT›{]þB`AºðzÞÕm³Y¼t Üpu=¤·ÔÃÙ¤]·­G(€‚S¥îRb{—Þ7Žd²•^¨‘LjÅžnd78R†ú¥x‹jþGÝâcwÁ&æëA¤9þ°©®2Íÿ|9ŸÅI°‹$ F+Ûˆ|ÎðU¢7žO³NןœÞVV9y¤F^¢p§iê²ðZÁ¶–®5?rÞ£WÙ?r´9µ‘ãj?n®rÆïÝÉŒ ~Å»1˜4ݧþPòÊh ³°;t甿QpÛ†Ž‚©êÕ‹¾…5/xB*e[¸Íù1BPbâØ\õïQ…â~î’ˆ:òjȰÁ5ë™F?0;÷C›¨ ëâ ¥*›T¶×uÌŒS°Îf¨=h.i{Û¸E,œüö³È–Ùµði)é¶ãÕÌìÁÎ?0Þ#ÜqƵXÔÇùC¿š=÷gP\ƒ%ñ)¨hÛtYv‘MŽëìì[²¶Ï¾.Wweã¶Ô«¶Î8ÑܩΨz Ó4‘}4­’ª õÓ®#ã 4Fœ0a|k¬4Ì °öê¯7ØJ‹±$_@V¿D A¼/…´øUÀÙ?nƒàËf°ËJ ÚEäxÃÏž*št¹zÄDPŸ|q˾Rr?n•³ º¹m¿AÆÑíAÅÐ ¢ZÔ"O)ryÃ7ü³¢c±ÅÃÓdzNáR =xÊD¸bÜ;c9¯ÔÀf^Ev°‚ý_)^(¡åJ> ‹µÃ=Ü€<¶Xš|ã†ÊyÅuäÃ/G¥Óë?r´ e2@ºÊ[¶‰³îg¢UÎ+©kŸëfÌœœ?r¯\ áõç¿ Ô~æ??9¨~×!^·y5p†æ‚ýžŽº¨r¡¤“_æäÔ^Ó[-%ÅÂ$º ,e»›ö¶}{çÍ;¬?nÌ9õràgP8hµËÆæWïcox}ävyѤbôaž¸ƒ”d•ŠÒ0<&P +ò›S åÄÉ¡–HTA*ú 7ï^gÁ¤ýøé–àéÚ}Löñè˜öÖwÛ-¬7Ÿ­û_[kl??†I£âå§ÄáÆ-&«Ð(_ìão³%ôÈ[bàMŽ:”Ü¿PÅÙ;m«-I›º²ÖYÛ²H…§Ö’þªqãËÜöÄ6OÌëoÑ$ImóÂqf;ÐZ1b˜örº9C?0î#¨´ñ|^°Âùéó² } åèM— UxÞëxÀ‘›æO3I’±Ù!Už%¦ð3[go+èª÷ ¢­oàNp*Úêé5A1† <&“,8˜ÄIÊߨDnm—v>ˆíOAgfûÁ¯½i¦¦¦+!!#|G»E tNŽ©Œ\´Ñº.›)ûþ ,ç-….Ênú k]?rZH<~Q”^®;êD?nsªãÖ QF¾g¿+‘šrøº+RÀa“6»„¥z½/ð‘$Á«ÔôwÀ?0)¨HëèÊœg¬;°j#ÌÕVÑK#ˆ•t…_P¿ÛVA†qXáËUùåØ ƒÅ馽g_1O".™ä‰ÓïSwôH…+.5a@üÓ‰g}~¾àt@N#Ò<£^Å dÚ …¾³¬"]Ÿ©Ç9 3èwkãÍP¢¨‡asÆ^¤·ÚæÀâGïI±õ®Š¦î}MˆS3)Rä†EHrŸ jDA=*ìÜÊ,‹1iízõ$×[G·‚²árŸNrŒÛn&;Ì;QÍxê¾>&ð‘ÂܸÖÃ{µI5ÑBœ-ÇtVÇ‚/»ÊLõWåiF|9aËÌ L#G°kú?nÐú&”ÀÊnÏpÂþòkNû`§ôëõsZÇÌ~?0j)1ʾmêë˜,]’ia›/^ºÝ^Pº¦€õ•gª,NrÍý áýê(©œï–|uþÙ£æ¬e!<Žu%†Òžv®wõ.àî2Ôú .£'§~‹ê™]®-~ùôÆQüü¿^‹‰ç¾MØØŽfÏ®EÜ>¯ŠŒ³ÜƩиêPä àt¸ÐÑѤâkìl‰D*ìR÷ÛjX_„éíÇO‰•Å²ÙØðšÑÈ ???0O7 Ù$SD  0ãƒìÇ4©ét«ch·C?0?0H>mŒórþ~H7Ëø{<œL)éSˆ¼)?nb¥ÎÅ%‡ ¨æF–ôvËÊÇøš9¿Dý+¨U:ÑÅCfñBÐ žÛi:ÎÖ_Åšw@ÒœÃÓÊ_hB׫¹º??I³¸zŽî“=^Z­½Õ@6æ„‚ØÊÕ§¥?rHù÷7“ÊÈ”¿å†Ü·Z¦”Üë//Ö®0$óÓÑ”Dã*M3sÜsre…x1¨cêÒɹeâ§Aà`tÜ lý3!È?0³õ34Šäˆ}å@í»WuÌjÄy%ÀA‰Ïñ:oá«3åQ# §:üYù®w¯>P‘m¯¿"¿ØuýBO ]#’%'X¼PŽ~ô^”^TÞ4)ˆè“wiP “c¤Ì;féßÌ%B0H9È6Q€&†[mí¥INâÙwŒnϤÛ,;¶-òììäŽ~*œógúN_]††0kÕ(·*æÝvãøÎ0]\©äõÓGÂéÜÑc?0ḳæ«û£ÀýéÞhŸ,+ÿâãÙ+Ét–qÕ”ÒGá àt„#‘=hÄDm/ôTÊ%fžN5¸ývý÷QD+?0NÎÑ™?07 ?00ѤÇyœáò`Å 5"5(_1Õ«i:µ& (PÀFö·EƒÄãÆS]ž}ÇõÅ8z¢c??ÿ‚Äc™‘ \ùyö¼¯,[~¯–u«Ö?0w¨jÆCÆÖêõýN«¹SZ?nÐîÑ„“ayë²àeMöìÔ=ñ0ƒ:j´‹§Û–Ç—ëMØáÇë=ØÆˆÎ¾¹MßÏóS{³œòÈ)>PÖźYX\Z®ê¼ËW¨aØIfƒt¥×áç‘» Úul²€’^Já??þ»-?nJ2[ÛM7ækäKôª X—U*ž"m0;}–¤©Êü’ ËÿÀÜš±ØzÉæ:®Ñ)@F†âtù¡©8ÓÔ±^ä“zU‚SE ¶Ö·_=^ ȶÄ­î›Ñc%"¦¸Ùæb-9؇|€¡i¬ÊÓÖïª"œi¢¡]ª`G(²‘ª)7¦˜`™°Í†/¢fG•#OU|ÛnçäÄl«ì'ŠzÜ<{’àÝ߯[qÿ‚ë³ÛÄþ¬j”EL43‡OÇ k’?rff¼U8WÕ8a]°Æ°•Èâ‹tdû=ùýkí?0??×Ä×’ƒŸÇ¯“P´Ä²o—[ÅèX??mö'ˆßt‰Mœ7ˈ7êÇáá~)³¸¦õ2Ö6+?r‚$ÙœÅ4ÃŽîë±;Vëò¨qHÅoÅ?rŠê(ÿPæ1Þ/KÐå…ìbcþóg‚¢må‰7ß2wá5¦B¹PÀj!¸Ð¡çNˆËSÙ.ßGwË ¸ø¬±i »YÖã+˜®Ár€\U3÷‰ÅT_S]ÆÍDÛKqˆãÍ‹›œe´ÒH2Aajqeý÷î—©/51g‚§Ž¬Dìòê÷·Ô÷›7?0M÷çgµs@Š|K|*R§¸­¡¢ ˆoa?0HŸ¥Úd†Á2nS[1§ÀzU…olqsÒ]¡?0q_\FKâ¡«È!J·X)ý3(ïE¦û³V{.–Š«ÝÇ)eGFQ§BÆãM™{Ý??`zŒþ!úf“%~ôàPÆ­ *©D Naà¦ËÛ¥]ݦvIÛ‡óX¡PÎÞµ‰N߯Í_Úß„‰¸¬a‰…ð©a?0g¿ñȸò£?rª:ù„ÅC†ŽYÉX´“Ðð´C7SR?rtkµ5*/ß"’›œÉo¥– nM¨c·ò/®]òíòö~è÷ÂòH¸®ª²u:Êü|ûÑÞ× ¯¶ÜNh u·è{J›š²0@±¼ÆÌ¼ûóZ¹bT!YaßÌwE¬ðYƒÀ§7‚ó;ñ^46ÚÐ+ðƧD<~NÓBž]y…“"Œ×®\?0/X4Å#_?n=Õ»Ò{Üi¾t/n”Wº©¶ÞÀð%¿&[[H‹«\ƨ&Î¥VË–47Z$Ιܠê'·š/8?nd ”clò3b£uÞǺ˜Ïå ÈÐ@Ähì6ÿËîÂЩ5º̤—'öǬ|º ÍHyù½å×z•c±pl3þf’džÝ;fžÙž¸d??ð·õv@…"†f|`º‚´•uY”1–ý¥’$4_d*ÁüäÀ??cJIó¶,ô“DãåF?03¥!Méïà‰›ÑceרÂß3Ú,â(qÿ=ãÇÒ6c/º?0dMË~ QŸN´óåF:iO8½áC¶@IoÍ¿y¾€‚sy ”ƒy<c-2©Öù`G…V_¨P3²hÜNãì£lµgzéÔç~_¥)ÙP„?r_¥Î@Jýµ‰óÓlîôÓhXK­9ú›Ý7ûvõdDÃD›7E?n$CÎ"ZU´+Zò¦!çõj†¿¾??Œ·y+dòú ¨s]´r©¬:¿§½!5¶0–5ýù­V¯¡¼žOiçÎ`h!à"«Þ»œSÎ$kÜÔ?02z#Ðgs«É~á…pšo\P çLÔd¨3ñ3eÕiãdÁLI¥p›د6 ?n¯ ªê]55{@«'þâ\H/Ÿ{tˆî6îr;ôlídžàÇ"¯ÖNSløæÆ3cC¡ßŸ#6пØIÚd¦Ô2E³@?02{{[¾»ÔVÚZB„¨‚$^"DwÖû¦Žf¶*%Ý:wX”‘ÌÎg^?rÏË)È”(õÆò?r·âµбÛë3%.ü뙄¡™Àg™§q-Ïo/[fºÐ þÅM°/'Š|½o1g·Ù•$´âÖbpy̳X»*à×§Ò¢–ó]ë*VG#‘ýº€Ä 1ÉK?0ÿ?rÿðþ¿‘±ã¿&c¦g Ñ7ü¯™?0þ_ýþ??=33+=?0==#ë?????0tôLôtÌÿýþÿèý€[Î # à(EáÕC±??]§ªO?rÝè!Æ&!–'éɯLIoX??÷•’¦o?nÚ{$Í^¥l%Nß™‡ßšßÓOê•XʾmîDÆÓÝš¬±ßä²p¤·ZÙN±¡¾¤Þ éz+âËV>Ó$›M£WpÇŸåÝîV°ÛѯŠîË-ÓôúÙ«õ1Ón8‘vÉx¥‹¥·Xò¤Œˆ3ZAqøŒïö3?nÙUÄbl–ÿPEÞ8~îoU{eÀ½Àp]î;k˜¼ Û/W¿îz£}>—ïÕ¶fCÔvô¿V/?n[xÒéN·4ᑽýûEÃ>¹Y* }zm3©ô§qÊÄççýÔ<£TV1„¼¦G¬ñ‰"5 Æ£YœA#µ¤ÏkÄá¡kYíbŠÇ6„Öd’‹[Bè|QvÑ*óÔœªõÐΖàzöí˜ÆwÅþ–û²M5ÑÀ¶{K“æee²Û¸«udç}¥íµ*óÕl¿Uqå¢g‘«ûG4*‹êSæáéÂôöâ)‘ÈÁ¦y…JQd†{=Íxb¬Ý½QéЈ[öÈþö´´,×ê——B³5î1V)ãŸuÞc)§Œ‡|§Ëà(Ô B’‚`?0-e/íå#Ëre¥sAþY¤ŒPªïm*žÜ|Ÿcÿ«=øÇ‚>: OÇèq™mSHpÔÚ6$rygއÜe%2Û¸«Qç†ê0›Ùåe³Wò{¶Æ[ÓHÞ:Ü9ÍAMê÷ïY_jhØÛpâ®Ùb˜ñ”Y5­á<ˆšÓõW*½ÄÑÊÀÓËr&ߟÑønŒ'Š;[£°¢Vº7jXýA€øñÿÙl4£`]úAοHÍʪçR??˜ˆ??ÏÕŸ ìÒ‚aÖ¿JŠ0£éŸšÀÿ©æ¢¨ ñ%%½ýâŸÖAþU×?r½dy•NFk¸@ çlÁÒqíDöo6øs|Ê +ßÀÇ‚y9‚ÍGÚ¶@Æÿ“ØbÔÁlÞT†ðö¨|ÿìÅ¿øàC<ÑÄÅäÏ´Sže¨7¼¿=LãCñA@?0à??Åȵ²kƒÛŸñQ5-ez½GÀÿC ò›ãHL~‰º³-KnßÑ>$ä^‹mö/«<¶YpW“¨ÁôšFžçz í`"cR0“1-åï»Ò7ëRpøÉzïôÚ‚ËmôÞúÞ¾~ö¾òr[¯·ê†,¬øþúCüvG??Žâë;DHà(„´°¹£÷Î$"÷v_*…ÒÖª¹¨Ç*P¡:@·Û’^G$E ⥚iÎÙ_"f|ºõœR³™Ž³CϨ;Ì8¦d~Èv‹ÛÂ@Á$ÿ#ÉûäÌx•£ô•Ài@ƒ#`6ôÙÖ½˜œDyVAÜÀ ¨‹ÌI–ãz. öÍÏyã¡Ïq[æ õiŠã®ÓL å‚JmGU (i$d¡ðìkð)6ûi»è »ÝL½£Úo9ëy5~¾B ?00Çé;çyÿÉrS‘£??X¦~lÒØ~òQeÈù† ç&謜¡š>Çß8:û??ÊÝŽ©;‡J¶ë-Vš-Þ‹,«4¢ªöVרNØjØ&¹·.p-î?rÓÖ9Vz™¦›u??ж š‚Å`Á`MÓÖ6 ,ü›AV‰ó,ú› VHMÖ?0Bô?nA1F™uÖ63-I-ÝWô[½«fÝë¬ë¯Ï:‡Öû-vŸ$6=Û1=lGÇÍÔglîîCÓܺ7€vÐAš?0[@û¨?0­}múߥ;E­`Á!ÿh3LSoë‹ØÕ¾1(Ǿ1*t‹ê€Å@忸eo}aÛ䯥֓oñ‚ZìŠÖ{r­*æA·o\g¶»p†D¶¨Îµäú]£À³þzµ‹öQ#Ú ØEi‚l†<çz$þO½ß&Þ:±}hÁÆfhšÞÖÎ1lsm8LCÞ&ÆÚ±mØ02töæ'Y½÷oîwKµª šÍ¿ø±ÚG^»D¡Œ¶Ö–Yóö_SÚì¿;Èúin!î¥@´‚´ý¹ä}&éþ‡“’…œßâ!*©ýcX&閞ϣ?r®Ý㿸¹ýOÜrì#ÛºwtfV7wyõ°_R×'ÏçÄ,ð†[#Z\uœ¨%±&à:ÔdÈÿÚó¬‚'„“¨¬ì“¨¬Œ"ÉþÙ;¡ÿÚ»8«xãIÇѨ[GÏmÞ6Î?0N\-ݘ¥Xí&U7mÖ+¡Ï÷D´ä^Òe{'ŸÞ[a"2ñe¢7]» +#rvïÓ£>ê Ïz¥RúG‘Cfºó<åÛÛîË$ç ÅH)2rúÿ.ĸÎÛºoÖò_5#-Ɉqê_I9¤ÆwJmÇwNmïÑ ¹:ŠÆ£†uUBħ¦s¥{çÒRÞ|R"ÒfI{êèÆ«]Q¿ zêšÈ~P“£œ´Ñ"\Sn˜¶q?nÈ*n?rå¿P5šÆsO^©Í2xvEú¬.âzDÀ4š¢X†ýC—jPë¸JŽï¸BÑ«…÷t•G ë.0„¨WMãŽÇH:d›¶wû±×å??ÍYq•DõŒìÞI]oüNiB!颭ßPm’uu?rÊ9é ç½b7™%ðŒYwË1|qCû®)å~ÂÆ4™%_ÿ‡ŒÄ9žƒî$û§ŸÔ$IöÏ")ØÒàÛ4N˜üDyVaœ @„>àð äÙŽæÌ©Íï›Ù .œ¡Né烨cŠŸdßß=ĹçÊetàÞ!óô?n´Ãítе[è—úD–g?nÈ?ru¦Î·#Ü{g×-RúÁLüyxŠŒ8¯Î?nš,àŸ6W|•ñÿ!?0Ed"Õ±5Z‡ðö¬=ÁjR,¦ö®5hÑ?0¦úõè&÷ ´½ýÕùŒx‘¿¿™¬cÛ}ýC¥Ç .××ô@ó,ëyÆÁL¥'WEð™ÌG…OHu.À/9HÑî¯JÚ ÅAo("7ë‚iÄk–—?rõ•º_÷Gc?rȰ?r@ ÄG1Jñ8äK’NpŠ2)þØícÝÍÂû‚=1å~þ™ýþ.)õðØks…÷É^g¬Ê RÆ.??Uã¿`W‘Å®8ëno„/M¼`ÆV™­÷@˜­·ó¡DYÇZÛ¯ÓFç_Gß³R`V;`Vް£Âs}çûg1Qñ…’§Í«í~ñ_xÌQÔtðšk1dVûå4b5ö_LT”<ºH|^ê¾ödC­þ©úrúâ*KH¼_–N®x>¿úrijǖþ 5Ï£Ê}¿œ–C-J¹×¹ÿÅÚ÷íÜ£²Gòãì\À³É*8öYîùìüìü§ªTÓ;—ÄÝëɉRvHÖƒDˆ¤¨´°ô³1§¦yÇêäiyñ™]1UB6Ô"Ìï_jô]Yd^¦¿*,åd¾^9,äù|ÿ÷ÞG´Õ^R–InS™–y&üK€?0Ž!ž¤J„=ÜÅC&»k@‡hü??"•ý '™3[þ© Ù BXê>ÂþÇ.ü8ÿà6‡¡mî#îRøJ¾ä…Î?0ÌV_t^Ì??hszî¼Öî\ìñp'"÷?noóU@œÿ…ó,—ǸKÉéDÁh¡ÿhÞî¿xlŸèï¿æÕÜ0OU ,qaý£ü3ɶ í4kwž6?rZ”{…ú_Úªì|«ûG¬¸íYrçÚEDNô\]á¡!½áÏ^±¹’–{Íû—Qþk“f£äÅEî²EýêTˆ‹QØ#¾˜K<ó ~BÇ¿]Fït$!îY÷?nw…°ô/4¾lžÂÁÜ›#6§[êǽúÆðüãü/Tð×ì éH÷ÊuŒÊ‡D¸›1*3ü5=Ãj¹yC8*©Ÿ×fäþ±Ê›)Ãý??Ø.´š¾9*wú)E)Šqˆ—“ƒÒ¡­¤ð‹~Q`¹b4žû¢÷‚~ñ/l¿uñ.ƒF¢æ|ÉÄóÇ£ÿw[â1È O-òD}:¾Œ\E¯ή;~ÑÌ%Ïy›¸rÒ 2@»TŸ{–,7Çj1¦¤ÙŒ¦f‰É„ü{ñh)!_r†Ø”`×aZ/Ä(áQÚ6ˆ‘8:Ž~õ‰‰¹ƒËxð¡Ñs F‚8?ruzÇ‘„ÊC’%#q¢–æÃÒ_jñÍ©(£8‰{..|Q$÷Ìî??ôdãÁêF ýÚ·$õDü’ýx™PΖ??œŠëèþ¬Õx&‡åãÇtÛªÝxé¨íö먩Q¦yû«ggÑ|÷žÀ§uuýÜ\3XýŠÝˆ®‹†³ÄxWµœ<É2.d5PgªI/ʹ3@ Ï‹=Ñ€E„ije²Ñí&™¡Á‹Ò‹MÑ F¨‰”G½0D+É3!vGú¨¿0„ÿ™DO¦¿.È??# _@˜dK¾×©¢Œ)p¥—“BŽÒ!©“ÒKÏcÈ×fBÆƧïT#3H/bXŸ$Nÿ_x®M"§S)*•4E†¢› A S1–O]“¤¤GÄŸiÿAF&¬O] °6Êgk8gVÉ^æc–µ¨ç??6ä+ªdC1^–{NĬ"© ¾(_ÙÓ´„©b~*¬œ±È˸<´OÚÑ.ÞÝü|¹oñþÑ.Y½¾ôÐ*i=5ìÔ.wÇZù>þqlÆë嵚å·?0£ƒÚßt‘f?nÑ9ïýíø‹’Òú–ó³óò=*eò~Œð»u Ê•é¿R§,²œ¤èÌ?n¼üwP7öO% ™ƒ%é£é[4Aj*˜ZŒKC×É[ƒ5Aª$0U® Þì)+AÊ63ÙúLKô׺àØñÀdgvUÎý¬hØ1û5` 훣8v\ ˜R˜M•ë]׬ESçµÑ¿Ò†úöÈ@’j)wÔ˜mÅý©Ç¤%S×µ9~¡ñð¯Ð1‘ۛ߿ØKâ&ÿá*Ê©®³ùæhõ??D¨Ãž½;e"HYü—??ƒÁ8Xà?0rè7ŒâÂ[DÐ~ÉÊ#³ã— ??^Ü®·Û??0Ò¼÷/E³¯ýô~-ºõ-‚FË$Úäë³ëÒº„d®ê¨”üçl&‘ßŰâ„îÏržE/?nŒdZ$äψõ¥„?nPk«@ŒY…>©™ó[ZÉ+J•\aÙ“h!‘‹¶O’óÀß“% -<šÍUuæÍ»ŸîŸW³œŸË­Aî_??‘3=à –KŠ^w<dû Èãâƒ8߯Òßga' ½bÝç¥o95ìu}^_L Šؾ0hua´øW?nEùcƒòƒòCd08 Ï¢òCf0:ôÎò%Ch0: Î"òJ¬ ö~HÚ|ŒÍp¹íï uû úç<8ª?0NÜ̹qT?0¹šBs`)ýØq3dCQú°áªóJ¡ôbBÁæõAéF×ÏÏë€Òîˆ !öÌÃ]—ÆÞ$0ÐWRýj@¨ÎkÆS–øW‡?0%7ò,üþÏ1ë÷%МePé‰'Ÿ§ýÍ-SðÊM}pVž±Ö¾õ!½I¨¹aU??½Žá¶wPh 57nhš¹?n@Ï;šÌìȇBÂSnYW¢täÔ÷U×QK_’±ÚI‹ÛÓv{éÕ5 6Á‰ÍäAˆ¦kslëåNxëC¥“Û¹ðò–UR0ŠK¬3S…Z«÷úÁ…J?0.­h ðغL€õÌnÞ˜{Ägµý4¬X.Ø‘÷9”áß˔ӣòu_A½½ªøX-·„Ë®u$°Òg XôÏŒQ‡O¾±‡?0mY‚—ð$QÊs$ òL‡àé¾tuG‚í¸HË*ˆ™l Äu–{'…üaÆ9¦b;ñˆ?rÊ4<èS¥±³s¼¶åðc˜p?rd›ÑAÝäw€BS é™'ö ‡ Jà#cïT9c‚ïhS9™šŒ58i¿=;ïNº #œÃÙ F»€½]€vq=6fêšw–¶Ä[º{M;³_eP8` ;˜Ûƒ£ŽP–à†t9u1ÞãLèffzh匎²/Úµ¾ã5†F䜵},M1Áð,'Y“âc±=õ})ªÏ‡‡u}8ywtt··w9‡Å}Ò9Ý\uçÚ3Ì(+¯xˆW_=ù® ¨–OfU©Øš"´Vð€’*•°LB¸úzuÎåŽêóé ‹Î7ßA¶"K+š+͹*Ôµ5!7píY[‚œBŒ„ÃS?n‚Sśߊ«älµIo=ÖX=YÁA27y`ÏCâ7oèºiDbäûk‚Á‡zµ¨Ålühdý™Æc?0f²ÏSóqþu+-Cê/&¿UZŠIr-ÝQ)-OV¿ÈNÑ;vúO/­xÒ½ßo©¯pCõák‰»BÀ¦;•C´õuæ×¯ƒŠmÔRázêŒÍ±XKµ9-êÔT­ +#ÑNžW§aÏo‹ v~ä=)!??5/äxxÕ`¸?rËÃiM}7Ø'î’gÆS¿09#räåÒ²ÏÌ|¾·­¤{?nvjOZ´0“R,=éK.â¿×Ǭ [VQ‡†£ uIîñg>Ô5|ß̈́竬—¹ÂW”Іk/w×Q¿10tÇè´ë±T:8å†{áKµ$1üÜÅßU2ÝUºlÇçrIbF¥¶äém-N“‹â—óÃKåv ¹sÜÃZí›8)œStˆKÕåg~3&ºAri 2me&º¹\¥ªZÕQ…ŠJ¢ŸŒ— ¼¡‰~ŸÁ²W°,?0Ѭm9ˆæ/1öÁ†‘VlÐKӾ¯f úô‡î:ÓÐl}x q%?rF9??8s½ßoJÙ‡¬UA~ʪ”qÄš_crèêþúìyGßT£Œâ¿ ^>F£:¨ÙËŽ»FªLrèq£ð{pžQ–æ›zRÚ¶gZÃ!]¤dhYÚso©¿¦Æ\ßüD…è€[zœ5nL,µH’HÞý´$Ys´à:ª;ÐIgq>±zðP¯ó·?nPðTí)7Ê…·ùYÝ­.`ëá¸ý(AÃðÖþúÓ*¢?r•LäÃ;VÙÏ£“ª¬øú\çA®ìˆ(D0lªYö?r-ä7¯Ïôÿ™Ï]iÌôÍþÇsßÿXþ†ÊôÌtô̌̌,L¬ÿšÿ•å¿çÿÍÿ^ºÊlü??Û [U´±?r¾çSõI(oeÄü:ÓQ¦T)Èl€‹ª>Á® ã÷ÕRtmç8ßñ­#“KÙáñ΂ Ó8z¯$ j‹&ãU°‘¡ ð9C¾ÍŸWBçWˆ¼¥è>C'hñíÙ€wšÈ£ZЖTJì·L“±øKÀèÏ£k!åšèü²Ùm7…ÏHñs~]?rú¨ÇëÓþóu£9X-ÊC¡€è8­¦—ZKEm€}îãœýU»¾6§ŠóD›sTzNOïálÈÍø1fu:Ð6q‰@à¼ÒòòXÙ6Ýìž…*Fë­*ŽNV?n»X¯÷÷øQö;!:œ%olÁ2Ù€!|v‚•õúë¡d–DÓpJP ¸ÌÈð”5åA™NÕê_Yëb0q…tþ²ú‰ƒ€sƒÁ'?ņSü–Šb†^{å‘Pþˆæœ‹?0ùûúpÝs|‹ã6d?rqŠôPþBÉxÁ‘?n?ný/Ü*p%þÒ3(õˆ xYÍ€¬ÙQ'õµÉ†¿£wâÿ¬Ï›åÓC5›´KKµµ¯ò¤2ɤëÛ9ûÇY÷ôÓÛ4ϯ ¿”+P7¿ç;cï€6§›j³Lõªz¨1Ê??„B°(lhaê däÀ,¾Ö‘O@ixoÂmH¸??ö8Àšë&,ÔÆì16,w#u™?rqOçJéDû3?n§*qU³¬×³šš}eaDo¹U'ªAiÒ¶ó!u:^™ÿ–(î'P‘$¸&Îó© ˆÄã?n¤F—c9?n˨‹å3 Bkzü €j§&cwª–—M§š7°J{Ʀ\‚©ñ² 1¼¨c-ŒFƒ NE :G$å Ü-“D*å+bEt­s“UxûHÜí !¦?rÖF­‘öÎÞ[Ëàæ’è bnZd*H[oìÁ`u¤e«l‹nÜü,í§¸ïí_sFGã‹ÖtƒØ[OýVÆ“.KRâfs_MÝâà*$¤v}?0lçXs\áPv}J“ ÞÌm {“ÇäÄ¡*¿ƒ¦Äºóñbeµ¥šc)N›%ï+I# ¯ÐtÔ)Ö\Yˆ°/'U/§KÔ,“¯–¨&¼®vÏÓÑAÄ ¤ì$òù°˜^3–iŽí'‘ñö]w+“€Ë†‚|ÿÐ* <Ä þA_Ö«õy w¼3OŒïœ»IUSý;z×fDˆ•·+„Ü]C*½bÝ…&UÜ—“W챿3'²b3*wë‡X¹±Æ8*FH?0Ä |Ý•°[ÂîÚ]úy`§û1ò's‘2#>­Ž_ÿêÊ Í#2´ªã²ÁuNgäõ˜ûbò ³!ÐKÿJÒyCg²8·Ï·5„ç÷|”6dº—릅3[y)dE]Üç<ód åÏÓØÌUVûm7íÍÕí?r?rÿne&[ Yûu«4??Ô‡T¶Š)'@G,O@Ù„¦úÄ»e ¯"Y?nËf‚¹c0þ+ìAѸ¨ ûµ¢‚½ÜÍá©þ–ÏspÑ™àk"fÍló²)j˯²ÅEëÃV|=Þ7èŠO÷ã€ÿÀÃÿÉüÿÙãÿ¿²¾üü/,ôt,ÌÿÊÿFÇüßùÿcÇæƒåùŸm0¬âšXÉÿ»›kà(m Åv³`蘺ã)ûÅ…hYH/–E¼§óy3ÂXÛÛ[@iécœº?0Üûó@Iüã{UÅ£ÊnzJåÇþfCŸ)ôøÉûûAojЬ٨G¿iœå´ŽÖ£)¡¤©ú¶î…;ÕÇÚŠx)VY•´>ž1‘Ûj®tR¤â3ì&fÏGÊ4Ð-¢ ^>s{K:EË"bÒñ¦<¢M`Ý$üZÿ´8B`ʉ?n²ÿç庅.Îo?0öçã?r×mîÛ¢ÈR)ëûé°¶·>>Ë|î6v³b‰0Ôˆ—ß);©Ø•?0UYF”‡G.E™{^À?0§~JÂzžc&KÐþ§k*¹1wëWÚÊÅCV½®C‹—Úl‰±þD=ä?nhƒãR»p· Å8ð7—RÃuÎtšcÚ–RkÄžÎñ ³0o¡ŒH¾À>‹:Òµ–*ŸŸfö(\0ó^dì“fCƒ2²w¿:€_µ¦1$+F"ò?0²]V寇!¬Týq “„‘¾ãQˆ«ÍÊv£c–¾ºÆDé£ÏòD¾Cäú™À¶°Â±ÛçaHã§™:€¤Û¸æÙL#Ῥ­´µ“2§ÏQlG-ÖYÃSÑ«¥~ui _ L&¶WÙ ·÷Noiýlñ†La‘C†J¤­ ÇhÉEŽÃ«e–ÅvpÞwÂy‘0™|„@^©Ó/YÑ/kÆÄfÿ¿‡$ÃèŠ&§ìÞYÕMQš`'úké‡ùñ}S 8«N§,ºs|2†ƒ‹·áŒÚÙâlÀ8au=®ž‚‡’žµïöš‚Šæ˜Ä­ CRÏç ‚ƒ«°ƒ¨¨\Þª?ro=‰dL™€§u_ÌÞWÄóÔ;Ÿ÷Cža0]rÖ{~é)Ï|»PgĪkñITìûº}îÃ-Ó1ICñÝ„ºƒ¯±‘“©`÷Š›ËFR¥símº¬£»R-BBΖҤÐ1(\ÍÛÝ»¡G$.¤—í~ç_¯½ÎŸe:Œåhj7ê¨áÚäÃеsމü÷‹ÖÛ^è…¶D2Ïè{JI/öîpCu~!Õ ÖÜט˜kw/ŒÁ‘Æ=LÌ¿¶´Xã› CwRÅ-:u?0< %uƒ"øˆßˆ¤]449²QÔ‹#ìÕ~;hg’ïg î;´º€ïдn¬/ªÈ†\œUü¥Ww×{gDjðø€Mœv¾5\%~5>ªsŸ«»F€Ýþ??À½Ú¸¾óÿ¯ÿkæúÿËÊDÇø¯üŸô¬ôÌL tÌÿÿËL÷ßñÿ??ÿƒ×¸L€ÿ×f°Šå–?r¢Ãqå>çÛ€^0;P„wÖ·ëœfûªM*$µ†¤.ðÿ¼hãD-å±ÎMýZ£ù3»¼wÑ ˜%RôÒãœU¯Ž¸x‘(´\z*:n#Ò1–w%@[Ñ89èXÄ$SðjÀ»«¤l¤ºÇEsv½OÚËœPÚ&š?nàü‘ÃßYãɺ_ã0Ê0öŽGïY“¨3¨Òç`Hj†¹ÀXß7ërdJj“˜ú‰¤Åiþüî@¢¦‰à¡·ÖÀeå–Ý`û\ŒØ8AÁŽ>ª\?0$²( Õ5k×J©?r(³ÙŠ'}&ƒ§Q›ì´øƒÖFõÍIÝXLÕ¹ßwÿ½/žË÷ìã/),¤¥,oR„Ço>????¿Q óPd³) tŽ&öÉ­$ü6bô·&öi-ÐÐ&§ŸÃáˆÄÓü=`w=õIã„–8 d?0µ;›ïe&€?0¨«ÅßÄ©w/šÖMóô©ô4çTiì›MȜ֕Sïc«£q:­ÚÃç·-Gñ.ß;væj(Ë7ìÜG Ë[ï5p,ÝÀíJP0Ú‹Ü™1óÌé¨å;Ákû„öî¥Ð;v‹=mÏ;CÖ‘æj¸F..[«ïCR-×C甋¢ˆ×Ï–’å`f+ë{vÖa-ÃܤÐÖ õl ·Ù7lf»mcÜ+˜U°WC’™M<^þΑæ#ä[OÛB’G1®•êhj’ef&Šu4Àç~4ÛÒLÎ;Ù10.9Òž)1AŠÉÞÞè†ø>¹×?0¹×œ¬5:&c/g៞!>1»ïÔéâ73šY>e2¼¤mðYÈæw?nÀ¼}{¼ÒÊõø®Æ¯2»çÝɹþÃë—À—ù'½ë 8ø7ö+˜Kî?ríAúo_×,ÃRŽA¯FÃayŠùe[ø××3·ÎßΛ7ßd‰ª²è„^¸—ìfYà…BîEô¡Ìß½–ÈÖñŸí#óP¼ˆòQ½ÁbCnMùbù<®v+bÜÓÜþÂÅzJ~èFüþý¾Ô¶Vsœ`v¬<ÊX³EMvÜf_uØtÚ•«õ‚³®Nrf}b$„Cö‘ô‘À%ûŠùJøGHî·üß’7Û…Þù»Ø-¦^QÏ'¸¢_ÜžUÉ·Û…_Æoàó_V'Ö2Þ%¬1«?0¬æÈ?rí™??qú¨žú·v»Uå0E.iÿr9ÜÉWdÜ݉¾ÿ诇äŸ_1H²/p÷ô¿yw¹GÆ)~¿iAé¦ÐÈÞ<û%nSºG{>É&w²ŸnÖ<¯>ý< kp,€Ò/ÉÿQÉŒ9#p†™¨þ??²Â¾ÿ£—÷èTÓ’£)€BY#÷óáB´ØÚ»WŽQ.†i.¿„QÐW—© 6’¢»Uÿ—>ÚOý´‡r°ò¨^·B.ÚkBoÐ.†^ñMÑäVºdÅÿÑïƒ$ß>Ö¿d­Ñýèãûá¼ã?rþU|Ú=u‡(úœI'øzMßÿG!¼S(ÎE/1ÿtÚüM¡å¥áÌõ!uì< “GküæA*Ÿš+ñú¼§%Â_¡?0ÿ[ïÿì˿˽c“T€Á¾ÄÿÒO4†ÑÚØO<‰[¾“ˆÊ%¿n߯ÿê!y=i–™|'R󴾓Q¨Y€yS|Œ†ûþ­ãÿjåV¨zÏqhlÁôŽMK(Ídáòg£Ìód^—ïDžbË’.‘ü8}\2âTŽÈdü(ƒèªÞbŒÛõ¸ƒ¯ˤ>P¢½&K›O]U”ç_>Ãz$ùO×cü·^«îÅ~I??À|9‡ï|£~(sù>ð³Òû|‡·6þé.¾S+s2eÚŠXb×äN´eh=p&ÀÿêPÌ,ÔÀå8&÷hO(ž ŽȸQŽRÿq×ÿ’<>UÊv|õJê¿Õÿúo:Þš×e £³mc–ÇójF`£{媌gsCÅÊ„Õá휋'ýààÊ´ãg°PÍ—Æmîá· ¦ï«÷“73%åÏfoìe:nGeEÖ³}èâ²o5ÞÏ»“±Òí•2‘ê·øq×ä7¡Öì:;l5Í]Íâ›on{l¯ê™wk/cæ.ʾGÃyª?nЉŸmwÏqàüsýs 7lÉç§VþªùÓbÌŽ í]º–S?riÑL ßÅS/g®£ïow¯$ؤçÙT9Ò§Ÿ©ÕòJ9³“ëYTùb>¬œÓÂbíª¾%—móKçÒMZ&\;û“Ú)MgEƒ\¶:­âNã ÚfÛÃinfìàýáχOåò:mwκÁÀ3‘?n}àÂÀâ@ÏA#`®×œR†jßÅ•ƒQÆp–r°O??ŒvÌåäÁã| ÅpnŒg«'Óì®ï©¸eAþªÝe´k—ªqœ ªoì3ã‘\®Äš6xa)gRô%è.ljIºæô–_Âæãç6E{ÎÉM·6¡¯æpÜRÃ?0:[cÝæÛ§ÊweÑÍdÝ**œinþÚJ%ÔÛ$P,W­›ǽÞwÄ)­‰ígõÈo•æfBEâ7>.:(?n^{à?nÊ1Œ)2->aÀÁj®i£­ò¨aÉLಧ"ssÂr¥j+ýS1[rç*Î{'èþ ²>7î8í<Áæ¨O4ÜáñªƒO¼(šãyÏ^ýÓÔáÞž7D0t;U€hVB ýCH;ªP?nBV’W˜*hÿ{£OßmÉ>ìc%ËÅŸ=ƒókÌ&îg—×6±Âh“œ[ð5bpÜ~”??ŒUÃÌgJ²ðé9 ½²Í••`zåÐ×'³OJ·2¬ý:pc>ÏÀà­§²6¡eóª[*??ÂTA>ÌðW¨fN²l„nyÑA_¾Keº¹7_ë×5ÄóÊã»Õ=Ìc·–Kv7wœ ’vTUÊÒÅÌ¿ÕÚƒB×°òðé.“j›‡õn8ÐfªÖnoÙ6ü½4ß+7~ž> ÛA¥«–5ŠÆˆó| Äw‘º‘÷–ò>©Q»UŸ!DYì¢8ÀáTt=î0f=ekK¹ÐPû;mö¤‚ôÖk0R¤ë ‡Æêú.³uy²S^J¿ñ¿µÄ×OÐy÷Œ ØëõiA&í—Ñë°#žƒÕà1V"öâÎf´¢¿ªSˆ‰kÃŽpf,!RÖ3yt¥¿<Îð’ý7ùhàúÎW€¼Tµ‰1=Ëë;–~ÜïBðx{ Rd,A\ijÓUµ8–€ï PÓåX†9è©NϘÇÑÊiry¿E°æ½å©Â¬tóµtþ–FÀ±¸ùñ³7k¡<<¤›±ê"GZ zÈEß”ö•ÇbìNÁOì€Å,ç»ãähŽÎ~ êòÃííÛ‰°o?rº+ÄœB£r‹†AĦiJªž¶éj?râ®ê-2óå°b¤×o[#Þå??·ºd³6U2^„ÀÞm•ntçÿÓ5®i>jÌ<”çq·ûC 'ÎŽìÂ8dŒŽ¼ÆøñƼÄH±Í€·:6?0ŸzFtÞý}E{ýǾNPo«¦8­Ž>ÖC0fVÜß®{oOʆ¤°U‡ÿ®Ã>ee…ŽbÚCžò5±$7›#Y±Å:6“Ýq¶ìýŽÕ¶ýcj¼ ‘‘ëÂ$ÜôÖPu£ÁÁZç’¯ ˜=о[ ÷Ò¯(ÝßÌ]ûÉP¯&ËOX›{7¿ÂÞ*šÔ¾ÚçšÀZ†KH¢üª¿{¯Cƒ·÷vÌcDK ³hI£ñçJ¼®ßšä.ÎÃAÌ$\¸X£?rZ6ô©áíN@5Z%³jÐ÷÷Oó?nÜRkD׺›ªß–ßЗ÷2Ú+ž­•9•Ûé>’¸6‚#×[Ê’ ‚}PûïY7–Aÿê3Ú³ŠÃžävºvX€­ØîJÄÇs6Ѻ·½üMÜØ´aSÝBJeç?rÁa<®â=®ðbµlóº CØÆ@6;ÈBã}ÁðÎt7‚_j‹@|uõ²Ûh=ŒÿRËF+»VÜ…;Ï Þ‚½×,î&)_aœÎË ÷öwæz…iîε/ø~³c à"Ý©»ÉœŽ‚~À°*´_KæŽ\Õ(,öîïÓ;d±×Чâ^§’ëÒÄ–+EçR¯[>×ëæ¸½i÷b;§‹gª]¹éÕRß=2Ãõ²¦¾L¯´z-ÓRæÛÑ)œtSð¶¶¿uöÞ¾NRÔá^fk¹¾c|uíÓ?rZ^Ôÿõ¨òÝ{P“v‰?r•/0Œúf8:pýêÙ¥•­w[›Tø‹&‰cŠ÷c„œµ†9 PaÕÓ1P…ê‰ÿÓu²¸*|‹|ŽülÜN?0–¦/¾;¦ºò|/›¢+¤ôcÂÛÙ’ÒÀã|á€{Û0³²+Âi¶çñC!ïµµ€¹ÑÔ˜©|í9ÔÓ_gE!x†©??âž|p#è*}Üxkzyøµ¶þLÊ'îɬÚïÓ–äaÐØàðøZsú*-çc·#˜…HæF &ЄSèKc?nV#¨J<ªàz~67Žãâüü ×ê,],O8fzk;ÝÕ$˜ò?0d¯ß¾JÐòZo?nž#®ZŽ|rSd鯲ÒV]ð[aÇžËz"»Y†žØû»c_j¹??-:cëNVÁ'÷ºÞ»W %JlÁL4´Êgç¾Ô°öF"'Š:Â|k8+GwºÞÏÔeK×Þx‰ fUð^ËïÕ‚}+çýd8¾Wr]x›$¾Ws??±îí»Î²®ø¸Í?nÓ¶??Éx–Î~G:†P'ÇÐËòà(§àúþ<®?r؆¨¯¢]öÐ?rcTþÓ'>¸3Hfê=b%÷3ô+oåÀ¿•ŽÑJgq¢f?rS'þBƒÖ§FúˆÝê{1ZÍoÿi°q¥Ô~}™õXûlç‚mAãæ$‚sd³b:¸Ìð籬™7-õb¬jt5yÜ!ç?0;îp‡í??8¸»ßLŒæÙO›ãÄ€??ÇÁPXd|Hÿ§ Ô™HPlja‡|côÓêYOà»ïvå.ÄM$:[÷Æe¸#í-lÊã÷õ[Õûó¥êXK8bŸì»ñüøþvÎN²Bÿ¥Ú¸@G]´ŽShÈ®äÞ ÎAyñ{;#ô_™Ûf*û^Â)ÏEÏ(Âá¨À]õtHVì;Q'‡âá½ísJ]Êc[¾ñ?0ÝÊŸ¼É”eLÁeDø˜Yœ…—ã!â¤y–)> fîŽ}Ý?r1}¿^ÙfÓÁL?n¾Už®N+¨™:¢Þ®ã=AZ–¯i ñø<*1äØŒ%Ê8ßÃå¯v–u°ØK{´:KÜöòmäp‘ÞÙx©Š=ÞkD½ÕÀ¿q¸É½Í\-dV±T_-£ªp¾x¢øœz'YáIrqÖ¤¬ ­þœnÁ|¦5xÿÇðž:ãÁ_ð®ÃÅ??…^ ÅÜç¡kðî??!¤ñª=)h*õõNê7Ô}+¾Ø»2¹Æ.¸¿ñ·Ðbîýñ´=ù%8¡iÀ?n÷§‹Ù5¥Ë²êM¨È†2SØ7b®ZfÌâ¹I?0ôX•òå­þJÌîdàRÍÝ͆úŽýÜ5ý¢±A[ûíÞëæ¨Ð×??FBÔ2¥~ö ?nnŸÓ±ò¶?rˆÜƒx_JÕ†À¼È¢Ñi±Z×ÕÉÓ„=Bû¡$J¹ç¶æÇ +#½a†Çö|£­IgÝ×y¾­ª¡ÑIfÏÜóæ´m“QãŠ×ó²em3·(-ÍMà2â휫úêØ›vþ 2¶ÎïŸq¿¨q¦ïlöQ­ÞUºT]þnΩxV«îõ»!fyÂHâ®g?? é{áe/ƒ¤º´b↙ê ÜzÞ%È6%`Ucwm‘öÚºÜBš”ñ¬·µž3µe‰(&·Bž´×/T£)e‰ÀV™å¡xåÀÄ'§9ñ$ú5i˜Ï_8³Ów¢³u*càPÂÀÉ`æ@ÇÊ.ŸoµmLèÆ`N‰–(qI–4^’CgÆÅhgqÕ7 šP‰†® Y¨˜ïR6Œ¯U_>4imv“´¬Kñ0»;4)á¿*쟖?n¬äÁ¬*1T^Î6Ç÷fc\Åï`µ„Ç´•~+L#²–ÑõÙÕÝ—ÓLb6ûò£C)?nfôq?r×*gÿQˆcXj…4 ùµÛªÙR,žòÞÄ^­R¹ìÊʾ1þØÑí·«½×œU™g9”ØÕ{ÐxÄ»ó5Àó°zyÄŒW¡õèéô²ËOhh÷»ïÏÈ:ûææyÛßÏȳƒËqhèöOMkèÌú¸$ˆ¡ÜT0 {½J![ØY“p)9Iµ†Ée??sHì {™ ·R¤J¨ÙJ‰£§%ÇÕ-›Æ„¼AJ ?r1N\æõ9løáôtú¹f~›Œ¾tCyª Z£­z˰ŽRý¢0v5ž0¯`f‘¡£%jðPÆVÇC@2¾ö¢ñ0£3 I9šŒ}^¬ã ??Ð ¹»Ö“¸„¿¥°YÌY€‘iÉ9ãî?rm ÅþËË4A;2H|£' l´jjj½"=Öˆ=òê%'€'¦›¸ÁX¦9×p¥9æ0 Dw\x³Vñår€ ¼Ý_©Ðù'ð=Bf6FÈË]#ýȉöúÔ~!©¤&ü°?nâ&(:müáôÈ‚)#‡òe­¹—G Õø, éoëÊ&R’qc:­–x´㮣®?r=!i’Ý­äˆB¨S†§?0=ÇÀ¸1¡$ì¯!â’O:{¦EHŸvWWêAõ&žñ^gG§¡exlz0 µÀe/…ÚSà5áZÀ??~×®{€ƒv˜ÓB^]Àòf.Ë¢å•Ê-9Ëìæi]»¾>к1—´âµ«íÂmÐvhû6°´Nõª%д.~²Ó$î?nýÓ8vî§\pë[†^+M§{†oTûê/Øš,˜ï%ƒ[ÐïÅ»·®V>û¬˜ºi‹¾ÎL5‹ðKú}h„sßÓUu7lÑÆgšŽtµŠvŠBQ?nh«šDI±»df¦5?nbRò%C1ÉðE¬“gtW´KpµBÎ%qGwFœªÃƒC¥)~2f‘âáós@©‰$IÚÙçoI™1tþþóƒ5F6” ïE;üq:¾ðP$as‘7e2?rC ê߸@Œâ D¤þʯ|^‰–"gcúðK”ù”´œÇÈŽ£Ókïû}ýÊûóuyÛØzèzzËëõõ´ž)E?0öü5«‡1ÐÛ7$ÞÑ¡¡kûcÞ²1šm:«ÛåµüV,¢Ø2¨÷¹îØà¬?r[3éY,m‹›¥“¥Bä—H•ÊOÇBù£4ýäÏ7†§ÓQwÂrT6›“42Ë&%µZI??Çsˆ6³¸’ê±í”Û€J­o?rCÔ±% ´´‹2õ·%F„:z–F¨dSgºepZ'yî¯î™Üš [œ?r°[=‰®žA{·Î§ @¾¬1p<”pr‚È·Og‡½à×ûõãá‡@¿3Ð]$?0¯iõé(-ç“êßê¢Ü¥WÞ®ÉbË)IðT´ì|%Üø÷V@N‹üÜT÷î¨p8ƒü)Òþ«wkãŸp‘òx>ý˜¥©??é«ò‚Aª 1Å‹…/A‹ÎOÈ^:ÁI†(ÀÕÅj"÷%Äï„ÂŒx­”d‡®_Ûw›kß\E¢Ò@¨ïË Í,´ó¿špøwó¤Y´ô‘‡@âçk†?0úTƒÊFCíQ© ðg¹Ta—ÄÎb.­f¾%ÌÚ›äyÊ?nî·I´ ȦXˆn…Œí P/Dz©$ÔÆ>ÀÛG¬¬IèI(Ub†¢ÞƒU4¦bièŠg }¨H€ÂC!Ñû‡9è4ð¥ZòËŸ÷ëþV†§Š¢9c‚¯"%Cy5L¸û´ ’ aͦ-¼4Ì#¥Î!•ô ~¤›, ‰ÔI ä1Þ:0?rÄ@°WKï‘ï”8}ÞI5†Rœž'ßÅÍ3S¸ô²d™}??qL«Îv.Gípª¦“YrªKÚñ[¿g–§†e }ʯÕ8/¦dN6­À(`3_‹Suáµld‚ìÀÍçIuKÀ£a×mãÚ§3å?0•œ4&+Î?0ýÑ9Êé­ȺN;i‹ K厶jO½"/=¥0ÙÛ‚ÙăêŒïˆ1¦`?nÞ.y¨ä2JŽß¢ëÇYß‘D’¶ úE¨P\ÓwÓæ½ó¢ûµë÷ºPøcëti#ôÈw•áéËZîú·ã„È>?0‚BB€Š€^6(Ý6'¢?r}º• ˆ¨ò%î]¾!j¯ÒKŒ°c-Y,@Þcž¦kñ®•_G]ÕbÛáå“:¹ÈéðZPÓ'i@#qbÌ5 È58(5•ÿU˜ÑKic“»Û–sCKˆ íÁ{ß ÁÔ¤9}€ÉÛ5šeS'nzjJZl»AŒ¹??êU?r\w•BÍ3Ü?0…?0"&$@Å™69‘]l.nr’O¯ó OSR¥½\ð?nÇ5¦Lö¯ü1ÄðB¶|Xç9EÀ*ÛZùJ;ÝåÈß©OIåÑ”A¬(xh|fŸÁü‹¤éŸ–å ”hž[dÞMyÐV%À'†Ä-|±¿T?r0]g['eÙ^ù@)gFàþcÕ„ì€J¦òO¯ìþÔŠQ€üâ¾¹\L¼:<¸˜óË6­ÙQ‰*WÖ‹wi—¦rST¦ÈR2Œ¯íín?0ZÅÛßh†-åÃ<Àâá{ âñäf‰ƒ l¶Fš22Ÿâ÷(¹&µ?0lwÛЖÅÔ§·D¿¬­3ª¬™°2î3…2`ýŽÆ -ˆ‡WÑUMÀ1d̼˜Å\ë-³5G6)­:tôV‰ÖŠ|ÐVÒÔ?rŒûŒ~„RžtKm¬$ðdáXž›@¶äý5‰ŠÖ®ñXnç9à2'?0ç@® ‘u3§ÃÚkñ‡ÃR Œ¿l‘;¬™0òœ”n0Ú’ñ! ' .W ˜’ÊšJ?nãþ™ï9bÊ”‡`û ¬ž¾ðÄ:úLîÖ.em KÙö…€8LÖcýÇá0Oh®½cЗΫh??¥h"ª f't9:ÌUäMˆnugz˜³|/íX>`ض[‡‚0b¶éŠÑ4 ‚9Bh*¿;”Â^1ZÑû˜{!¿T“9q½ոܧ–§ìšŸ®ªÛ!Nf7\QÔ§çvœàìË?0ÇðB‰OÄ6A2óãJ¢nÿÄ,‚ŽÅýRòóÛ~U5À'Ù :REÙq¹ª¢„‘¨Ž¾ƒ‡¼9ÆéŸ°Š'$\>»´j?nXšàs‚uÝs8èÇ—“)‚g ùT|…½Hñb¥SIé‘ê<-§ZÔâ´Sºó«@﹜ƒA??¡¹oMOöˆ©dú—ñtô:fýëv…e‚ý¿Íþ;þ;›ÿoÃÿ$þ3Ó³ü{ü73 = ãÅFÆÿŽÿÿ©ø??Ž­jü_¶ˆÆ0w,äxøü‚²PP ??$?n¬Ýë~¿ò΀ȄÁ§?rÓZúz%eŽŽ£Ëy¥§nÖ¸øŒK`ãpŸÌ+Înj² Laö¢“BW?n× ~¥îܲ#o7ŒêË¾ÏØ:n‘’åGû»£tb…¢×¸îõm—¢ŒÑó<øBQuoéٳʻè"?rÝZzÛ-"$ßõó±Ö§/ƒqk¯„rŒå?0WeFû×[r̓½FR¢ßìÝî|ž9ʳãÕgâöõk)¹©]÷6@Gu²¿i‹ùXp„öM˜È Á·ºK‹ %î•®[û5¡—"õ–+oý·C…ÄR€'-‡…AM'¹ÀðÔA=Œ£ÜL[ Yûýc_FâWjcâ‡pˆîá ÙT´xl¯xìˆ=œµ«¡DYž×,îWŒ§:eOBá%ÒÓ¿õtÙÂÙ¨?0YÚ!Õ6×ü1oº™£$‰;s~YÝm´žY”¸.f?0Iz$3È­š,rªsóöê³É4#?rŽHÇ}_Å{«zÕr3E;3H#è5¯Œñ'ƒ ±lüOÖPO—ë‘„%Ù1Þ¸K…•ôø,q†hzº¡€‡uó×ídøœ?nÉmê=%è²ÐÊ,zC¸Ltà¹äÜ(kO|PÊÂE<8©@¢åâ LvÅB?r•.nI#ÒúM®rl¼ÏTîA‹Ä#†0œÿ/.}(-L7±ùà¹â]ÁXJ3 Eù‘/èp••ùŸ4Xgóþ€¤^´.w"ßêŸê^÷¾÷Vû˜½.³ÙçãåEß¡@=°Zÿ&ºŠ¿I •½Ä@ý|ÂÚÛì„j°ÒŠÇLÙGf˜ÞoòÖÁ·#||Ò|—uölcXØò*?reoê¡WšµÉZx#(Üšüc= +#s$–¤Ç äMµ?0É9oš;s­¸ äÁ«²/"’ŦÞÈC…ßø>ò¹îŠb¢²ƒ%ìÅOþowH6*baÔªþióqá+wÓôÃ(ÌnP/N]+g\L|áG4&Á?0éb#{.èýt˼5¢–l8Ø?0ƒ½¨ÍëâÑÐH¢?0iáÃÏ}.ZÓeˆÝÏþÊè‘t"#ÏnKÒPç@[‰§Èœ¶×EiFZ²˜0š+a?rO#ŒU܃èņV¡óFø !ž—]#Ø ‹?rl¯D:˜;…á륆D+J¦&ç_¢‹_Æqæ;±Ã𨯻wÎô>ùyÑWoöýUGHWNâ?nf<;aã¸q¦Ù:ø&¬­֛Èp²€sÃÜR€Ó¸ï¢ƒ»Ä…Õ‰ÇD ¯©xnN„E°Ÿ@Xã‹"‡HºÌµ:ŒdX‰8±<ã¯þ˜¦_1úŒ x:Tͱ")7$xÝ”*Ù^dÀv£9±Š¿•Å4Èî´ÿ•'(KK??BI†Æ°ÂE|ÀV ‘.â;è´ÁXBüsÔ!Á }~?0p±n'ä‰JûÝeA«jžÕ…Ãë1 ó¤ÃÄ ža£¡8ÚÏð£ý—Aƒ!K¬ í"×ñæÅbN£ó;Ãb0õ³ùzbÌ¢±YØehähÚÚZÞÊ̬ÎÙ8?nƒF˾qmÿÖ*†tæ~?r¼þ‹wNrŠÃõ°jÓû5¬™Æé õ{P¹»`‰3I£?n}˜XÛÞp?0½£óCÌGS«­\àÚŸ†ïi)#-í™㉴4¨.ø $ ÊZÂÕ‹Tj¾§BèˆLÎqæöžxãÈWz‘ùøçñ£±¬‰Ë¸qWJý?rÝø¼w»ÇÉh1ê=‘Ž”èoÔá¥ÍaY3Ý‹ô½rooË,ŸcnÌ)Ow3­½f€+÷&»ÔìC†ÙÎ :u­ìw%ƒ%ã7ãSAw›ŠÜm>…ÅÑèbNÇ…è_Yhúy¬’ÁI MÜ9ðtÔ™NŸ?n,™aÞÂÇû{f®KµØV˜[y¤âO§€sFJB7#¡¹Cüñ™6¬??Ôà€>'°]1°1’ð.ÛäóD½·ôD4#¦Ì E:+Á[3›?ruÆ¢±¤c5n&öä_büöÎÃüÅèÁv7<š=/äµë‘\NŽr“…oy4R…+¥hv%pÏ®âÉ0?n"-àz-)±äs«p÷Ssï-ôZ¨¦äL,بí Û¬„œ¹PŸ~~eÄn†XŽî4íÿDRâõøÐ)7q;RMß4˿E¤Ûâ¾Â)–èéÚ|„¡µqσyû¸€«™œR;ñ±›ž,Ôa­PF-³$=-<aSAÎú{¼ò»Yn??­Ì!HÛ(ãÚ §¬´ñïÖltñ?0Øä¦H´Íâ—ñÃóV°S²['(5¶£4UíwÝ0çÿPì[ô]ÿp?0pC_x¿kÝÍêJ?rƒ"ÐAÏiÞÊÝríE1Î˵nϵêrêk®Ç]•J'8娣*4$ú²ª|HgíTÿ±yùk'*ÛÉ…¿f}Pša¹zíbyzyÆ1P86¾˜‚žGÕ4©1xýÕêéŠÑÐ ¼68]—?rl?0^§@BrÂei0íšì¸YļÌi­€=zÓ®}ŽM)q*`³oe×À 6¢rd-Ì‚¶¡ýoÜ23YÐb…×+¨¦½C'¯À±Å:ª†ÅäN‘yƱÜUµùÌNRê>+e³CSæq sæDä1UØgü‚`˜RnÌE–?nJð@¾JX°É_¶ÏDV,è’ê~ùz¡%S06Ñf#„po2ç·b?n~Jÿɉz£ýÂO¤³pQÓTǃb]+œã‚9TXJÊj¡ËÉàâv™U*ͳð¬®ö€.Y½6‹µGŒ2P*¯""¬x¾üL??6™¼kFÆöѱ5z¥P)gûDk?n§?00¨}¤3ø#G”çJ?r}#Ï£M)ߧؼ?n¯@,”é(›;|ÑpÍþF™0a\dÔâÇ#ª±â³”|\ù?0sËh„Ɉº|ËùpÁl+(Åfî-CÙú¶õmÇ-éÍЧ9 ßL¸‡è)%ƒª±±sSX/žeK}ÉrÖ(¥±v9‚@ïûw'£sæúèBáàßkq©Àë})ÿ¡â\v«Û9Î%ÕÅ ü3· …{ës°v]ºè•LÀÁŠº??²Êû\V‹),(v³¼„ü¢ˆSû†YZÞ׿…Ãï¤7‰¶9žNsŒŽ|éýãíЄ€ñGoE›x!i§v­JˆÅn.P…ç“3ØbÓ^Ýg<‰´>í¦K©çß}Ã;#žCX”LŽ}Ú²vp¿/äúÌC†2åQOe{0ôWÁÁº³Z›Ï@Ýê³ù¹¼T?nÆ.¿ñ¼îì@ÿ¿Ãþûþ¿ƒ%Åöù/ #ëÿ2ÿ ã¿ã??óìüï¿ã??οçù¶¨Òpu=hCýõå awoÑÅ6kBf¨µ¾í®ËÂFð¤OE4ï|ÜÞínŽ£'Ðl?0ÕüØÞ1ÔûK_aRŽ™Åëþ04«Ê¥,ew?0~ÔJ?nòSs˜±•?0Ï͸{(Pƒ¨¬@˶ºIEÊc%K2Éõðy[ÔáxsÉ+¬UÑ–òDTóµ ãá ÓFLôëWúû}wIËã)«Ûó;¿zÛý; x§Ã¸EŒn ߆??iüã?r÷pÇôޚ䔞äÚ­!z*»ê㇫ÁiF“Ô¦¡||ŸÄŠÖüЖ´ÄPY®± û·j»•Ä#BISM|¹ÕJ|XâÝR?0C8ž`8˜àPJÛ¸)¡l‡Sîî-­H˜:ù­K»>CBLY¼?0¤ß¬ôY»x¨®ž„­ªæ ¡‹HÊ=¡ÊMÄqÄ\$!™ñ­éÊÐkž.ûÞ²>O–ŽÓR7Ù¸Må¢Þ€@ßÃ52sf;™¯blç‘ÝÜ{¥jœ°ç%œE@p¾¶í~ ãþ` pûñkX<­Ç?reÂi!IÿÁFMÝÂ’ÛÁÌ?rg?0æP) —ÒEÚþqØLªÜ}O]ywÝ-C¨1Þª{¯T-‰T?n0¾<‘‰iµ??ÎÄd??„º¤g~2do0jº\©çu:ô—ˆ»BÆvÌ5ªrõ·æòm´_%éÓÔ*|Gà$^ÉŽ\‚ƒjxëµ/ƒ¨ÞïˆÁ~¿û~[e78V¿ñϸ54ÞQAú™—¤a2êPé°j§WY¹ã¥ÊÛæ8& õ'õŒöMŽIÀR[UÑzºéQº\¸ZAµ¥/r0dç–[[ž¹w˜ÃrÛAbŒ¤,ü‚ʾ¯?0E$†¡7žGJ:.T>G•ÒOŒª–§$šðÑ5üäŸ#r^Íüž!`¤l·rà??¥ê”!#œôµü˜>±> +#fXÕ?rVðEï?0Ͱ…ãQÚZ´![ö5 ]¬sA`6ˆöË-‡%ßXÝÆƒVØøwÁ`„9žïÿ\jÉmö½ºÂe¶ÔÊø\?0Ç+;Õâ_™’xð#1ÅÚXvö`©®J?r()ˆ#ÇÜ(c"–³È¦–¥R¤Y¹7›‚¾õª‹¡ø×û=ªƒˆ<¸{Æoµ¢ÙÞ/šéʰOo¡ÙÆztæÑ»´Ž?r¨C¾Š5ÚÝîÿ m=H”©Aªqp3[JÆÛO?nîêºÞ6®ŽÐzÔWd?r6½ryÜôîWÈ(˜80ä9s·?rí¼ÛOPúÅ úa,@Sß{êú~¯~Ží»€¿£íb÷—”½ÚÃ;°ÆjU¹µ ûÝ{%úƒ]ëÅÍg­|¢ã›¥‚ñzߟ£}žÝ?n‘»Œ£eÙÄGáào.ÅÉ‘˜7R™Ÿ†Í({ƒ¯ûà%TÅX+²`pT©4Üm«0’±5”8ÁÝ{ÈV¿Šhz.{ØnmŽ-z^:§¸Ï#ÏÞÛ#L˜doO]\ßèdôí8…d·+*!^ãÍPªxí ÍafëõK”µ]ùi˜ÓŒÒÄTU(ðÁH¢¬N¸è.¾Hm¦ è¦áàætzë‚¶FÛÊ‘E_N£úOæYË#iΔѥËEbõ†Û‰¿k¡ÐuˆÌ„Wåj:ìÒ´÷ÞkkÞ¦u9^Ë]Þ?0ê%Ãn¢†$ÂÞ8ˆrPlé‰ôÃRÓÓêp GÁSá°Álxš­ŽÍcÅ`®Àâ&UØÍ2©iŒcPë¿ [¨»ÄV¶?0]??•ƒmª öQ¶…&JžÿªîXU´r€oh5Mæm Mn$R¥äù».Lƒ@¢ÃaÀ½Ð j¢öMÖ¿ç‘wÜîE£2$ý•,ç…Ú2@¯åB:¡:2Iå')}Ýõy!ÐP•jBÃ:`}4‹°˜Å7©¶œõ¸ª+Úc¿ô¦­èL€U4kd¾¸Í¾å#¶ïH[eƒõÐà./ð0è´¿÷'Øð6HT(‚nZÄ…´Ú²88©:eñÒBWhcçr"*ÎpÁ±n[oÕè({Äý«œ› w+mäך μúe5»Þ°Ž-³ÒfÎåÈ+¥7³Ýlæ‰ð§ÙªãE¥IÇ1ñLX{YÁΨԧA6àˆôµ„ež.‘È`60Z_"˜}õ: 'ö%ø²á(MÅ!a>îQ· ?0²êÌØxäââõ su_νÂv„0$–-w ú*ýt”³V¢ <¼Ïîƒzl=:hÂ× `Û(Äh[Úø&¬Ðá#ñy%»¤‹ì¦¤sð=ÜVYÞ©Op(l?nÓ­[à^u%ÿ!s˜×K®áù÷± ™H?0ThY” áÈ fŒÞ~‘vS‡+ú®þ°%'ߤ©{NaËßC¦šØç,ú‰d_Lc'ÜÓù7П_ã(°|£&U™Fc+•‘CûF‡†vUZ.ˆ®ê=Õæ4Û`b-¼T’õÂÇ>‰ìVr¤oóÁÅœyÍçhjþG ?nR5Wß0–FÉÄ ë]3žeŽÁ ¤Ì™ú^Öዘõùõ©­‘m²ü‡- /—f¨í v“²ÆžŸB«±ýšÃºòëËÍ$Õ{!8d¼@fxZ—ûzµÛCáÙžY?03®ð°T¼Oü½Oy<>^˜/8Fd??E×€;£’ÜÈtëDªÑ§úîxñµ·‹LßÏ@ÓJú¤Ýb !×ò Éäì㪔 2jµtíSc+°û›œ9xkάú›þ\:À1ó*Eß?0ŒrÛ}Gm˜º×ʉûÁŠ â‘Y)›2@Ë.=Ç¥iÅsÄ9sµaæã Çøùý¬?n_JåF£â²Ûú)è©}&bÑÿRÝY~’”#|Nž9ÒG`v¢¨Op|ë…ƒcì¶{z_z?r„?0?rýMBu–@ìÝ™wsaÛe_PÕ¼÷•ÈïïžBíã¹ Ý˼s¾®ÍP>ËÐ1Oë.z§iry§éÖ·7á Ï7ÜÑWââÚS.¶ŸgÀwÂùWŠ‹??c§âO©àG?nZ‹;…\Á¥vÍ¿QW-ç À„~OOcG=Þz.9œßtiè?0üè÷n8ÃBÖ¯ÎÊ:çûŒý؇R-Ò²Ñdnö=$}/ÂAGÂg»ÓÇ£CòH—3`(½Ûó™÷¯ü=fr·LV¯Ñ_´-ýŒ½.[?r¿É.™Àñ®Ÿ–éVÁ^‡#À®¾4¦”§“²¶zÀ{Ìœ·ÜÍ?rOtülF‘°…k¥°\Y‹ÎnÔs+Ú̵n07Štus½)-ÑÙßûÞöêwù8ÖÍŽš >vÿ¶'ƒGzï?n­1FŒ9Ñ:uÊq5·gzˆ]\xfct°óuë]Ž…gƲÕ'ßËü´¯»25ðn,BÃý©ëC dJE½yç9Ÿm*T‰Ÿ<Á»6gîÃ× ÓçP¯CŽSµ¹á hª-¢ÅdOëó?0¾ÝnM¨ú>Õw²(œèq¬V¬;[Pg|9e?00Sô³ëï°GÞïV©-Q†ŒAsá¥G^~C”{Oyr‰n+Ceä—ú-[`½´ÎÒ^bfßò(žgv‘”­ü«Öv‰?r†‰)Ëw·•Ñ´U‘ì¯h8ð)¯Pè%:=2¢ÇŠPéF+aùûR››×˜åžïcÌÄÓÒ??'àÐHqMQ¹:·QÅ_1PØÈÝîä%e–Ó˜$Ÿ³ƒ@¢ Ý€¡09~v èÎ㎀D:ˆÑ›Up–‡[‘pÀGŸƒïÁä™ôö®‘ˆ;û\N¯ÉLö Ü'aºwjB8ÏCPjw®¶zd†\½ZŠÅ[ïè7zDÜ·ˆß`ïkùê¥ú13¬üˆâ×kÚ°6??·7É–_ìe'•‘RiÚ”¿éáV²@Æšw ã)€¿¡:Ž‘¨Œ± ¨ù :Šup®»âð¥0«µ€G²àFl¾Ó®³fÜKàxZö÷O…6é3dSŠŠKL QöPIúî‰x?0¹dÔ€‘)n´)Ǧ‚ͯ„?0üÙîñ>kEäIŸ‚XYKùx;ÙgÒ6|KŒS$‘Ã3kÏ ?r~Ù]6€?nM˨'µÞ’ì²;}AXq'V'Ú1Sµ¾úÍ×Ó¶cw¤ú ÆÇ·¦`Ú”?0C>Ih<ÂÚ§1›Ô]Ñô Ë»û‹y¼d ayzq;‰Š4g×+‹¶ñö“§•ns{?nfÊRñW pùÌ¢iCt´ñÓ4“XŽ ?n4ïŸ_aÀ¦9¦Ó° ÏR©4"N¨ òòqš]ª8 3vúÓß)òTüí݃S,ÒÉþýŸ°Êv)hUàIskmInDÓ6†’™˜„áÓ¹0tÒŸ-²x-ñ ºà%‰DÝ¼íŸ 2>B”øæ…êEcpÃdÛl ­Ô(oÕ%ü͵å?0OÅ(s—ý>Ï{‰•Gêö¦]ÛëV^8Z¯‰L[ÚA™Ãø†íÈt ÚV°ƒ °$K™®'¥+ƒ£›fæt¥M€»Qlãg™ÕÓ.òOã_Ä/‡»’_1“÷â7ã‡îìD¾¬_‡·§ØÞì›êY_ s½œ_¡ð·…F«—«º{ÇÔ„0<]”ãÓŽ/)ìïu§’ü÷Ó$3ï;:«§jJ‹Õ—†hwõÑ šy5ƒÝ£ÕŸÃ²ˆôÏ•°KëÄ3 ƒ0V=O¢_Ï^SǪ€Y‰^ïz-F7ŽŽœ]Æ.¤,Púé-T´Y[Fl ÆÎµÒZ«?näéBsSPþ´²1Z¤WK“ÍÌÂÜvW¡Ú«›#¹??¨"§r¦ Ù C‘ˆ¾?0?0”cXš+ÇW’žð¨#ò ˆÊƒ6þ†ÆèЃ|t>£n¿Q‡³b›V¢Ð$Z…WÇÜ‹½=lÔ8ô½*Ê€T ŒD(Ó „Gn9mŽük'QB?0DGº¡ßU_›ì«–¥òc™Ìù ¯nj«ktロéVYu@Éf73R-ÖdËXf;DWšÉ…¦œ~² ˜‘gµ9Ã\Ø­À©K°ñ#LÓ?rFÎÍ>-%iòœ2õ´uØdá²ks.À*ó¾Í§º04·ÖÔ!qÖ‡/”epìP¹›ˆ¦A?04µž.$>§…¿ñìýÀôtµ )§ˆÄe~#%ÞИ Åb$NšPÍM +ª4³5VÓ¤Õb!)’Ûüj7tv½%Ehš/M·+’€‡õŠøÎÑÛ­.œt$œ2C˜m¸À˜¬¿ -)¬$ûb¯ŸósÙ÷ª«ê°:„ãù‘^£/:=»-œ„fêåŠûbd·ëøZçó"JRz‚~¸ÁR¸ùá$\ ›£d\jbj ZðÓJu)ŒJÒ~sѹ' ÔÚCj¶ÛïÁa¯¸ÞùfWPC*$,6üí"3C¢fµåÌF;R+iªZ0YæHˆGOщc—1cY±Æ ®èt‘Å~!úiXóëv€q2bê¡Kc8]þQ?n¿¾I6Yn9ë~¨c‡%êš[l;ãñÙïcç%äëÙŸ™©, I T͵»ÙÙÙ‰µ‹³xå?0§Jý?n!z›e(Å6nl›»€šqs l/¬Ê2mEˆYN¿h ‘²/ÝEØÄ¶Í5q`AZ盬ŸÑÔ‹êä2ÇÖÑ<ÐE¸2fÌ+Ajf&¿†žªk~+£ùÎ+¤\lÌüO"Ӄ÷±Àï'ÜðÞ™ƒe?r“dá@k˜¢)¢ý|²ÕGU ¾3ÊõŠã^7Å7¯¢G•ò.X5`.#ìIfž¤im)´»uÒ6:V¼£ì'lC6ç§™Ø.Afò™'eIñ‚_l‘)ç™{$]£%™'î1“C<ò‘Q§TÄVhþ,§©¥üQt2âqº£–;.Ü«Ä$ûÅ>i¨?r¡æQžwÛ|œNÙèXuM˜üP–›Ð\$?0¹ Jù÷Õ·`—ÆiP fª?0æœZÁh—œøäˆz$ªSH•Ì”7]Ò¯Åáä÷º9,gf¶Ü©?nv<ÓgÅ™ ÍÙ*9OuBì~ˆŠÉÈþ`úÅÜçf¶y6Žå??\a|»: ý®ç¢y ÛYíà£t~4¢ï¡ž¤}Xá"…q[gå5œfµ¡nŠX1\QÔO-|I±?nÁY6£¼Zþ9ä1Ç1"T«WL»Ü¯§Jr ¦îºwC©O?nÅxB«‘œ=N[ %"„“ ÑèuËÈQÀô¾ÂÓ[‚H]ÚÍ$ë_L”©°öýú£æ*¶ÁØç±V.9ûÄЮeÁPeŸPH­Ü]°s¦¾@=^(TL{°åÄ6GÁþlÔk: ·Z¥h±½x6íÚ.SA/„’¡„]=¨Ì4]wŒÏhšók'Ø |·å¥,ØÙ?0l£ãÏ‘ +#IK[:´ÁßK\lkêO“|Ša×<ˆ¬¿òwžTºJf4ç̆ŠíK¿ðÒÇË?? e—ò}is±ADþ]Ù¿=È£} Žª–’—Ö-}ϯ¢aÅ%žÀ®“ ŠlÁâ0N[døËâîÜÌÂóðc³ófåíôk8éváûmj&ÌÛºyý’¢Û°ð@¼©Ð4Õè¢Ú¡Œa‚1éÝùKYfíÚPMªuïœ#k4ð66÷%-FÚ ãØ<1wã&ï©ÓÐçí‚ãóÛÁ4 "µ'íÝÄAûvd2é¸ÕÞ•ÁÚ¼Ygn]!m×k¦ÞúoôðI 2`q1üZvŸºB{n·¡£„`Rð\Iö$ š”Õn=pšQÇ®]mód¨€–&˜jQVrJª²EžÑó1~3œê§ÏOïVF,ÈìÐZÜÀcŸ8OÕùƒÓG©*ø©±¢<صrRÕ֚͊¹Š\a–=tÊUzeÇ`ˆ`h­Ï×ý§ª4ÉŠõÛÙâ`lµ‹‡‡²PääÙ“bQcHécnè$]¤,ïKƒ”]{ž]YË$Ì¿.xÁÒªÇÚfz^æL½ÂõuU~³nÈ ÷Ž-¶Þêß?rçWëÞSˆ¬z¹V²?r— fFû´L¡Ì›Ùo‚„ÊrWu-!oõŸ‰µ}ô^oîȮ72›æûÀ??¯Wþ€N……T—pÙ‰œzœ÷~ë[®½u–Öß>¥Z}®妦3^§æWÞȈ+/ƒª¿€U7¨gŸÚk¨Sed_æm"•@3Ћ嚧•6þ€¤û³&~Q#çj(,´ˆ(×úIëév·„2eE¯öGL?nâÑ5¬[Ô S%4 UOÈ¡µ??ÓŠGP22´\øhQ„'°høÔ,¥£Ñ›3àÏðG§F³Å BXYV$Í®W*#¦8ÅÐg£¥{:¢b¹4:Y­ª%gÏ[Œy½²8&¾›tÌbOMÈ´æïM›WC>^ö|3J…>¼äz5*7ÂZ,h ÈY/‘z¿¯´_ÀTsz¿_­¸y¿{ÌÒO÷¼ãDpTñ25uÃÄ_U.¾1ÈÛ1ðk͘'=WâÓÃ^eIg„çÕsHJ;^©þhm×À9Ý8Û)0« =V^"‚ú(Ëg…†çÌxZNϱ>K­Ïg¦JbòUÕa«ù6bÀ]:~O¤’Ê^tFLbíö|]]Úö5[Ý =?r“‚ôl¼²öù6ðb4—zœ%”íÐlç½§âæiŽkBãE«[ìºÈ¶l[açt¢ª _Þ7ì_¤™Ý¤{`ÕÃ?n…wG¾®Ñh…\Ö©ê>ÁŠN¦ØRüö¬¡°ÇäK/cV@3âÐ È7ÒÝ…{F¶Ÿ'†‚ôWkþ¯K©‹óŸQH-éó_4'Ã@–Sðfîe9ŒxŠm!„ûE{r=d+Û¤çÖýÅ`Ï+B}‰vžèÍÂ)i©u>SnBGG/c_ƒD>·ÊY·d’ºÝ!‘ËòŸ‚ùÜeÅÞðÇ€Å;àyØÈ¤Xm§ÈÄ«ý®À#áËÙž¬ÖP-ÊÌʆàò¸$eH[æ@8;œ1î‘£îãŸ#Ë'*-:Ñuœ¤­µÑ<{•_aÆá¬wø6Ñrá 0Æ?rÀ]psƒ\>Î`Á#ÿû­ÉŸÚniÔÛ³ï[IGçk֥ʺ±”V’ügÎ?r׺JÖŠzŠAO[×-|pA™Ö!¤çKqë…ÚHá’Çœ¢=%T(j;æõ½/‡ö¨µä^ÙÕ×ïD–·0}^í}úž`šƒü9R¨ò¨ÌÖ Ðgz†T³ŒaB-auý¥d˜p]Â<êš&†ÈÇ.h)ö¨¶ïMtwÿN¥F"j’€ŒÎGkïúfå LÿO‹¿eËAZÛ ö©„¨@Â0'u£?n…„êW7 r 3´J•ûFILiTž:}ÝÜ?ní??ÔF%¶àѦ×ä-@BWï,½mZ•U…¦ß~'4MÚíÀa5Q Ìtu¿•›Î6pÚo´I2KÌ?n#b–‘©Ž˜Rñg€Š_Ëvä¥ýq32ƒ„†å…ÛruZ¼ÈoîÖãÉÌÚ{_ÒÚ`Gö8µ]îu°Û‹¨˜hWo¬"ñ2}þ€õÊúi/<âSç¬PI³B¿ðÕoÞ·ƒƒ´k¨Ñ­O•ÌRÔYeâ´Ùˆ›W3NýÖeîné” ItÛ·›ùªÑ¢b‡ö|Ñ䇱i±7›¹7,’l$"´KGEÌ6<ÇäæX4ú8,]sÈà]à¡g÷¼å^ÄZ˜âÏÓ1“.JÍ0 Íª£xœcF–rXœ‘­Ç@à$LTNõ;ÄŠÑo¼kúe¼0Ü•O(bìïeJ`¥+ÖSYJé{±žý 5ì&CÔjœÔ+¨fÑ'3.ŠîœKsKÕ]lu¦¸*À&wfOõtWÁͶÒÿbK%|ZUíb˜Ë~+Ó§8(³N‹Ë&­v¯™øynz“Wu+hµ`´;:_xa¢Íô09Q8%k ã?r¡¿=”ì[›n“¼!Í¢%Õ’£ˆ( E¨Ùb??ŽôDÄ[Q´ ªW_t@lAvï0‚A¡n½¦)½¼wÕW˜:J5¹ÝÜf|5˜e Ì¢»Òu^ª¿:×y—¬ŠV'´U|Z”¡ö×Db¦iž;+ík—Õ˜+èû²úް rl‚ÆÕ´ìA%óâçù'³v¦êLôJüä\f/ÿýÅû+Ê¡Ú|Kƒ;úŽf??Öé9?rÚúêæ\Ä€™oajÝ##_Ù>Ó‚C[ý=V«"ð"çJæ ÔûÅ«ì¯OÑŒºè0I" ÿ÷üä!"´„ý•?r2¡©n§Sû éë+’9)ªA\“ºÀ¨NS‹â@$å‹Ki¨ŽÛ'ÂsÚÏ}DOÒ€ÉȲÖÙ‰Þñ¡l‹ô>¾²?rœUç =ýqKj•C›Ÿ3ØtŠúi§)©ô=‹ý)èfu[_jc¿IayìlãÁÐþÜî­$ÿ¯?0\X¯êS#&µño^C³Ô<„Û¯Q]WàâSëíL5*Ÿs6j?n,-T;ÓàÛ‡‘$í·BpQ¿$bQÖ­?n¯(ÂË£¥çá¸õGÜök¥c{䯆œ»¥±@…?0Lª‰ÿ7pÀ”ÝynÍ£ŠÕã;Ÿ›ÞÙš`Ž˜BeŸ(’Í^ª(•¨'gÝçQ þ™±Ø\0J»Ic@ £ÏŸîÜ)ÊŠO6ßî¦viWºp†Ù¸;mª À.8µo©Ô¥¦L:ºš¸ðÔŒv¿ØÜu5·þV9Ó?0à<]1s0ÊȺ}âCrló(º¢ÇiÁ¯sžíó›ƒÈjÚŸËqÔçvZ6Ád=Y÷ý•6¹1•°ü|d EìcàS˜û.óɰښ´=¸Y–äÈ5ÿzŽ\ÐÞ…ð^ ÞýNN™Ò¼ÒAííÑ(ÕåXÿæþdúý0 ?0Ôw‰eLúZÅcÜR÷ýöî +# $ÖERç>tOøåµõÁ -»'…½ðÌÌcËÇ‹dtF=Vâ_0=6MÒE;ÿXµë{¼Ô??çè–ÂÌ?rãcÅlÑoK~j?0üã\²úñÉW*íé5ï!©¾‚-ŒjÅNÃÇyÖ½c·¦FL~+?r<TWaáÃ]#“ʈ6_æÞцøàþ2Ó9 æðŸT Sä3:l‡¤ÀÅÊMeÒ…COTê)A»@´)Ü!p.g­´mž/R—°¸Õ#ñ©b–¤ùH”¢“ªU'M³™Å&+&,ŸWëR|À&½¤2ÐÛñÁ¹ÖëÄ‹kŸ*J^á¼È¹EÚ€(š2)ô&×îðÖñs´@¡#"L¥Ã8QDZ æè˧ ©#.$µñ¬ÆÅ´v@%]ÆÑò•èópÁªû{wl«óƒ ó¢Å.j7à?r§œgÒž¥ðüª$·æ‰Ú ôçñ2ºX}™ñ@ž´¸IF?niZWt›E#”i&TÞé㦢±b„—Â[äjãá:­áygAˆ®ŒSËi®3³³“²•˜dXÙ©,œA®y j>†\VÒôÂH{Œµ:eÐÿσBah9XRÉgì,´HP^H8×ø{"rÌ?0{^ÓWnB<ÆôòMÕ<ª—jòqW4-Â{¼S€•=š ‚¶=??âé+z£(Ó4.k° Œ†!ùõBwÓÞÝúþâ¡iâ¡©Øïfàx|?0¸0âŇžP§–V"/”Î\ïétl,÷ eóãÝ¡>ã ÃNãèËàêÐÖH©‰’¬Ã©|C+ÄEC´Sßk^coUàf‰Eµ†T^Nã~Œ!÷8Ìg´PÊH3 •ášðãÚ‰-æ;B{ †.ùŽyÁÒˆ ¾ÆÀP¾{© œc¼…+Ž ÜˆŒÒ¿&X†kçGp·vÂè&ü>W—›d³ƒ“føŒ3Ñ œ+áyGžµÃ¡ˆ‰çš©7•ù¯´k@pÛs>«™Ÿ¢·CÀâן_&»°›Ç¹.¬‹ªÀ9O3îï:‚}ì½^¯PüÖ-1•q ÚC“lzjë‘»bTfÆwµÖf©ÜHÅü7eAû‘6 K;Ò- V£`Öë…swEÜX~p2ØŸÙÌróƒÉ¯ø·ñ!o3ž«q 3ò$<˜üäÇ…hª)ÆOw]¡°Dl7­ÜBV@H¤^Ëí_>E=O·`ùiBewh4½ÌGÎ 6› >CO€äæ[øï)‘ÅK2"©,&3{&  ùȨ(#:¸pje`"ty™@$ L‰ªÈøÌ(à¾Ç??”??³ëtK¤™s+!ô6ÖõsÁ@š»B¨´»JvºÜ|OÛdLãd„Êjm©,;Úµñ…dý®ãdu+)²ECò$i˜PHWG6ÿ*«ê—ñëMÿK`*8—Æo¬&ÒQwþ¦0Ù+aö}È Z·ã‰]P·=:?rÅl~3‹Ù•)Q£WôCUÉ ]áaÎáGÁñÚª/°-Ýå/è&½pÙQG£Š'à¶ÎòÂÝ©<†ÏÔv±›éœUd©~Ü\èÞ"h·:rY`"I]ïø}1yRøt¡¼ï*Þ?rÇ€Æãæ¸ÃM@Ëšò>gmÇ_­ÃŠ~¤ÙgóžÃù^d¿F‘1`/Õ??D»¡ n6ò;bÍû ôÂÛ¦tƺï´Ñ+S)†–‡Öu“¿…‹)‰Æº=±©”½¯þ5Í|þ(@³|Ž@¼f¥€ŸYSñ\Ùäî3`Bínž:,a×¾¤«Y¾ìähe¶)LÔt9G#”ƒ†’ôBdÓKd¢ØuBReŸÿK¶ùâ~¨  ?nOç8Àl=wRy;puÅgL骢 3ô"¼ù§„#¡ER5“™Vÿd–Ý-u&IÍAaüë›rÞÖA %A²m%‰?0ä2¸Z„6}!‡ðê ¹øÚŒð˺15Ãõay9DôâãÄîýBø{=ù?r|á‹„O??,Ao]ÍYsÿîr˜Xݽ“—± •" ’tr°J±†Z··¸C#…r}D-‚X‡È|O>•%dÚ¶žÞ[èqbÈ C#CMÆU$àþG??2ôÐ)¶l&oN»?0v=zÈã{’oõp–ÓŸ½·£¸ëC§¯¤3æ¡öSt©°k¹ªÕ²ÉT>­bA#Ùª‰!r –=žðÔöˆ1ZàªÝ'M¹?rBÞÐÙîé!E$ã!5)å립˜AŸÝÛK9ƒEV`}å¾³©Ò=Ÿ;©¨”°øOÂw…ª*pY †Êž Œšû%¥(h[é¶¼ˆØ×K?0MtÅY72VjFUƒ[ëÓ¿¨2f‡Ï¢ ¡…?n²Ô?rö8WcµG?n”'dÒi£ª^õU÷~ÁqiKO<*2ÑEuî1¹D†\Þ2; gó=§ÝLîŒÕ/”½™/øu顈|°œ¹ÂAû_–NÙù/ý3Ï;%Wܺ»ÖY=›e=„4¸|ZlaÐÑk£Ý%Í]TŸn¶ž(ÂMè¶'Ò×Mè+ø®Ð{ Sh£1øAäÑxÈ—hâ'™3s3©ð\<µL«aA£?räµÍì…§Ù78¸7ý™¡,ˆ4H—æº,ÏþXÄ{J+!éAZº¦fì{=Nýn•ÿA|~xr?nOûÕÖ•pîü£°É ¨]*³ù$ºŸoRd¯å6 bƒŸ’±An[Hàgi~¦²«–än&½™åKm¯ÏpX(L£ñj2,˜ÙP™fÛÓ±@QýÓ)m \ð‰é??кªt2À)8ÃŽõA²«v¤¸hä(HZе±J¶$CÐTQï0(¶Å*,è^â‡ì?nÕî]>_¾ Í?0°ì ]5YYBjœ¾œö†ÁY[?0ÊæM8Fé⸆ô­S•þš%‹7Ý­??›œZÉ4?r†owö@¶¼ÏV?n´àå§y#š­&_¼Ìz}•Dæ¬ö5q³zÉË|,·ð±Nžà õ±Õ5+ÌˇŒæ¡WÝ¢wÁ%¢UòF]¾|~Ÿi€¼Úÿx1~š;ÿ ÆO1[FÅy¡¾?nÝzÁ®lSšõ×X.»¦ØÎwÂöá/„ÁNN‘TE½– ·¤£®Ð_'K:x;ÕK1¨Ð .ÝÄQ…8ͼӹ:@Gz,&„<.$¬šB³éy)`?rq›û³ŸîÀ.t[±"‹IÏzî¡«ŠÖ>ÄF¡È*Fê˜?0´Bʤ»˜ç/aï`WDŠðo÷Æ 3åÉ;(JÉi ÛŽGTõ*è4[¼W[3À)JÁ~úM5ÁV]•‘-2e}®çþ’ÇÏA@EQI(ºƒ¸fçM²›‡(F?n=ð…zšZ¦ùND55´GÖ‘>"(ê_Ú£?nBYOä¬Ú ‘jâm©ÃËKºŠâJ}"qºNðlBßòmÉJüfªåp4ÄÑg1g%ñM <ôu;?0Ћó¼âIh^aí"½¯‘ùµð×ÀØ?0A­áDòã–Ê.d)ؘsèíb÷“ED¼•§žŒˆv–pzŠÖäŠ5U!Aß#1¥J\÷¡3îšÒ&ªô¹eq/åæ³¨%^¤[µQáGò9·bŒà Ýë¬{a¬Êûò¸k'ͽ^Ž{‹˜³…z»×šìP•Ò ’¦ ¸?rNè8¼¼Î¡·¥öZ¹#À•3Ágðd;ßç–q ¦Cz1ž¨uÛÌÛHoS<ØC6J?nº:[n©Õ¢ø«Á”µ’Ý›«5‹ñÒs¾Šg??Í=F€ÖJ°%e„‚>Û@œRóÎ%µY¦3ÛÄEÂeâfÄà–!ôæÑM[ˆDÉÒÔë=ö@¸,Ôä( ­ Ú,ÀaÜ å©Ç2aaŸh$Ô·K¹ryº~,:>CEGñèFj³¦j)xêÔ{®ÉyOp 倳@SSðQ_®q‡žà³<\™ØÓ„c…’[B…‰ìh»8PaùævZãSÛ#þgyádárn|¹0ÀN™Æy8ùqÇ{ÙNÎÈ¥ŽSG—Kg_bsÒ C?nRA— D??CKf µoÖÈCì-%q𜠿XOäÐ1Hôu@0Ú•gˆ§nZµòÖ³ž9$WÕÿˆø|\<ÖöŠ‘Êî³]â3 qRDv…×öL·ìžlÚFB×~¶£HZ}xøãÒ—­œæOÚ?r6™È~m;ˆþ½$!P)m•OY`z®œ?rÑÎônhí135½WQÜ~‘ÑñgR;ÒA¤Çß`³÷zzÊCÞøß|u?0nìEX'Ò¾£­—…É”¤ÁUng“׺"á?nɥἤ á??²§ŒØ‘b™µ³”ë¥ÑÎÔd:gó4|øØêÄ^˜u&-Bô“«QŸ–"&Ý?0qW?n*2@­‹Ö†èYRM¥²3Ñ2æ=ˆÝHºD ùhéº|­—sl‡ƒ•¹Í\…ôUä$•×þcì"+zo±¦'{öâ&¼yPv~N¯û¶†Ú/ò1Ç2裌oóÂËŽÛrm `ÝÇ%–nœ3W¾ö1á6ß"½²M™Â¹ŸçÒâ÷òy²í•??è½Öb>HçY€†2=Ý®L±ž`)cΖÉ͉4N|Å"<61»¡Å\Q#E[î$š ½N«Y1GöN1"ÑqA±ª©plê²M™gVîýq_­‘†•¸<²bÜ4Ÿ›?0¤Öƒ=—$iÑkQ6­qF…õ^ÁøB‰8ü3M¸ñw·€Þ,RÖom뺸?n› Ü .¯¡†ƒâ’Ù€ Þ†Agª¬}µ}Ñ7“+(@,«ÍZ©ÓÜ!¼v%;³g²®x8p¤jëä”?r•¾\îµþ6àUò(¿',áG®Õ©Ã¤gÞXK??8Cßœ¦®[¼š „Tft4É ¿–s<†*6,ÑnG?03§?rþî]Àê _4¿Èþi{|ŽF„ÐL!“SF-ü@0?n|ûñPÆÅ]Zh:üÁ³¥ò¢£ÞüÕ•™em¼Ÿ7±ôœÕpƒy•TVÔDþ%bž–ë ú<Í“v>d`gêAè,ÏÛ±>èVn©½„€‰ˆI;K]†ØnqÍÞM†š7ezC.L „8.xR¿¢hàðF>ŒAû§ »M¯´YȬ~?0ð–}EžŠª.ó.ßïyº”^„~è”æ@t?rœL†œ]_#¥Y×)@.<—Ä5JʇäåãMšIh3Ý?0=á(S>I²{[†!D´_7¡×꫃YCšˆm?rD@mœW??ÚŽïÌ}ó—h±·óùýÇÞ6M˜JR: O/Š%^Ñ‚•¦ÉV+W &X¬X”ì]þ.··ºÖÂP0ÆQgG2;& ]B??FP)RšMÀò@³0ø*{"sVà‡ÊV'Ž’Jµ•9Vx'ÝôŽÇ@áž|“~_¿ ÿÀLW#…—5¼LYFM!•¨ES9›Å’K"Ì*OË«(¡_ŽѦÕs™b¥ Ó¤UwÌÆûŠÛé“¡cVá©%Ú¤”yžÕ8G£ôh:è1‡<²±úE¯DÞ.£(~O{µú²ÛÌŒúI5aèÜÑô.ïoÄü_m?r½(ÓÊ3I½8v¾~ÊhOÏ»1îµ]fýÈwä(“èäÕù’mâHL½.ÈSóM¬ºA‘™YU²ª úš7c¦Aà‹ÀÔZ"ÅeñTÂD„1aßÌ4JÂí–If¢RÎ0??_qÌðôÞÅ­Í«›S娇²¹ñâ“ØÃA,ùÞÏ žÆ­y¤YÎæqoÀo±@˜Ì†¡­«¼·pÖ¡ÙëÌ??Æw´¸ÙÇ…¶(Áüz.ÚfoéÊδK9ä1ñrOr#äsËèå?rß9¸V.Z‚YÎzßȆØ5Óß³Èg¶î=V¡P¾ó›aó0—&Ÿ.…ÇPS‚gƒ„ÀÜýÕ§rfЛ …??njþj[·ë*VÚ·oâ/ë«Éõá??¥â¹”2’qÐ?noí]©ÔO£ÑJ§±š»Þ'S`z^–hÜ¡M'£).äúñ?noЕA4¶—êÇä}ð‚¸êk¹—éz??ŸÓXÇBÒÂñc‡Â´P[BÜl#V¨¥ç8+r_TÚ}8ÚP‰¹zóWŠÞj§íFAf…áåaJvÁ;o’v³Ò•‚p??ñ|¬¹:ËoºŠùæl×F:}MÄÝjïË›uÍ,¸'–Ä_發+½9%´­«q}ye„g”®r¸&,¸ÜÁ®0¬rÜ0ã™"€ŸÍc Ð —Ðâ¶¥Ÿw”ÉlM£«>mhè ?nÃÈKÑOÖůHËÄÕˆömXw7—ì]ÍftëídqAçzá†,ýI±ª?nÆ‹¯é4¯bUΛcuòŽýtŸÊˆAƒèšbmèL‹rh KÁÁ.„âOè]yR4E]‚ûu^•½þ³ß{·çX’â7zU³õÿ'VvGs¡™sçzÞf*„÷QòÚ6Œf‘q.ûso„#¤§ZÐeæyÓO$aÚÀU•2„°ÓA© 6u¨/Í>ÆäÓZºnšmÿOÆNe>3›äað6€À-͵Neí.Üö¤W£+{4G¡‚j!¶¼Q!æ6k>ŽU‚²輵Х•†^6’Èš¼~~0‹Ì¦ÛŒ†3,A+¢[Èøèa=Éz¶Á;èU³B~ª§ev.»˜Ýo•|oòuïsBO?nGc,èGôwß'ÁÕ·§çŒÏËók¿ðÝkdŸ–[o”9ï~ ûßF«eߨILøØ°jÅ/¼Îfƒ‰)“Á/y•ÅþénöYzgKcýÑRâµy®WL&÷sO[¬"ŸeÚ`h»2 ñDîü?0õ´l*dËgÏ–,÷¬=°±Àvä=!•@wz—n¯F%¦3Œw]«LâdŽMF»Œ*ß»îQBÛ"Søšs<æI½ÄXÖi+£á qΫNÝò-šÛe©x»èÈÜ$“,ëär6ú¨ø¤„Œµ¦PeYXV|¡¿ÚŒ‘aµ\ÇÕ”#Þ+] ¶$¦;`¿ €_4ÊäΠ?n#è>«ßùOK¡Z9öl‰F÷Â#”úõCÈÌÒÃÉ ƒÏÁÙŠË„ÿ;=šˆ·ÅæùQŠä-B‚ 5 v¬ï}µUEGM–n××äíÄx?r;ú·@Ùi½ Ájý&Õ¢U~£ S)8±ïäz(æï]xåkïÇÓ²êõ«3ó]ïôÄEÓ¼Âbe7i˜úuˆx`w uƒ}f°À4{ŽúU­É.“ÇNy¡ì#QÓo+½ÇϯˆìG%ÅöÊu˜³›á’d»÷ɨ¸˜DÆ7IöÝ`ëÒÍé’[î˧ñ€Ýö|’ƒ`ßµ=_OÐÖîRRÛY¢úê°±tYR\¬sîê͔ޛúhjÐÆªê®&`â?0ùî!+I¸­r§pÕ ¸Ê¢£ìÄ‘a!âMð~?0eŒÝ"7-ws<¯sÊMøÐ®E‚v¯5¡ÑÇgB‰ K_‚•Dó(<Ü€…!qLœÅ«Ù¨‡­çÓT5u£Ì=??Þ] ‰½¥ ‡ ”A.J½KEnÎP[Æ´1’€Ô“„6!WG‰çªØ’eü>Þîê®æ®Ú…–&Ow¹½ïÛi¶ûô™D×Ò¸—H;ݲxË­çB޲&»Xm©ô¤ií‚?n龜ŸÐXÌÑ;¦ò[=ƒcp:H1êm|v¥žE<­ãXB»‹[×'zgS¶>²lÜ×<=¼M,÷d­r¦mªA©VóN­CE£ŠŒ8Ô‡¬ó¦âîî…˜Õ;ÃròàØóø4Úx"J>(hí2÷x•eùx9nùæ?0¶Æ ¼Mdý´?0SL»{¯­ñ˜o’çÄ8 µ|L¥‹)Sû‘3M€{µNmß[¶éƘN’/Æ.A¬gã·ó¨ݤ aêe`ˆ~V€šUÁÉÙ•qs˜q·DÔgåÖ­Áu[e)×;tÁhY2•´žÎªMäè?ng›‡]Sr0-¨ Ï8ë3Õ²¶«2I+û¾^°D×]–­ ÝÍȉˆ²•¡£¦‘ -­ýúçîkàð×8Ìõkæ+`wZ•òÝ@ÍA4D¹yѽ€z‹™ãµŠÛ iuM›Íø)!‰Kù&Ë"Ã%§öre??œfȰ&Ë#·ú‰,ÞÔwcBYMƒùëÒŽÞ©°ˆùý3¸ø+V$0¦pn¸»t;švà¾dQh€(=üÛŒò*Æt(‡j#\ «DHUõ´áðMÀêkö‚Q³êB¢t’”ï‹ô³]G˜Ê‰´óöçM?nƒLÊULô7§®(ôc ËÙpñl£”ðíºH .JÁ¿FðÒñ&ûÿ¾³8PºVe K© f@wAûsÁ Çm„ÿs¨tInD?n¡9)ÙS¼€Q*¶±w -/‡Ã]Ã/zì¢ÆM\y\ágFæ|“k<íàÆ—ßbaêvŒVXr½_%]R„ýyõû…g7_°.̓táꌫ3U3ãÙÀe‚¯¸?nö7@Nb›Ó6¿£³¨rõà¦$ð§tpl%&SÃ÷_HÙ¥|â}jQIPïwÛòÙ]vB¨­Î/+¾ Lù>¹±Y뇥gü#…˜ÜÃke@§E??4÷ëa:ªý÷or?0Ç9>??Ú±%. r†i«Í°Îš;’µý²H[OÀÞåÛ—?rbQ—›nžÁr#}í²?r.ÓŽÁ"”é‹DîqXvñÓjZÂãµ6ÿgšœv€bÉJ|PŰÀÞ_œ[ L'“ £¤ça:ÃmGÃå¶ÿÍÌVó¥K¡ÅSþzA®ÍâݸbtíjGZ'‘ù?nÇú/€0ž«#“j8è-oœxávB@¿C«,hn“;2Þ˜ÔE??ßÖŸxÁ°:kê½DÛDý‹%\™^e]R>ÔûºN¸IË,×?nøðˆµ{cȬÃݵ©çû௎)€àÛ4XÍûö äÈO{§ä?n4#`”)^ÞË€“‹ÐM±×ÆVµõ£×ÜÐíÜKeU¤=§°^OSsG”pÖ¹'g—‘%k3¼¡nZß·ýc¸nWª«|}ûÆ“ù¯¶xÚyîØkýd¢Uø³fpİӡúvˆQrÁþûâK,®“Ò#W^/ÁÍ]â`’¼©9YÈBf~¶ѲñP»újèH3W”Ý!Pe[Jsz¬ªBb-”Wcç—÷î{Q!O"ƒK;«)ïI´R\&(0ÿ:åóž•úÍl67¯¼,Ä[=y¼ðÍÝÂ\’K›PTñÌzUìC„èÁdoNn¤SaÜ aø‡{‰$aq`Ä\o·G+žÊÃéEiVWUwQÄ*Uù4Ñ«rxûø•g GõÑ ¥ ÊþÆÏ½iA3ÔLç#þDa^QGtÐ??éûP6/¯áþèľiØ|7c÷¹J²ölNp'Dy ®†û¦uÜgûH¼-ôRÌYð`Ì+8_ò­AŽGÂr7·u›¬Ç®3Qžñó:7…á”sÝxš(¹½>`\‹¤°ŸÃ$‚Âÿ’]}ê¥Ù¥?r¤â½²Î¹"1ÜeõuÎU©9wxê=—‘Žï3…Z)»×Öm`¥™0' 쯮‹ÜN0Û5ÆÚÀ‚Š/¾ùM܈+Ry3$Áæ õÚ¾@¶©ì$5‘¦{ ƒÿZÍðMþÛëß=ÅÌ?0{ñþ£>J7AOMXò$Û$ô®¥ qÆU/ÓÓÐâÕ”¦ ‘!JmQԣǴñ@/lÿ/sõ¨¸”rvˆíãvnÁ DŠ!?r@M_G”„QõÖó–ðßU/¥Ù å??l‡ÀésÀOÐ…Q&*ŸHòK/±/ÎèÍ¢;¢‹…-”â Í'_&6^ÁÃ~ §gË5݉ŧ,µÚw¹??“¿Gº0u:O?0oº$‹ñg²¾vä‹ÙÀN•ëÐFÝÕ ämU†ûØÚÓ¹îfW〓¬üô@¢Roa 2bìñPì÷ÏÁ?niûñæ3½ÚßHØMŸòÊÈY8#ÐóÝ`d¬! xÒ]–dùØk­Œ=?ràšìs;®7Y¥ï†ì®&˜à\ ÜÝL•„ȬKå[[aÄ9žGRû…”vÉ4ÉågÏÁÑ~^=Ó•(Æ=Ò .,•½­¾Û;]ç6°Ÿ]??ÄÔììÌ’¹˜AsÒÇo‚ßß»B^téyQׄZµÏR¦ïmŸõûÕîK;Ojv£B¸mž¬‰,­\«®Õò*{ëÃOŒV7?0®õv‘¸KaJˆ%wë¶UcÕG'kö«:.0ÈrWì£??ãE)ÌzèÙnâÝÇnLc#ÈŠ[f´kÏíøŸAö“ãWMܶ”¿@ñÀ=ñþ\¬¬w oàÐUIµk™Ëí&s…<M-q/XØ??¶Ênjnî/ß· iá]°ÙSªÇ½¦³prþL 0t“&[Ô…"¤’ëàÀL2¢g(*1G› $áhÝQÉl÷pàro??/QõH-üA9è2\åÁöDCSí¢—LÜ1ÓôïÝò ÇH¬¬t‰Ü\‰çühú<å7—È2L¸òÒ»¢£/€™¾FºÄÒCíž¶½SŽŸòëߎD´þó”Uí¹øl;™£àoäŸõ¦2v#)õz\ü¹@0ׯ­ÖŽæ³K‡¼‰r£¼´÷Êâý*'‹Ól/³Žý×ú±/×i&ÅþúÚŽ»£'_š-¢iŽmˆ{MXN3‹¯'×)Éž…ªQ·½sÊTŒy¬ÞÉн¬uÐØ¹Gp¤â[dœ7ÊrI›+Ä™‘£ë͉OŽŒ½0¬N€Ç™ÆßÚÝWÉõà(°µ½Š„É'ꚥ€|º7ÍBü^>;]ï ÚðMŘ"Á¿²WévªõUºˆÒ¥nÅÌå߀éÚø$Ä‹ 7¾¼rvªX…›Ü‚Íà€+ Þ:z¥ÄAøœ t u|ù•ÙrÐoÁ=ûÔ€ üþô>ß??¶éïÚKô?0ƒFµåñ“ŒM©/²£ÕñFxH’7S™ð-]®%±°œõJ¢Pt¿‡óÌ1E‰O”ò–Âlk«7¢äšS5ª.»žàvLC6?nïd +öggÿ”-zO·™ŽN±Ñ H¸½‰Líï~ÁOµeÙß|dHjÃÚj0Òh™F§ÁõtÔ):J°v|7liŽtá€dÔ’fþÑî^Íýi-L>ɳoSûsu{?r4%‡È‰[ô±Õ.iZwbVánrF\ë{Jü:;hœ6 ØLíö®Yö°ÄÛ_±ÅÏÄ òÄ/tIÈVhٻЭ³èØï†ëõÔAFƒÏUQº0HÊq½å´sc“ŒBþF‹hV§[C¸Vø0{ãû‡vž2Ð'knhg_?r€—:‚*Çj‚,âgl¯­ªÔp"å†BŒ0€/5úþÏÞL_ãôôF×.“=©¾“aÚV"-|1+ÎвIrØTܱYáëþBçnõ§?ry¹¹WÞšÑÆÚ.÷]–ƒ²j½!ùBÿ)„^ÚùÇ'•vhjWÈä%þ%UGqÎ;¡FHf²#úñ?rߥ©tSΆ;Íü­aþtŸ?r•…Þ”Ö)©ýGQÀÓvé¦Lë?r˜A§…þß^U&|¬¦\Í â>ŸËÒŠÍÒ§yÇÈ7)â6õ~¡&†Ö.]ùÍ&WBÅîâ¸ê??˜"‡åD¤h¶TKÕöËÕv6H‚{¥ôïꌄºqFLO9kΰª$Æ"ýÔ(–ך"•íµ›E›ÀHrYË"  ðÈ>=W¼AGZíŠæT t˜‘ +#¸UØ=¸(tVjFf;º¹ìu0lµfÒŽ[¤¥a¢Ú)ïÏÒ÷Åq3Áû9»™?0, ~‘mÛ”ù„)9á?0ž1…”?n“ $1µÚ÷eü?0pÛ $/_\̺?rNs¤ñûÔ‹È‘’¾+òf†»Gî›Ä[ìšÙ)O­‹9…âø¬8&b?0eŠZVèÖN°r2ô| òÿ¾î£'EÓÒCû"?n'é³iZ›é$Ñ ³$I$^žá†Éü6Q9X<åÿÊuè[3¦ŽOî¼l»fþÏ3#«ÚÛ°ã3”³¡"Ù™×ÞVÏ80S×2¡‹#›fþyYOÞâRERue:ìÝògkfuthßsL9*xk§M§¦*V l÷,i¢®èÀŠ^GÇzm¶ûå—:ú+Âú¢mÁUÚM'}›,5¦ ,—LÀ¼“U ÀËì/B÷€Hló™G±•*)?nNó¶´•ON2ýUžµ2èý¸ß´µh¤± uÉ‹bîÇ—Öµ™“üÎ}Œr.IšXRN!MZ‰p:ɼ SöEØ[ˆbHfIË H8fZ^_+ЉÑL^J$!!õ¹Ìú°,sÙ[ÛbG¹€qû~lÌ üJWEL’}ÒTuk¢J aæ·ð•ÜIhDF€ðÁD^ºDtúT–¶F·O_ØXØe×èf¿Rh¸Çt™¹ÔÓXÏ@µËa²–€^x‹X8[o¶V{ãê‰ÇÎÊý¨BÆ–xDIšnb[³¨¨“ÌÏPƒƒÜàî$$‰»Ð¼§?0&߸—lá;½—¸ÓnÁoíÙZò±ØŠË<€bìxúÛâ ]*H?0­ÊI®ÚŠLA?r¸…ˆ+.Ð5ý8Óæm ŠPÞlT[êP?0ŸWej"??#ü¦Æ¹šày©Ž\û £Í%án¦¢èÆ®ÞäZz¦,MÐÖ´`¯";ÌXÃoÊ™¦·Ýؼí!’_p’RÅ5ú)ø3îã6…ŠúžqCÑ?rÏH=mOÇ??GÛ² Ükž[A‘TôÊÈMsjðÙÚk,ÄÒUûíõIÔm'ªG§‚ʽÞy5^¬_z`ÛV£JûÇ,3Ôà>Û¢5|‘«Lš}s¥ÈÐeÒ=8Úñj‚S`𾘾¾»˜ùüز÷pô6'¿{581ágáðFÿAhúŠ0] 2\")þîÕ«H??鉶õâe´êàq!M¦ÏB·'x¸<üT­#èÀˆ§N%é03ë<½µ9Ë_É¥xL·Ì ÃLëÕ忱rÑ_£‰Ç¼â‚->!*Üb>hk­ÑëUuŒUy…õ< Á5×VéÀÓŽ.ãÆ™åžU=µ¹=Á€¾ÒŒ??Ó¢ òüª×Îf «2+uÉÄûWЂ¸ÒZ2™dÙgFýe©Ó.!<ëð –ÇñÖÞåذ ù­v„W ª¡±£z¥õÖ+M¥1œøJŽ•,WëŒ9‚<§AWâë(„è'ªûTÝ%ÆFXáy¢¤Ð³¦X>yÑyç8|ŸCçæQY!Ó‹°À²??ž¿`‡£Ó…!iï‘d¹«PúíZÒ¦ÂÓÕß=æM)¦­T!ÞÏ7f<žYÛm“b£9Çx ê=<2Ó˜ž˜‚8·Æq ÙÂ?nŠ3¢®À©üì<Ó‡B×§ÿf¡1wS1„¨~m¯ÑdÊâòUÌ -8•T À¿›,j KÝo˜•H8R€§óÆ1Gç9¥BcÍ:®ÍfÛ줈pŽ)=½ñ„µeuºFÒ[ûÐÖ°?n¤Êß#‚ð™?0Zí*™ëÉ.×Ñ~G¡¯uÑý~;|­Åô”©kg›">/„É ¾gµ#w’4!}ø´æßpBS•g “ÂHž*_±ö`Òí!€êʦÁ—P çå<Ó”þNí-[”Ñ¢Y‹ / €¸Í>üŧÉI{m·˜På¬??^þ(ûkI°ÇåÑ•1?0¨t•´íûC¢ò=ŸÃÉ´‡Lâ_ÌèB±Qá??H[7Åü¡)Lúnèâ‚Çèçâ%QâÄPÁ^4Ë^¸ 8á©e/õÈäQê¥Zg¹a‰êFÎ2_Ö½ëOU1¨óóó4±#h‡ßGm%/¼›¯‹ÀáþÇ­‹áˆJÆS¤Ý¡¼xç”`LãT9CG³2òå?r½?rЦùP^Ï"¥=[Rlå×D“=ÅÐ3”:ó %ÛŽÙÂ)Q ƒÿÅSÍñš??Mižñì®(¼;žJê4˜±KÍÿN 䨋AŸÃ5’ª@×<.¨õþï}´Êæ,¼¦ÞMxûÇ_þ©ÅkÙ 67=žú??ÌÃÕµ¹£ƒþN?0¢”G|þ?0ùýÁk”ÓæE}¡P>æû©¥dó¤XƺÓTkF útЊ–8C¶ø¦V¤d®sËàb4‹„ÇB:S;¡Ñ:qê??„šý’A0…»_â½âÞ‚,<øKJɨâ5þq‘»ÄØÞz%¨Ï”‡þú?0þë_ëx¯¡>… ªü;ׂvaø''²Ì[²?0þþ¯ÊÿamdLcñŸÎÿÍÄÀÌúïüotô Ìÿ#ÿ7ë*ÿÇçÿ/0þ/Û,«mÛ¶!øÎêö;û'G5' ëf}÷éóz¬ÞØÔHû¡ŽRGSé)ˆ*ØwYÿ¼ÎÈý¡VŸ]k½èo1ña̺g6§¦»ŒÛ>º:;‹ÁR9û01tÜ¡S g*g¢r@îü³lŠÌÅÓMC¸ŽðMF­™Ì™"YzÅ+ú‡v€"(??¦옡Üñ† V-! aäQ1YÜ¡Ï6ª—vñè-Њ~ÚŠ(X2Ñuüéâ(ƒxwûo|DYxz…}dƹÓyuoÇŽ|Wa¼‚Å ¡#qNÖþªe ¤Å5E—ç3EÛ­|CRcë•‘D‘Û>†úâ…²êfu¸>°†È0¢fÓô¦„ÂÏQÚØUF÷ñË«¢Þ›±˜2[Î ÌòoÅùþ™¬#fÓdÿ‡é#6ýÚ®ô^ó|¯)²¨ÄV±°ƒéKßú$¸F%P!ÿ÷÷‡IÓ'àÛ@Ø©Es²š?0#^y ›a˜a®Œµz°Õ:#$ò•äÌ)0*Ò,¯ˆTI›ëU™–£ª#ÇXf‡æÐ£’¿^4µ„¿ú.k˜“·{uô9ÏUÔL,j߉¿$œz?r ?rbqdØ£)yºK#­{…›ÎÂAPnKëhÓ΀B_ÝË3¿fÉ, èdtL+Æ¿<©}Ë^}8Hƒ„ä›UOß$Xù°îµ)›ìl4 ‰ÆGú²ý¢Yuét­Oß®í)‹§aü0Å›+愘üvWó \‡‹õy:¿ö?r;$1l³òX%,t¼}6ü–“B$¡k–Ü-S§Ò[Fë`ÄÞ{¾P§Ä&üÜô«v-•#žjU,llòÙlÉj]úþ¬N?0ônó2è\6ܨÏ Ö°x¨Œiït|+?n0¬w»ŠÌÐ’„7èmýÝJ|[>ºàÈÞÛK“©ùŒJúLµ[þ«».M÷ˆvÀ)â<_hN]_ŸÚÓíz÷ùš ¹”¸”´8¶t¡P»ŠÈ£¢õFÑçàú¨©É*ãÚJ|q@RÛ¦!o?08å•GÍ@ñ¢^(]¢Ìqžw¶êÊ|_´[-žÈ»Ì?0ù?n¦e‰œhªµÔÑÈrñ;*œa$Q:Íå¶›ü‰L=Yn[ïÁ‰±´ºˆòÔ·­§Vƒ3°Oã¸UÕÿ¬’.Ð1Ðç\$ý—¸.iV·MáyûYÍȸlŸô4J­v¡h¶”¸ú5LHÎeËÏïïÒ?n6ª@¡ý>}oâcª¾_{n·KÔ,fàW…\%–¥AÙ9¾œ«?0ŠGb>~³1‚ݨ1ô¹ݼMÒÎU=Qª‘­?n¬ OYõœÝÇó¦ ˆd.ŠiãêH&+(}sCŠðÐîuVZìuÚs}?0ášÂI^­??J4«8M¶˜­—œ¥íÎérÆ|y U’ÒTÍãæ”QÈž#]¾??}ÙÂV›kv¯7z9âÂj<¹Må¼v£ocSn‹~ ³8ÑÊ«M6èó´RH 3_®V>)Qg¬cðÅ}EØ)Úç‘fW=ÞÕñfzož·çïVøîºé{½¾??¦»”FF˜³Ò‰º³´4‰Ù`wðF},êm[,{x=ö£Äž_"&®À#f˦Tº0õ=UÁ0îiy4Ò‚©Ü_Ø Ô ÕTÀÖTCh<õ'Ò ØbÐñû “æ÷™Òp'7k*­kR-2ŒåÔø(¸“IËØÒË‹TiœtSPÉ©uþ\ú‡»F+éô::÷ÌùõcïXà¾q¼Qž©@Y/£(Ùê£Ú–ëÐYà‘‰‰¤pQâ²wy±I5hQÚ´©³`Z¯.ß‹Uuv—X ¢~$ž–·Þ¥¥ï:•£ýoÎÔhüŒ·nOê°³Ö_¶JÞ7‡ßU7¤^âB~&Ú')S¸??>iÄí¡:O×Ic®®¨ùœa $?n/ãVÿæ'ð>Ù+ù’,¶zcq×gèøp?rœ±]æÌ®™×’tÔ_ÞýZºýM-+‚æW${a õ`¶Äa…%gºÖ`Ü×ÉÛl¬ç÷褡—êÁ•5,baK£Jh截m—[×çž‹°ÎȈzYÍJ§’š#§¹\D¢Ò1"¨QÖ—/̉nÎ÷J2|ì¯ +#¹ÔïrÒûˆ¦Ø"@?nµèz:DÈ„Z’ë'(,^ߟã+…‹$ŒÍBbv»[`»Íµ§Š¾‹Å->¼ ·fΈ˜m^=nyŠN¤ ¶UÝ;?r¸5{>PZ`a™]Œö~Òj??º§OÕÓÊ€ÏþÍž>µ2lÖå#i<à| «n­ºµÖ¼ïÍ¢IœÛÎoÉ‹AÙ§-ž?rÍ…›ð¾}URîwÐ^,|œ½2,èîtñÚƒ%kÔGÃÑ™<þZ=ÜþºÒñq!UmE@GmˆýÖ£ÆNÓ§MÆ>{r…SŽézÃJÿæÖÒŸÏùÅ ä½~ïö”}öRüQƹðÇõáË_0!r‚ï¸A‘3?n£Ò&}úd?0†Ïei×K_ˆ™$aej¡Œ’!ûÖÂnLÏ90b¬ò<mp(›ø` ›Û±J??{?nB y©ºÃt$ÏH>)Ñ+~I¬µ¤ö ^\¥zÞðú{·EÇÒãórk“ªSýœ-|UëT¾k¿Nd‹à&?r÷ÌV¾ÊfH”¬_£W®Åýæ–ÍU\ÆÉk¡ñå_2,§HuÏF0:I–¤tV7߉<{Õ+GÀw†î}ùëL.Aâd' gr‹^qƒ½ýXr«&™aÕãþ ~…(?nê†ð€ÿÎÿl­ï`ilô_×ÿóf&ºåÿc``de`¤gþWþgV–ÿ>ÿÿÿO??‰›ÿÏf°R2uÜdƒÿÎÑt´aX<æÀ~úù‹ƒ¨JhàЈJCzÑ‹Çß`s?0«·éÙiëìôùBí’é,Ë??=ã;Ö;»N€t(YM÷,€5·skÞnV,9#++++ë;µv¢´ëg«×¼`—Üøt>Èz®„YÖk案Sý—Ù%žVÙù‡y±ßg›CM]+A)M@Ä+±lXUMAAÙi9Pƒ¾í*™„1ó/ª¥šx{؈ÌrK0{q"mBÚ?r‡`XÆôÀ¯–Ó3öâë8rª—S3´„b™?rE£Í²@“-¦ùÉê(PH??_绞¢u‹WfB£§ÄBX5N9fÚ+U‰ÒSFMœÔšc꽌Œ…öAÖJ[Þ”4à?rØ&µ&¹kÝý=bÁUM­ñf˜—ÀC*v´ê`!ÂÁuªá/—' ±®Ùr`I%r?r??ð.ßóoù˜2&x£eeL‹ueP•˜1>ð­®®3ôòI‘ŽÙºxÂé]½“øLrÕz=t½¢XÆÛʇ½-“/wN¤i‹¦ß ¬˜9e&­ð¦ú`ÛÜ@KÒ݉ËAÝ_f?rmº è—‘õŸ%s¦v__8¸Ë*Pˆ•ÛÑãðÌ¿F8´†":žY‚r¥{„t¼”9»6®…??”Ç™áWºËK^úQZ‡^¿!ïÙ¾°òxJÙINªgÍ×?0ãBÙrKáÓu껋£ à' }¥„Ý*E\]'ï #ã¡/&í‰PlÍÊg??wÖØç)§·›•yWÚ>Òs ÑEü06RNp$A<¤Êší¥®¡k±§þ+Ç':??…7ó‚?0µ€:ªDJQ¶Ëø]ëV¤üây äßöè{݉|]¤…øôsùéA#î1vâúØlÖ é’H}@x`§xò×ú=öN/Eˆ˜ÊIJ·Ï9ƒé6»RJs*B)™*4ß9¹ö·ìŸ"o'ÖŠC¿5c?n8ŠÆ-Ñ7Ýõê*$ûóÝàEÙ5B÷Ï9hw&ö DJuCsCneÛ9v"jB3Bÿì⃄(Lå×-k¦./…Z²HÚmís¿ÈÛWèX£À½Á§/0›Ýv j7Ðó¹=Py¤Ýχ9êx5'5–É;ç\]C^Yµ“6Àƒí\´Üu&R3…1æfÿ?0DC3ô7è8×?0ªä{ªe^NBÃPº«‹¯Œ9/vèBD+˜*:QÚá|­G0c“¤c•ó%Áœ%øÃŠ´øíŠgNøî 4™Ún)9¯–%Ö²FýR´,$Šðƒ%¶×£ôHÅì¶¿vó™lÅ4ö¯XÎŽL5Ú&ÃCäÞã‘$I8ã)‚¶ULààdBÃze©j(WüÖZ6Ÿ†¡e=2¼jP?nöçXô?0½oTKyW¢¢O•â·¦©Î³ÕšµÅýlÙÐ?nòÕÝ7ºsàtÆí‰ÕÉ´@òn9ë°Zf[ {Çj'æiñÄEa“ù kkÇ6¸/ú€›*Éü‹pJÛóå#D1¸d‰riªâ”gMèäP/AºWINiþW¡ ÀÆÛÒ®ÐDÒ¼[òh‹Ö£åìG§JpÕ?rlæÐʽ¤À‚*Ù½t«9‡e_y„ÉÃÁ4ä ]™ANhüu@#Ég]LÜ!0Êâo·ùÁ—MãÆQÊ‘B-Ho)5­¢ˆv§Õ©ÝßI?n$D%¢•ŸžY¬frƒÊ #€"ÓáíR¸LÒ¹Ï^C/??*+SxÝw:4­!ƒ6ïõ $¢0ÑoejpŒò£„i¢Ð??ŠcV, Óó~ð?rh¬{½½Á²èúó‚€DÞúӨݳ9âh†>\åRÞ&Â.+ú.K5NVâ´ñݱšQ“µ??„eO¿T4µÇr5ÅôËǾRYÇ…Y´y"{¿î}«bÞ¶ikz]L£¼øÍ˜³ÇλΚ“q%ÿ®`,U_¡añ¼×_‹Þ(¿¨âi½È¦5]¨cŠ??‘2:rndÔNØÝ¼eªEš«*ºãrLìÈáXûÕ×/ óaŒâ›±òÃB’D$u?rpáÊ¿e4 ÆÁ”åµ M*gˆM^Ú(mÀ#9aÀ‚#7Ôákú{œŒG6‘aÅHfÛõiLÔ¹c¸a¢ÿ)uR™DñcÙµ!¦^FUôG‰zp v[àÔ—WŸ¢8óžÜðI·^d˜2ˆë–ÅO€°[ Y N<à°n«ï±¹ÀØÈ y´g©H%Öb*Iq(#µ+N~R,µèª´ï-Rú["o³i­\FO8œÜÊâou(çàv/G±S,$ÚÔž'µ³e ‰Þêµ1°÷¥ä8Ñq—%i?n;¥.%lgyŽP݈x‡êa¯š ‚,­è¢c4ñΊdÏ)%à@ÊöôKºNù¢Ыßt‰tœ8ë3Ákja¨Ë?0ɾ)›¶´Ž \W–Xœ<q]?0šR^{O-6^?n¿Ku\Ë{£›¾äMédÚ>Ü>>nQ|TPM­)/I§¸{óƒ\Ò%%¼ï%‘º¨?nnÀØp˜g¨)€-´¼nܪ½n_y¡_!n¸¼"AÑÒ‹#`s+¼Y¸e‘ë÷Õ[±µ€ ¾ƒšÙÑóãçöKì+¾Ï(r!aª0ÙBÉRJtΫ™OF¤„ð T¯‚üyThÜð€ä1 É‚NRð'Dw$Íy_™u)-??+jÞªp`„â«4×§€¦˜ÿD“¶¿O½ä0“qOu×Y«É?r ZR4›Ë¯ÇÇ<ÌÖ­F”ÿpoä¹ák•aüb6L£‚‰ØÇðÓ¹m>ʯɑ7€C=\`Z«8µ5Ž.ü3®Œù^Ë Íó¾›4)…Vïõ‡ŸÓ¯ î½ï:½¼t\2zB q޾Ӵ>ÁÝ#Ú«¹™^à:h¼=l9$Ákôv “Êláq{·¿Š}®Öf@‡ø0Ï~8ëÇé(q…“)œ€å2 rÚù,éé.£¡+©n;DŸÚáoùQÀ‹ºhÍbI‚»¤Z¿‹¾¤Ël‰ÕŒÊU°§áeüd]5²Yámú8VpU9QP9ª"×'ퟶñâ_ĵXkž`©(ºsõ2Öxù7}­zÑÄ8±g¾]Uy¡?ra‹ð tz}èUžâ®pzúQºÖm•p.õîV‚Ï !–|Ô ,º„nôp²@BZ??Htk9(2=m}ÄįTPYv£ŠŽ&Üò¼#tûºÅ3¥iq¡õÉ'¤µ4ÑVjê„–É‹¥ö.Qx1©¢çãd³;å¤mÇúМ˜w•Úõ,ÚdÜ_]=m1oÊyâpñ&¯‹aa:êß _f YY2I+cpjVr‰²Þ®.!ps1îjÉ\O,Èø¡nKϤ|C>-yÝÚÍg„OV$vWTø´^#È´?0iŠ%l E5ɳ95rdí§:õíô6ýùzïÝ °öéx©Ïú‚ß¾äòšVý·å4Z×»´ØŒ?ncÓ wß ÓŠí6 V,iØ“&% ¬´ã§8uN“ö£3‹“29P\`ÂÝ膔©rY;¸Žjd¶?0Z ðQØÁ[{ä×-Ž‚)rš vHO{8Êç??ʧ1S„aP ÍÌŽJSU›52_ߥ™˜š2ª½»³kN4%µðQŽHÊiªa~Ö:*˜Æe¿+ð€NÔw¢\dœ°‘nï^àèe¦®:Íó–-{›bJ[7ăLXÔ?0­µéu¢ 6KB+W”†d²Ñcÿj—•ëK¥ÏßeÀÞ­?ržt—#·7ã%9cù¹?nÁZ4Y7¬ÊÔk…Gö¹`×áX³ V¦Àö¨ˆ«÷–Ï­¡øÔ0”œd—S?0ÈRíRÖŸùçr6P„²»‘±ÂY¾dR®gû͂ʡšL0š-ÌÔL¿™ñŸä üø¾«¹&?nXeÿEn2­ t`ëIq¶åi«@äí5z…œ‡™:þËÉ0hÈ6Ÿñ»Î¶€Éöw¯”QS¾r¥m©Ú¾¤¨”†eW± èblZÉ׃”ï)½ãW/ŸîU³b*é£"¢»ÍtíÁ*u Ó 8î¯ÌÆŠLGR¿²ð>Vü9vY½¼Z‰3>??ö6œ%¿r2s½Æ€›Ú!™Ãˆw"E[Þ–ÒÞÙRgGñ„ÜÿuæXµ~¡™ÞHÖzeXìÝ:tÉšÓéuÄ+ZÑ( +# c·J>8Ù¨àŸ%]ø¯~‹Q+Q­ÛK•ÛÆÆ˜eÖçðŽ?n«žàâðJOnm4ˆøÅÀ??Nó¯Z¾¡Ê[ X§ÛŠƒ\S2VáºÏ:×ò¶Û€é-EE¤ç®Ú—d®ÛÜ?n_—$'î÷÷/ªìÙ„NÌã¤^?r¹ÎZF}±A"vØ8žšvVŠy:ÜiJ¬[9›Dkž "iH #³X‡gIØ¡1ÎzÜ~:yý2srÀW?nö›óè ¯-üi‚žOœØN±bõ–˜²6ЀGl@‘̧äLñœíç„d±?r‚LßöêÙ„A¨Ä_0¸>Ö:¼Ü¶^²Õek[s‹ XBg‹ŸB7휢£ñä5°«´Ì›£C£b‡n–˜È«¼VqáÏôXÀ],~ÆÆ±#J¹' m!ÎvÒ¹­µ+RTF½´MÜô?nâ»`¯ †ÐV³àž$œ~a¸º8Ìl¿‰=€¬­< ç3—UiÿîäÉ©ExVÚ%"—ˆL??ÓÉèáŠZ›„žp—a€ZmÁ¯N:ûá¹% ô­ôa¥V8*¸ò—%¥È,2½müFldC˜¥þQ]u¸ÝQMIÚ·ªÿ&(Õà­˜8 šû0ÐMM¥?n-gyÇæ±¿ˆŠ%ñá Ø‚®@ Ö`êX‘Ö¿tÜ z‚·Û”çUe6õƒ«V®eg§ÜlTaµ­0?nzQ“ÀĦ/£þ„œ?0c~ž¬Ö®K¥ž­1un†rMy_·¤øëbòU?rÛÐa_õíËMq ‡×¬³œ³`ÍòI·Þ?0]0=‰¨çxW'!wnè?n î Õ±íÂ7÷ÄúVµµß¿;i–ª™j»}¨Qÿ–8•Z¢f~4Í‹@1ˆ_t“Úè½¢‰-ãA»`4ÂAuSȼbê(n?0êóÍ‹úOè¦ [imÿµ|ó†ì­Ýåþ²»±ˆ?r5óf?rêÿf½û„‹¥s âÇ‹9Þƒ€?n0“( p3Rz€3Oíéª8©|Zœ “ä"Ø?n¼½Uûc(ͤ…þçÒ Åæì®Û}DH Ø°2}·¾’Ù¬“?0Œ«ùÀxwWPOqÃoóŒ£Ñ Ï”s§Üd©Ù[Ú£Þ·štÝjG©{øÜƒå1‚ íDV™š‰mDÖ‡-i#¥~fR¤n¤¯1„ýË.u£Alt«¿i]ˆˆZJ%MYžÑn@ và'f©í!õ,åmäxáê\˜[ø–š®@“ ¿:KÿN z?08&Ç+ÞÓå4›zJ%’eJM®”+¶Ø=¤Ë—½|¼ìÒÝò¼rÃý¨FD­Ù‚´J¼ÿhN+>’{?nk]A„• (*{û¡QR_håXŸáåH ¬ŒäšPÎ÷ßøÜëHC“èþêE}áX´<œÐ>kÂ$:#˜/AÝÒ°¡î¢(ënätŒ£ãdÀ±ÕÚFõÔÿjúeU¸æÚ_ÙèÐ8ªo*?r‡Ô¥Š8–Û­¦Û2s¬‘V÷mˆ´ážAAÏ$)|ÔguE¶Hß´)(úþ’M8ç:™SfDBM·S°h}}ë=š¸Ä„ê:ÞÄ2ìÏ&’ÉW]¸ÔV”zÝì Ô”ÚÝ!‹§€Ñê{¢ËŸ`>x!Ê¡9’Ý„ßr.¦ªÆÝSh~’—ÔF*ÜâCü“ oA^ó‰¯0£çb•,\Ñ–0,Ü:DÔû lA™XíúÃøÑPV{JúêWø–BBzT_N:1dàRö'¡Ò¸(2F“9ón; nK â?rûŽäçS9Œ±„v€ôeebUç¹fL!åç7FDŸH§Séÿ e»…›ær×=¡ß#¤“õ(¸7E§žñgÐRN.ÚO^â<¸s Æsuˆ??Öd§…(Òìê~cû£ßO,„¹½£™ã:ŽׇrášúÌðM­=” g„°sÑžB=—™ªsM¤0Ã;‹Mýq ×ämí}–QGe¸ƒÿ{–‚e¾wãr·gÕ90®Z³óÁõ=DªD™>;’,7Ã¥ZNݰߌmGB?rS®s·êr(Åí??é÷²g`‰ý[Kë:™¦Zçî% Å­7ôpˆþA$‚??òE ??ZN0…ª‹“Å”5u¾GôÐw}oT.„áf £u.A4¡ö sÐ=»“YûÕ.(w¥iç®õŽh•;Šç©0óg=tå^ü±rþè)?r÷ÄþøÁ˜™ .åzÒæ¹FS縚¶žºaóØ:¤âÅMbµæ¹Z¨z¢BFÇÏ'3Û¸›„|sÓ°!;ˆ?r”Øës¼peããâ†Ãˆä!¡ƒ`Ú½ö—%æb?n´ P7ܺÕ½Íõ'ÏâS¨p¯'~Â잎{fq??cÅÂé…¯ÈeûÂ0(1þ‡­¼¦µ…'Á?nÕ4¸=QQ#D²õdäÒÖ¾:‚àr*$¯2­s}¤¨„%ù a<€ ZÐrOJ‘DŠŠ«q¥Tõ,dŠÚ²Ý;0çj!P²`O]ŽáÐ7¢Ã e??ü=HžE9ˆ-Ü¿kcø@TWÀOu×J÷.Gb¹êÙ"Î]l&E{CVˆÚH´â‰¯‚RKäö]ŠXJB5”LÚ“cYskp'„m°0&D>Äu?05TÁÆTÓ\O ý|šë+ÛÀþúù€”ž Š…ð¿?0?0ÿÿìýx4P-?0ƶmÛ¶mÛ¶mÛÎÛ¶mÛ¶íyomÎjNÒvŸïVέ§Ûöè BÜ@Ì´ÀR¿mÔM]gŒš@l‰´Íç9YèÊé.hÈõàq<1S×??…EdO ËÀUlZgùD Zÿ'æmx↲/´w´ˆ¡“‹Åm@to”ÜQ/7Z¦P¬ÆrÝ'ÚˆÅc0ãm8`DZ:ŠŽCælp§kˆ$6o°H±š-1­vÁ`厺όŠ—f4 ›‚]ЪGþ‰(B:ŠCd7ŒI¾ù¤¦c‹ÕtD¼Ššîĉ8Ç= 6‹ÊД‰“¥ÁzYoè>\Ô@åvD’‹µ6c9óŒ-díD'õÿy(#c‹Å~ál,BŸ¹ùÕ÷ÆÔÍëùQPe´*¦1“??&Qg¡—‹e)rœò~‘Ôçú4#/Ôið‚ÉA›á±Ï!Ó‹ÝúÈݱD4&óöÜÅ!6ÇžíødŠo®wJ¹íì~Üúð³?rW*¦¯³h!wfÏ¡-ÍŹ³üuYy³O¼>8?n=‡‚×q?0™Ñ¬w\hxDl8šYŠN{Å`Vö˾‡ˆU8N뿦ÔïPrà ™›Ø ;aDð: º8ú?n?r‡`]dtЊ¬XÀ‚3bYUH~øˆÿlàÇ??©mÍý†êU‹l¿‚p=R^˱÷̈??NÏÍ}xßÿ2ÿ%È?r”2:5$ûíú†çf¦Ö[­Ûߣ8núËã“Ã+öÄÍ?r×8Ž\Dfzð…ǦGl}ëuEdÞ«Ãà“U¯ßÐk/–¯;FQŒÄ!³»K¨Oÿ«G©Ñâ‘Õ~¸pÛ#°,U®± oòNW‘|£†‰VÏ9"9÷¼ë¯}:^ÔæÎ¦j®¬.^Ã?r÷ØkÝ"¦ØØ;"/ê²q¸9ü¢¼“±naégû옮‘qßÔâ{Šö:7“zl±þˆï¤2°Ç\Ÿõ#??â"’Œz¦¯+4V·U•×v]5íÆ"Wp×ÚOªid;©]ü¾ü?0H3ÒN8.ŒëÔSÇQ ƒ ’D”™äíbŸ¡[½)Þ÷”|¢êD"îlι\äÃ}.9sŸõc¬&ÔøTØ·I¦B™]ŸD>úG+Þ¥ó8¿•òîØU}Îù£@óæþÁ«??ɾPãf kª*EÖ”¬£1l4š-ë%NÓ.¤®ª§Ù¬‡wòô??iûýÂòÿ¥åù©+ü??Ȧâ` Hô@&µä?0=“`ç¾èü^hç¨??u›Ö¯úª]¥BsÝ”7uH¬jÙei’q·Á­?ri4˜çDꀅòcxÐõiW²DA‚1‰ì‹´83Èͦ·š©ñoé\‘§bWµì^® ¾Êz<Äl9)‚V ¶XÁÀéÈ -€ê™Ò,Oöcë??4@GSH?nµœZ%?0?n©—TÎðP6 J^vÌÊ}K–ìÕ¶ï x›<sÿ?nþ¨?n…ƒ¡ùÅ£b‡…#N>›»ï'¢?næ³ùôã$¨·¸(€á8}êÅ©3ÓРU+^<‚Z9A”Ó2œü‚m¶ý Z†e¶ RJû¸ÂZ9bM· £É)ºf§û@}î*0¶5R0©*¸æÆ|å§‘ÂivìàJz{Bè§§Dìm$+|û5Ñ©ù¯‡…íÙ„/­]êOõÚ+ï& Q=AóõŒ§^­¾!ùÈ¿½#S´TÏ®iõhY¹@wZ¼^)¬Å=`\¿É·SÛÈ+“›ä€;²ttvÓSðþ[)êAièN¡ž(P‘­%ƒü0b«øÆyñü¬l£ß.ÊU´:À†‰—:ZÊ·S¿8‘¦ û§9›Ë·vç/ã%}ªS¡ÔĘ–’Š ¦$¾â´÷àMc+êš™ù"E# Ÿó ðuÙníÊm`$ªà@&ÀZ²[Žˆ\’ruÈÔCµPEMa2è0GÇC £YBN§ÔÏÊ—9“÷Ö[ês7Ô±®—<˜±u ¦KåÄ"LhŒÌž7œ—*\ŠFSU× æäÏÌmƒf-ÀNG¶È.Dî@Tcv¸o$°¨î¨ùfxv©y;Ó×…ùr´&ǂäÝ?r®*j€®?rYˆ»ñr&`D…ªþSn}UÌw½\%œ)[*¸" Ÿ[>©ß‡Z³à"ó ”ßd–Ä̹.x{Ư9²á ‡¡èFI1Êg[šñE›î7ÖàQc ñcݘ?rÙ]ÏÅ ¼o§‹ÂæFæT>!Ë­þã ÌÆÇ7Ì¢Í)?nr?rŸ&ç]A-??À7vÌ®ð F²›7MÝ2OíK¥ŽÌ–´™¬ž(37‡Å¡^ßdÛP$?0”j´P:?rR¸²È?0Ê ”^žX%¾ÏÑäͦ rèb`™ØÓ»nê³6?n-©Ú2§ü3‡¶†§ìì?n §ÛU'_fíý›;6g­-uö÷þöi§£Ëa>潓“)o§f?rÛóv逥àßôÐôœ.Ñow ÍÛû„?0´Œ,Z ŠÙw¥¿î)uʬÐøÑà?0reº¼1J^;1|r7J†…Ýf¦œŠ^›ºDÔÀ[ŒÒ:Ô£¹¢Ã÷Âd!…©Iòõ½%‘,Á³kJÂ[÷Ô\e‰»£{±ÖŸ«x_Œ¯üZÙfÁvû¸åüßòþ|oÍø!# Qub??³tZ»Óµø3¯‘j?07bx J¿táÄÀVõ×À¾–ïn,£Fôe± ¼qB6éO ?nÓá±ÇX^쟨uBGn˜Q ìx ¹' ‚¢”ûÛ©|¢ÎÛù38¢Jœþž{Õ7ªëËæ \ÐÓ†x7q?r4¶@šùD|ÀÄ ‹éð‚Å¡#Z?0$i%\¨ £oc#•ž¬lééAfòUõ??@HüZÄ–ùÄ8~eìwLMlË |ŸažÌÎJÆíÜ€ $SéhbAfÙ.ð ÜSfቿ‚(aþ@+aÊ$„û–P6‘Å¥1ÎçÊh»I¬Sl¶|XžÈ4rjTÂJ$g†IN6èî(“m¥zl™{C]zl€*ØÙs¥¸?r|¹=²@¦Êÿñcš›ËãkJM¥Âšëd=bЦïåù"Ý?0E?róô„ëM.š¼üÜ&ûŠûeTC›3½ø‹C#·;ò˜bvŽ)®Z¿0;®ŽWóòû™ëp{69°$ƒ3nC0(ò°M»S¡rÁÕ8+Á0AÚEZaÇ¥+v–òD,éîxars§¶¿[™V9²xá6÷?0Q$øƒ¿×­G¾ÈFÈ9G??)`è ¬ M`zÝì…㙌Q*1˱“‘¹ónÄ!>âÍ\‡Mðä(.€(oô®n³Y>ŸìäA|*l5\!î7€5Atà²K¿wWt¯Â—±3BqyÐÇÞsy:u_’êRÑQKó!²¶Ó¶‡@Þý­)[‘?0n ¹Åp`ºøBÛ=à;îà‚Lã70È¿[§«â-ÑÃ{UNJ«dhquîdæ“‘DRmþ–ªÊäQU%¥:yS4puöŸ.j .¤ï¥_¨›Z–ºB¶'kwÐò­?0½ßÅŒ\¼Å¾µ£ÅUÒ¬²g’CÊ/ôµ6`ç¥Ð:Ò,I™õðkã'´"ƘQ3yìO˜ÙKQ¤—pçeNÑ\g”õGRJ%m¯vØ’?n‡z|š'¥™•Eïzyš× `?rJ¨ôt0?rwu/xsÂÅ5ý {ùD¤(3Ù,¨/ˆ¶*ÂR央¯'Dį)I@|öØY³:„øÝi³°æ@?nŸ>5α„°{{­^"Ç`LŽ ^™Ö†à˜^.ƒ¯´ò[\ÑÆU£3è—ÂÁkÅý‹_! ܈sâùÃÔůɴBÍž'¸g+š9%XK_9=œZgâ£ÍñÍ…±+èÛ›FKrZ²hÂ$èN`±?r1‹ÈÇ?r1ã.»Æ½Î]µ·°\×R®ÙB¦÷Ky¨îvs?nl#޲@Ó—5ìR?n¨ Àú£L¸0CŸ“¹ÙdŸSàßÂaü”õètaYQª àº2‘¼ îñÌî²²T‰”(M’Ï9:ΛH•‰æ‰—¥ÔÁ2àX„ƒœŒCÅœŒMɇîâ¨cebÎÍÓÝ@O8¥/©üЇS‹\V¤Ñò©ÛžH·>9 ™II¢!^e²W޳RTCáñ?nX”pb•P Ë{ér§îÒ`=œL÷’¡ßÆÈ_[nŠaí0vÚŠ#|ol-ímš¨¦ã§ŸK˾'š–K^JÝ¢/•9¸Ê—÷íî9=ÅíÝÒŸE±Ñþû2äÜ?nJ½Ð¸^¸|õƒœGô®zgJª|“Iºš$ðHè§°¤â‘KŽs¢·I©»o˜Ö¡´® ë‘‚d;'˜ø¯’¶²1’^¾ ŠÑËz%A8?ra0_ÔÆ¼‰C¼þbštœa¢JøÒTw$|ÌYwžÌÔ–êm$äm®NuZsº?r+ê¯ÕI[Û5æÔ´”Bæ¬Û)m†¤Ø—81×öTufUµEJ¼uF{Õ²e‹j2(á@ÞO=˜sÿ\—GeJo@o)úkæjSœÎ¼èáÓ?rá”Û1Ì”Ê<ö¡§Šõa¤Ã³S_F~1¶³[ƒùÙnÍŒ?0íæ MTó=ökMUûχÍ%hñ„ûg¨¹’º,=‘áºyF8És”ð%ës8}®¿ú«’·??}|ôl…ª\Ã:ã”Ûc‡w$–ŽõpíœVA…j.H.Ô¬U "¸s£éÏߤjò†¦¡<>Æ×ú¡wŽûšˆ]XË%e¿p1_4”ŒìWkj˜ÅôŽBýÛʾÅ}??n‡ñóû¢Þ}:n›e&ïWž ëê³µÀã¶9ÂãT•&}Gæu¯y¼[¼ð6”眧ïni–_vnPs7""B¢JXa¿$;ÖËGÕ‰F?0PÒ¼y‚Ee )ê 38ª?0¯DzJV??pá㈟"#ÞÉãîWO®Ê¤*§=—I¦ÀÀvä¶%qÒKŽÇËõé#Ô2!ƒî?rR,Fƒÿx§“*ÓÕÏ »;†þ'çöWÎ~c¶ýªße@I÷ð~ˆQÀ{³j¨¶åL|3¯Æ´»«' a¼Ãó®¨õ|lÜküíZk™zΊê‰rÝ??¦ 𯃡\cœ}%ݨ¸2»º"ô eÃÓ"rŸ+—ÒÀw ·Œ>*t±þÒü·Ÿþö5|d#1:^j¦°ߨEjXG³bü€úo*Ã>’Z¨£‚ ÇÖåSàÆ#b\·]цšGidÁ ù<¬j…Ön°IdTçûrD(q槪(õ·s¨üp­JH«Б/ý½ÿ-çKë#¸@ðè ½_Ò £ëÔ¸­ŒÔžž‡ù’÷ú]˜Nq¥þF¬}›F×qÒf[óÄìO¥é§“ƒ??-AA©4"küÃ}³V„ù"úáÌOü"Ñ]>âëóÚvÇ\:0Ûû땚r7Jè‚SÏÓöl0üòéæìp!†”ÑôØP2}ùH™8Û_+ÂB(›ÊþvPù7úšâÚõñÎeëë´õ×ݹkZe\sA«ŸŸ8‰°Ñj%6¥?0My#8P‰=†<¨÷õt榯Èdßמä¹UeâD‘¹…€YÐÁ+D*ŸN5 ž¸žˆ?r½}{:ÅÆwmZ6Œ Ñ^¢~ÍV=„ƒsÆ!£[ úi¡ ˆùס³wfú‚()µ¯úþ®@Wc8°¤XÔáúî½øÆIFÞÁý *[³¼åãמݱF¸€»Q{oi´ß_Ù †q“}?0ì òæÚÞ÷¤¯Q»k—Z‰ø»IøâÃC´9gW?0`[®çšýãO· ƒ”ah­âéƒçáš•\ÂD +#‰$À1àx‡a:œ‹”BkX-gÊs‰­·ÝG²1"ïœs/Ù¸o\Uñlæ«ç3܆^á+Æ|ªQûu%yñ¾¢C±˜W:öVà$nÉ|?n”±Ý€N+3€:ÿÈ×a¸’Nf?rÓŸ??¿|–²>5Ñ.„‚€ïÌ{„%ФZ= Vý²Ò´?r¨ä!itøù;f̯RXv_¸Rç¹ÙIšh¨s×L‡vÅW}Šž[°WÓÿ|¦‹æö?nÊ8??½ì &#¸êrZɹWÃŽÁlâr7Ò*1Ъ.^Þ<ÑŠÊ©4îÓ+eñ΢ϼÓÛ§/l…ޏ Ðax4€»t‚º¼¨.Á„b•| }ÆPì -Ô°;}²òúOµ#ý‹M¿æÓÇ=2yAEÕÝ@®Å‚´Ð56+Éb9ÁÂ8°R­Ù8»R •6}o³ðF²@¯ÑVijõ4–7±èÛZ¦Àø­?n‚ƒ´“Ã&<ý­;½fAc$—"þÓÜþ"Âú,ÿ”çQ2!Ìß1C»‰[s÷W¼êÂÆ—øÇŽÒrk+3í|É=?02š½¿#O8ÚVVdzOdèñç[÷÷C¤ñÚ0…yÓ0»‚ÂÉ(%¨Mÿ;ÁEh`ž.ÇÍÉKk 9¨h¾—|SÕà7º•XËnK¬÷šBwŒp6¨ÌÙ— ¶’o*óI©¬á«ø’)fªÚRÉÚú#?? åÑd±319»˜ã/§­ÓF4Þþ´04cJ†ô­àß??ÿ÷äèl(е<ÔAj¶:´c??‡[ñ§›·Ï2MZÎmÁõ»ÕóžNß§ž:g“ñ¡Ðõ}¸W–p²Ø=m † ©{©¼Š »¸Ùzz¤€Y½áç%nfk£¤þ–î  n’Âw`Ü@‚;|hÞáÞ‘0×Û??Â5<Éü.nT”‹_ãK“ij¨¹÷¤½{Ö¾Sêß(ü'íùÈ÷HoÈW”°û‹I©º~*³²è4èÉ=´7Yî’y³F~ëD‡Œ@LêfØ=¯q?nð°ñ#ÉS]ÀL¬<>Ž.,øØòÉŒes6íÐ1.²]âÛ=©wX‹/BŸ<¶è·6—æ1;·ÑÆÛß ·½ï9‡pó§|4ÉBZ\…%“S,u&1‘* ¢]6} „ÑÞ"o '?näÁr¬zZ:²ÓØ>Øãý׎BOqð—Ò©”OíœVMº€s¥G¿¿a%oX_ïÒä' ²ô1=fN5+;‘-Î!i½L4¼Ñ³Ò£ðþ&4XTõwÀx@ÕÖÕÌw_Óõøû$dáQr©ˆÎ+8¸ÏŠóõ‘RfFaÈrЬï&}löWæ=“9F|ËŽ??g??˜þùÅn¿bq(?0ÊÉ&°«?0Øá Æ<–!Œ"{À- ùJˆhh ÌwœüRGßòl¥gÀ5×-Dcj$_F?nßù!@±¤OÙ*{·­ 1ä??qO§ž)âµìºl ]L†øø‘Qã.?0²–4Ž#u!D_”úBÊ• a??íŠvïò¹‡îƒ-khöm=>¤«ó…‡i{ˆPÁ>+%M;rák1ôeàâÖVJʆ{‚`Щ¬^”Å©½›øCpÝyÓ?? P îÌ+ª1#Ÿ®nÖᯙܘ?04VÚÚ'Ç—þÚñ9£ú+4èuáµw[QI¶Eóe·ŠŒ8f5ÄÝÔ~3‡ºÁIùC!sú9óRÌ[„RÅ~kɈ— êF]ß{²©gg‚hj2Á܈°¯QDD¨(±CEÈ{||á(—'¤«©Ä,Qã¿¿A¼˜Ì¬¦›¶1Eü†Š‡Z©JCÿè3 t{!?rˆ¥¨™o5ØßsPk'÷µT??VŽ7ظ»VÖcìVY;†0;ìmÊŠ7÷Vunçq¤'ýËduUç$LËfQ ¡†A+œQ4`òœ|ŒCk.)m:<œ2 Ck¡°i;äþ?n£­·O…¬¾ õž:ôï [Nß×Yk¬–YzèaYkWX´½AšYÇlÞÌ©v}H`àV@‚æ¹h´ÔGdu«­\OfÈDȶÇXØÆÑ?n%Þ¡Nri)îþ錻£Ú?nÇ|”CBÌ…üÀ,¹¦?0”©Ç¢ÚÜÑéC™øR*üä•´R‚6ªAoæØ>Lõ95· ½I")¥7R´j%®VR¸Ì—òÛUxâš5θ‘žÏ›l“4›÷ ‚?nÀ£Ps¡°tén¥¹LUñ Š&2Ïõ·£¥šÒÊf ¼}$¡e¯éæ‹Ãi>õW»OÔÀ,–›Ü;ÜŽ?r„©Ï÷¬ˆc„¸Óïoô?n3?0ÖÝ’¤¾×î°{?rŒþ-9âeüƒ^vÒ´¤­#j~j@ËŠïé?0‰‡‹qÜ=~pRËéjîbÇ?0È3Ñh,ÏÈbT †ƒ Ûo®XLíVüZØW°_×fŠ4‰I• q9²ŒÁ§KR'Œâ"`ħJ)YîËŸ¼(¡?n`#Lcd†kæÅŠÏúý„n/@Ac‘sÝQ‰”ÄfNºDz®dLÐ_ ®‹@Hѕڼ4¶k/ªª|é<–R¹}g¦{ù ¦&…õ>«æe…SP£Ÿ¹¨ÂÞ‘HÌägîu)ê+ž,¾IR$ÍK޹E”5×£‘iÏ›hG¡–1ö `ȲcW 4!†íƒ™² ¦(K >%¦T`½ôrÔErH)yF¼’ÊäØçÜB V\·Ü…lýêN81õ"WU?rûg¡¤–ôõ:¨[2ò·û‚´Ê]ÒcH’?nòŒ‚1|[¼2²Pÿòýac¯¥Ò”…¡ÃKh¦fŒø[W@Ešõå<ÇäÇÅÁD u;=8`.>íз¶EÜ=d²‹ëëò/ÔÊm¹õÓØ©¼ ˜8ÖaÉ`j}©Vò›Ûõ]+„›l@Ö©š!´%í¤e]t7Kôê Bsí¦Óy•;Õ ß¯??:þ;ˆЬéJ:}„bÖÊõ‰¾zjÝÞsv¬60Ò‹$=&¯åñ¦gyÎÖ$`Å.vS£‰dÖŸ¾·üØ¢3édZÚéE©Jè³S èyÇN??ÒÏÇÇbåBk·xÕTÄš€r¡?nŠbQ Cj(Õ¸&ÙÛ"³…UÂØšb J€„޽§;j4ÇCÀo3‰¬Hœ””‡o’ ?0ìß­ñsQ6ª%±‚˜WÒ×Ð'×r;-–_þ‰¥ÊP–P»h¡K׃ ³ý” ºlñR Ü»>§¤Z\¡d£žù›Xú±)·®ãàUÌËÍ©¸'nGcŒõÙ˪eržÉë»Ä(ͳq“dý>-½-:;¬€ƒ:ªé½"ØL§y91Ë\ÀPS‡hsF§µñCd´No†7&tí¯±$­çR¦;m92¡??§vЍZ~Ù:‘Ö5Ãaá)7½wƒauÊÙ<“ÚU$û?0ŒºûQp¨uÆóhGãík¯ZøIf6v™¡Ù‘~Ð+ªÇIyÙHb”M-:ã/òžÍ;P>·P“î{Qƒv8?0ñÖ ÑÐK×ýXýÀòXØL<ßñ8x:ûxf¢¶y’nŽ–Üq??¤ºÉ0T›0‹:6E¸†ƒ¥F~$¨ŠQþTv_Æ¡ 3€,£÷Û¦·1³qUÈañ"5݆[?r‰«À)T)Õ,/…šøåÖk&q@§ÈÍ%ýÊszØoa’Њ¨8"u_‘\ºžÉ0¬Ñm7¿äó{h$1Å["åü¼UTk2Îrqý¥YÖ—Ö¿Y¡lÑyÖ°ª³LcËAÔÀuóò²øA‘ÂçÓÔIÓi¶áä‰2’*“íC¨•„DIón£Ü…i~¸"¹J¬=¦ê¤Å ]j*íØð=ûhæ7“×B”HX[…£ ¢ÙP·ì|§Á8†P ïTôÂ÷àIè>'&Ï2Þ-©TàÐÏÕ2ÂP8°ƒFmÅ Leq2y0œç3}-²Î :ˆ(ödCjñö0d*Üg(q*{ v¬ÞQÐüðcÜ1VŽ‚%2 @ ó^>Aã%w¼$Ýlø?r?01BïÑ5¿äÃtÕâãêK¿‹qȈ›X#˜Ö¹Ò³Ü] ¿r;¹}h~Gw…K"¿ÑG}­.}„RF;Ëc®ÒUn7+~OÍâe·ìªîYÅÞÞp¶Ù`B&«€V9wÓk­™};{Ùíh}N7öI³lÚiî:_Bg®3‡wzß¾(ß9%Ò ×ß3]RekoðÉéÉøÁc:ûÅ‹øü ’p£”’,›r_!Rè?0OzfL”ǨX?rèAG®!y`wS”¿b¾²M4»ªø„Ô7€±æßÚYXúÍÇù[ïcèa¥èusrr{z9ƒCt{±i±?rL§´æ[ Èi‹¦€“®ÖzÎaÍÜ®ª¦NRüÈÐQ@lXõIJ=¼Ÿ¹$îêÊPa%šìáT€3’nSýË Ä pèL]fÉ­Žæ²S¡öžÂñ˜Ú8ÍbÖ®‰7HùgºÆÃÜ}qRÿÐêš??©ž(Ϊìþ?r‚ÌyÚ’]½H‰w¶¦{ɤÁ¯iMÖ1Iz&‰B–ÙŠjãObÓvˆ©O1Q]”ö¥¯a¦¬o5r€’w¸¬Ìx?0eN´N“šÐuñ%u?0×{”Ú?0Ö¸ÝÑaÊ‘%#Dæ?n‚6NSÏlJhŒÞ1õ|ÅÔ¬Zͼ¿(ˆ©ÌVæÅ&‡<`kWO=÷8cèÃ?n+ ¥o¸ä…>|ð\‘¿_(Øm6??yP ºÇøT÷JvœQM¼ƒô¤W-Ñ[÷uÓHòGÝ×´‡´oì=ö«$yGÇ8©œö*e¶aé öž”!žiÿ5òíõÆ`wÊ<’™áå~l üS×ï•,z´˜öçÊ9ƒ/máˆyQijÎWyyÎÓÚQêb)Òaœë~ÅLBñ7Á<½ôóêh× '>`z8âGÚ¶8q™nW* ëTÌ\±ª,A±úG× AÈÏâ_¼Ã­âjË›~?0kýpÿøÂ>Îçþ¾Œ&!Å@Çä"#ÏG`†s„à%>Ðt?0ó{CÁQ͵‰ÎwgµBe‹6Átÿƒ-ÆÄ½fÁJö:Cv[F1¿TÂ(f¿R€t+5ü|?ne”ªÁOtUEtÞ‚ÐDncùÙg& ¸SLâg93¨Ú)ø¡=Ú ÕP#Î>ŸRg}ß®™€RMµBJ-ÁqçvX;ÂáŽOÀç»V% °ÙàãC*tm¼½µk§]ß¡†ýFï/ `~TTµjÒçîÒ0i½¼Ri]w,Ó¥0)|m¥q©be75<Û??(•‰6&N??¾[H%ÂM‡¬†¾•È 3[ èO«ï¬>KÓ ±Ò̿ǨÞÛ•E„¬ï¯Ô‚·‡G3zEHB£§âòÌÜ€1zžBNÉU0¿Šy¯ÀQm(°ÔŽL˜‹Z–>=†ßè[,]kc^ xhmäîê¤FGÍ|X]&•ó#%ÏL‡a“5[êiª ànuÅâ¨F'º’ÉZÆ»?rm¡þÅw³Õ7{Åu•mç6éèÂïî°@F¯±AÎÈ4DRMá–7ÝÌ@ú_å«wíÄ4Â]Œ6–§³™Æró??s?r|w€ëƒ$󸍾búm·Z•°™ÓcP(Þ¡o¿ÚGë½ô9U¿`¿ìŠ?r^‹pVÑÈ—>®õ¤¼ÝÃÊù(íÇ캯š.=íÄ¾Ãø^:ºgTRƒqfjß|\Õ¹«¸œÕ¿gS€§7>Ò-Åè§:°ÏŒó½¨_-ÊÙaý¯Õ­Or™¼Â>I¾¢>riê“—4-7Ö??Ǧµ þdnö[þªé™l—èY®“¾YipZý¬tŽè_Vxà™vS9yO-çN®:·)Ò³dÚ`çÎ,ÍY¡öìÏITY_¹‰'ê.Ó27´¶a½kÄÝüÉp³uÊp®Á„Ó¨¦g/@§’öv™±É‹.?0ÅÌî¦1 »¼%i*.<ÿ¥ˆ±ºÑ2"¨ÀZ ®*ŠlÌÏ÷‚}–—èL1‹ÞÎÝ?0 n!Ôìã^„Ôªs3#Y@†2•‚ ‚Lµ3‚¯F`‹ôZ5-Þ6 ?r ºÑyí°™Cä˜UCkF w¸QEø ^«ÙAá+?nc¾‘õ«?0W}Ý…öZ•cK€ctО"Ñ È•™Ig#™•ûð¼Ñgˆ_Wá&±Üî??KoÿÀ¸ÁïÂÒLó… ü®.5÷*K¶ªªÏ½n8q¨ªt¯ŒêR:$;ûþ̸YQhã…u Eõ?rîÄCÑ&xHwÇ‚PÍ´=þ{)EZ2gK%ôÊ‘ÅZò‚$º Jí—3©I;dž%&Ãbcç÷막ž÷¿¡¼{ð¡~ÿ‰òÀ ‡~ƒþ<½BùQ ³&ÑHfЦBªº¯™ %¼øSUÆÀœ$Töru*ûæY<ê¨*šÜv—EW÷™9Jqè×ÄE¨•ôü…¿0g¿´);=\@ÖJ½bœ|Ýže`Ì•ie .ŽÎß”Hõ#Ùš%:‡ªž‹R›+B‡1Îñ ž†”!æÜ'G>Ñž  Ãñ÷žôóE`G´!,<$šÚ£àsè,?n+:¢Ÿ}ôµ£pW&7l5/\•ޱ¿ú™Sn%fJ åç˜áÓÝŽù03FÝU‹{·ûßyí{?0??¼³»áÜßÞû?r$*1,ÔŠåe;Þ#€¶yï9'Êú¿nïBµ!@ÏåO¤ÈD£LKÉä×´=…’fœCàF't™ŸÛ}WÏz KšïÏ¡à•ìnûÄ+A]s_0h¿]ÏåbàêhE)ZBjŠ®xáø¡Ã§°0¨Aµ¥Žö&çQ·”€'ƒq ˆpiånXñåÜg$Ʀ͇?rü„$ó¼3=EV‡…îG.€4FàÖëz;?r,®ÚãxC(5e#n‹[j0"èõ­Ý{³¯1þìýê料•Ípü½©‚´†÷¨J|vùÏ=â@Îâ¨ÇŸ[Hç³ðN{ï}ÄÑhžr6~í½2´²0wŠ-X ä/$)믣¤-?0Ç'þ8DhïþCãíé.†šLÜu¡Ö¦f}"ôñèFû}FÃc|IFÍ®A|Û¿r¼¿î°í`Üú¹»ÿå—Šúï.é2÷¶›b°t‡-¨ã\ªÿØãø+ñÕBD;ž¤eIÉzxèÎ:hRü²uuE·´€Æ1tB¥ÿ£§j·Ž­S’îÒ±??½“ñy)FÏ¿ªŒOÔå8Féýwî÷Ûmûþïžç¯e«9çýûdÏã7Wçã;5h\<ž>.ƒï•ì_À!¨??‘òÝžZyõ߉íÓôè¡ç×K¬#Eí?rlw^0à4<`^A…ëV‹`ÍÞXðÔ[ßwÕJö?0²"ÿÝ 7yð©ãS±ç&<HíüêþZáBÅ[Ý\VðÒ¦$3ÒÌío¾§ë*,h{9.¾;]vì€g®a¾ú]ëÜg°œ;?0Ö?0º£P³õËP©Ø&úOrÓ‘›¾??n±pÝPº Á³:¡é»|É;=âÄýžÝ“_ÓR„ÍÜm YÕ‡0ª(ìs×+CÓø}[J»aÑ·ÚÝÚÝíWåât‰y#7¼éûdŽf¶zR&M£¬æ454dÉ>DXX Eœ4"æöCÇì.5â Ñze™ ñ„lå)¶0ã¦ÂÍÎÞ@ŒXÜ~¤÷¬i@Œl!‰ü¡Ý !(ùEå®ò·?rI>íâ6äΫ|Ü×D«L’žU~\ç ˆ GþH4±U«²ÑqH‚o;LQn?0S"*øJŠ–ù"‘`ÿ<7BýÌxN¨í']…ÅS{‰Ð|þ¥†r^ˆ~=O‰¨Vhœa¿ NÓ»ž.€óˆw!>¥KRHc>C:?n‡ Ð]X9fi›åÛÛòføØˆ«&ªç†Úµ2âçFÓs‹SÃËŒÝ$ûðµËp?n;?r6w1$âYÈsîª4¥Ëã¸ö¨ƒ½ó W2ƒ¤Ôcçöß.¾Ÿˆ7ˆ»“(npy¼#v<¥Éx1æ/N\¤[<,ÊŒ kQ’زs.…0'z»R¹#â“ 8<¾ùù‰1NÐIÆÙø"-ò5’P&¥YËY\IPk8ü£ÏyBPúâ!‚t¦ Œ‹ãœ$qçömòôú‰øE}oosƵ»%ãy)Í~k¦ñpÇl›÷r½}µã)|BÙ¦¨ã ]'“O??ai[ªÔ‰Æ|QÜ;Ü=‡]&Nu\è °¹Îá‚Öâ´(ƒçSóGÄ{…‹µ°.††\œ;¨â)¯ßÏÒx„•‹¡‘æä™¤?r©RìÆß¿Åµi×CÁ'L×”…_Þ‡7“Ïã¼’¨ý(¿,WÓÈ£ã6:a Ñ¹V??¾ƒ·¹o¿Ù;þ÷Ÿ˜¼ðo›ç¼¼ôoƒqk¿¾“7j¿??"a«r9dÜL²É»æzÍHëÓ@ß«ïÞ\Æì Œ…]›BÙíØº’tÔ™“ë`œì"ÚÖâUÎH$?rd ´ânï»M(N¦ò'òÀš—fè²u{™YJX(njp5Lh`á«᫵˜UÓJÕe$ˆõ.¸êµê9ýiÛ]<leÀV?r:UÑ _®vy÷?r5¼_­Lz¾Ûîï·¾¿`û‡oN:hUð§pjæM‘³=îkýæé“U JFó™d“µÁR—TRÑã=èø®º²²0Á‰K¸?n تÛð”t’F™BZ?r+Eçu–êŒ7.wÇŽ¥B…~àÆòduÒ¯BînÇÆáÒŸÿïX*1¸IĨøçšè†Ðó¢vNk0ŽÖ!mH%¨?nóCå??4žÎ¢äOãUqâQ¥|O»ûëåFÀ[÷¡'Äu@V6ØäÞ§Sø_LNªWoX»‚“Í‚OqÇßüV*ò@ÑÔÙòòÕWâȧi²7Êô¹ÏS¨Ø›ª× Dfá;•xÚ1xÀO&1p#ý.?r •øH©ã¼ìT[$k”òÕ­Y›X°wëÁ;‘­U£ ¡q‘™óeà +#œb~Æ7AzV.nÛ°ƒ½Îšå‘ÛRíÙ…Žp%V€'päÒ䆄ð»(ìZ´1¡Ì’•"g½g4Û–¬ð‹”6 yjF•æ‚U<÷V”0Á—ò yøy¹ntþéõ)ßá¼f˜‹¡·¬ñØÁ^ŽáJµûVZú€ÁÚ!Sõçï‘xV®Ú[=;ÿÔÉʼnÆèà¹ÆaoNà?rÏÞ§?rqÏþyº¥ÍqXïÅf³pµ§,AݸKÚ_±¹¯ 9µ­]ör®ß³Ôq^L-Xrëû)…,Ì®Axž£:”I??då²b›“áGP(ÔÊ\uÂG0šð¶{?n?0™âèErU,=Bv)F$où=˜`TÝRõŸ'ëßÑêÛ!,Äý,<.“Ú€3²%ìE‚%¹ŠÊ?rÚ´<5«$€àdÉ<€÷?0Á%ävó×ÜB•æ_"da™Ûnú¾ÏK´-•nP=±¤¶¸©\CpfÛ6‰Ùòˆ&G«þïË;~KaĦÝ2ßU,oSÀX¤Ãm)â¶µ 7ÀuaÆY’TÅîÄpÈLÔʰÅ??pß 8?n†ôPƒF¤¡Ô~_!™N­_Ž4¡¨©»zgK‘ZŽjw¹jùñãSÜh@­ÚW bÂnqe\!¨qêJ½d`×кáð½5•„fÍý×ö|‚ªQGA“ðÎÝ>Šb6v¾£MÅ'%¹ï%}˜{f#?n¯ô³)Ý‚²‹*Œ°eC(ÁTUe¾aV/ÍåjÐlùΩBÔW4ÕLáUw#E‘—拚§hÆ8ù/É¿õ‘®†È%‰Õ(wÞ@® B,OžÉ|èw™Ê‰î“SZÛsh6%™V×ÁÖS'­)i”³ñyK:U ñ§¿‹”JòhÒÌÿŒ¼Ú×ÌÎv„!ÓçÇc0‹ªåÒé!Uô.J)Ka5A‚¶9?r¤Ì&ÎUQ’è—q»Þ£ðo bôóx­)©!4õ³Ñp­ok€#ÇIÔ½:ÔEˆëø‹It æ?nÅ`)ûžEÌ—e4i@û_tZ×"¹ïd-FQ!¥„LÏBÏ SôÎ;ØýEËÂÛÚΘ:ÖïpÔ÷/SEMö ¸sV?rkнœhIâÜÛµŒ—cúÇ´Vê¢<Ô£Òñt„ÐødO†o??2Ô]ölü´>.ò=Ç ¾Œäò¨~XzŤ‘_VMinnÂòí+*†<‚pa»Á:4,GFX_8‚ÎFL¿zÕ<|CÕàLþ©9^j€ÚKè™.HüA–\øÐ˳HëRZ’ äŧGäey”Âî…¿—\Q*OlH¦H"êôâ:@Ö>Wý»àõK°eImW1cŸôÉ¢¥ü%ËÚ»s+ì(k0‹c…Ùxç ©¼ôÕ3§è{¤kBzÛÝ’W2ÈVï]ËÑ u($ÝY £Çy??öñoЗ„ÅçtA-f9bûºI¤´ªŽR+,rÂã?0Ç‘x˜€Ø\â¡ÊÚ«˜hžëÅyº“è ¹Æ^ŸcÄ Øós"êši£C\w–†ˆ3& ^!n¥J\.Ê ÊØ,â¨xf¯|c+Í7U‡úȈí ?0yI‘WIrM?0š¿»üÄãy0.õ>dŒýÀŠÍh±û"?0¥cƻϚ¶rå[+êê2µ³Z|ã`óöÝÇa T0H>oø$¤8b?nþP»ô[Û»òl•³sƒ•ÞÓKTh›?rz™Ôøyq˜ÛPG“0I£3{'#ÌÀ)W¥xKzI¥-/IF¾â¬¢¹÷ ”5òñI{„“LDmD §¤ g°]°’÷fSŒcOõ&–D×9&½_Æ„ e–Cž?nËVå@”~¿ë0,<ªŸn°¨4tE[îÎ|mUd…Hâ“ÁdôÍJ`JêÍÁ ç#¢/é„MUM©%ƒsöÄBËr??àëÁ2Õ0Ø'Y7âf.Ô†KÐ×$xî¾¹Qbõ»ë{4΂ãÙõ‹ódÆJîÏæ•%ë‚sZÓ:õE‹ÍNmÂèp)\òk{²RFʰ{lšÈ0KvdËÎø¹ªèn¡H“Éî-Ín -•k‹wÎ…êuæ•Àƒ¨øØ|)Xhl@cè³ĖµZö ë†Eª´•™á2Öm¾¬…÷Æ£ûÅþ?nľ%”©‹¨”å a1.ýTÏjȬ ‚dãU??ä0†ÕFZ†z.ãEäygY€]µ"NGK=¸ûÀªZ¯—Á’ä1p%ê}dfR gó.{ äEþ˜@gÈKU½¬«o”1x¤^K¿«z÷q»ŠB‚þa^à_r™€nB¡DÓŠTó{»©c/¹±ý |1׃N áÿ‰õFÈÔ¦/Z§ÏN¨‰ì½4iO=DÕCü÷èÍS¬€|á%.z^M€‹£V¸÷¼æ&‹0û˜ð9’}wAYóà(³?nkv+a??¬%‰²‚÷k‚š~Ñ[£ª¶~H6õf,»BÐ,³ûŒ7kÒ|ÄM)–ÝŠ/ç·mòEF%OÖÙ Ç>Éeš %sK5N&H¥%GÐî÷“ý†ä¾¸ß\U?nŒo\¡ÐAôY°(F݃žiÐè·)0öþÒ· VæÆèlà^?n׳S*-T¡‘ë&méà,Ø6ˆÓKÜw‘ ‰XÑôdôù³Ù—R3EøˆÔ®¿ÅÔ¹l‘$hz$ˆA Unjh¢J¸ú~“Äj½=(A *ù4qab‰«Þge¦ub¼ÕͶÆ0X 72]á »d)NOzÁUa8‘9cÓ¥ ¬üOìE5Mú(TçãÞ§ë)O{sù5ÄJV—d0Ôk¬)[$uË4´›é{ì`¥~âýÿµƒ|þ»ÿ·³›±óÿlüý_8ÿ‡‘™‰ñ??û³0100±32ýçüV&¶ÿîÿý_Õÿ{4ÂøÉ=@ÊŠÛ‚…¢Ô€]Úäô´ÎÈ??¾R|Í‚Á¾»áG¦Š gV‡;‹¬EÚ?rn-Öþ)å’J6™„PdåwÆXz0Õ&×í¸¸´?råÈ"…2ÆÃæî$v½b”Œ—£Ü›†m9éP‹ûò;Al Á÷Ç•aÙ¹= ˆòü¸‚ÖE9Pô¶¹5³[-C–Y±‰Š#zŸ˜¬Iô>¸h¼ñ>s‚XS€Á??`ä¿(ÝlkWæÏUñdëqãVžÜZŒÓ9¥˜t¥ŠM…ÀØf#&˜»AäžÃènOC®??ßO‹P ;RUÊDsËfR$$M??³‰àÅ‚}$É(fXWé­0`¾°ôjVÅCÁ{?n’]nøÀ}"¶T°”B‚TLŒ.?rPÞUÐvô¨¸pÌç|½5¦Ÿ??TeRÅ©y´Í5ñN"VTåÒÁã™a5qþ8EÍ‚ û£IrJ5 †åþ1ê;‚9Ê\ôYçY×z?nKIZ–EeM0FdþO°??eªâÀ–¡œW6“–Ù·‘½çó(óS%Öˆ@ë_o0<Ô°IéW4Z‰Bf¼F™&®ozfA©H?n?rè¡za‚2iѰ[ÎuÌo-h•??Áq??6ä£ÿâ™Pª„gªâLýÉá,ËšþÐ+ñOµ~ëñÚä)P`Ó±¹›33»ˆÚ¦.6Fow7³qB®™·v×y9Ë|·wîhhnN^EÀšÇÀß.PO$µ÷n §%Fõ¬êçjö+¦ÓØ̽ÈÞÙ²n "+eº­Œ g™ °Å UÜ7‹k"wÞSÓûØ›£¿Õ>‡_¯ûæê³·Ù‹¼Â”k¯Y ÒšóiÂ,¸X9êØýç;Pè)ÍSÓ»ìvÏOÉ•KèIu~À?0þÆÏ05töüÏa`ÆÎÿùCðÿzûÏÊÎÂÀÈÂÊÆÆÂÆÄÀÆÀÀÈÂÌòßöÿ¿ÊþÿS àÕ•´c6ß½: ÖšLΜô}m??´ÉÖ8ÓTe*öê +#¹ï/¬,ʎ軞 ¶½^©^Y1U[ÂÌ=]~݉gn6j'щ }–Ê?0+\°3úÝP*ÊÌìr³¦AÇ?r3u°«Ì¡üŠ\Ø‘šŠÆv jãbׄosŸMêê}E¡þ¸ˆJ鯉W›©•Œ6ÍÍ!†®Ô¦M2°^$/™)L-qýõCù—fi·¿ç…Õå@qD“qö´j,°y@$úiJ—¸ÙÁèØ"±Vü¨õJ-/Ç,Åý2d¼tˆ„É ¿k?r ¿÷’¦všá_ðàÚ’Jü¶r‘æ‰FÒØ‡?nù‡ç­ôsž‰¬Çj¶RÄÕe± Ý\l³ÃvQ|Í{Ù8è]æãzqy"??³PñIÙ¹¡?rpa,X¾Xt|ú³Â·pæª.\‰s3Ç}ŠŠÊÍnß?0%IX’,0ºïØæ›}濜ÏÊ™FtÏ#¼æ"œJíX®@àë®aÃqÊp?0Á­N3¸Š¯2í~Ž ¸¢Íhâ'ØEɤrúËs|“‡‰ÆØ|[/†„ÂR&ZG?r´4‰}% Àâ0Ó‘I¦Âó.µ¾ Ä‚¸?rã¯h‚pR€-õ’í{w° ÞÙ$=«tÏÔEôt¬5ôc{s_縔›c „¡‘°X«Ø\ìKE1 ”‹À\Ù!æ6M¯epRWŸØ·­ _È›ª–f$Lí$VËNxý@JVVÿaÙlÞÏøZˆÂ’?rÐ8x‡¤¼­¢áqj”²nÒdÑ2’â‰=Ÿèähõ½CQ­ÔŸ¦<Ò©4túNÞ‹¢Š'#š$¯ =J]c?n^j¼ªnå6YÓ:÷¶>¥hèlˆ'~@õ­+þ{¢{ܨµ77 põE]á´ZrÀ§fà­¡ð5ƒŒT«ÃvLâ">Ií÷ªœ^Oªq®'ìχ7GV5åRÏTIGÑER£;"ÏDªF¯þ]£³][ö×’ÇĨkY'œ^óøé¹Ûq<ÍñÍ7v·2î yüÁ÷˜Êî?nh©’1¬5šËÍ¡&¬ŽÖ941¸ÕJÛÏ+æÁlèRêÝÛ¯ ˆàìS–µõèׯU±z`??ì•q²|ÄÐJ.a¥Èœsj¸ -²-éæ­÷r<³h{øc,•Å̵À³mç“s<@á‹ežwmI?rwÞB½3•ë§™QóaIL¤ËËœ]¬iAN$®5xiš(j9wâJá{ט2?nñ­˜•1hÒÓ»>häÉ„\w'ƒ??C‚ÿû bâi5äšæ|~0È·[:õ!Û UÜ0@ØŠTú ýªm1Ú³ËÞ1‚‚ÓÄᕃ×Úܾ2öžIkØcò6QMwÖ»šèp„©‰/çlE^í¿"ɲ&¼’8$ÉìkM^ÇéX‰X½zQŠIdwro.£jÎŽ]P?0È&?rÜ¿?nþ,ÏAÿ ©È³8‹K£Nb‡ÌîžïØoêòpòœ“Y ªE³]‰FJÈȘ•¼û¹Îz¬-Tй9@~K?nBÅ®äË»ÓÅgØ»5îƒéëÔtaç«ÙÆ,„°†lB›Ý#Î&ÂÄoBEÁŒÙÀøÞ^D±Ã?rãQ?nʸE¨îg²X­ræ¯RüÜûŽ¥Ñ'E‹ì៨ú.µçûbÁ\˜t/b—ï;RIU$D]ÅÜ77ç*W÷H( mHõóeþ=eN«˜aòLZ§™PN³mØ›š<ýÚëû{Jiâ‹j<Öø»ª3àõÈÎ6>Ű{×w×Â[wB?n"^`I'û×䊗‹“'.Ïç8Ù“^äQ¿ „&kÇyD1ÆL2¹˜áL½PZh?rÛ|Jº¬d¬fDô€Â—Ü”Ug6[T.ïžá¢³kš%çê??'r(cöXÃ÷8.•Øv«PXY®vç~ZFnuÂy¿¨nRïK—}AÔ=ʰ‰ú™âUø™G„‡^ºXŽ¤Ú¹ÇCçÞÜÛ¥™¸H2@ÏúÑ™–ïß?n%ÃJ4çܪ´oQu ´I5ãóÖ È{öÀ?0]]“²U m!6EºúYb??]ö¯hJVÛ£ëÅÍ]Û»P©8eµ…?0õÜ·›ì?rœ ŽÝÏ›ëF9¡OÔÂÆ˜.ƒœH’9<̳ A£¤C‚FêTº[æ´*ƒPù.%èýõ^+“¢)CÍ’4~÷­‡CFO6̳ æNå)¬Jq5Äp%æIq4sŠãæ6Yq5¥ yŽòÂôž~ÞAi›ûVê1ôëûðû|‡pv#3Ô÷„G3ÒçªÈÅh`ƒÒzèùbΨm:SØ¥f2N,âÆe*£Î3DœÄ6£ÌiÎ ´4ƒõZ…é•éòåœX…Q‰V…¿Kа>”öþKG!˜_‰“»y†ÀØJo¶ëÝêöXÊvѪ_€+^€{tÐéM²ƒÿÒDØ>‰Ó_¤§ þUmÄî>ŽOz ­¯8@á©BNæFW+¡Q¬ZDXÄ5}‡…ˆ ¢úÆÞê,/´¦q>’w‡êŠ4‚ÍÛå‘ÉÒÛpÊÞ¾9oË …ª»zu3K…¸’.2¨–JvGÁ"•g"tëgß×`ŒÆ]'$ÓÿñŸ‘¡¹«©‹‹©‘½ÇÙü??&F†ÿ1ÿ™……‘‘•ý??ã??væÿŽÿþ«òöB~@Àÿ»Ü¸(¤ üî•°ÞhœLç9jº»“NÈNÒ88ï'5\wÑÏ'Jµ&ùœ17÷¹?r¥iLJƒ‹8D[‹³1|m/`ÀƒB„ª‰Ô‚ƒZ±é`îUò,ÎJ°‘U^›õéCau¹Ð?n¯ôI¼¢O~åÎ6ÌsâÕÉJÚ„»B—1C»x_w}“;¢e3Iµr%Vvj€f¿d>õQÆæá@Ý’ˆv•òaæ½³cb‚S3k]F¦8ˆgLGa홂•Èt<ÄIsç~??ÿ;f;^B‘Wk?0âU?n"é>3µl+ã²V¥Fòìkêb^ëDÓCÈp7æÙ5Ù”Ü âCãlŽì`JÿSVÜÃvÖ¥³PöŽgŘWÏH%oTZ¬ƒ]FìAŽÚg§?n¦U±Oü§Ô†Ø?rŒ¹Ÿ›åqþoøBw2‡1ÿ1iº‚ÿÅžm˜œ+Ü¡?rl-ª¾Ž,ù‡¢Z‚ÔÃÕî R—íŸÝ觯Y$¶VÍü,ž”ž^h£ëëó¤ñÑJÇÛ­n†fRu=ÉÃS¶ê4¨™›˜äܳ¿ßWÇ›ñÇ—Vúj§m™ªªòçÆ÷QyPO#'O³ëްÖKçÞ[ÖVOoFE•ùe·<ë¥üN¹UâÕo¬«tœuMŽ'Ï9Mþ~ºDQØÅÉqÎûrœ#¥&æœh@äˆt=< żÉÍØ4è/gÛóüQþœ»áPn}&à|t¸µø¼ôðäÒ*À\—$.ò¢177ç??™?r–…®›ÝÚ\šúpèjæGtÚÚÁ?0W(¿?nÿj¬3¯£öU]BWê¥Mi÷ľž›ž¶¹ ÄúaTQ&“:¥ßSÒG¼‰¯cI&`!,'?rµI÷_cÄŒ°ƒ{“šÑ–çÝ«-¹@C’k…þ1ó·mË·a¢Ÿ` nøP Ôðhp¦X{/ÝA?03~e²ˆ"1v/¿ ïì_ zóü<`…ùƒCZ­éœð¡RœU×jq_E(h¨“ÔÃÕò~??j\ªÜÌIÙMªk†ÄÉ R’ðõÖØrTAõF¾õÝBØ…ì¤SÑñ‚p£_ŰGÆð£c5`VP?0eÊÜ7@¢Y}=?rq„S0fªÅ©¢áÕJÎeÓ1‡ÝŒ ™¯`ÒËîLš”LÒâ¡!½ÑÍÈn‰jS›WÙÂë%h#†ºÈ[Œ´9ámš-š°špOÒ95ÏÂ0ÔV)é!A ×Ã*8¿üÕŒE/M¤G?0i2QñÆ£iÜp +#‰¶¬©b_ïs†¾â)ï¯Ú›â·´??vkîe 㟸ñ1UÜ©l8dÓJÀŸaõ­VÚNÆ#ÅXþë˜à{s©RnýÀê¶`+V3z¥ÖÈ÷¤ìÝ@Ÿ­J>­Y±*FÇá^?r-¥™ñÕÁ™e‹ÓÀœ+¿gók^³§?rM7Ž)ÿ„¸°™Œör™î¹)4ÎX)^êÕ?ns ü§h„V÷â‰1ï¿g<¶ë€ônÐGùÍwvÃz[…%ö7Ê:jèðJ³¶kö?0rF¸ëçÍ«£ƒZX&CÁ{E,j˜äXáðîl•pá:Þ¨óóNè£`ˆBD$«ßŸÛh€µÿÚÄ ¾å©)Õ*êT …’Q‰+=ô´œR_\PÉ¥—)QJ˽E[»iJZCp n3F-M^l¥£ÉبTCo?rnZC";™aÉøn+¨YÖæúG‘ÙžN®´]!=˜aØ(>¥V©dwô/ËñÝš¡É[¬Þ†.ÄÕŽ¤Øàe ˜£»ˆ)^0/)-˜O&¹íxV›Od’áØ&Žh2!Ž2u*Sì=÷=®ã“«˜ÁŽvP2?0ö!nË~?rJSFߊà ¡*iåb¨¤jltM+UÕQ°rÿí%>µN’j7éé|Àjf¡Nâ}<¸ôWéãÝXÒ1HeÓvqâì‡gðÝ›¬æÉýÁ–†¨èÖOMÊzÀœÞʆdÇÝpÎÚnK¬Ó{ÏÖZgq¨¸sûß“†—J ?0-ŸR¡óÆÓÏ·œBŸAí‡òýþL¾0vµL˜6ÐåÔjB˜d¤ô²Y+É8–êNÀÇj5–‹ù»éʽŒåf3Y?rÕ®9º'Âq7aÑ‹üÛ-WŽKKå¶ûšû¹ñúâ-Eó?n¶?r ·ä?n1lð ê² gÜÔþ]ÿ·4(21H6híJÕžy_u!1\1ƒ5ÖvdÁþùá5H5å„x—ÖÖ‘°³ß?r±«ƒæ©±,ØÉœ†'YDþ‰(¨¥D¹™­R•aÂM[Û(µ IvΠTÎ¥ÍÊÖš¢¯óÏ™˜²ñ,©iH‹X¡\Q¦£¦k6ª7?n®Ñ–·º´A_ϲ‹Qf’5ÚáȶD‚¡•ÝíCEßS†š~‚Mu$˜]> xO£ÕÊ‚(nL¬pÉ•-Â(Ó¤i`™½7DsWž¬Lœa;Pá?0\¹_`½³??VF– Á,ØF¿1âĨŒŠÛ&7x¢-’““—ÊÐí8Œ=ÞÛqèÁ!ëÅÙ(J Ð«¾HÂ9õX&hâãßÔ ´I/”ñ,˜çv²JbfHôÓžÿ¯·??•hò¤ÿk±Q³Õ8°·ÒÀ×fÍߦ»èkZ§ð˜wԃɆ¹Kmÿ±0ÿÎâTNÝJ)%P½@pÃ>]sox¾Jp3Îp>MÐ:ò8º¥‰¡Oø´²„±Ù_Ã*X°– ux%à¤k-Ĥ(X°žÃl<&ÌkÄ('Zð€Æ?0ǘ!Ã:)1Æ¢l†:NX?r´·5ÈaLf Ax ANÆVb ¦´‰‚£+…µ‰ž8)SºäíSc°ƒãæVɼÄ6fƒrÀˆ|/ÒÓ4Í2–ŒÐGΠ’X!€Ûó¢°2WJ‡µ2bº ¡Èmc@À%´ âÜ)À'ˆà‚WÿéÆâ£pÞy ¼@‚ˆ›P÷ g=€eÊýæ?rL]¸l¿7„EãbP§„lûˆ¹fç$묖+0+½Ø!£‘n`¸éØànn¤Níæó{A`³VÿN¡ZAƤÈßÍ™”˜ì¹#Âw$qL_àÀ»ì~ªD›Ž?r?rà+`ü:H÷Èp›K¼ð}ñÄÈEâ??ÒÉ[üm¶„¦™©9VJåâf'æJD#qd–úJÅe¿sS@@LÏ>ò¹Á÷6 ‰Пœ¨PƒÃÑh½`-?rà€‘“Ñ4Ú…Ðol1v›A†üNµ . ÕTKªaZÀA‰yð82±sCL„K®.Ô¹jЩ­??(príÂv8 ×Wâ‹qA`Fë§@ÿàŒ?r9ö?n•îÔ±2ZYïÛÙ2v'jZTí¡I¯Y3+—ì$³N&ña ¸#˜ÖT¡¢8A„'y—ÜŽg©'.\ Ÿëyäè²??€Sk¬ßmK`Wý\=[njbOÏéeQi?0&Wd‰P«Z—ÁUÆä€å÷Äq&š‡ùØ;­Âà?0<(¶é¿~2x?rd!Lö??²+~–DýGÿ½0YvMºph>=Øó§•|YÀܤmª¨ëM: Ö@ã´Ã¢Iò쟘OnÊŸm‡•˜äIò<9°®Žº.6LdB ~¾s²g?0'›h˜7‚.B•xËå /‘Û€O,dc]yú˼xLK™¬ Fïå û+•ÎÑ‚0-åyÁØo·§í©L¼íÈsFG©m;ÆÍ“U\Uý;ŠÞðº´j¦’¤½Á6ú/ ¬?nÂÊ|Ûéð†>8Û×ÚsK‚{´ ÑÄ&‡z_8ûðnè]Óþp²ƒØ&ºÕ»Œj×;ó???rcB¯üFÎg/­r»ªq[2p£Wý¿–© ¿â×dó¿NÛÆ­Æf«¡??¯oFþ’Uôû}•ûèÏ]˜ŸLÉjÂdXŒ›ôñ1)ÇŽçî&ü<¾ÝCÆn˜=®Ñ3E?08-£p`Ç{ÐgPD¯qHã’Q[¹JÍM~¶\ê¿–%±óò(Øâ¿ÔÎSDÿjç.¼Æ± ›cü`Œ46÷ai°YMð³?n¶eÅ)mgì¸pð­à­ƒ¬l°B`ëeËç -êq‰1Þ íÌxŒÚzqšö»ý×jL0™?n›?r}å¯ôéUt„h†«ÄÊžVœ¨f5î ??I¾Æ¼ÔKFùSMqIØzIÕZG$0R‚=²fêŽ?nhÎ ¥k|´á¿àëHåÞ.PYëè`kP@’¡¡¤î»­#šfÅIãŤ‰Ä‡^waN¶¢”dFéÉ4^8”cn¶ ^8Ù@ÎáÝ=Z¶â-ÛK`Y` ^ÒSs,†²„-;î "wˆ¾‡Ðáÿñwm¡hUôÿ^V÷§åN??‰DJ„:ÁDKw(?0Ù*?rOñ,?0ûuÏž%°ìºšV‘ ­i6PŒ™‚ÜoüeuÁ‹¥a{¥jÚ~8ã^RKØ:Ìrnñ—S$¢hR'êĵèT»€«??ƒú?0àŸå?r{`Äôûí,Iÿ^×™°p,Hlß™~ÊÆBmãþ#Y²ô88?nxÓ}ˆìºî ƾ9˜‰hyFÕä?r‰ÑZeûX¥ùÓŸcWÃËÑw‹ X²ÌÈ{ròçÀâ¥nuá(…̳7¨nçR&”a7Ñq®¯/÷"À—b¢ð•-ü6œ=N„^¦×–ÉwL[¬¸1ãB…)H§†l¹o—ŠêY9ù¨çñú*æò¬òº»¹¼ßßñ]éò’ÃmZVÒ®¾zÈ>°h­Õö½³ð±å8fa a §UƒóógÊ49È4ÅêyÑCƒÄê·/–„ß2°ùhìÿƒþ:#÷tÁäåãðÒ.4zÖWðøº_§$™ÑïÓo|ˆ¦UÖÞ*ž&y ¢"dŒFw;MÙt¢X¼ýxo»"­@ðãððÒ!‚”k.’ áEøð,°+=̳SVŸzDþ(j¸³Õ0ß^Ö®³B2NÙòc*:Mó°àõÝuߢ¯6eÁêkÝ£•”/üŸ +›Z9U×Nê¯ç?nA¨V*†>2'472-þÃ1ÐPƒ‰A{ŒAŒ‹ÁüÎ °??9$ R^xŒ•'¥ ÷d߱㥫îàon¾¶è¿d¦ìxá‹¶˜õBÒØ¥‹yw]8C”ò«ç*;vúí1!Ü­}??†.i½—xñðq¤q÷ðI ·Ìsu,Ð"¡•<6]f4ãMàð¾n1 ç-TA.|AØf7 •e$÷íu1_ÃGFŠ×î,«`cý´ÕÒydž‡€Æ<ÔeˆŸÍ`Ž`€SÇí'¡rÈO+.¿•ꢾQ…mw&ÎÃbH ¤ð9X®©ãl[:ëOÑq±®sú¡Üÿº˜É ³ÛVdrÜÎa>$h™9¼Ì¡ ‹Ò`”ß¼¯:òçNAͱŒ °›0n'tò‰ÿ4´uyt뙲殶úÞæN:AÊžÄcH:ÀøÎ}ëúŒ£}÷þ.òüÿÊÞÀ âí8öâ/Šuz¸éÀAÕ^„ÒDS3ÉÕ„WV Ö¡Ë…šk’†„èÞ›²ÕG$ucÂ_4dàYr%¡lbçýG™hQ?0+WR–ÿŤág4–_;Ñ?nËmg#qm±¢1GeÄvû—ºˆÎeÚßšX~(¢+/LŸò—3c¡O®Ÿúš ñµ%·áË(a®!&W@OެÚÙ‡D+"Uà ‹šÜê‡!m0B^??7ÞT•=$»•·ß$êd†ÇÊkœ(Ð6¢ ôŒ¬Òÿx6©½UbûÞÞqìëK,á%óYa¿»P%:j${ꋱ47³ÄÙŒ?r¨uvóhÄ,¾YDÛŒäSØ8n‚^”g2„9uŸwÂUEøûÏf†°~õtÔæ!_{ÙäkǘԒzs=Ú­’u´»??f©£ç4zõ¹u‰;ÑäSBuØ.{^Î$£@–T h‘èEio,TÀh~Áˆv³ * ú!Q]aò®yoÉã1(!M[?nÝ1=>›ÿ©€xNuëŒ`€ÁŽ©ã©ç„E py¤!åþþÅàê&²$°jEGšÑ¹Øq]®äž ¶·E°¥cÑŠˆ<Ö¥'MÀûšÉ4ÞË4òµ Ô¨ªÊ7½Ó³ñNŦ edàT뵗ܪÕÊ×WSÄϨéù¾)Ħv w9±ScGÔ,S€—i]ݾ޸6ýäØÈÉ“ ©êzº€#‰$[ §=©*”¼ÕÔcÀT®Z¡%ÿëŒöÞÀ§Üü‰Z_€—GfÅF]vd«bÉ)P^uk¤[q¼x- Ç;+DÐ:gO«©¤nÄKSÙíqð‘vf÷^êÅï÷ŸH<@¤r¡¸àh ƒ•ÿ!’ä)*[!I³±¿ççØJ!Ùz>PE•úIn˜:ª¤WæOâv5†òé+‡ñ+.D`¶f2æ!ÞêKö¹9V~޽q+u-¬eðä ¾ÙfðÃ¥³ÝÍäŒ{WÏ8‰Õ¹®“Šü5])\k{vÿ‘–=«>óÀ‚°ã€ ×tÇo4ئÚèÚT‹¢aïŽ×©4í+rUÀêRóš÷`9%Ðdwë&uöõHMÌŸn¯ÊÉÛÕÑæõ½~#Òñ…¨Ï¦»úÇ´)>‚ËJYóÔÜ5H¤0§PøÆ??W¾&Й'¾PÞ6}át±aÛ/Ò»žoÝ´n\Ê6РÛëÃÐѽô?n??D7Òã™#|m{sŠéf¢þœ†£ªY—æP”øn†‘÷¨Ú˜ÇÞJ<¤SÆT?0WçPAÆè·Ž˜_^ÐÐ'ôþÜtq#FñV§þ¿"Í dŒEÄ.¹°Ÿø&Dtœ~?n®þøÑ¦éK÷gH’{úï‰fO¦h9¹ˆÜEeSK¬¬~Æ^`@ÈÌü,=KxÆËG‰:þÏ?0…R@©¥3Y_¡›ñÍ܃Ù??µ•òž¢²T³„Uˆ0Œ9ô«)Kk–šŠ”ÖSÍ\­ë”?nlôe¹ƒ'³”sCf Q},r‰æ8 ØôÇêÛkH}žÊï¦ùt8Yê‚Hxú׊tOüæve t¶"ÃK??#8C>ϳ{Ð'Œ:˜jy?r ¹H]°xnÃÞ멇®OadQ”é"èÍäÒ/à› 0ÿ-k}ÖJgÞkCé??èÌÀ'K@{e\õ¯F_`CÎ?ríE2†v›aS?0¦®œÙðpVùØ;%?rù¶‡ç¾„‹@>‡h]>‹÷a¤ýòmAáZDóu½m?rôÎn9â)^ðQì »,ýW[¥Ƶ£ƒ$¾$K¾·²¥gšóíK|æ_ÙWtD·È|ˆ„¡¨Ò¤Í¬Xú·Èz.•*P3Ž??XMÅ??ÍÅHÔRÖQmRy±Ç…%ÊÊ?nT??#ië-£gV{µà¶{w(0=QÜ©‘NoÄÜžJȸ·Q‘ÆŸÎé:|ò«Ã…ˆ±II‘ΎȹfëÓ¦š?n??DÎLâMÞRo8×f­”?rõƵ‘•Y&Iøw>ØQ."B%R€.­`€¨Óe´euU—r[a#ö)ÊýðÁB éÑÊEÒüêß„{¡G@ò]j–Ÿ?091}VìMXu2Pw¢m,­·z‰ä*·†À‰ôÌe;Cå4ïÓg_ã¨7|U ’u¯¶ò^-eñ¯ÕêŽU¶‰¼"SÎe²JØ(.’" ùd–Ù¦+C»vWH£ W`'ÑÔL??­"¤IóÆ­Ú/tù‡BXµó)Á]%^Éav½Éaº æO’n¦'½Ç!¯£õ*}5WÏܲ «dÆN<”öT–‘w#²C7N/Ì9o $QŸ*sH½šŒÃE]Iì,a ÓÝŽO+€?rÔ¥<·GN›ùGÄZ´Q=7ˆ©¹žõ+vˆ>¥7Ö>§Øû??×??²tŠ!<ÆåSØUÕ£‰ó<±'mz«Þ’¿ˆÇÁZöŸŸ2Ö?0?nœÁQ'Ïïgz(˜[y8Ü“b"­ZýSlÕÓ§˜r<LMqBPPÊIû¯(ª«Vb…ÖùûtÞļæÜà€”¨‚ Ê6\©)?r½ˆ¿ÆÔ«±±Î9f+?n·ÑQ~5Ù7qL$#ŽÖÍôÌ‚„ÉmÕaÎŽ©åw&D'…ôŸ†!^O²?0„½‚üÒ¿‹tÌ‚n[J08S?rdPûžýD‘Ã>»Øñ"ó!¤Ã–twr§r‡9Á’픽MéÔ˜}Ɉtvlz™—õ\"?rÅ<‚}ÎXVã¾¥yâ–ÙÕu tã¹ßßÜíÂgvO’Øž–—JÁAÞ¥íô=0RÜ£p§矀\ç¦AÚ¯ t`›ÿº}½ÉáÉéoð_MÉ40צ.à‡Y”±4ÍüÔrã0¡cÉõöÜT­£¦¦J¦(œa'j»¸kš=s] ™‚Mtˆ\8I‹8wÜDµÞ¤>)ĪÐãoÍÁ%û7ó¬@ÿöQŠ‹³}Ôa/ú!+Ø Ðdú™Œ»¾›dQå?0>”`äKÊïæ?n”-Ñ_ñr\вctOpMð 3Ý™MÎk÷r¼×ÝMÏà!®B´~n†„W?nÚω×2Ì¢nb¸V`;DšàÓ{€éóØþ(¼afƒ«¦Ä„aÔÜÖÇíTÓ³y’g•4'™¡å?rœAL1_,âdÅxFÒfµÅ ÒmJÙ5L&v¦G®Ÿ.dù·°O¡ ´_Ä€7iž«­ü0–‚;y÷?rŠXã¢7\‰Î&y|+[gÎß2³ð'€ôhõ~.­hõb 8x™#ð?nƒ³˜/®¿ÓsR6??¬ûÆù—¦ì´s+¹±{™T…ç¾åˆÄæa—LöÂn¼xñ÷ó¨Æ;ñÎû˜¯ )X…ÔÈàƒìA,Š¿öÖsÝ?rðqÆdu€ÿ4@9×sýM¡?rjC^Á‡íßÿÀ GùÑ¢?0þÿ½ÿÃÁÉÒÙöÊþ öÿ121°3±?00²0³231°12³0üÇÕþç¿õ??htÆ@Àÿ+ÚhD³v`vçH'ËT€vRàæåQ´é™–“ðŸ‹?0ƒ1þªâçü]W¿îž²§¶æÒ×Ì'ª/Ò«ªÜìw/LË??¬jÉPϦL4õ4 e>÷k "‹®0¿¸°@T!©àÞÊ œ™û?rYUµ/@­€Èqr´$xH?rߦOù‹Ìg»iÖê•À‘øœÂ)¨¨Âë—´ jÈtæÏ!„* ªaM¶Ñµ4ÆA‘sÖ´þ•RæÈqCzCÍ—C7{å×F$“ÖíôŒ ~fÈÉ- ,LY·“ÿ>÷GoÉY¥˜×g^/ŸW ùQ§pIÇÖË7õCòš½·NDè~ý@el*•ò’é×Öš¶wëÝÙÖæˆç:§ÂgÈNL䲯??íÕ½d 'l—P«õu­ìü6»@HV±È“þKýô®b1ü<×í±ÿ±S½±0n„M€Vµ!™ ?rxKåp;€¦ºI…‚èb¾¡x¤Sê{'mÅ °¶?ny~™¹ÍŸ+o,E·áÛñª'Á`“EH5ëf]ÛÊ#/z8'f˜øî›1•Ä<èü„v)ñ·oõÚ×猼¿ž•IÞ]h&ªCÂú<ˆ¢siЦ«¼öýQ)²,„ª?n8D¸O]ŒBX”'½»ø"uõ›3±äKL&jc7_L)Õ¢aâC&!za©Qß¾1¢µ«nÛÎÎ+ûmë!#þf‚ UcÒ²ê,lámV99–NÆÐIy“v…)óG¼¿xšôð <§O„å|Ýþ´ì>âøÉœsï˜ïŽŸeS+Ûœˆ q 1ßó©Fâh]å;Ê̳àÓ äW0xš!!ÒЬ¯ÍL®ú5—LQÚ…ý¨$‚g…"šH +#3À¡š"#2g±¿T×Å‚-“méø(¬È¢x¡ (+×AVR5%¨Ó*ÙÞY9YATÚÐNØé:sh2-rºjŽ!5ÂÞHL„iŠq ûÈZY%ÉòåÁ)Ь¯åã+”Y†»6”‰½CãHýÄ8T-ŠŠB^©íJÁ0×õ(ùm*Û…Fk2B(äx½†FvðÌió'JÑ'öåäx–´7?rg§?0!×døÒ$ÄÒ7š‡ý±ÂöŸ9Zk8EÒ%BãGî3sQž»YItY`ìv?r´îL»«Ä+?rFǾÃGòö•7«¯£¼ÄcPi2ÿ’å5ÿÝ=úâ– yÉASK_ž e%ž¦µ³ÞЀÒr%½¿ïïxºZ$ ïU³£¾ñŽº¢H™YVŠ×h†ÈXMeˆ æ4í­ì¥ÖŒ¨V¬s3#•+ܰæ˜@šæ-zz%I˜ÖÐÃ@¤œÞï|v˜p0?rëwçÖ±ûÊ~ïÃ?0k{mß6ymhÖ[¼žžxÎñ¸D°+QiMg³zzæ9¥ò»¢xC@&Úæ7³7Â4?ræ‚ÌÝî5Ê‚pÜE¡¢Ò¢–ÞibW-†r3=m{Á)Bk3'y^вÙ}°Ù)Ɔ_<ÀHŠäw~ªh«©VÁuÖÉüÛÇÍ[W­]~¶ë<$Œ±?r7V!²ÄNµ §bÄ`I~7P·oB`°HfF³%§óGsú§VM³Ÿ—õh ”l~ôå}ê´­&ûY:KÕÒçîÝ ÉØÌ…éøP¬Š²FÃÆ= 1¥lü›mÊzÔ“Šb«¥Uy¿ß??ø¿à?r‰d®ˆ!¾{75pø ,%`Â85J§”æù‹ÌV2:4ýz¹´œÁýåÛþ ¡ýóŇP¬÷êI……ïZd^UÏû…´„‰}, ¹ ÙÊÚ\ïR„Ù²¿2.‹‰ß-¨ŸtAQýCZtR(ª$nzÉ–šŸ¶Òã˜dx«ÌGY «T®“„HaÆ£B§Ö¼E¶pZKχ|úg¤QDvÿ?n‰Ruv¡K??%¨ÅŠ,ŠDCtP#…^¶pöü#Ä‹)¢±L'܈êk²X²¤4¬zõœsO÷£”S×q¼nªf¨¬‰<¬órWR2ô‰9Ék–ŒAHCW݈P³^ããíí7³øz„Üc5ðÖq¼ûeébȽXň`)µ /9FÔ'RYæÎšR3|]Ó%²´}C—þÄ÷†O˨›}Ö±‰†ðIÀnÚ à¡­Ã•Íb‰¡„·¦f³ÏÞÆ€øÐœää$/ šò êEŒ¿P×?nAÜsµKßqEÌ´UN¢z5•Ê[nи„˜Î¶´¨7PJ kà>Öv¾Ü%/´?0;<ȺFÇ&Gïz¢=k׌è:×<£\8j)jDô ž[ä$’?0v#?0iùTÛxöÜ5 n®á—ÊO$Xç!a•$úJß©R¾ét¼°/ÅúÀMñ9¿Ú‘‚±ö¸¢]‡<}Ð`ç]Á*1B¶&4·ÈHâ„&<”’‰6­ Hݨ̼b&V1À/+”-0…tWÚäÕdZËžQuÛ~Ÿ^"hã2âm誷àxN …ªâÑ`iM1Ñ}à‚„Q8ʽÌe?nHw”kÁI¡–”ÍÂ`ü±éžbŒJ·ÓÖoIk•‹!,y–¾®ªå4/à„Çù}ü:ªS£ 2HêGØ×CËŠ°©~üwÄß§Ås$?nÒÌD±{MŽdˆ.ôlÕî°Rs«©·²Ñ4jó?r¶¹d@2ùwT0߈çx)¢,Ö˱†=£5òáSNceeeL "ÞvÂ(Áœá{i×}Yèqj'cÈéªFd€òbʦýBŠUsw´&Ee1¥4ZÅô<Ô…œÄG®‚?n¡È£|G®¹´~¿EéÕ/LV^/—ï¿W*͘ãÎjÿéʼn¹)5ЂGsA¼á&ë=‹·fM—ÕÆS¨Qž­ÚpøÈ85_k”ß_³¥iE|0ûˆ?n”yV•Ÿ-@ȳmc¨6º,QÏ=³¯^³??! %·\AÓ“9éÚå•Ñûuä—wWšüÙÞ¥¨ÍøD™ŠTi-‡£}ùýAÈËtP«™ÉcPNŠB1™—ôyͤE¢qˆåo¨NÿóYa=Ø!w_kòUÆ×¾P ðÔ^ój"츘©+ïû´Û.ÃìtVÀ×ÃÛ‡R¨AÑN‚ßu×k;vz]Ñ<Œ UöЯ;„/ÿ—µD=¹ñ9U?0³±cýJZÔ.eù 0>‰úåäGáý¾>,–©¢½ð¾Âê=YË-Eª¯×3†ñÔÑšK­î’¢Ûd=7d‹Ìäºsvòúc—è ªOb9·‘êÇ…=»¢3¥ZDZÖä>mÁ×”`ãŽ²ËÆwèÞRx€ÜeoE7OÏ×gHŠú%‚»yW^‹¯æ˜÷ðν…ºwÀ úœ??;™¹ÕSrðâ ˜=5+ý?nÇÆâ ®€+º+F ý bO¹Ýëw±s?n»Õ!+tDö£bêúxÿOÜSÐÝ÷ðý/ g~òÇ:IAòRS&ƒ¾Soöð™Úš¡­R?078bÖ“,ÇUÍM^Eb~–??ã’ü8-Ô¶A‘ÏÄSvÄŽ‡R—µ.“|Lu¿ÐÔ¥N.+wÎ?n é(¦)cyK–çBnlÇÑü¿$·$1,ÑÓgiög`àÞ+»–ȶ†bIò˜‡Èߪ›nÖ}°3‰wôð³‡çÚζ!K;Ä)a©ùã€@Âo}‘ϲ7rS¡e!ûo¼ø$1LQgÔæJz‘wHá´ä6„ßÉø¯¯5÷ÛÔ m§'Ê(3ßî°)ÐxæÐªã[IâØj¿nX§Gì^«Êñù²°7FÔ‰ƒÕMÉû)‚ÛŠ"e>=äïúŽôëç)ÔŽä¦0«sŠ»Vš¥Ý^^€y‰?r/ S_à¬ýÜ/󼕱gUX¨Í|My>EÈN»<³GÒBÔð= ¨ŒÒ}U+ì´™¬¹æ-ÃÓãýkÑo‚O¿À‹±µÙYK%KmØ9ž´ð6è/p‰2Šg…ÿ*©¢5b&ìéŒ;¸Ñ<ñ¹2^®UH¾AXef§Í6V¬V-+t³JÐÁ&mo·ö(H[„â‘Æâòï;üØ!E9‹štm;ê¯K,‘ÎßMbsª„ÿD E[íþñ.×(©ŸñzY}mÄ';zaÜß·‹²™RQôöT #T™Âç$Lý›È„ÛŸðkËß³c–vÄ•äÛ??ï°5UÎ%sK hƃ…óEFžLÂ!!SU¤fEÞﳨïW¢ÊË$1#Љ#óRkBhZÐW¾Ç]¬–^|ßOíê‡ñ÷¨¦—OÄíR6æz~ÂpI]ò®œ¨y9‰µ´Gµ¶t8?rÃy¦Âª8Ñd¢ßÜO§p˜µ ˆ?n ÁçâñJZÎn ›ÙãÍ FY-?0üŒ$î)_Š&Þ´×=!ð9£ÆØ#I2ýYÙ³ltQ¼¬T'RÒjr`ЉÇ8 5-^¢Lé£5þØ(óˆ]ŠÞìÀæ@÷þÇ„Bóky†Hæb Ïçé’ÍhQž³B2èˆC8nÕVVUsuõuÉnlL»k³µ7×ñøg† ¿¸Ä}­f*€M“BùÝGÍÛnב9®}ÝÕP¿åŠôR'@1„Þ'V(èÜoÌ®9šö¾©ŽÍZjȽk P·z|j8µ¡˜hÈr´ eAUÂySÙX†Ö f9í„Ïv3Qvi=,áåèÓ?n0×ÐêÎ1â8,6Ù¤Ý>/öHwˆ´©°Âf¹zyÕ”¼ûöü&nì\SzvÏâ‘í?0ê09ïÙLQ‰\d??‰FüñÚ†¨ÛçáAU/…²OŒ‰Xö^íŸÇxüu6š*ßÈWE¡ÁP&g~Ê6s*?rü²ÖòµáR-P•4)‹ÔPöÕzÂyÏnõä Ɉ6lÿŠ€Žn ŽA-Žä¥dw)ôž"D¸vCo­²˜)²‘¿?0,n~‚ÐßHÃÆâ—˜Åþ¼2mp%Óž¹¾¼+)’SRØ~zß*é»ÍX¯×ÃïÓQÅÑUôXÒ.ÞkÏ¥ÂùýÄ“ã¶Åx.+„y;ß;Å .~¶ß‰úÇT“Ä¢ü¢„,>wON˜—–r$"?nfó|Î)K™ÜÑbD;&Èž’ô͙ĹudèîÂ<Öè“pÉ…ðò}þBØ2â°*?rMó”©Ž7ÞТ„«éÖâ'@6bGØ‚Sßi¦˜›®Y!„yô®ê¡ïDÅß8LçF¨²gÇŠ®ºñý¤¼>´àgeÜê7§º‚«Ž_ÏÆ-jUHN¤??&¶cׯí[So‡ÛÙ¢ú]&Q^ÕN'¥^ÍÅÚ´fÂêGˆ o× ¤®õ¨_+˜è 2)HlíI%ÚùUmI¶²]Ýü±…–YÑQ¤ ÅùÔKT²¸ãkÃt%e_ýæ¾ëù>ûvèÙÀ²jY1‹kŒ­mZK?r¡wÎ'Sè§ØÞï¹my_XødÁ ópö TýEå%í·Š:X¬éÀm¢¹‰ùG׿`ý@.nŒWÛwàrocÕk6îºÚÌK¨qŒLÖìÛ¦³?n|Š8k:¾(ì Îí¥X’×2Ñqññ®¤ÍÐR°n¾·UV¨D—­×oŸ¢n\Uv"[̈z¨ÑVß.è–O òÆ»6v¦™XFoUGQIä]Á`gŠ•;“Làxf¿ë3k‚Ðí³ +#¥qd­?nÐî‘£hc¤¢Ñòk4Ln}Ù5¯@n†/9ö‡¨T€Â=V8{ÁT…©’êIÎDS=›× Ãt­ip24B-Xõ¡í³fXÖêÃz‘šf‰IÐl±$´y»ÁÜÞO˜쀡ð§[7^œ[í‚ü3åîå¦Z§Z¾É”> zYeƒmmvêkKÎA¼ ´^Ï×W¡ôDUþ2ö¼ZÕ%—¡d…ôˆÇBlüÄ$IV,ŒGã•ÉŒxÂ,Wz&{ÝØ¿…L'éÖÄÖ­®)lCµŸÀõÃ#ð"Í£ÈÕgäTYqZÁTKlîqG+šgâ}ñGªgz/Šñ'q?r`!Ëlò­ÑaúL£)5Ì??”"?nÿV&<{ s_z:çüÂ^ д§Q꯷o÷¿vN??QA2Í 6R.•aò`c†Ì¬`Z¤n¢ÉÕÏO¾/ Ý ¶A£…–ÒزÍXœ—\6ó˜‹ày°‡ì¼3Ó«”ã5)e@B͖ۛÐAW¨}Sü3”1×¥û;ãŠ?0=è¦aY%?n¾Á³‚O´u‰Qg¦ ÖºtÖëêN9*¦etoh{ƒxןµúJŽš“jJ)µ³Íi}‚J¨•+PµY>mîÔ¦^„XÒ7SlÞUS®HtÖVœù}#ãs!¤Ñ[5V?n0µ¡eÿޚö¶ôm0M$¯µYÿí÷íîcôCú"z€'‡î: ºOXÛþX¤®Fâó˜_|/¸•¸Õ÷#ô>­ÑŸ±Gˆ–Ûõ©Gª>7"X±4†›¬ÁøAC ³Wz_]Ž ìr„tÀŒ¹±|™2ETP^}>>csÿÅ(',ÿ …_J(ÓâùÎûž# r??«B—}œDŸ}ò>z„&{\(0•¢Ý»µuk첡†D/ heTV†û Ç:P<äf%(ûVÞ‡š°â¥<2ibüÛ÷œnW!êP£&Cà­@C¯šêªZÇÙÛ“¶© _¹žäkŠÓÜ/cpÖý‘º¹A»ñÁ›bOÓ—'!Ï÷™àÚmØ6žÇ‡Cˆ¤Šü o’O§1ö±W˜œz$ WIzÝZÄ¥ æ~ÖjÄô­•b§Ùî¿t?nä…)/Yi$nꔿÚ•9áq¨òüôA‡òÐ9Xo1ø™/>JˆÏ!Âç&¥¸«éRûê€TC&|”‘cË ç‰Cb숺›ƒxO)y›V,²É£Ë(öM¾=]復O©ÀÍMSÌIVƒ-L®BoxÒ¡lÐga»BkT6"€žÈ÷i*ê¶ç霪 ò:öŠš¿ÍŽ!?nû7ŸÅ7'&A«ÁYÍF_WW@N¶ozúúú^vzñ ¦ˆÎŽØ#=Ò#ÊÜí-#ØÎÐç_‘(æ½~îó¾.@—4@\÷’T )¬.ÉêÙu™’¿¡«8¾ŽÑÄúdUY‚ѹ¿@ñ=Œ¶€s·Ó@@.?0jŠº>½Ý ÝéUŠCÇâüÑ«2Œ§qä8‘Ò˜ú'/ŠÍNqcº¶ŽæŒTzñ‚ÀûÆ ê»?0wôYÜEAe¿ƒÏ!_õÈr«€åëÆèÕÍnC”°û°€ƒ‹|~N­¨,ޏÛCûXgþ!»a³)sÄP¯¿}’ÈÅú<%í’B6,?0UJ%)ßÀX¦¨œBeì&-ˆ…^Wû¢+V ÞÑþ6¶C?n¯47ì MÌ®±Îã³ØŒnô3/¦ÉÀ½@¯Ð+±{8íSÑ¢{÷¬îgmsÙ?nA’Ö¢Ÿ CëfeëøvÅ6‹|¬·G˜΢:È"ß??b—±.îQ »ª­1²·©??NäÝVW‚xÿ²‹HîÈ=ÅîRØÃæÜùÃõShQ'Ïwû!ú¿¡ÄBøõ¬™ˆBO|&ÜkGOk÷_Õ¾??j~¼ß¿ô”r•“çBoÒ¯1‹Q/ãŸûï•Ф+Xú,»…[Â!ûêb;Ó†a„žÜf«Ñ¼süäôÐÞ×»ôUâÜÓ~÷s²Ak'¯ øKŽì'e¾Kîº-æf?nmõ—Å=1k'³‹÷™p(e]Qç|ºD¥0ÖÙçoÌÈKü[‡ÖåIœ|-Œò.#ÍºŠªÆÀÏȃB^¦ÌWˆyã?nK?0«!ϰÔÈ\â?n™ºš—Ý1ž”éZTzÌ??‡à1FY GüC2ë”- ÅüŽ'ú¨ ±Sõê¸jÓS!ô äg©¹û$cøWÔ??Æ0´±öˆü??ôf–ž’·2›RÉG\í¨??B¾²Íõ²FK¦¯OõgÒû=’}lËÀ¹G7Ĭ!”`éÍY Å:ÿ§~õ¾8+qk%ÇõµºÙçFp¯ù¾"Cp.È)lÿJZßmA„Vñ” E½\¿Šõþ/…»ñT¯ðÀ® uÅ›NJßàL뺛{N~ö:|Òð$õõ†eï•Áeog"“£Umü.ÒÌö¼xæ6…xØwÄûüœWcß|Ý9?nÌÐÓ¶ú7Tì#Ùqáƒk®AÇ1Îø¡ûOý2:2¹=·ö+Rîv\{Ö£' Wj¡!Ö¸p¨/c•w‹í!«C^„*öÜùjôOÈ8.­½éªß´C}0ŽR«<àþÚ»ºæø–RÉ&Øæ× ´;„??K5l½Óy!Ì7£7ó¸«_Iž¢õ­"µ¾Ó²(eöë ”r÷Ú¸çém¦$n’‰ZÍ@u U‹ÿ²ëÄzñ·ndzÕ=õ’ìTrÂö6¬Sˤ¿û=eé×{£b[=Åȇû¢Ñz™µÎ ð÷²hrœÑOA ‰’$È1ŸÍ!":‹ûios â¿"ľÎ{|Y?nøÄNWÇ]ÂÎΆÚR¿sëÅç„«1¹õbç6âïï€úÁ]F!DSô<ÛGœ£Ý¡ßô"¦Ôà¶ÍˆVÿ0¬ î–'b0Ž¥ð)ṵ́6Œqöt[Zì§óä¼Ý>€ù&ù˜?n'ê“PÞ~!b;Ñh9³ªõ Œ|ޜЭ!ŸðË$Ò´m«Ûò–9ó3q??«‰?r*UèC\%û”çb`ÙÏåÑŽ™ÁP)óÈ8€¿1 ð%p#eñðX¸p§‘’ãa›Í”Tð9<ÿ)¾•œ]AÏ@.v[¬??!éw}Æ#'9y­WA BXl¯Ë“‰+EmbI“ sÈà•´®Vñh¯ÝB4\G](9½NîÇŸÿ½ü‚|Z8ï¾ó¸„i|u$Æ"«7é=G%Δ‡*^ÄZYl×ÁR—>ây~ø5ÉËHÅ‹5zÁ”îÄn43&.OÎó¢G¯â`öÍ»àQ}nö4”Ét·08‚àDÉa6í×V›³©ÈcÓãhބަömmѲ\Lžòjó>JãÌ„¼jü{ìñ„áîg??}:5ÖájÄ”„ÍX”ìs„ïŽó¹ÂíaÓfîb´*ô1s'·‘H‘2}º¼Ýzö†ó0[ÖáíéK •PÛmQ•–‹+z¾*\¦9‹×Mã˜|·Ò¸ä‡»gê ±ð¹ÞZŸ@®ÕI¢??E:³ÃwJ™´Ðä¢ZDˆÌ3;­Šä|éèí9U#é¶`ÝiàçâÅVšæýû޲è˜H%•þV„¦½5Hžº¡#,­fK`+ï‘ô¤¥Y†­b3 =I&g¹y`7»‡ÄcæïmŸæIZHCÃx= ùG[k²-0õ©Ù®O®¼Q‰×SÚç¹Mgù ®gÀP€Œ»(2 àj´#œ;ÙèJâfç÷ÀVaÝõšÒúÁ‹ës ¦§•N[)†}dÝf›åÉVI5ØžöFˆ||r%P&ÚÿŒž@¬ïCýRæÛJ#˜V³z7od¿â‰Ýg{Rè[<9dAÄñ+§²ÕMàöÅ?nÿTæ[|¥æå»2}q”²¦À‹J K)îW'ÖºSª(ks~mY~Rýªò¥ËÊS®?r\˜T¿fšÅºˆ4o+©¾h)D3AߟNïØðü[èP.vuMÓÚ½’Ú8J;ùC~Så·6E[]ü£?0&XÒô³IžÕhF—ØœÜöì$8ŠÒ9HñáÂß(‡–“"3Ãc!{²ÔQôi¼ÇçQ‡/ó‰Îj»*´#¥_I*&À© ñ‚`=Ýb”»¡‰‡hÄM±…CUX.'öÊi9{di©¦±µ¢bÔ‹­£t{Ë+Û¾¬ÔÞY5Ís£HHâ|…¢Ÿ¯Ó¶Ÿ,Jþ~zT?r—Ç-çbäèHQ”wË›ÑFdSLêkCný†N†æŒ­®Ñ¼J8.?rÔ{|Ø$EuƒªdÖÎIKoK Ë¡BÀÕ{á·ÏýÙ´«vó¥Wó¥A7oó¥CWÿ²u§×g­6Ú-°ns%ˆmÅz/¨mžö>}òƒ‰NÕrçïó 4Ú†[¿±Ûèãß x¾¦ŸÑGæ ú¸4Ú†^ýõ??ê´;M??ƒÄò´ôÁgñ…<´ÑæE-‹úâB†¬ŠN™ã&„GR ¹¹J¨¢H¬ô‘ ãä°\7æ4Œ†}D9Sâe;bä| [èB,ÅTE)9Vä3/•uY‘‘?n¬0¤ã.:c¸Òp!<ùúAËÑ@ý G€ƒ“¬ößÈ=óÃfBÆ Úõвw£ )¾YKäHðÑËL¿Z³Âðô~ê„wÐådðϸÌU5†¯sòÝ…È?r»;¤É ÕBKk«­S©?rõê²§¬þô ^*ØŠkrG Aö¦‹JVÿЏÊþŒ¯¸]¥6qQCêì' ’L¡E„[ >Æö¼Ùˆ%ü€…Ùñžx„Ý–pC H£…=Ln;jäò)‘EÏß§¿°ƒè?rTO +#ÀÊ™Ä5ïo) '÷ëŸÃXïÿÁTb­ÛãæQŽSJEllMQ@F©éœj¶bž¥xòI³§+¦ÍªGÿÍRGkÚ9'Ñ@%M¹"Ъ_L›9fTÖ{Þ£õÐÄf¤¢?nÓENÅèJ7l°ÝóZéiŸ‡†UÛ e>©ªÕh"±m/7oK¸h´Í0[h“ñl‘#Z©I^æÒe¶ÖÛOÌPêV±-í7Ç?0ÐD£ÓßA|}d Ž|‚ý[^4\Ë>Å«ß2þÛ¤‹Yÿ®4˜F€ÏÁëGÔêÇ‚}-áXfÅ T®çœ»›6µî2¨ã,ãѬÀ!aB*'>œWêéÕ0 ZÄËn¬[™"—Âì¶ò’»œ€Ä»­Z(Âä¸À#–,ü˜«]޺žÑk¿ÜUÙ„ëá‡6JÓÛ]ígÂ1ÁÐŒ´?0ö^þ‰²CÜó³èûørïûû?0ï¤õ}??h›ãÿ?npô ØýƒÃú|¤ÆMrçJh{Õ×ú\ïLlHñŸ 'Sˆҳ\e·­,$9¨7l‡ÇéòNî¦g¯¼©ºÂc G“÷ºF‘^„w¬À„ÉD»‚4¥Ÿ”èn€D³ïR°C2< Òýàa!2G¦”«I€»ëþ’í„éŠs±ÜDUñ59KH’áDÒž*íë’-q??òZ +#Æ¥þ©áìg‘1–>)gh8ØZ€~æîµ¼Jœy’_¸´½Z3ñ¥5F¶Sƒ]‰c›UŸv:äïZŒ&·Bá3¸ƒ®ãJXÅð2}¯V[#s‰ãôƒ¿bl'Ä£Dh†LÁÔO‚¤ßÚ×ö¤&ÀÀµ.'€üŒ…L¨ƒ¨dlºFµhïãb²÷–ßL·æúfÜ—ÎàÜ%Ø%W„}ÜÕ%¼øe°eà(û” ®?nŒ0—Pž?0­¡ãÖÄ\]9ÊÚßlY‘²ÈŠ»ÔSùñýýËøã??û«;ø‘Ê6…XþƃJ¤¼˜fVòv%ivÔø›ðÍö†DaK(`÷”ïÈ’›Œ¯øQb骉ŒsÕûáh©Dæâ ‘÷l÷Û^ Ž—¬N_A¦|@^òð¼£±¸{Ú:£\ýAèâË3ÎSî5¢ÔxW3F?0æ,¢ÖЪR÷¡¥Ý>jÒ!\‡œbTC™[è7üÔûÝ4³­ÇsN„¬ƒ(Ô×ìZ<6$ù¦Â¬@`zƒ}øçÿ¶«Ÿ¡ÒŸ/å#Ò8±Òýå·ƒÖÄ ÝBÙ¥½ž4Yxû«¥‡P?rv6•—Óa[z7Gí±§ÅìÿÚÐä»´?0ç´ðá}Ì\Ð;¤(Þ0뢩Q¦Çëñ•?0³qÚ¶ Ç–«˜i-Žó?0ö”«LOÿŠyýšì4T¼zÔÌkÝÍζ˜=ò·R>Õ@“° Q¯É’— ™ ±4U…©±~ie®{0ø1lúv‘ãWQ³ÅgHì?0Oú"3À §;WW‹~t»¢ö¯¶ó>øTïÔ(œæuìGGÏêÓ7ŽÕ©Û ³›oá¢B¾N¯`Z}?0uÒçö®6%Ð…›Gjä‘¿Öý^äG1˜¨¨xâÍ8+ï³NÞ¼§sÉ+(&òË¢j%ïHhíŠ?0ƒæ¼®àoŸª¿ÍŸøÖ7OŸÇGáÈï·½?0üŒY©ß›ºÛÉFÐs}îŸlå'Øí¡:L²e^ÑÝ›\pû²?0ªœ·ò¼(±¦@Còr ,dLÜŽ¥ ú¤AæÙÇ× /E‚õÛú ™fA°ß ¨”æ%d­ZÊ.øðkËÈÃQ??â¤Âc:w·jbÚ¨‰$£aä²AÙÐS°´Ñ½»Ã(çT?n¨½lýØï´cº›€“Ûø]úèQâcF•®óAdL­??ºDùÙݪa\šÁÃ[ãk׌aç}éR$ý9Ò,;Cmè?r8(è&ô.¾;c;'7=í+÷î_ºý‡Ã³öucPéwô~õ=8È8|ã¬HFÍã\gÞðªýìKBÏéw´ñ=Ä™¶5\q3”cÇ -I³×då[ÌÓhM2Rs)C“³°aÖ9Ô³˜Œ=/[yy¾’nZ„§qVŽ–¥1St?rt=Oh(ºG\ò Ñ]ý?nQXl/h…¤LBQ‚‚&ã ¤¶ ö#;ëfQF!ÝøYµZ¾k”¦*´*µeo²ºiȦæ*ì`eu‚B2‹tÄ¢tÀ˜‘iDÏñÁMÖúØKc‰Óz<ìj[ˆ(t®Z±iZÀ î?nÓL¦)aë:{I-B’ØEuåÔx3S9¼zÆK2ÄöÁªõlÿ=åe­HvFäŠE¤h}ÃÐà}}#©ì4r†³•ƒ(©„ܪ@\ûÙdP» AXUÔá,G~…ZÔ%=dpdµ%®;dMGÁf-N›vµ³%(— ˆ¢ þ¢o¡ˆm™ªY??³_VšýSÌíHÐa²0¼"S¢â[û2Áä|2R–Î=ùñßóx·CqÇþOOªê9®¶?0Fj;”è<Ñš°Í<µcëz¢¨Ï?neÔz„†b@ÇØ¤ñM\\*y{þÉUÅ;QC‚E¶Oòý&R"(DG¹E9«^¡¦*µ‰êéßxIU[†Å³ˆæJŠ4ÆIª@–è@Ö}ùTºQÄúìbÕ´WJ©Ñ ¡#ÓYavÍ-z‡ò•a •©æT˜‡*²KBÜ^ý">ÚŽTÍÀÃ¥ƒñæ»:6t×f/îì¥E8l;(Y"oLÇ20ùÕ>|”Ú›ÚÙv,ÞÇ’t'LDÐCŒ™sÀ W¢ï‡— GÜ5Úï )("ºë1rÓ‡Y{“ô@.!íw:ìpÁ£.èBoãË7¼¯{øÜnÒøgTèÞ,{Ehp_pñyO'‡~Dá]8wRÍÛÁŽìÕÔD“'x×îŽéííÙ6,†ÿå+3ܶ÷þ÷Ú¥Éy§æ:fh.G°_x—ÄNž¨|p«é>CUNÏwxôQªîI~¹Ug‘õ¨q]ÎÓG`´ss¼{¼ìf´ë-_BKzÉÙÛ¬ovè—ÎNÑÒ‘Á‰n“6{+—uu·O²í«§“øæ?0€RúÒAÕ‚ý\÷ÚÙ}å1W×óAÌ#{­î8ÚØFª SíR?rߊÛ?rÓï~å´…¸vØ`zÏ…î;Ãp?0ëÀ{Næ†oÚÓ}Kpë?n½cïРCdQ*‡F=M‹âÀâňFÅjûK~IWŽqsª—¿‹Æp½Z©ÉN ð¥?rVéŽâ8dèšÔè6\;%^®{J‰Ð²¢«¸|ÇO$¹T´1Mê@Ë^l5é3’5Û%N³Ù~<ÛVP*wk{;¬®¯âõº6¾ô<¶Ið9+Ïyw“àæ%j¢è>eKaÔãÑo,ÌIîÒ??Àà¡›kŽ¼Ç§èžÇ±…VXÙ§' iŒ©‰Ì—»1Kc™§C(n§Ê´Ðõç:ž/c:É~ Žýtêbu…;¦°Ùg´†ŽøœÄ??²èŸêî|àÔf?r÷`m>%ùF~.xYK§JŒ€}øé'@½’@|wÂTG¶‡<¿ë€}#î]´¸ÍµÛKZh2£(ür³Y1½º,÷ ž8QœžÐw…”hjX¾4¨W‰3ê`z©9KÙ ¬ËÊâý?r¯¬ÜÉÈG"¦×ªr•Úò˜Îðu×8§éÕßúתYÁÔº¹Ðï—MÔ–­7eQ¥º„æÊ•žIÌ™´—oáЭÐÞ§º.¸Ò¦Á(¢*Ì"G÷˼a: ÿf&A]-…‡N??Õwyò+vâÄÓîý»S>¿êŽùËíœI¶D‡xž%/"ª”„°.3çÔòÎËß':6ú2ªCÜ”>J¿2§G{AÔ‘_²mÚÞêf°WÝŠ˜ºqÄHI‹qÝ/SS6«‹];üc8eêÜ-3c¢ÐØ0ôeðV‘íô6â6àÃ-[sšæ‘ÝõíËêõƒWB©¸¼©ªn»?n§–5…ü'C§œ‚- ©Û™›¤s?rõSšCX\û??ºÍ̹üyØso;±m9¶-NŠ_k6 Û?n¬Ú×¼§_ð?nÐW$ݶ·cՙǸßp}C??íÙ|ØOÚPøO®¹2¶FSïõ3Œ‚Ä×Îbi¨N^v©e·‹¸µ_+trS÷~{w@Õ £MŠRDw‚~¬‚¨¯ðM/8‰‚›Q‹{80eÈü½;ÓýнŽõSxYäý6çÚ çZ¯&Î.?0&ƒ¯‘Mò=ñµBÌÉýïø±ÛMó‘æ êF »0“X\$vî™Â[O#„Å,<°;©ÓdººTÄN2Jo\@ ' -CŸ+½KŸiÕÄŠw?0¦ xaȆAÞÛ|žUå}#üÍM“'¨¯t;d±~¯ ©=õ‹€ì’™£{"ËуmÔ9ëû†¸•P?r‰¬¸„舓ýß„­@Çóe!¤M«žã¥0ïLÚk?ržK™¡™ä¨Ýaûê2AƒNÔ¼8ë^i–ÆÿZ»¬Øz¼´Fî‘EÃ~ês÷ôúóGóR™,ž¿‚™ÌÁÉuÞÉ[È;S/·‡©e²¥ûnœ-µ”Äîsõõ„¯{B®¡Þ²G€ˆ?nïjf€¤>^‹жU3ƒ¥4%‡gþãù©Aä¸ÈH+ƒxðë IwvEÔ¶"Ú}‡¯jwŠ£6Î÷õÚjzõ‹Üãûk —xÏ3fvžuS(ÍæÒ3Dôe'q|(š‡Òž•=±k†ÚYm^¯•Oƒó8vëgó²ï%ãñÜK²ir†òS©HV³f-"—oÈ*š¬ =%úÔí7#@°nîUw/v8ÍÎÆ­aßÈ S(|x6ú?rg,Äîã8{w–>55Ÿ4¡%§¼s$%O»}´JQ3Æ[!‰“´Èw–5äóøÏÂf}búíKäBóšéÔÈR‰ÇE´o\¯°7\vȧïw„ ›Ní%ï‚x$0V‹?nÅ?n?0‚A?n@?01?r”B´{œDôyþì”2w&Ùó;ÂR ÉnÓwÇ´?r<¨“³+Û4NÑúlX,išñÀ)N#x*ŠšQZø§½ {ò7LŒn"Ô`!îT”^Í{»KQô ^+SO?nèŘAKÄÑ/i%Bˆ´(z9Òù*Rqœ¾Ö!‡¡p š 0v'r,Läø‚ÀZæcž§”räšØ2¡s`G©.Ý'™ÔÿbhùOãæ°CÿÆ¡ã­WSÌF׃O†£¥pô5r24¯cºÖYÏÏ-œÅV«@bè ™1óM_ÀŒpº ¦pyñ]sb˜Š¾ Ê(°´$Çøä¿=b4„ÍÀR,që”Jã¿­c5‚2°GDd1=¾ì$ýŽ.ç'Þž†Ér/FÊ'.çè–c*Êï‡AÍõ??éb?0–†¯:‹¥s9ì^1ko?rï8™WÓêa©Ä˨ÁÜaLs‚n€7â²  šUÖ??hƒ ûþbMê°$n{¾23|:~B¤$Ú†<À%8ú h›¾Ǹ?noA1ƒZüÖü¡”#̸ž‡(BÈ?nïß Áõ­bÅð ó1eÈÁØ7 r ³]ÂlD0›(_À"¶3õúÓ94ƒ1šT¥‘‡Oºd-׎Œ»$ìAÅ%ÁÆ`l´lþ€ •«£[X*‡?0lÖ)?0ZƒPwËɱq̵÷Jçr3Ƚ¬· ØgcÑ™~Ef3<]p4ùëp}«À_eï1Ícè9óÉ}|™Õróxq·K?rKü@E(÷žpð³ü;>–x%÷Ô„ÕôæÛF_ÍU\F~™Â  Ä÷Ì#³×Ë‹@7Ý(0Œ]S¿€ùˆáÃŽYø=°xætô‹œÚíºÚîÔŽ?n@M<Òø®d„äâç??»…Â÷I/dÈÄ·Š»HhŸ©~Í2î~­°X¬ë€h»å.~ä›&«57&ÉaŸDqüÉ©Ó{¿ÝÌ}Hè1_tÈáKjK™à#˜?rVqÀ(6Ö†~•ôwPF¯pÊ|ƒíµc¹ÝXë¿5d>Š¥˜¬Õf¾GÅ ¬ÛàW‡ù,?0ëíOlôNÇS ¤Æ?0í$G?rwUHþÉMÖóõêVŒa³€†×{ š2„";À®[~j·SLÂ??nÝ:UPö_’øÝ‰«{ªZ§ ò¢ƒ·²N¦ Jª•ÛYö}O`з¸8áDÝKÁÃ'ûÜååaÝ(w¡$_–!ûí¤’Ö?nŒ§Û^Av:þ´Ž·SŠ W?nS¯´E}¥æôÔÔÛÚK´¹cÕ0Ж>>ú'*`$ÅåÈFoœ›6 ^†Ÿ¥ åù«% ðüú&c]+ª”Fñ·¯w™y*'n¬=²RäúQ?0òøÀç1œÊÏK¥°0–kBiÞb‚Åxm} ï Tp~}xyê–šÎN1R)ÊMÝç92ËÄþ5|&cÌ “{PoÓ­éþñN–gŽÆêFàƒ= “‰Á„¡ÈÜ’º~n¥ÀGr³ÐÒvŠ3¤…>åEw†‡ WU?0W×$ô…F™¤€µ»¸ñ2–$ÞíöâÑ2L-š4·Ö‘Ö,lªÿãÓwršÔúø¾“þæ[?nW©?0‡÷½ØGìôÈÎc7ë´eés(òª(žâÙ“ðÈ{ xƒ® fÚãªçåµ€'/•°Áä“àèƒs¨F]OÉ5ëG@<£–eˆ-»„™Âl¥ÖM¼dˆWpÖÛ×eQÑe—“ÒÏ[¥] AŽª/;رF޹¯¿• [èÞmµ-š 6¼ö"VªaØó¬ õÃê7O*è&ÊóznÄKï‰ÿ‚Z{`þý>ãæ|rŽ4R±ysO}"yÉANÁè`ÛÆU=x»+ÜdÔ”þ޶ü;£½'.®JĪL³'ZÄ/Ã?0Ó:¡iˤV·­¹ I‡z7É6À…ܪ¸œÏ&»¾¬Ý´Še??1ÔåY%°¹N‡–T*˶âmW%iªÕ•«Kq¥“ƒáÎÝmÈâjãæ·ÞVV"‚D’ô;`Æ£øÏ²ë~ŽIäš~ÉÝ×A÷gð'öõšda\#8^Ú!ãÁ\KC3¶H¿ðÇäl¸Ô`?rŒ ô[¨)* Žê ›(8öÓÛÕêÉo›9ƒ*~íaº?n·g•vÁl·Cê¿^+Té!y¹¿­ã0޵9Ré¹ï£²Z¥J×ò7‹PÍ€`ÿtmþDµ!µ¯wƒñÁ‚Iú Ô@òѾ.û®±¬°ë„–e(–ÈUÛñlíã­Ü׆ÊmÝ%‚¸ƒ6ïùMÜ);ãëDcЛ û%_Û!ߺ¼v}ÞíÑöeÖºëºÂ96á9JMBÞ÷yØÞTïèºßi£~|ü͸Ú=­})Æß·£ïä¨ÔlŸ½l[Ðì+ŠËð+(ϧÍFÒ¦1sNbú—X•Âðê­SЕ'Ó]ëÄéB7ÊN"DQƒ?0¿˜Ûó¤±`=VAÙeROý³ øïP¯‡RŸà*Ûú·E¼Þ¼qð«Rº/é7Ž¿Þ7ô7ê.GÅY{ëãr¦˜Ö¹+ÿ¯ãþ7ù.Ä–¤hƒ»—xñ¼E/åbÇ©r²Þ嘆Œ‹•qŸ¾.¨æîbѸ«}¾¿R?r§ý)zŸ}óvõZSè4e0äP?r™H?0‘Æö$êÝ’Ê8GÚ„høGDxÈ‚2©‘Í—`Âf8QÏJçW'_IVhŽáEá$œò’ºSåkîòÉXþR€[€í¾©Ûüàóúª(aëî`…W©„×[:üî¡€¡³F#Viz·]ÈÞ²IGL‚F¸¦ÊÑW=«'…zù å‚ÛÀ-”%ùàK?0ÿJ×<½{Ù8;U-È’þ-ÈŠ,,/3^iú »Ë«Šìƒäš]¿Swæ91¿ž’Øïp¿\ýö4Ñ‘ÀÙñ1{uÎÌ’½´äÁ?n¶ê[üD)±þcÛ_t”!õ/ºBä^îÊ%y*’£Jˆ•nCŽaÐ oép*¡…ˆKlÓK<{ª??=0Â^Ü8Ú¿xæ¨Ê´GœDÍDK‚»tVãèଖÁ™íC‚æ¶~ju2ôïbýϹÝðþ× ¼ìSô’ï Ò°7éB;‰a,à21#‹L'Ʀýº?0Uü~H„AØŠ¤‚Ÿï,°SóÀ`Ýw-\§º¯Í jeåæSœw¹Ë3G?nh}CFEnÊ–”¤Vt13¼çdD¤FEÛáFßèV‹nHîÎ^›» f—¿Fx1§þL“"’µ¹}GÓýÍa‡¨5 ”¶bòG2ážã'{¶V³ÅÏÅá.EwjÐõ-ªŠJ¼´ ³žJ°¨ª¤ò?r.4aïR ¾¡°Ð`?rf ʼô)–Òñ™Ò¦,Øa Íæø}°|½1ÂÍ“‹Â¹¯ˆ" Xßõ*ÑèV¤x™Q[?n*Û²lfô–X¦kEz^{b«ÅCi¤NË(zÝ,¥1Ò'??UY ù0éxk×,‚&xÎé|‡O@vŸ w·¬Ä(ekg¨ÞåŸ|º5e[Ó¨vùÎ…Ü> Z›c1{¶±U°¹¼b‹âÆÑŸZæÝç©òW3³/Ô˜WäL«ôÆß×à/gWþüÕ}ï‚??'ðV?r`V’„!$M•Jº{˜w쀹i== ¸ƒø¿Vë·ÈF6¥@¸'Ùp`p´L Ê^Ïm}‰ Ö`”—M=Ϥµàyç©áfÕÀJ??«Ú¥ÅûhÎÇ‘°˜—Œ³¥Ç“”aÚ ¥·F0=´ÜÞ$I­„¹/ø#ý@» .Œ%´êÍÜUä%³ë¾#kÞ,¯´³VR<›-að ÂÞÎiÆ÷?nÚs¤`°dâ#Ÿ>iÒ-D€_ªwÔŸYëLßyɹ+J!nIƒ|û5,ndó”âý\½M—¤Ñ$â—[P˜Qöµèšuá(,ž Щ2xJ‘AzCHCÒ<5Tê,5Õêç,f tT%ñ)?0†.,"’«Û¢TÆ{ªôÒÊ*«e·O:sT©ÞãÅyçbO„y'F x5Mr΂á•q…‹TÆ›£?nµ,áJÜ?n‡y”Ίz +#Á9®›)Ň – ºíhÉ‘-ðÖ?r+F;8#‹>£*aìlÊÑ%ÕïRlÜ÷µ‹)¶??…F{bY >–9ëoªßbÈ>ÃLUØ©eÕ…‰AãxÔ| ²xHŒØìÓáºP3þSNûP[ÿj]_K•ˆæÖÜ {??ØUOPÈîEpÜÞµàN,ȯÚH?ruUTf‘Ÿ–˜ÔÕÉè5ÕÍ ìòŸ9›ÏòO$é0‰jÙQ!4:xe©‡”œÎ1²ö?rJ?0C4íÉìcµ¦š`Ž ¥F=pê›Vq¾ ¥>¡ch,d??ï+6üO‡‘‰¢b¨7ãL7)C#{ .ÔºÚïbÁ˜Ž{åFGù³Ö`þÁàñ‰ÒÍŸìÛ#J/¾ô»×æäïÔú‚½H!U]¿±¦¾hOÂ68Ö¼XœÊåk$6F!õª–°`”Òº\}lÌ’å"ÛyÀ£G„âHÏôk@• „:Á4™§¹÷€ÎYÖ¾©ª{SKuU «²’{ÏÊ• TÍŠñJÄÂÐ_¨µÛ2ÓÅÄ èæ;˜Å½}`l¯*&fòÙÉ£ß2óPk© ?në†p/$²¢EB•¢K¥cäÛS ä£Ñ׆ZóÝ\õ÷^ôíJ}À†ê6Iá×Û|€àJ£!§ˆM?0š'ò²;Ζ2³Õ°ˆ>,7²ÄÀËgå 1¿:S¤’-±$úÊ*GºïÊåý=oa^ijº+ò3õ#1œÁØšªßLïšà‹éäÿÛé~4û¶¶roÛòˆ{ñw>½à-⧯#¿Ïk¿æy¸é–$˜&2é N&nWú™ÈçÕn@4Ê^Ov=ê|-}×Õ!ýÒGX¼8Ùß„ª¯ÛYèÂn ¸ù·wa@j2ÞÑ D¯ûa7L|!ñÞ8•ohÔðÄ­Îû¡€íëYÞr™ió  î''Q9Ñ…8å'@³\70?nE°^—ÐþW›èdó¬j¡=ëLxj–Ø4.˜l‰¾áS¹ÅW$Ôòi óÁ"êqMÇ+|³EcZðOiu+š‹/=´;K4CWCŸúìÖq€GêÈ\½Õ^7ÃM«Ÿ[IŠ†ÇÆ·Sê —ê‘Š  îOf?n™|ŽÓû ÀWu ž¨žb½ë³þÍ̸™yö‡³M¬ôúz•ÿù}{LâÚX–VP-âÍÕ*ë`w. k??we‘YhÉvc?nŒ™nµÈSûMȲãÈÈÍ ŒìË~8r‹·Ð‚˜ÑLäp` 9$¡ÆI›c|?0ÉÁ•àpÜ?r8¬>1Çþ¬ê“óHk»;xÍ•»øZ-Aâ,¹o}ì+^Mz¾}¤co?nq4Ͼ8AÜ`ZúióßÙ/µUkÃ3)j&O4i‚>$6Á ™—„æ8Ó²8)?n&Sµ½æùä°5l’“ÏÇÕ˜=eJŒ)»wÐû%Öåܾ{-¥%ÑU¡ ŽõC”¸Êšì1ñw ¥­ßò+°¥oæW‚N*JmG[Óo/x眚âUácl‰Œ5^ƒðI áýÈ"œÈ£üÜ$ƒekùþƒxR·3šìãSŽÅÈ­FÎ7—Y{}[ý1õΩօCGhæBn¢¼ùy1Gé¢??·‰D3#ÝU©gO‡ÖÙ;A)®hü¬PO(Ê$g$´@XË8½O±¸JðF)Pò¹ލäÇ8eªBh,m²Ø#œxà1š>ŸÎϼEä€I.†€oXGõרÆE_Ö²|Æé⃘q‰2>Ì??Ôî3ˆÉ{y5„ªoVx©QPS‡~¶¹Ù;¦Š]œµ‹‰pf\´ËÏCÖS¸“ÏÁØ™€ "kçâ@;>Ðø#©H¶ùÏì¡¡8œƒj:¶ã÷ÆöãaÁïÛ!Ã|ÊêW롾òN8SI!„÷ŠîÇÓjÖ܃DZ½MV»bmëaÄ,åèqOyêZf²cõD5À[rýËàlß‘Êâ·b’I¼é‚ýÉ?rç^ è–~Òf$†0uç›¶Íߘõ3é$¡¨×€°¶ì> ¡“q-üÃã]”±a‘yª%\S'J“a׺fÖPœE™â… ½‹K-óFœ€™4ŒÚ½¿bAÊ6‰5hSヺO¬¡¨dPÀ¾åVpŒß™l§Ê#öžü¿çŒ29…õõÜ@vß'ædIêÀFÀ-¼ZÌø¬,(°ì>¢{´3U¹xš©cóïúTŸ2ÜÄB y˜_º x†¶âÓgsLŒP‹o¬3;:ÀébMq4ø˜>ߌYaó¡ñêE]"xv{†ÞF¾C¥¹Sª™{{n;½=òÁ4 ÛÁ¡`W<Ç>©Sþ¯sGÁµ"Aû Cé˜@"ô”¸×>N¤vlò…%:¥ËÔ£?rÙ7—l+Ÿ¹s¬S†Š‡%ú¶Íü“×Ä~û•÷z6„ΖR²/Õ>aÿ"­Á¸@8‡ ::ÊFÒñ+QQfò†º+™Jm?0Н0Àˆ¨atýÆ× ªKêZ¿nÒÀÚO†1~ÅÊNÉROLÀ%Op¾Ž,?nÆ×Êк ‰•ÅV!³gôö!Œ°¶Æ??k^#+Ÿ; —॓ ?0“ãsY–êç!¯é“'Å”*Â)XâpçH …wo rL3!EŸò]>Xôð•Þ×[Çâ—écâð|RLÔ“â\Ê%.Á2†hZÒÃhõÀ`u‚Ý|Á[Ës—üútïÞHy0.Öß±—Íei”Äh7Y—;[Ôj,‹Â ×e n!KÍm:Ãb‘kcÊI:ιdWµ·*yñ~Ë+7 ƃ-çG[aW{‚[Ÿ®}“°` TD?nV_v.š;Ðö;”¢ÙvPq-H'ÙÜņÅèTÅ[`½½Éï›ØPX¢sö…çÎÐ-SòÛuß ¸A+¨!€Ø>[Ó6äèªÉÉœgÃÚf,M~œ>@yáÃà±a?nx€ÛlÊQ·*]Ó‚K/ä¥ü˜¤ojf²ê}{9±è?0ÎWéÖ?rlYa+H©k?r6\ÀšG.!9ðÉU‚æ„â^»DT?nÏôhBEëôjˆQž]%²ÅÎg¶ž4†Ê+m XÜ× ¾+Ëwã7Ô”W7ô\©ò¿?0,kV„ Ÿ­1-5¨âN¸—L ‘äp›óóZÓ.¨—¿Ñ«ï§±u£ûYg%œè<6†NÌŽf¹Ã9ó;CiéãeÛ|S `/S¿t°§Q²JiÈͱ)ÿÝ †ð8ôPYFâ˜z2©ÞëÖ~°ÒY .oøÒ#0Sâ}þîì‰ ŠyDñhõí±Û7ÜD„ øpµ‰÷"—ÛúIÂ#ëô_4cûæê±13vÙPG['»yÈrž»™ô;v§’ìF [–uot®é2 Ð×V°xsž/H…??¯?r(ÿÎäÏ‚>c¢bâž|ÁØF:¯Ê¥~ÍeFb‚Ù4âàì‚ßõàwu=?0d‹úECôÚÍ­,œžrë§#zToÕâ_ >‚ùào^Ø_óX;+âÈúaZ8WAÄÈòœûx>zšv€>Dó½ÙZWá ñ¯†ÎáH\ÍjLC[iL5‡?n–½KÞCÚˆÅËúg%5Cÿœ[!Cå¬Þ!OÛ;ƒiá¯Æ0Ö ˆâ/[iؽ½[Ë>+áQŒŽˆçû??([û†ê¯ *”vMmZÜÕÉæãMå9¯¸|á–µ5t%ûE¡Ù·uÖi¬^–Ôªâ°hüYÌ+2ñ·y´„Å»°r0Àcâš<b‰È}Ìâ?r¿ÇÍÑü*âr.Œ‘}rD¨½S¼%‹^³LeÎÙ?rÑõIÉßþþÚ/¶Û^ÑéØ2Ÿ…X¶å-íO¹`¼M^;<¼þ:xæÆ=”pãT9Egg8¶‰’ïìpô£ÔÁÊñïÝä[tœc4«åãÈ“ž®3ž[‡Ò•J»=Ò©ï>í÷ÕVr™YÞ’âfÚûmn7œÀœ¡)??•·¡"×éÉ18-@úÒ0Ã;åáD$‹º0ÏÉèƒ1kMÆ]úån6rÖ ?r4)&úr ÖÄú‹9Iê8ÿm¨8Ÿ7+O`ßDi˜îWG]âNWÖb½Ud?râW²éæ?rèÑ2×^?rðì?rÞ–O¤Ø0ŒõÿC«ÈW©—?rƒ\ÌncÚ?r±´éÍZL§µK‚3w­bJÈ%c•XåE—?0ú;GgØZ‰ƒXqŒáþ%.ÜΟùóÞ¬MsÐòñ@iÙ¯Ç?r‡š·ê.‘8gÂÁõÞ>ï!¿m_ív_ÊÚzÍwÓp»&~r r³.ØÏuH×ûÃÇc?rPjòÙc??ú­Pë?r7,qü§ëÃݽG÷Às–ªŸ :iËÚà[Q9jUp?0ÚDàƒîtÄÇ™íµ çe€~)Ù4?0Z"±(}/Èþ!4îQ.FM3+kYMöNBGZÃHy.êUX›\eÍ‘Ò*_r#þüSïñòOÎÃ>ßa9OÍã%=Z‚ý¢Å5ûiåÊøJäâÖ±~~Rµ7¢^tX—›2AËW ÿÕ¿*F”Ó)M÷MsjgmHlƒ’×3zÀ޾ÖÑû8·ïB\Œn=¼ÃÏ„èÈãБM?rÀIVµÕ}ÈÈü!ÛQÚÚ#ÔK¶mtc¤å,ŠxhÅŸlµi +#È[}¤iÈÁÐÍǘct¡vòr;ÁºÛ=5­ø*|väG¾sã§ÌO/fθ&|ßM›‘±=¨ÿâº?0>vô/g¢Pù•€H#†ÖœJP€B¤?n?080?0Ô}Î3ü¤½ñ?n°À„4sÃÕ„9Å!`þÜ ­Ó)g üéü™§6O]Ãç«VëñW©4õWi㱑O¦~?rIÇÔÕÕ¶‡Õx–¸î„æ|v ·óÞ®šé#9Œ%5ÑX—ûKOi½ŽÔû4S3ɃҽMNÚOw d#,K”^Ë_ˆ@"¤ýcN: BÐÈÑgè—ê]ÂðL€í"1§:#ï]–ñ8¶ÄŽ&&\ú½½9.[RÇåD.vl!bPl—Ï&’ÿPá wÌVpöˆåº?0Ïò‘H&è+Âpe¾I©ÀØ×0×R.0rÜ®£eº¡`š,ÆÏYüÿï4ØùÏ"!$LaÕ¯8€uß¿\•>óñ,oeŠŸ*òвÖjQ`²Dbv‘‘ü ZÒ‡Y”f?0[§YACð–q„'Çï‰|péÖ¨ÖLþ3Úg¸ÛçÀ3¢ÂÁ‘5Z§‘÷µŒID¦·Oˆ(;?0ñИ`“/†Ñ½ ïÕŽnVcü.*<±›+b?07Â;u¶ÝŠ |™²ÑÊÙjp6Ñ`~“ã¥4’ù cœìKi+v¸êÄüe|Ϙøw€Š?r–÷êþöRÀöø¸¡ù»§zqþR\/6¦åþE¤4/‚?næLè…„Åç ^(/c“13w§‘xÌž;c|y™ÙTd#*¤M§$BÀÂóçÚV”¸£Ñ¢Ø¥o[&ôoð³Kè.˜?r‡5°ƒù?0z˜ŸÊw@œ\F¶^&³ª?0ÚØ ¸ûiv&ÙÍvÅÀØäß2üTaö/ÓwCŒ¸¿+Š+¨Š}LB[¾:æ?rW0'ì°þxvá p-}tÅf²Îªü~·®nkŠãèüʹ¥V¼ììÀ-÷›׳âܘY‘L³Â AìúÓÐW‚·2ªƒ/ù5žâñ¥Oè–&s:sA㺚ŸÛ *ÈK-š8À^pFóù5šÃÍ?rœeUáL|Ç!o”g î—ç¬Ø‹smg\P”Q-íáÒÃê§vu¸þÕ5ahŸ¤~×§’>Hú|¡£Qå«ZNe<­,jVª@Xà6ÕO5»hV¯éËŽ¦ðG×òR , ª–‹—ÆñøÇÍm4øPO(·œjÔ_q‚ð%ÍÈû*ì· A0WÄÑg,ÏpÍ­Û‘âž¶W —¼hÍÌŸ6=Dçh¹Ëiʹ­X$xØÞIº?rê{Ü“¯¤®¯p¢æ×ÁÇçM{sÿ-1Ï«6ê^¡·®›)„†,y•Ö}ÔŽf>Emn‚§ÏÔôá0ྥx2·æâÊÕ‹˜ú$2"£É|óñB¦Í1%Ž4#ÍYœN! ¨E8Ò÷7ýôH °b ¯9ñÛ5"w»õ|v‚öŸ¤ÙÒTWûÝv\{°÷& ±¯Á¦¹qÇ«?nœ)ݿּ£åµ[Ã==Íêt÷êKê°È« 3»Zwö#6IÞ3…˜~`ü—=nù½Xú|³ ™þè©ÞToŠÀ£áЧ÷µ?r¤íQ‡Î@fÐtšµPcØ-ÿ¢Uh8®Wâ2,ïüRæÆ\y’Ú¨OYXz”Ïáî0ßN¿ýÁE!¦îèð<ƒ'…TNq=¡þ¡GY÷Së‚N2ÚÉÄrÇŒL5¦³“ŠâÝœ›šu}M V´«º TŒéÉ’Ay¡Æ0•Öp´U*Î>iCÇ`,ú·ýLMD&C¨þп‚ßå¡êÝ48ñàL#ŸNg…úÎx쳩zê5»–~~ô ?n ž7†C ±Ñc_þÉ/¥™î³sùµ>²VJÿû5ÒçÌO+¦Þ…°ü‘ÓëAŠ&ÚöÆz?nµ¬‰­BLöÙ½1’ȶ­µa”'ΗÝY_Ä n_YÆf£ÐÑ»ý'ñ–®°¿i—¡Ð‰îN{ê??‘û›‰°?rqÐ?0ot‘Æy—ÇÕi9· –í'°úä8Ve©%y%‘jÉ@Ò¹( <‚Ÿ2¤›¢’òÁÎDÆË #ðbW /yIaÛA‡ˆ¦«xc&`n†'–?0ËqÓ;Ð,ÐÄg2“Sqš«¨/÷É–_üôX×üÄ™ÂáëðñØWw÷ºª6‘ÒŸÒØwo·¬P'†½>£ ”ÁAõWðáÄ ¶}¼dÖkª‚>ñ»o£‰TÕÉ*õLr’’#ñui¾.};MÖž˜pnc죲ï_?rTGpcè¿ôÓ ö€CQ,%‘ô=×*åâÐpÎë‚ê|æ!KÃëÆ`ý¬©¼ƒMR2íhds4ùf©ª[Êö†?n”9Ø«½ÁÜÓ+ÕÓ·EsáL¡Çÿ(Q‚Ççäõo]ìƒq¡ŠVŽB¸Ýˆß˯µ‹€¬¦òA!~eÀAá}ý°g>Dõ´}Ôº6͉գštøm^Þ]ÿ :û¼‡`“ØÎ7÷©pó³ïñ!>uâÊÿ™JÕaešËáàá9I¦‹±2ë=L¾RmDÓ¾NR ÞNžÝq²w•äVŒ.F\ ¸„Uò“)P§i»ÔqÞzµÜ‘ÁHk ´ôY]u³ÇŸ?0Öþ³q)<ÍÅè Êh»Æxu¾àò¬ã*å×%Šýyžñ›£e¥®ì ã£kÒ €ãÀ«mþBÎW ‡"Œ¹î H??LÒÊ‹–O8v²@‚¯ïÓPÖA(>åècØæªÂ› ~÷®Â€º3cž]%J6n=ý®¨ìP=Ìwý…M—¸i3/í¤;"ÊÄj0Åd,w¾ö„.K??(ŸFUþ¢yYIÁqÆäx73òÔª…?r[½£ìµÚ°´“zx´ñ°fâHú²:Ž]X ½SR$A“Kꇦ`d¥ÎªŠ:Pyk¦R-eÖÀŽûWžNj‚œS_ŠÂ29RÀ Œm”› LS-P4˜0l¿'ûcò{i$J¸Ùqm.ÝÑÓ?r_ ý‡Ã凎"‰ßd¹cO/6å6)¼CŒµs¥ Æ8S—O'´8g[‚²ßRÛ\šN?nlô ÂLl÷¾ðë‰|+ :PVK .[ÐfÛs÷Eµtþï^M$ëmè¥õ&¯'[µŸQTU¤Ïx!ÙÍáÜÓ‰˜yƒo“ŽòkPʾ¤á/Õw?nh!Û,¬©>lëtˆqÜ×îvK¸‘­ÓL°Å§bTG›‚”vƒðÕc??3–ŠŒiÉÐlË-˜×l”8ãbI¶@kD€Æ~’©°Çi";‚-r?ruêúº–ô‰•†[Nne§æ›7??ÜR;Ì™¬©1˜+‡}©Uÿ[üˆˆÀüst€§\&Ct}ؘ»Ñe¤§Ð0++}İBêÉQQV¼5£-ü" xÚêÜC®­m6bú¥á† |Ö“¯$p©VYîÚƒ‰z,¨?0¢pI› S×Ý6ø`2ÍM`’rì©]%D8X~Æ2ntû_× Î „·uGüeï<9ƒxì!ðŠSPÂBu€3“WÃö=­EôÊl•cä8ìŠFÍèþ›6¨Ì^_]){ÚÒæ8g窰oóÓ+yË «õ›‹ü@iö™DV N"°îZhÞ!‚–@/L„}Ú¼ßä É¡ÑT°ö‚©A½‹#ÚS8ꊬ%¼å˜Z&ƒCײÊ4#õj‰Õ¶oïæãdF- Є§¼ut똥;¶/n6ÝÙ:\0™YÓþŸ&›W÷Lz*ëµøHç»Aè¶4ÏT’ðA™¡ÙªA™™2}hµ еï,4ñ á‡íóáù•ý‘à¾IÞ-mpû æñ§(º'®&)b™qŒ¼wÌ1®äÌToÝ¢Ê??‡é8ˆ >DJöÃxŠÛè”?nˆ…ÔZ²”ï LN™‘\?nÛy”|ƒé@Á¯È5æñ|Ð?n½ù¥vi¦béÖÌU|3èèn—–Ál&Oræ—©g‹$$Öˆ§ Ó™wzBtUâ??늖?nWÞ¥+F«”FO»Ú㎊®U¥™îá*nWKc‚´þ`¹ÁÁøp0Þ€nÏ‹{¦½æ,,íý†P*¥Ÿvåç3ßòLúgWH=Ós†J7px7F¿í–O°¾Á<ßà5‰ë;åŽq™ÆŸEWì?n/­­*D/"n ¿ÈÌã @|¥Û™ûØ;Ÿu©œžÔ?rÝä“h*Y†mKglÙ…ƒÖÃøáêñU£‡F1V¨©‡}{úÝ[ÑešàÄ•å›#??—ê5}c^ù¹P&(³èkµðì™R –S›š´¡ìœDÄ^Y=õ“/ÏP‚v07±oÏÙÏÖš„)FˆfÖ™º?? j™*¹zϼ±°±Ó§ã#3?r:êGÁšæé¹¢•v8“…Ó·›û¤+ŽªŽÌõbd“fS ýÅ&mñ²RTì\I¹ÍqЩ¹ù)pãj°&šb¸¥æ½¥ÉÜÞJyC÷\`lå?n …ñ|ïWõ«¶Éòs™ÄªƒC c +#4þØô‰˜Ú}´G@¦ 4rà–Á7*U fªu??ýêÚgj.Ø„ ÏÀ7k•Xwºùné>Bn1>ÿv›i£ugMgådI+&ɦtå·— TãQ{±P³m¥i4cë£Þôiß‘s'`U@cY‰?n™Ê™jÈü_Á/¾fßOéÒn¢P­ÝYÅ Œ¡HZíü¤í4Án@à0±ô©33HÕ˜t”N";ÇPhÕZý}Ö.÷¨á¤¶PX€á„Ù‰½³µïo L½êmã”däŠîåôEHÐ×MÚÎÒ?régø¯}oà4þk¬\ý÷êM,W$›1Î4œƒ“?rzǼ õ¡×Ñ©cÖ}k nÈEÁ2²âN«Ü Üf4¸ 4#›ê—»Âº‚ R\.r—‚Õ…óiÜQ¨ö ‰pì‰À[˜¢³céuùÖi, ,)"áH4?n®¡Ð–0óÀ›lÐâ2v‘Ã?0þù9 ^ï>¬c)°(Méi˜tEãšo ‹“™6¶&ßÕ¥×x®uØ“/†Á‹ûf°ìîòé¼:qZrÒoÏýv·5aÞtEAó= šû’r˜\ûg»o{Ì‘]ÉY¼?r¨£c—Êå RÏ­ñq†Åe-®@Tw.mòG-q«Ó`wx}ñ¿ßèEé Ú34„b5.ÂôèÖ•‹¿Z´KÒuk:5P¬.z ·5t/ªz¼•nù¥VÔ¨[°Lz/-„µ¥’Øg¤AáVO’8êkáO€?rK¤ªßÛhœ ï™(¥+œN¬ÇfüBà³n¡ÑK†Y ¶0$<7m4l »Á†_$«!®¿@ÙÁþ?0ñ<ìŽ73É …C’Ûq…?rX@IóvÆ6ÐåÖAnP%É2׈ŒÊW&fa·?rJˆÒo'çß]" 9Hº´³%ŠS!ËäÁH§¼JZARV•ŽNú 6uA;•¢™ZR6“Õ#œ”À6OdÖ²“Ñ ©½ ÖÖXj¦!N÷ Fqt~Ü$`üÍV¤}†…gEG¿³e=º-+Ü#Ó -0è½H2êÎጛÞjÉ6âÈ ã s¹9‘6âžKv"1­V[Ü]Þ_à‚= 8Í›Áro÷<·/Ä~iKMOà/ï”`Ì1ÛÖÍÌRA’=î0I§Õ"ŠØ^¤b?røN뻓??/÷•ëÍaÒ¡à*R5§gžB|G'CäÔ&àÀ”iÆ×¤ÈÄV%àë‰ÕßòÐp0Úw¯EþÕhµªˆ™pŸÍ4`ÑWϊɃƒB&äi_⎠M!"ˆ° g´9 ³5W5þ…ê#õÚ‡ˆ0ƒŸù¥G &•^ÈÏ3놛Þ*ƒ‡·~¯¯wïŽßÚ\˜;Óòtžz??Ÿ·¬ÛSEuQ¥œ?ná#`ëȤ·„"©1(¾BL„!í fä<£¹lb =ʧÄqìü/ÞÈ)ô‹ã‘"æ­(×ÚÑ|¬tèÆ4¶ðg‘€Ú˜q'±=+Û{ô•!Þ\ªëhÀÚ6bå¨è=Æ?n'¾¤ú*µ˜¾ ù°:Eò„²ã÷±ÄÓ’õ åx?rÙsøQÕMF¯<â¦`Ð]Œ®éäM.x?0-b¼ØñŽà_¼Äk{??=+1 ÛãÍÚ1â™ 25œ+$¸€ ½ù¾5uÐ~b_Þ ‡CG¾qEE,Â|¥›_òg÷)(ë‡??\S0Ï‘ü!Oß¿òéL] Ö×HO!Â}tbéN·ƒYªt@v"0†X›„Â÷ö2ýúYs©h†Pøt*!_IqÜï–QM‰%ü\#ÿã9”\Y 1y*Opþ”Š=C}jÕ.¡Öº¶DHû‰¥Œ #éH s¨!ÿ5*ô‘³°’©¶™µDÂøó²ÕOïú)(Gµ*ܲ(6—‹@&Æ H•~A€Ü(?n°¢·n"Ž›Ø??ÄÆUãø{Äø'@Õ+£U ruìñŒ€¶H'?nz]Ü_8ºõu«R.bö(da?rð :÷rŒE3‘`”N1ª“tŸë¿ãF3õ­mBœ·”böôK1§¼ˆœ‡×Y¾~³¢â!ü«¨; ùYJ°Ëë=è›rG¦%ÖÄè]‚nŸ`ÄÔÅx¦z¶%›•޳܉äØúç,,Âô7Ñ^çE}< IN²î½áâ1¶Ä_6€“±VLê·JSÿþòx—õõò¶q¸ ‚báEú8¹ØZa{àµÀŒ}å ŸM޲ :çá6  Ó2?nù üE­aß/™U’¦JÓF؇šâ3sn&Øä*Ë;Ì+-cqÏ{œdcæKÙÚ6q.Å„÷½>¸»y3ºç ÎUÒŸ!e]¬È!¤CèÞI5àÖ˜Ö@b‹Ü˜WظoÜr¸ûMÌ‘´$û©š­ œkOM>[~âyƒÌ:P?r"??8×6x–ì8ÇVâ‹à[>?0 k‰??¬¶æ`iôb’8×Ô'—ú¯?nâPkØt¦¥=Ìe”5?n’OC§‰¬û>¥¢É³a®µÐ…‘äý’oÔ£ ëY#¢F ú”| h}uä’hTtÊtžzÈJžM?0DÒÀ8ì6üååÎ$hjÔ¬ÈI\Öî!Žf$Ja4½àäÒ̱›ÒÀ¨X?n%M4_Æ„ŸÓ;‹yTKÐ=is¨´œËÃCôz±‰™òòZFq$ )~º>¬c€µG©&‘œ8&‰ Ø™*c%*ú€N¸bucÉ„±ÿ»˜^¤vÈÓËT¦ÙØqÒ¬@qƒÿ½??EPñ6…áþÀ ‰üVΘ3¾il<ÞÁ€ö+¯)O÷} ÃȥƬŸFÖ¿`Nî#ûÍ:”x }^ ˆˆŽÅ›Ø]˜Aw‚`å‘*ñš|cŸuA,h˜….4E ä^ƒ½´I™Ow 0Œ>›ì›¡{F ™CõM)z/É3óË7Λ9;h”îÃ`Ÿµ¨ ÀìaàÛ•¾¯??m~{ ,4úIAÐabÈ_ÃWˆ|qáÌ-ù×ÈÑ_×z^ÍXÝø×.´ÛkÂJßšÕÊ7?0ë²ÖºyÝ"ínMñ£¶K³-`KT㟾$#*¥/”‘a$xç¾=ô3ØÑ0êçò¹ìàQ[£ëÚÝι :Ôø‹nñ¦J| BÍ•0ƒŒÃ¶„]ÛãÉ––þp`Zz„ÂUˆÅcù<ÝÖÔ?nÏNqäçÌ{YÇÐjRþ¤‡–Ÿ}¯Š}/Oˆ“GäiîCºf®KþÃä°26F`^ߟ^š¯æŠo®Š&Ð0Àê‘ôM»»†þe¹¸åù³½ã´[g…v°™Ûç/@ËâÎPPú)½v§õÐ{Ùy!?0Å|Ä?n^œ\ÜpMžqd1GWXÛ½1­?0´U-|‹©sçklXΚªÞuÌ]§Ü˜Gµòþôš«¢+Óæ$— 홬Uãø[åÕ,5.Ô€Ñ-9ÿ‚¯¨ÔÖºãÙ(œp–,öu«Y­ò’÷s0½pàŠ•ÞÐÔÈñÍÉ}¥í)Jc?r®k³D¢B-ïèB&,v ±#¡ö„ö¤<øwZ48ÿÝßK»+‡ï:þ2W=®e5??­Ø¿  +Üê¾±–ÇÍ,Òé³gˆ)ªFùxpÜNÝJ??z'ÇG®p%֢׈­P±Tb[l%_-Æ EÙR®Až,•v*þ¦ø“~ /¹±™¦l9f„ÙëáØ êVµÆDå*Ó¤YM3½›»5?nccT s”YgD÷hÚÕ@Ré™yÔ¼ÓaÂ3X‹¦­«Ñ¡À3´ˆ©êˆºªyéݧªÍòE6%Míªe鹃?nËL-ן‰@ L#C4Ë-líÁ, HcZó5ˆm,.ó™à +#@°(€­Ä>ŽÛ yzµ3Ur½R̳³–m\m©'f´…ÞòâIù~-hA&ªA2?0e ?rŠœKh:.TH|à¶lF||ÜÇ/ìþ??·°8ó~7í K´™ƒ_éß2³%§én ¢?rÔNè¨Ê´æ qͳ‡¥jþËX8ÛŽ7{FÜ%åH Kb?n.»8vû)ï­œF)ÄP®½»+䨳Qe'¿•¼o¼8vƒ ~ÕDío|9ŠQÞìÿäZ3ˆ¶Ùc€ëiJ>ÛŸìÁhÎÞ«Yo*bÆã …7‚j?rýšnݨ««Žö¾%=†m«ÜлÖ#^¬âjdN»#m¢lʬŠÑÎŒŸžpW Q±bF¶iê @¹+Õò¡ò?nÕ?nÊÙ,™.¡wk,¶ËÏËé??wÆ9_YðVÂÌ}t鯋T(•T‘þ¬@[øD¾ÒªÛêßâ‚iö’Y½:9àý‚8ûó è«tÍ?nsrÝêý߇3ÜiAø^RÎÝÐó¥×o\Òjo-ûïÇÆ´î‹ó© ÂàŠö–ÎÎ~b‹Œ¬5ÑÍox[•«E+̰¶m½ŸŸ®.)J [áayøû¦èÆèŒª¥>g½œ@™&Ébyîxw2ƒÍl¶s×`Ò"Û-wd…‹ä7¤µ¼è%×;ñ"ÁQ¨Þé ½T­SÝ5š??YŸaÙAènnÿåx¿š‚Ë×c'zü,`öüõM¸»ûËžy}r-–!¹÷’Ö†±ÐÚt}üéÀÆí«½ wïiQìôwŽY¬+ËÉó H›J³³2ŒgFÎJU*vgâìïBcï^¼šb~ÿdÖþ%¶ÞrÇîüåYÜôÒÚÿñ/íú¶ÀkW8¡e öîp¸ÝåqšÈêñ·”Áe]í>Á™À<ÙÀä¹ÿF[£Ϫ#"%]^?nbìi =r>˜}ru :o—†pN_žC ÓÃBY¬ò6¡mHPçÆ8vœ}íYInìQ?0|ÝŠ¿ä:4‹ê _5}| ÷IÚ‘D³§ úý*šh. (Ú·ôŸÓº£Tšn!ï(쪩Ü,I÷|€PQ;gfŸw÷HÛ‰fCìèܦ«¬j^ߊˆz¹;"¢ywo›ar Õ·šqº¯ò˜Èbñ¹¿è9w]åj9CêÍ=ž¯ Éü3äpÑ•Q`À[{pV^ YX¯LœË±—¿ìL¹ë²7úÃË34Y|°ìI—ª¤óB­ÞˆÞ=4èÂØŸÖX19ë/˜²‚V2³°•¸£drù†.OMhc;¬¦“‹mV‰(Xdf¤užïíYa^¦+!-7ô.¿<>ñl¾?n}ŽU|'°d8ha-KRË÷ƒ…ÏWå‘èxR°…+0 BEó)±GòâhhŽËÞ۪¨çú’•ë´})Èb(=ðRLw€õí}¿üA26‰še+™Y2äÆâÐk&Í;¡®Ì'ÚÖ,V_òJ® |ù2g»:®R9 é+ï lü)abizÚd¹Ÿªë ‹Ì'*?rÀ/ëÿyOâQ¨‰5­õ‰ÎE\õXcÊÚ¬(aé÷E´/±´»ÿ¨ÉÌhÜ¥»ýSæ#Pð™ÌZ BFÖ˜Zk–´¼:騔¹–‹i‰-\q¬Ç!ñ¯„ Û,gúý4o´nzÙ)š ™t*Âm$$w&&ºc¦¡)ööÒ*¼‰(}œ•:Íï9VÐTGœþã¹Ói¬M âX¸‰à÷á݈1ºä‚Ýj]rËÝžEêpÜ•þfu†c??ÔÃËz ½Ó8OWÊy¼??H?0y'v”Z+_}z^ß#Ô8gøÜõ9J?n ÓFÓs$G<vغyæEúÊc@Á2æD:dïZ{ܨaˆ.Óe¢¬šAC‰A#õvC‡ò10 ª4ŠqdÿU5©Œ3 Ñò5ÐÅÛeÞ£ÂKmµ>÷bk0nÓCYð«¼wéj ÖæJ$–DHýeÇcµStù©)Ö?rÃ#¦º^X”Ô¨f$-gç™XmIºÆÝ¶–x¶¸€êB= úì²±FeS–=îæ:¹ù¼™ófJ?nÊÉ{tüå&“CÖ¹ÈÈÍZ‘|=>­_pú}ÿZ(:Ÿœ”KÉÞp[ó?ruŒR:‡,æ‰CšF&˜æÛzÎ)ÅšÛµ³‚¼0ŒÞ/Qq¥{‘òr?r ^áññ¹ÞH??Ëý2Çû¥E/'P0©0öPDvÂ3øËJ×ñ{ÚÌ+ ¸ÏÄ΋î^®Í†u½Ô£œK…W*Ob‚þ=#'é®}²ªJ÷idþï¯k:’ýóI” úG=2Œo (®&8>ÊXÏöIrŽó´7¬ë׻ϱry˜"W[[ûÀý=¨_·!pÖÄšwÛvfvéñ>9+Üîy& ^T+YVÒìy¶Ô«Ìmîð¡Ò\ãº0sný ÍB¶ÖÒ]8ü‡5—ß›šzÝ襑~;??{ªÓ‰œ»û¼˜†ý“Ykràãìw»ÁÁutKÐŽ›R“XâçqSDzdà ´$Au“»>TÕ²rõÄÅÖÍ£oêAT‡ò“”ìXP *Â@ÿõÄ3vžÓñ/}}â/¯zÆ)§-P}bSèµ…„8Lb³fvчtr£íŽ·ª$ÀЮiNþ]Õÿ‘ÿQ}åï$ïß(o…­J¼j5pöÍqÁ2`(W¸Sl.ŸQ:(¼‹va«:†íÂñÏï¯~Ðï¿»ÜHÉJ¾)¤V.­D›Ùá®ÔƒŒ6)/??¦n87©Å;înKð~¯$í(·z¯Æ‡Lpÿ~¾”[AËÀv–œAP#oÀM¨”½ Ôf¨?n`Ïüà”½²c?n·¤lsMëùõÞ¼×ÙcÀ…ÒYWëlÿÂÁtŠØdCÕÇqÃÜ\«9iòâ çÛÌX†_‹ÓÃ1„ý…˜“døùF¬Ýd“±kE°2ÏÆ¾1²Öï$ÃQ–eoZ³_ǵýµÜ¢~Q“i†afJ¸¤®ÝrÇZìê݆uonöTd¨d *‹P𠹬Zcs?rËrrIc`ªœ^Ò§¤›ck\‚޼?0=²ŒÝFL©n¥ zF‰yÏD¨¤Iþó¡??rÆj¢‡™Õ{ÌÆÜ&âÔúóƒ?0cÕ9UÈêlýõÚ4XÖS§Àq“¤ìÖzðÌs…äŒèËqÖAü`xp;Á¢I´Ë$GJq."¢»)ý›F¨ëÁ ˆn…G“u0ÿ¢"¶ ûÊkR§ æ/@@ŶXe"xÚ`$vìJ®åo¯‚•wµ0ªBn§ãÖÆMˆz­ò??µv»uöâVûLUðVÇáÜËB›}??pg’·ÒèÖÇ?0 ´F×÷áþÙ›¸„F°Ñ•á×¢ÌðåPþ?0MÖòFÒÖ0«!ãW™,x{›äçi^Ie–þ&ÞÆöÍŒ|γç7Y+ž÷a$o*õÊ­üؽ3Ü¥ä@ëIrëiPÈj­b¿mµÊX4BY€Ë0®3Âä½vMDnC9ü-¯3ÑÑß׬ùuÛ##tS‚KŒM&Aµ¿Hd•¼/tÏpÀ™!ðò³0Ëh.krYFNøÚr Œ–å????¾•*j ód$[]ÿìù?rn‘X7ÑF/ìŽÆ^øVæÓÈà)º<Qi}%Þ¦´V‹!à û¦×¾q úO. Bæ°6%Ÿñ¢%ÁŽ #r¤ºtØDÈÜ™:¼”“Øîãj§40S€8G¥J­¦´AÌI|îKA‚P5µØÏ“£-á*¥ÿJ¥¼RHõ²©÷R¡äӘȰ<ÍRè´Ief<%!ðÕI¡¯™ej6n-l+=÷aÒ'÷Ñ}ê¶¼±SJÕë<ã7âääí+Ò¹ïPx­µ:FÉŠ3_žx??g{ô“9ê‚-'??È?r¼ÃèoT‹¦üLzsUJˆ,J€ñlÍJŠï°v&¾©…9??x¾Rˆ'Ž?r`n¯g¿¦ñŸÆCt|þ•¹5娸ÇuŽ/üø®»~äÀº×&®¡Ê<ý“Î^~Úc/l–cåµÄ xštN¶5Žçª¿Ë[¼/ˆõ²&žs(¿¢z«83eG%"5Ú xì˜q…»m”-;òmšm °(‹?r|b¨&¿«xú)ÊÞÞ@®ÅOÉù¸U~G¥GÒϬܲ+í»w#Ý–½ú}¬j†üŽÎ}½‰gƒMy¡ ÜIBH+&?rGz3éP¸Üû5ã§ŽºÌÔÎO<–]5Ó\9}ô­E¡m2†??\Û”]½¹†=aùu¥>u‘äiÙîŒmIðjžQ¨§ä–èfÄ*ª#‚ùíìã»ÅÃýÍÂýõyGÖ'ñ€6Â+(~±á±f÷.ðÅ]Õ4>ØËëØí^ãÅTðеGƒÊ’ºâ?0˜ŠF°??USæÄäagŒà<”ùæ!úÛ/ܽdˆÛ”kdTD:M“`ðÉ]l)AQ8 +#„–H|ž„Ñ733¸ˆkÐÄù¼™“:mAÄÓ¯@( vˆ¯Ø¤jV¯}ëg!ùýý}•$R úØc€z€ÚØ·­l%–ÒÃcÍZÙAªUS‘Õ â9åAÔôÔÏ+ÐI'ÑfŒÔ¤ÎGX5³-mò8qÿŽ@É_bU8ðøg--´@z€µü¼WÁ;ó†L¹ênÔ­Ù¹øf¶•?n‘TÈš??WAe²Ê$KàÃ4:v¢œ9 íBJ<%†ÒÔÅ‚ûnù´t%ÉF5ÎØú½åQå"€Ó|tO^n^¢ÓÂ'ñuBå&Ì0kÝi¿X:ùÊÀFIÁ|¥4ô—þ,߇>æïaâd¸„‘·Àm2#®ÈÐÞÒeýèÄ< ¸âÚžÕ\bÁc˜)©«­e‹éâ¥Ûö$‘DÛ•EH :êôÃæÆSߤ8ãÆ$†VÉÈèwB›¶#Öt3Ýùì_za+(‘ÔgdUML:ªš¿€XAf£²ŠÅ£ªß K㯂ÁWÖ™nçeì6‘™¨Km"ÞJÌI‹DhÓ b›°¢zÄ>†¡‹wž¬Ê0¾S¶¹[Æ5Hìg`‚XÊÈ6êî‘ðƒ!š~QˆžñR¡*Y¼™?0îÖu°yÃe§V3×´&¥í-¾‚Gql?nôŒ_w›B±\#¸`’19ØÛ¡ày¾çõðéí¶æ‘VŠdJ3TQ¯L”‰"z?r‚û^ž¾þ‡­-§S¹XM«p+<öúÞ©€?n˶\ó¨¹x³ìz??¿q`¥º–.³¨ /¹jÖÖÆVoªOªqUÓ/?0pzÔˆMlqÀ­"Y›“ ”(d~ý¾ò¼””GIºë!JB+„?nÍÓpæqÞj+“ØOe!bé+Vthõ´Éj Y›ƒ"©b¦5GõQÏ%žÕd+òn°·ôˆð Ç…Tþ9Hê‘Ê,,wöÚR£K@_hR“%á¿ìM¨‡™ÕªÚ\CV †h!†öÔ÷é¬çCEKPýFÏMBÛ¹Ùˆãåæûâ@§æ,n{Öõ¹ÊÜ'Ã:Ȇb(w??Â[ÏÍÍí:H=yN:Œ/*„[(·‘°ØA¢*ʺ¤pîûÒâ€XiÚ§ËW mÂ>» |˜º¾¾Å-·W-D–€ŽŽËa¡Ë®–Ýs‰‡Ð¢1ÌîE){÷IÔΩ¶Òåà㹦x¿=?0LkïjtK?0%ôÔ²œêË(÷ÍñX$ªãúÂÑAª+»Í[‰ÖX‡0U¾œ35 œØ!ˆ¡æw™À"6o¬‡Ëb«vÆ"¼½ýy ñh w ªÓ˜á:ôäSS_\ÒY>8¶Aï^¹_1¦i¯.ÖB5ÉוJ•UnþFߺ3áƒn.㉓—š·VµÎP}ÐÕ˜‘=²­æO˜M¾º§½ý4ž&÷г*??)îô„eàѕШLÒëÉoWÚÒÏêýG¿Ò´îÉ£"Æjm‹GÛßÚwwѱP¤lÅ@'qýF´ÐYšÆ|¨h¢ ÿÑs†Ñ4¿Ô­ªs°öDðçÅK_K*$4ï,4¤'j]þ£ÞYkF«ÁØ©…ìNž>25ÙÚë(›Y; ¡Šº£.Pb“¶³hTÇÍ‘ß|ŒÁÔÿÀ5÷/`Î âÊí ÜæX3l<??¸”h|ÝYb6ÐHÑ.]Phý8!îã?0pßÚ,Æ[‡Ó{¼êàñ;î…Ð ä?rÐ-Ø?nÑAeŸ‡+„ºáNÇ€Ã!þŸW”Ÿ÷=ñÙ7 ¶ÛwR€ø¢#ûU@èRè˜5·î°Ð14â0ºâà?ngaô¼§Ž áê??SìÜ1yæÒÌô'£Ñh‹ä?02 }µDœß |‡uºÉìŽñùàã¬%Æýo¯w €ý¹¢AgÀÓ#ºBŽz)hÃÜvñJa¨ÐuÄð¦|1—ã¿Ss,°ÑÏϳSàD’Œ…ÂiÀŽŠŽ©\£.þ!ü?0áPüþ˜ŒXÿf‡Ìá»(rà~!t› bBÇHHxŠI™â<ØÿEì·ƒS·ƒä#|‡?0‘øðö¼·±PÈðüŽN€ùœϱb ´ò•Aðˆrp5[È?0ýüBs_hƒ˜tX>X!ÏÀ¸BhþÞsY6mضKmà±C,"“ÀY`sœJ‚bËäÂa·‚ë—& n=`^™ÄÒÊ ?0,ê'€3¤ñ|?0p{Qð½w›¾ÁƒuJÐ0hÊ£œsl&a?09ýÃýù²(rxÚ©–‡?0ôC †ÙÓûœô¨Ìȹx`¢Ñ=tŠÙ¬u????B»Ç+m7Q`xxÍÛ¨ƒõ‡7\îBÚïñY'u¤mzÚ‚çx[fCZ "T–…À¡ £Š¸MÐåð¿/+‹‡ÀQ9šêÞèU‰~*w{»??›î\*d'À?rOs°ΗD>Œ )<)Oᆠt£ƒÖ e0.Ô¬M¯¬¼¬Z…TÁ´G©©üÔêÂ=êxált–Ñ~ôFV«Œî€Ší(??Qsê[Ñ2=¦ÃÎCƒ“ú„ïÍ^q†×ÛÒLìŒp|7¦VÕ“™‚u±uËÛ)Ÿ×8Ÿë[Ÿ!i³xKÕ‘ó‚™;}¯˜›Ù;öóìB:?r3Ë;ñ |¹WîçûÄÀ¦¤°p+[/DÅ?rÉdîÐâTMв#t °r®ÍFWŸÇ¼8† Ò¬²²?0Ùökê?0ÞÓÌÇ͆n&.ÕìEª (I5,I)èÇ\¥ˆyz„aÖ…y¿·;G"lÚ/ÙùÄJî³!æ5dÂlž4ó°{eGY¥²¸Y%‰…žæÇ`R ͺã˜Ðžk¡Ÿ² ¥¶I§³ë¨¤Åø6 “á›LùçTÑÒtâ«‚phÍ+ŸLÇ=»1Rü@†:3fÅ3?00^On€íc?0œ—6èTäyw"Kþã¼V]–_å›GÉbÞ“©Î!ägAUP’ DíÏÇš ú½ž¹Þ¾®šPż7y„»E¡Ý‹Óž©heQaÎʯoP׬/RN¼H¬\ˆ´kÐ$~{wà&Ã#Ý¿¹{ÌLlË~8Â$ñ±ßÛÛ÷͹ãlb‘ô‘x»—?r/pƒ¡³è}ùti‰ÛÖí öQ¢®ç)??ùyZz`Á­ÓØsÑUi2f»èN®"Ü“0÷ʡԧ?r§þµx‚ô+3Ö5ÐH%TVÖu$sÏ™zT eÿé&¬>¹#þW-¸mS›Þ=¾ßÄrX-º¾H¬SzÙOïtò*Ã}r'î‘Þ¤Zgô_Ë\?r÷¢š{†¡Û•??ý0ÂlÕ??Õ…+è†$”¶¥…¤§Êód£M¿'ÿ¾™_@ƒöØ,™Bzž7Ö†—C5b ²|DžECyKß¾ne=œ?raPÔâîÆNh“ïFTìÂíè¢^·#ñÞ‰šÅn}z‰T»-?r¬à†gøbŸ)c??{šöTáôÏ_àÔ8WQóqù}ý)ÆE@ÂS©Ê9|âú…<[\Øz Žm=J÷ýà!É/‰¤»b?r&Ë‚Zeœ»>|\Ùk{À³ç~¿¢zE•z@‡ÁØ—~ø+’ü¸µmwúWGÏuY’·GE7p&°?0hÔþý}²tñÆmfN?r·Bƒ¤ò™«ÕeDÿš¡„€ ÒDyV%%Š6pßÀÇ‚šÁ=qsi{¤ÙÕ<+pS#þ=âÛ@V¢”’I`ï/ªk· Ãƭ·o®ao przXøõ-´?0¿žö"}Õœ×&Rº…®dB°c…rj©&P»z«½^;£c½©~ #á\o¬|n¡Èí¨È¡o M-é@ÏAB–,¼^¹ÿÛ1p¶ÊQâ»Ôf,ÂɆÖvFBŽ0M—1׸JüA9hY¿búÇwØÉÁ`œ­éïµ$£öKà2v(u³üÔääÜS"%¦¼•íÏŽdsKåðK8±žClR…òæ+<¼¯õc0}''>i]öÀæ &¾~(Á÷ã—rÞ51—Ž4wz/®kzjðÕ~?07Émš5BÜ“qEdC»¿Cm·‚–4Žvµp +#??îú.[ïvÓc <„áh±×ûà¾4º½©l0 se0hB§Mcý0í¹)\|½—⧸8g;TsIb²|¡ß¸x?n¯ä:À¬+û??!·È9Ò;ÛÞðåÅ2½–ÿ•Ù&Gž”úyÊ#Í)E°ÿ+#ïÃùÕ˜P??-¯v4ã„!#^ Z™,Ëf<,w¦9:ÐMîsº´À sºÇªßj™9Æ›¡ãþ7Œ×?nçþÌìu]^|ÚŸÖð¾‡eÚÄmžëØûL²-‘<~ÑæQh3'Ë1Ð…“5Óï³j‚–&…ìªÁí?r—ú γ*NjxxO».b˜M*¦L¿¢¬&ê2„׌³O)¯J,˜gãõj§›nëB›„ÂÜ"?0‡•!IçÙnÀ—˜ô˜Ø'êj¨RN¤8–Gù£Õžm±'ôëŠRÆÝ?0#Ÿý¥µd¦,±×?nG¿ù?r·},¡`XÃÕ6¤mnä/ìáÈ>÷ÃÔIÉÂÿòIOW6‰ñi. i¨ki'&Ž8xäèë ìb¿Än‹ìº3+—Õ£ð‹˜WQëû($ÄÊnKn?0ýthp9ƒÄ/ä%bÉ8ãËC” å÷úLIÍš>ß¡ž¢Ç›Yþ¦“Ø(CªÈñmIï¹U8e4Ù„šd­Lø›¦21eŒ7’`·]¿„ÈÂ" 6Q¿t Ôœø°A´xL9y3›˜®‘b*R´¢Ó}Pëîè ²YËš+]Oû‰ÛÝ,(º¤..Ä… äk$ëÆ{X¢vÓZƒ[]‹º<@yJîp¥¾õ2—ú Öðªu^Y±‰¡’ Ñ‚¶o‡÷_×E>N+k˜>‘ú‰méÙ-î~?n^øTk»ôŒé”¢ÀR dœóTæe??˜9èw¸‰”ùJ:ùb4Ì !²2MZlNé«Ú|vÜYè³7ôNv?r¡:¨,’!ÉŽÅ5H|NÃóOkìÛì??îÙ|~‚O¿zÖR´CNo†ÖÁÅ­Ó€¡˜†ùövM틵OÖ67¨µ)¨h2"°6æ±™pX4?r£‰™zÖžõnõ=fe’Cq\'‘ᨋ7&ªÑRá))±¥ UÒ¹žx£®QD;ynã_מÆìbeÃÓò}=À©øÆò”𬌢¹î1³·d"£9åûíÅ•pnIn> œ}v7]8ì‰þ%Ù&¼©+¯&òU–g½wÜŸÁ£fžŠÄü!Ìg¿¯5@.ŸçÛ14?n€®ê*;hÁpi‚¯©QüÚžœBnnÊ6öŽ‚”/¿?0mùPß{;º¾»uf3VRëƒT0þäjx)¥›³mML=ª·3\æ\îypÈÁõ8©ºÓ¾ŽTc˜û¦XÓ{PÛ™Ç$ÃŒÿ¨ëRˆû‘=è<ñoHøÐ‘)MŸàD‚çRU;”ï–?n!þEN•úLƒ«?r¶×(â’dõ÷¾' vá%”BÐl$ÿØl_°6Š{Þ¦ï`‡µ71LùßO—äI°3ÍWȺüÌèkíYƒÍ´R–dŸ`;üí‹,5õ??ž^ü¥)u±V±È<†©>Mì~Æ{k;2ì´+àc³?? ÿ=sí‹:…LjûJ7•\ÔÈ?rˆy½C‹;½–‹%|m:SMó°9‹˜Fdú{¶§§6n¦ÒI¿…DßñŽm¥ºæìÙY¿q&Óïeø¦ñšM¸²³ÝX:ûRÚžØB?0Ð#JOLRœ=Ö"ÙM¶ò‰){ROhò”×o,ƒ‹°jöR²‡'Âúz׬ÝÉ¿{$ôº)GÓ®nïx‰NÔ¦É6vAës+??¼§A,2fxžÙS—T¢î“œ‰ùgvt½ê`ýÓª kD á~I^¾÷ .§Bæì¥_I'[}‚ŒÿIJ Vìé×xŠaZãXö¨…> ¶–¹ù€Eeõ¸½€Â~LNùÒ}:¿FöÍX¸”GñA{QÓô&‘s'è †ªÌEí-/¼›Š¯wTsÁÜ›?r*ì6Q<(ÝLÆNK®G¬O‚2ÞQ{}Q\wåkµ‚øÉ7<̆[ÔéØá+ìZöoŸ/œ[è›®ÊÊóvQyuãÉXÏ“õ»®«C=—-v`^[êÆ=ö…%~p·µ¢“ÈŽ¯L¦‰˜F‹Ýã87|ýCjÞm_„¹£GˆÑC†Å^Ŭ¤¤­½†êÁä`À\BÖåÔÏSˆÆúîj\ÍÈ™ž…ÔI&Í.PÍ)Ñ•æ.Uç¡·£cxfƒ‘b\†ò9MrgNM3S=_™k¼@ÌJý&2¤”v {õ‚BùR¶–:y(:dô­›¬¹"Ö1z&4z·JÑAwb.ÂéºþI’ÆÜ ´u³Ô9ë?n¸”«ñ3sPi«ÈW^¼_X(7ŒàÓ¾éY–Œ®Tûˆ<²R&WrÕ¤¿´UpL˜›??|ŽùGô øØíédJ¢ÀY»ˆúòúþ¡}ë!ÿšE&ÂÆ! VÎ|«Œuåe.•7(ˆS?r•º•¥cjÀâ­¥-Hêêëϲ8Öiʾï@ëÆÒY}:„ #Ljö3”þúÃk‚®0 ]c€åø$„½ÍQ*¸[âLK"8I1…ùprI‡°’駦 •o¤îØÚPéúÛDõûêIôøh¯/fE×w„„õ¨J«R·ªô§ÆáÙHª49åx«EMõ©ö.k£¦›/?n_~I9IwÌa|˵㬕~˜Ë½åV’ž®CÈO¬Wë23¶‘*cµo8üNÉ®]ÅS‹‹j±ûX©ÈÎ ‹j„¥’£œÂ5{3¤$??~çÉKŠÝEG€ÓVdØ-s??¢ó„¡?rå'„Ãäq4²]©Õ¾ëõŽ0ÀúY’j5MõV Y¥ÏïçlnwB ¡e6Øé<Î̽ɦt?rÔ'^ßhà9 1{]ÁpdzL*ʬêW¶M'‘ýÔGó\í” I¼c Jæ¾°g%,L'ÁÏ{V…ºöR+Nì,#§µ˜E¨Gkç`ZÄ×ZÊnNÃ!C.±E©²H¦SF}(_µY8Kä¼&}[ŒºoGå¯ zJ[Œ½åEßêë¦H“…À²]]Öý.D;‚7¨hXˆ±¸ÂLÙÁéBBﳯ–Áú¥»¹TR6Pø‘ $æÀó2‹^ªò| [ÿC„ãdY,éù1òû¤MK sÐÍh§S´Y쥽n…|×õ¶b´¢þÙæ0å¸R˜!×¶¡Šš³Òo¨’ˆ˜Ÿbxhë‘=å¤ç6´e{Å©ly·ißKFþpú9ùŠ3üy9òN먹?0Ù½~°ØÐ#+fS@Ôã.$pPuÞ[7Yã8éWâ?0hà5€´ ¡ýQ_ iq¾ùt?rHA¸Ò8Çàfƒüu„§Õ·5?nKš]•Nïî­ ÎºvR|F¦QËã.GGª®$?n~Â0ξO®˜)Þ€EFq:„½YÙ[Cºûê®÷9#œWý'LZfªÂFby„ζ—?0%:6üWzñµÇገYç:§²åÀ]Tòû™Fã\ä„ï‘’&zžêÂÛÇw,3NcEb‡ Ü»Œ= dÃþÆf(ÛÞµÊáòKÉ/3Q¨Ö&ÒÒ6YSwøîVàx]çÀ«3¼ß­öÁ[ã–Xf•ÇP“??SªÚ4‚$U4޳é`­w??íe‡WÊÃìΕi‚°ô«I€ô¹À3Áu¨XÄoPŠ^Ëh3ea²ΰ St ð™m™ÎÈûÌ'TUÍgUî% Šª„Ë1PÒüeÀ*WoÿH÷I½æ¤&Çä­KÏÞ$I”%R¯½NZÉú)µjsE¢É'äÉ_·'“>ß'™0 [J2·ø¬YÝ6[äÂlt¶IšiÊ_U|é2õëîÕ"Yê‹ó÷P²su_÷3ŽâP™éŸÁô±õW»˜‰£Ûû½FàM|M>ÓûÔÊzK׳hégºî6¼ÕÛÝÏYÄ_Ò)¹^ßO áÒ«l`é·D7‚JÑaðÇU_C‹ñ̦l¯EîMðü‹f›GAíþ°|ÿÅ6FÔÔêzR½®Øo‹â.Ӄɸ|_þÅ}ç;C{ÿÊ8^@ ]l¬¡‘ý€ê.­¾ÀI]æhŽÄjDwQq¸jÖÅ´=~%[þ9¸Õd¶ „BÞJ»þÃ5«Âµ ê–“G‚$€ƒB`¥Î¿ä=>R[=½<¿«öÖa‡“NÆ}nï}s ¢W ªÅ¢ àUl™W¨SÖvpÙž-EÖ?nŸ)r<)_#Gs‘R,b?0Ê”tB}A€??2 ¢à.«f.FìB.3ãÕî4ÂÐø‘˜Ac!vY q É­ôÌ—;£]&m¡uæÒ±f{ÃîòQíE'{z=T{Óªv ཷZ«O¸/[´^Ác8€]Ïÿ $¤œ¿ð Œí< \<éÝMèmMèŒé̽?0þŸ †ÿ?0 Ëž2²³2üoŸþ'˜ÙXYØþãZ&&VFF€ÿ8eff `?0ø/€«³‹¡ÀÿŸ??B²À ør«ñcÌô×' Ü"Õˆ®ñ¸ÂBPóúïèÕ…i¥ÊÀy”EVqïç;ÒbÕ?nø¨áììÁ~ÔX½°¢S¸­À€õèâéÀaÜI£»ÏqU9…Ï8¦)I1µžð…ö†à¦ÎY ì~|M±˜–Ä¥)÷|%ä°|ð¸邎ïàôK23Ö~¿cÐ5»I ÷ãØ§¡F^kêðùçyÚQžö*‚ ¦”áNc:rµTr´Ó”t?nS<£,E1'¢†„ˆ17ß*Õ˜í‹`±…µ;¶ðj^àWÌ" `:­Phï¸,w¤É Yx6Ä@¶Ò…%B×G¥Æá]Y=FvŽFã@|W2_‘XWÕAmMQQP(16Ë×ÀÛIg?0~ÐÃ%øÓ«¾¼+}±MíÁ[û©žÌžE5V3Ÿs6€Ój1²¹?rKüpÕȘW§­ÄJ±ª— ¡çg1w6pê.'‡ó픕z÷FLß¹S]Hêf¸x’)ßÜ)lO¾Á â§mÅv†:é5¬.B!„iŠ{bÐôãÞ?0b¥i9H¨®³P×[Lsmè«t“:t敾S7–ÝY˜ø|]Îݪ5°ë(÷ÍÍdvÚ¥o„¹Èä'éÎt[†Ì~„þƒp‰åœ*b5ùd[„¯òâ‹2]l\BÒ£²ü¡X4/Yèñ6Ú 7Ž~Ëû®µ – ™$Br€÷¡æ¸~?0·7\÷qªÿØ'²½$‰>ynç-9d?0L +#«àUÅ?0ÏqÛ0QJr?0áõFÀÏ‹T:[&÷pðôzé.1vF÷ ’K?rä´ŠI¬Én*b huô ¤ b„¾CSGpÂj=2•dl˜ƒ„é‰çŒãÄóq¹‚ío‹hÏ à£;Qao2°kûh(ƒ©o/ždW‡™Çy´äýU?nG¶Ÿç!޾¡™Žã3#†c^õ/e¢Î1 ºCÌÅ>;búªOí´=J5 dÚX†JÜÍÉÊWQµu>IeEªlžÛ~jâTYs¹t$Y­?0ÑF v®Eº¤"J“îÜI¼nÒL{¡Ø–…1ßnü˜æ,vYá??Š]ù¥œ‘R??£“/ñ.Ñý¦=??P²ýµ½AËr–ýSG¾¼¿n*;Dǧ³!YéAq®¹6à*–ŸþpH¦†v‹(i@O‰ŸU©ÚG‰;×T[ôy›:µà§¬JÿÞÛ?nÚpjÝËŽVH¦F+5“2l'§@›©1í•fAP<|y¿OÓk–í¸íÌ$™´þM&¢­ai¢áÃv™uÉô??r†W³Gwe8‰i&MQð~‡9)i3ƒƒì#yñ›¹’K7?nV¤±v•¢?nB,…K³eçÔ?nóð¥ì¬Uwf¯ÇiF_Œ-R< r%­y˜i¼:q?0ñPº¬kãÐe© ?rg<û*´ÈþéÐu1ßÒHŽt wRÉÓq³1¸ÿ°Ö¨~€6²Ï"È´q­/º¨G…I]“çš§5KCðá~±9ZBÔ-5Xœsìžwå˜tü›“ ]“^ˆŒxº–*éX°ŠÚÐ4W·-ÝWwO`­M?n–8aO`v£e¨øXyA¸‚`ÊQýqÕkfµÊ£PÈh€È \€D_'=»Æ(°šó zà™›ÑJt£°?n£úZJû Mw’ë‚/¡Ò|yO öË#eeœ#~>Ž g^l~}`vçš¾éðßC]ê‹«êŽÝɪ¼‡??KÅ{Üí³J.™²õP]€g,þÅs'j/‹[â•%èŽÊ~,ÍŠ›M¥Í+j„»•ÔñÊ¿l´þ÷`æ½À(¾€³º™'ž§–>ÖÚž¯>=-gCsU¹²dUJŒNk᫨oÏbÏ4z¶kÕ<ùE)ƈêzQ̦=¼LÕ]ü»Tÿ1Ù|ÝdïH%&ŽbñÜö??xð ¤4€ÿBü_öÿ¬þ‡û÷_èÿ1þÇ3?0# ;  3;ËúŒìÿeþßûÃâÉæ@Àÿƒ{€î3¯#ޤ¿/ÊùË+Ò–4Š.ûBùYN›×÷oAC  ¤È;??U" ­+é©Û9²ÈIHÐ?0<ÛƒÕ&÷¢‘&sn½¦Á”¶Ùãë¨ÈävÙÑds:œŽ:OW ª»-âlÒ:K“%¼qàN^…• °Ê×€g,\%»8ãw“UkR8l;ö3†J™Ù\®!ôüµ¦˜“ìÍ” žW¶%!”´ŽØær Š4k[׸Ü&}M»fV)“Éje½º¶’ùÙ8ôkwyûÔ¬Áy­å³¡{èxÍh©‡÷o‹¶é:?r@þ6 ƒË2Vü변̤ˆèϵ´:óõ #Zõ¦¶·5?0F«v¡¾ib@žÔ¨Òß~d®„x½º%¾TÜÌÁÙgêê6^¿÷àl_R—7Cö×Öÿš4ÉðDà8Œå‰t,€¡­={Ìü[²zÔ^ÉŽâ(jˆXâ@#§¸m£Xìâñ´çÛA›Ëù¿–ÖôUõFÖ€lÔƒ&Jþ5]»ÖK¿‚ýzPoB#?rÆo/®*dlsÓoAè™tW†ÐñmK¨·¥ÿòÉ?r$tS` £<'|(äF­Á©J­?rîDéŽìáš«MèŸ!3ò¨Hu²x²cêçíÕ3§uwIŸÓ®{?r¦,»—;0–&?n&ùÂÝÂíýS/?rÑÞ—Ù£¯3€±›ÝÄ{?0™e¯¥ì'ÊtǼ7qNk†µ“;Xøš†zëunѱ˕H–¥U“$¶9þš·?0O•ÚÒ:M(™ X‡0~ݲ§q,%ÚSTçcæK$ eê Y …’$Ÿz¡íч邊^¬ëcG¹¢9“¢õô ŸíêܯO})—/ºC¿ `«ÙÏÅAaq..ÆhpmD1‹>rrJhdøt4t"õø)£‡}§máÔ:€3(ß éñóóK °Ðƒn«a­B·U®kwæŸ$}ÙT'+ÀͲ<’YTP olÃ#Ûõ¡~·ç‰ÉKÜï¨xÝ??\^ýA]D¸ÙœmÐ83d˜ZxÚËZŸ`d3@HÔ?n´©6`ƒ7{­âài¡ ?reÂŵºS§‡Në¥1£‡ŽJ±/’<¦lÉf ‚l\¬A’òˆ.%¢[¥ ‚÷#T~ʯUÑËßèN~¹c`ÏÅK!aæ‚Ã<{ÿÄÖƒ’h¹?nÞùm®©¶ ÄE/Š`3pXÓØliÏck&º.Éíi.Šdgˆ·‰9PP8çÛÉF#­jœ øV˜¢±€½]Œ쯅ô@q/#Ç™è3ø?rŒ¯%„vì¤õ@ñr10]YµS’K+¶\ìAÇ*OhÙ2Î<˜DR¡K·àÇÙgÞgŠ,,gŒC¸ðòL>Ö—ì"ÀUߌ0¿Õh+#,^SÅË]dè1*?nâæSõ?n¹‰Þa¥+´ZêrÜ"å¤i< äN¢5”{àÁEN ¹þ,éyà/ç@ÅÏ$äP¸¥CûX[Ñ=xÐZ?rÚsuº•`”·ð!ºÇ[Gù½ØŠï5]iE³éA]•ê)ÍŠÕ ÌÙµLmÃüzK6õnQµâ$Î¥Û×£šÌõ…ïáMF󻱜hH­4˜UYܶ®|öêp4^¨{¡øPŽî±é' 1Vi©ÈŠ"¯ûn￳Ñ-Š¿r|§Ïð› ª…ô³‚©M<”­RçîA)nv2ŸùÊ—b@íàèš ž¥Ãàè?nü€¢6Éç2Œ:ô.Gnúp?0›kµjijÔöa´sEJìb =?n8-ET„×Dø¹ “¥w14uÔ®¸ùT[v]‡²hñkü]Zµ|,¤A°±ÄË%ÏýîHï@‰"M-‘v•tTyL~óõ :@ kpH¨¬mßz¢´<ƈüR=®|‘V¿ 0)Ö/nùΘµÏZ„@,3_M+@px4?0V¶mAiL-·Î’ó¡jž¦ýEþ°¯P¿M¡<§ÿ½bdÚœ­õanÝè©ó TùÃÓà™*#U›03ò`§(‘zw&mú؅؀­…U°½&el¶×lnü— ³ÉñP.Nè>BªûÖþOHÒ´z˜"ó©„Õêjx0ÓʯWÁèd_}$¨ÐÍPJC)8«Jsͬ*ú#Z]ú»?r‚Mó¤ŽÜV­iÀ“ÅÊ¥ÖÄïíÜ«„TS½lLO­Åƒ<MÔ?nP)††ÞÜ/E-,qnàÄhŽMиYöõò%ßm‘NJ¸j.l¸€¯Ý+~››¶6 BbÌV‰ÓýLù5»±½f‘[á¯õøw’’5JTRÃÚǘÀ¹}JÈVÆœ¥éd‰5bµ‘’Œ¨;žr¿óM?0—“Ìàôx¢~” ¤”@VÄÑšjû7‚-ØÁZtyoňß lC&y¢©|­Þ¹Þ¬ë@»nòteàh8”&àЄàžËÀB¥W÷—Æ)ÝÑúD«ŒvƒLHj#; ÞY7lAI[°²?rõF¥ð »¹­°º0®\ô¾È¾ï×N{j…¯IGÜ•6xÙG†ï¯]y‡Bê0¶qŽ K {¬Ø- 7(šÖ}"Y]Š7ÛM ÛX/ñÁ){s{È[)b:Ê©‘É“ÑÀhñpWZV¿ìh¨(ܰ|MÌÂɱ¿³Ö’´öz¼HU:_ßX;äÛÁö%gÞy¤Føyb죖Û2ÙîéZưTJË\ӊݬ?nÊÆ8ݧâÕ†Þ2¢9UÌ[LÞ9ryeõçn'È9$/]²¦'ä!®ËáŠøºŒüCìØÝÈßÄw÷¨¬~ >„‘R€yû7ªŸ?r2üÃû_¡…8Ì€õÁIái,Eaæ}›>íþq†‹?0{·]àïÎï’–,ï!‚Ãö®§»|ÉPE;0Ω-È;â`°¨&Lµ2Ðî¼Q?r÷`-Ñyú:ýÎ?nÄÐ=Z’Ϩ™É±7ŠJ£¥,†¾(‹BXýÙý…?0hgFýhDÑÔ=©{îéþŒ»\ ô—5ÏÏÃÎÂ1NþßÛ­?nÊæBF³‡Xø Û49Š-¶QˆÿÛФJÄÍVƒz;/$d²›Èç=ÿ'ÇZs‘޼eì«­œRõ»N—J¹\gHM‘É5.z8Q5@daÅJêÅTžÙnî[2r êŸ@4@FÍØP—µy¬.Ρoë­`x ,³†!2¼Á.rH–át©žf»fœ0A[ ¡?n7!Ð^^ì²eŒH•–ìO¢ÒÒ¦f¢©£uèü¯AI‹nßÚÁ_„Ná{o/§ã#(J<µ·›¤¸ž"ÃôïÑgÓŒ¬—/£ï +#‘Ž`CKo©;ƒì‡nu€ƒQ2ádÅî€vް#*Ãr4»f±u§ íZžZ®üioÚˆl¶% eÞ!úD]nßض(Òb(NúðŠqé?næ¨åª‘ ‚ü¶pq@“î9ˆFgü¼ÑY &œvŒÿ­vjÕtŠšJƒ±„~Ç·ãÒ‚¦ÆUa èri½Ë¸U´bòáòÐ"*p©ìñ+ö­‚<ûæŽosy 8-öÁí7BóËrØî—?n_¢›Ä‰Ë.!YÆÂÌ5îÐg²4Êyˆ¨>ïFqÒòñß\%EÁŸIIÔ¬ ¸;sØÈèÕ>pÃôi‹"Ö:åíÒo&oy y#¹R}'ÿÀ.Ñ®a‚×·?rùÍ5l¥Åˬk/£kyWï¸qÝÈt Õ¿ÛU¤6mge5)†£ôµÖ`¿õ7ágmcç¨óÞ_Þ«góq2rëP6þPðQG µ®ÒÙ°*Qtmª›vuÅÈ\…ìL¾Øí•qx#ŽOä] ÍO&ß-µâbå&Õ˜ï˪©3*DYQF7Ú\3¶ñòµ›ÔÃ÷ù¨ –ºN]£%B‚“ÀÐRÌ<ìO?0?nBñ#›ö= R?r¨¢tˆ%Ѽí-ÚŽmªM¾ø°ÙÙNM¶é“Ü2‡LÞI3.ÞÌéÚ^0¼vp™G˜—«5–Pûç.¦n˸D.g[Ì·…Ü?nt„dÈBü>e [á¯tÒ#á ×ÁÔúÇâÔ ɲvÓ5F€Á‘é£P‹>Ìgçðñ‡â{¢V ÆjQK7"›­Ññf]ÖkRß®?0‰0Àð°¸LáïÝ ’ «Ñl¸~]&~ÜEó8~²žDnÍuN”œßq¿[ÑHãZnE%fqÌd§{N˜OÒùâ…0Ów&–Ë‘ùrÂóéÓ¹,K¶'߸º›öÛz??ýäI^9Õ±?nˆ=D ù½”¢Iɤš9@ˆlÜPý+ÏÑûe4MÊDî0ìo¦³(ÜYC­)Ï?rÇ®¡ÆfG€KÑ<¶j <ý•A»^ÖªTÔÙÒÏ›oÌ»Þ †ÅWSõ–%m?0x ‰ïêšF…Þðï~ÖÅ>å$ßÀÙqÌ?0qM”>3;dUˆ<;MüÑO×>Óf÷ÞŒ òÃ` uÆ{"wÆ<­ñ@|ZýBŒ W±Ð{Ë«¬^‘9€Qlb6A6ä#??õä?0ÀÄñ<ˆS{›j>K¼ú¬ç—YÅØ¯»ôˆÆY¯ò˜8Æ€¢€‡ö€4YW;î{7üãO{??$JœÅÉΩP«‚z‡dGû)¹-¿î«çÚ&|º¼ý·êÊ¡Xoo}È@·›6÷–??¿m<Ã0FnA8á¾JŽ´\ýÏwÑ5 2ó¦-®…÷Øõ–÷ÖÜQÕöé7ç{iñË1¢})-–&²¥¶f×±p›‹©¸Í%޳)××(dØún)…’ãü ê6PùÀà„㟹­7¿”ÛÕÂ9–ØánÄ…VÊÐúö…TKÙÐÅ~ŽVʰ©à‹^(ãq§à`&X•GåÇJñëÝ^«Ü¹pþAˆ|óÜÕIà›{òÝÍ»"³Íïñ|µbÒ ¥ìÑa¦Æç_7Z(¬}þßW¶]ÍŠËGÜEåߦ÷ЖNn[ÔŽ??-$ñ>¨ç¯‹$´%ÿv§mO¯c×D8åFg8|}¶ÿ1.?ré÷šÄ&w/iž˜ËVxkò’8Êáx²Ç0?03’jÖþ¤s1Íà+O?0“M(¤[°ÈJå+`y-Òš»ÏGjóqoÓ:?nLsTpÁˆ p¶?ræ½Ï·ÝŒõp½,HeDFÞ9 Ë5ƒCŠ@ ¬;aÐÍ3u1¾áý›Á†ë…ÒG]ÿt@;ãg—?n:}íM“+þ‡£”ÃHðž$ÆÖOã³¾„.ÞÍh…¢¦éKìyÔâ4ùÁË{ÆD«ä!0þÔ¡º·©Ìð_…ÿÆAþÏÙü¯?0ü_Pÿe`aføß¯ÿþÇÉçÿþ‹ë¿ÿ“{?0WÕ´a„ï9ÒÕÜjjNYcH{aw¼ç÷é1]­*)1"¼ãâcž?0ñL?0å6ϵèøÆP!$wIv·bš#hQ2ò(:l‡“›d€À²¬¢?rYdÎé‰Z$H›Fz•ƾš\f o7›‹ŒxÒZQ££ž·…7¿©«^]ï+ccc… ÁûW»le¶Î؇??h/à?r¨c­Sž•ý?n⽬;O~¹·UkkkA%U8,ÞB˜?0¾K•†¯JÆixeõk b©…·/þ¿ß3a¸çw^lâšÈ7ØW2Ë­F??\¡ xÉ!Š`Ô#9¿X¦È”¢*=ùý8“Jh4WÓò&˜A“ÌÇÿ|   ¨Ê¤Øœ°Jìì ›»™z–zzz‚çËk4 ˜ÛÝGRÀu¦­0„hÍѵTOP­ÑÎ1§Q%?r:qÒ”íê69ô–WF„î£Z˜ŸœýìfçêâáÁ_ïÚVôõnVôõ~ŽVøõx¹]cú.µ},Ðgšžh‚à:¾Êýª/ …VËsÊ^”8%1x²3IÁ¶%ê_G# H‘r0'®Xg%6,¾ðŽPãh¹ðéELÛQÞ>ð?0ˆëË7JTç¬üRQ?r\•†eA÷’fÎ(.á˺+ž¼b_¢GqV)ëÅ8N?nÝ=l3„À:pâY³ìOpÚÁaUœØô1ªã N½vëW"€¾H¨‡5²UƒOšæDcMÌ_ÅN×úB=wŒQ²B‘òn{®Ï®Xtö÷eÔ¥Øft¬2:v¡å2óëÊ:t›Ê UªÖ¥¡(ïûx8aÂ,û`fQ¢Ð|< ©hTh²È6‡-’;\¤ùFÁ¨‰W¾d“¹Þ.5¨Æ#ƒ9'ú÷í¦tØ(È̬¿¾¬}ƒ]0ZEúêÜçw?n%¨có “£‚N}Qðia6Y Â`±_¾£“µéèÆ‘é¸Ê’¢ôˆÁ•*l–öŸ&O‘DÎø¹¹‡öÛÆ> &íÉðK]ž›‚|ÉËa:ܳ9 )V¦ÿºcöºk?rw0ÒßIÄF.¸n#n~!±@¾°ÍQàò€³µ|zç—r˜jñ/£Áy_Iâ OköoçÉ=i2©¶µÊLƒM‰É=GÓ^Óë~徺6~U€çó¾å_~ó.ýÔ?0¨?rTAV¢4˜“þm¶D›]¯¤+"çûÛ`ç›I ÊÖÔdR??'c6÷°µøÆedDg‰ÂŠ.E’ÐÒš’.À€± „[jbbؽf_·èû¬Àz7W!°Û’ôk*…¸ã|E£ïãùœ»SGáˆÈô%<«.#ŽÇ¤Wë+.âôš§…^7ß{õA3M ßýª©O‰a%0BÕÐ7QQ^ˆtÏ~†:‹Ž »1~®ÑiŒ,àSš¤ãîž-¦ß¤õøLŠìä;Aè?0éX UNÍ×A.ˆegeíi j/R4ú„%ª½"<‰ÐXzáh=‹OP'½ÖbHNdÏò5×)dFc*§·èDm/rÔ)…½%ÛhhwuúU8ìÖC=;»2Pæ¯]¥{í:ƒã"’Þ­‘ï—¦)3[î8m¡„ÀÒÌⓗо\×þcÄFMbþZSÜCÀ”Àºqymp À˜õjXðC ê@(çûz½ª3X§{¨:&beb‰*GFpo%i^2ÒâØ\~QeNv݅󸆢ìiʲf³ªŸZ÷‹rW0àèÎ æ¾·Ì™YHsy¼–êdæàâ ñvø&Ë©v^U“L2§@'Ã3“ZXUÌYÅ8s]!PÞkEr¢§T:kk"{jQjŽ"Ó¥¨]œ©Žd´}õXMßeÔ«×;(È~2šqQÝÛýC Ze!¦n ÐGßl¦…®??£² —ŠzÅË“¥Ãš¸t’­«€K2Úc4×ã{ð¢XþÆ £+IHÙÙu/¾‡±L—,cn;wA™«‡‡¼g>N®pte뛕cŸŒ„óawƒgåcoEñz>'­›Yîò9Ÿ‰‡Lü¹òvõ#છþ«Ññ½?r/‹wiã6–W]úÄf#ÞÐ\=ˆ²E!4ù¨i1o#&O.åÎaÄ„|(Ñ*æÀðâ«’ÉžÄÜnú§ê1â˜9 ?0NI;Ôk?n0ºB•ßù³®,oSvñ*Êmà¨8eŠIÚæÿ\?rxWÅø\¡ÑT£ñ¢:d¹ôØ7ë‚‚¼§ô8ÇæíX.äôS<Rr$v”úZûÖŠÒÚ{|Å‚ß$f|@ÒˆgTvh`‘°üJŸ}º¼ÙÞw-­nŸßºuÎ…ÑÏ=¡M²3ßEÛkŒ3Ë9ìëq„»î^¥E_to >K$ª5oOƒÓã Ur[‰…‹k_å06!ÓÊåÔêËõ*ï°‚×jŠÂåT”€_iQ()ÿ8 i»oÑÙº!€ÞÑ~¥È‚è V7ÔÄN‘[?n²Û™/*…0wºCny×sK:›{˜üíÈ4>¡ ž`‚Wië–¼=“C&|—?0µ¸|•¾ŸÔ¾D‡…¡4 'ñ‡Ãaö—fq—^hÙâÆÂêØ-Æ„¤Ü©ì‰JnŸä??œcˆKНíà OFn¸«îÄÔªþÝùPü+½ŠÚF[„†¨Ýõë5ßåŽÈåÑ)ùU@·ßæMØâQ¹vûZìwÞJ¾Ò°»b‡›}ŽáÔ?rÕ¶(t°%‡P—Ó¢¾3˜­Ú²OP¯€àMÕ©7‹ÙT-_Ã?0e‘dÌǺđéh–a±vêhyQSYMI#ÖÌó5Y‡ƒ²£ÙFÁxĺz@b^Ù™?0½Ô ̺ i¼áë_kí ÷Ù¡Ž‰!)z½É ±j±M6›ë‚?n™1©9ÈÛ' ú&AÕ©¨–ÔÄܨ®1NÙþ7ãÑÕ»ÛÓ @zÌ®:#a:´¿Hx8[gæ¶1ÀtCHÕ†žÂÊNÄÃZ’J}TÀ³’± ½"Ÿ¿!–„¸ovöí¹/6(z©Œ0«©‘©$âHí5jnõ??­¡$í¢ÞŽ??Ú˜ù“`ÔþPGÏÂUw5ÃãL¹7•*¹HÝzp¨–0ù]êƒ!{|_VNóÓå²ÃÙús#[ê”hÉ*1牦8$_w_›•ØåóMÄéê05ðÅhù™å[4W6ž‹ËŠy8*éR???r…úBìÃßYmÖý%šÎ©™lËÊ€5xøÈyóÇêÞge/JôÕ&ùqñý:*턌ÎÞµüà<(ÊFùÐ:C=eõ|×!óvYÌ?r*w\îŽ=X©.èCúZGoüµ‡Øêôè,!4£Z†>ÞÝ1_7Œ–¨éÒæÏÇ{©ÏÉ=RÂßgFeY±«uÇ®ìå{iµHÓ<†Ý楚%?0 "Žå±—òF{ã,ÿÊ”ô†88?rVdKþ@ìú¸Þq7dt‡å=_¿©Þ¡äí{6!”ú[mÅY«!Š­m±'„2A>j»è#cñº³f†髊’C0èŒ.ÇP6C”§ ¸ã-u—Ûð™Šjxp;/ücRÑšM b¿9ðàbö2§€XØÊv…Ž %Gå˜#=ú†œÂu”ùÊSÙ¬0ÖÎì"œì²¢äb2Yw´L(?rnåå‚~h’ߟ‘³Wì??å/wHz_í·»†«á@"‚:»3¥Wo´€×*?nƒ9g±bzc~ï`U©•žþò òÖ\=NÊÖЊóPrgzTÂiQA‘áóÑ´0ÿŒL¥`ÎÌô÷_³xp{z"èÖêæ¸vª‚ß ¤f¢õÞšªfO —%gã—=‚¢LÚÔ“ê'z§í=åüð1yjD[Q*²©æÊ"…@°D4Åb0ŽæËlÿÕÅŠ—Wä!«{?r›" ó‰ ³¼Á18>î²?n°•Šò“b)mLHA&#„’O;ǨÇÚoBòªþ¡û–±9º$x¨J•шCyp¹¹:8®ržëù.IƒïuùÂ’q€¯æ ÚÍ ?nãÂx‡ëÍ=€cÃÕR™LöùfÿŠæñ¤5t߈LìS=ÄŸ•ÿS³ÎZ¹œ’Û£ÿ]Â6k3mÁÊ-Ûå Û%s¯p¼ëÜK??¥¥tm^÷€é´ Ú¶e³ÊIb|Ó6ìЀ¥©#âà½í\Õù gQú†ÀÑyÃsˆÍ6]¤qó°<`“³¬z‘[©[oh|@`ËKãÕUpOÏJÅàœ8ðˆ¤¤À4?0é¥ÛÇÁ¶›™HÝwRD‹ŠÑ†ò?n‚Þê߬}ƒ’ ]´ƒ^QmænÑÞ-F)Kl'Ï&Ǧ¶|Úºœ´*$¾ÎݦR?n<ìZ‡g‹?r×þýãQ,sfo»ÍËŸ ¯Ìx:ÒκiÖD ÄC'¦Šº )EéàÜ5椵FÕÌDˆUæ¿„6@è?0ïÔô<-Rt¡HSöþ.­I±rûˆt•‰Hñ>î?rÀ™ G ³Uí¨÷†jOR' €2ÖåËàì+tÓ­K(bÕÖ5ÕIÛ“’uñ‡J ÛÌ›Ww:’Ýî€ÛÙ^Äcý@UCwѨOµc+(ˆÚN§T92È·mG[¦rA¦íìæ±G”7«û °0A¢tAI¸_§ÕYÆÖd†Ï¿¥dl$´k³ w½rW46i†yþjÚ[3á(sû º=ó‰À¢[-®òqŸÐTTDÕïÉòù=‘UÙ’ƒF^dV=Çs´ß+# íCãñµ|1(W­»¤F8s´2£«Ãú™ö;’ÕŒ?0¹P™*š^Òµæø¶ÏqAäâp!Äß™‹?0å=æ&õ4™lzW´Ì݇“†S£ß?rMÞ½Kµè-I§ŸÐ,$¬bý?rh¹Ðñ/W˜ê&È©¯ŠV ›úýUëTծѰî"ìQ¡°…£*ûš˜Œo¡¬‘œÕ‘úo¡5H:îk\ø9ÑPƒk¢üµ'Q pð-JNñ²c±ëÑHÍ$È­Jvo"'cÑN‹µ&õÔ¼t4àðûŽÓÙ÷ƒ¶6f>ÿÒM•µ|¯ìYuÜ>íÉ꜃>¾*rß9~}??Ueú(=JÈœOÀª¥{b2É>ÊñN¥¶-ÙO1"E3ó½)ÂD[‚v 8¡Àz’?0ÔJœSûF\ƒÅ¼a".×m5DÄ®8†d0ÿ}HlÍkȫǩˆ]«u ½?0Å£±h™ÁÝL*Ý’Ö)ÝCµß‚ öz3Ô'Øç)A¶¼R–‚Ëý¾ói†»XÓ¢"æs›ƒ»´‚ˆ ­_ƒ6ÙAÃ5y`à £ðYË¢„“2+`?rMÂ]4Áʱ™j/hFc|[pa•PSåSBÜœPûqÐ,{´0Á:Í#Ó.ªy@)¶ßh|¨ßô®Ì-"À9êÞпPV΃üŠè®ì¥éß'bÉ…|4ÁÊæ€¸LÝd±Ø[5Bg¼äÅ|ÍÓ÷uÞÙAáÐ –yh\ÔŠ×?ne5¦¹ØQg‹–¦™AùNÜOìcèŒðÄbn¦­È9w„#ªd¼P·MúFèå©á—k²b-ø<Ë·~†‡`äTQ7â;me¼Ï*ND|’y,Ý|dRG0@Fèw6v¥8³SKž©éøsYÖb­líêMøè»%ÛS-Gê]‘QY¦_IãUޤê"WârD S1!üAP?0Ò~“p£Ö1¬s +#Ñ<õû¿eXD­{‚ªÛ$7oxGÎä´`‚¡p]/©YÞE5B>4»ŠØC½Åûµ(&º;¦‚r»\‘%QÔ6c¯N ÍdÊÇ@§6b޵äAX‘ýe-ØZ,ÌPuبýG1Ô*eR|ú‚¶œi¨ ;ô 7luRyìžÕu??µ—”ó²êÏ ¾nôX9Ɉ°Zw6/Îæyç 4<Í­Ò§'Tt:yá¤ePXÀP6án3Ô—áJçÄ[§6ûÉG;Ù‡?rž8,÷«•n??|&y}P¯æð›Ü#èfë~îzظigkåÞ&N?nß´Q{ËB6*„î—@x³¼€Ö“,}R??Ô†;¦,Gb°š³ËTÿ@vÈ_›•ƒ¤ž­ÉJÁ„ßÐ5¡èJ¢ðþÜí m?0?0‹² UUÖJ`©ÞÊSLƒeQd:£R`yR¯Ä/\¿½î뾑¯5ÂxD)ükRÏÒx:üÊŸ¯ëWq}4Gî^è‚fÎ<˦I?nÎÇš4pÍd">J!zŠöm¾ÏgåïŸãäÝìÞ¯6ümjn)yHÓ¡£¾xÁ^Ò ”úwñ÷¦Þ–ÉílÊ]ì?rô tÇý1ýL\¨J{žâûŒÈ¯´¾pѳ/ú©°:ÚÄ£Q–n½Äè­yÌ™JXû'?0G 6/ª@ ÇIRÓSâ])LL·[B7WþyÄh¶Í¢»Ð׺xšBuï6ì{2£íÊé™çWD,bÆQP ÊUÇhUC»¶ŽN¿%ñ'ióšÍìÃÀHX1&÷ÜŒ€“eà¸ÓF>LãTeÌÜLÚNvÑerõ±±Ö&#º²LÉ–ÌϤ jÌQ.ØÏ×~¬/ç/ɃYp3£Njþ–<%?n¡™ÓõŸ1¤Ÿ£u??Ò1zî)àL&Wæ±7Y…$ H­æãüã™ô"倹–!~"w3»‚=ÕǶ¤Þ^ý?r\8òdv6ƒÖ¼XŸv~龪FÉ’¿‰5Mtæùç|t>þöz2foÇÎOu¿–Y<Þ˜ŽÅ€Õ ä??^º]OË‘¨Ÿ=)ÛOI°ÇËeÒ±9x¸ú§ßiƒej‡†”ªàvÉ C=áQ[ØRÒ˜ólá€O‘¤®G1„Šîƒï??_°b§t°ä9ôc×¾¯Ežb{…ÊŠ??›2çÛòZ®¹•qÊnìo^¬e†Å‚ѾÂm>¿D_—ð´óÐvç_eøgµtBætñÖN×z~!Ýí¢¥Ÿ˜>wÝ”üðt¢à°ù+XÜGdát+¥ –‘†á´=´¥jÞ¡¦??BbnüV–Éœ+?nªôÒmÓ¬Â>²1‚Ìüª‡&õñ6‘ÝÅ¥t>Ð3ÿ„»Iï¢"æ¼\Ëdwz?08³lÀŒ×}Ϋì{&:^¤>Øy‹ø–tؘïP¾êô&k¡ß^Ëzz&"÷#s'”µ„/ðúÚ'ÞäC‘Áóð´“î¬_…«m+Ây­õÙ@/|ðå©Èü51W[”߃{‚N<|¢@X5!hˆO®K=-—vkÓÛ„üqô¯>ô‡°õë~¢mæåc¢žs A¶¡“î¥?nœ¬ëFîFm•~A#kWã)T‘Ãt%®«H7^kuð¥CØfÖU¿†Z%~'[Í2íÝ“~‘ÛÔùMó•LŠl5ÚøeJ–ãž#3ÌN"ô‹z^~çÑze[n¼þ8Þòqd¦>?nMº”äÏ¡9K?nÚÄÑ»ŠÕx(SôîȦîü_Æh ƒY~ì­Z^²Q‘~oc~,‚Hñ¾,ʉãÇ•ñs<6É â—JËjÖhyøl'Âj]O99±–ìB`݇¦|Jýÿ˜õC^±,Ñv[Rñ w± ¥]1(jn¸W…©¤M4¤â1+ùÁT  Lçû?núû‘|2ùûtç$I']ÓNþÊØá_â@º5ázº²}"¾oÞ?nïþ¹¦‚ýÒ?r}É×nƒW:¹Ÿ²ôI¢<þjN?0¾ÖÔ/wÒDyÕŸ¤Χ‹-ûzkq]ìý4ÌàôŽ…vuÀ%Ír?nøZÃwôŠs“™ü¦%ò]FóÊ€^Ö¨LúÊ\"åX }H†—ŠXD†%˜+/tn|‡Ê‚Óø×Ôž«£›mã>ø†EÌâéôá,àéÎE‹P=] äÍA‰½¨õxã!iã’ò¼&#?0éÂ]ÈkHq±¸ø·A§>èÚ>bÑ›>xï=®VPy§7†Ö£tÚ‚aõóí‹Â_éÉñîš…ng¿Šã‘™î÷ ˒1Ê×rüUär5 'Ë`¾é?n͸Ç/?nÜZ[\Zµ4s—;ˆ%¬MýU‹4Ð*ÔÇÁÊc -o[€'Ñbª':PÀp½î‘QûÌý*BúoÐE_Àõ¤à¦Ãä>k*?nÙ®ÛÝD `e‚³Ãe¤üη©YÓb«®Ô¨Dâ¦ýðý½"€v©rOmš@÷%œ5 2hí1n;¿è^zƒyG(pU¥ý›áÁNÁRödÆUª,÷ö‰ó«[§*ô¨[É*×X >ß妄¥‚²çvÌÃw´Ã1nL‹‚0'¿§L0£b(|_NY_ÏÞ%‰ e`Þ^Ô‰‡¦žÕЕ"МØË.‰-ù7Þ«Yf¿>i«w“;öOç_3LäéAñ‹.N_DæJJh??ÕWÝåÙS¶Ð¾Ï½tž‘r´´ër ´ZzLšîRÄJÁW•{ž®˜NØ’Ÿ6gá¿p9‚J 2åÛ=|’??È!Ó [b/€ÿ—â¿õ&¦Îô¶–v–´f†ÿÏþ_Ðÿ12°³?00²°021°01°þ§þï??NYþ[ÿó_¤ÿÈã¾4ö_ñ*f@¿÷þw¸ŒZ…¾º³Ã N¸Rœr¹æ®OÂlmiŸpäO¢— i€zKiypŠˆ¥J¼½Q[’­ô b—r#~ت émÐÈñ{ÄbTy«ôÏ!lQ'Ȉ1¢ýZІßD* Æn/LßZjõ‹o3y?r>¯ÿÉ[ž vѵ½jzÿbˆ=ù;µU?räÁÊ€Úè £5hÑg˜±y@òKGóMÂÕJ‡I ßɨ´?0Œ¶éׂîèKp(‰!¤©ŸwM'Ǥ^%©Ä‡ƒéUÄ?0Lé" ÂÝ-D·¦5i8ebñW2Ô與!ËtÞ¢GÛybkû\"õä*E7N÷®ÞÓ>DaRµ;ú’¾«??êOËy]ó?r]õŠeù´#dz?nµ)58f 'c Ätp+-×é+¿Pñ‰ˆƒQýx°e‘ãõWɲp¡'öý×òa,ë$ÐK®F*¸¯ÚÉjÒé??[/6öi<¤š•DÈÈzÌ%ËæU-›ªY??JäNFï]îJ‘Ôlä•@¾D»beÂ,-,{A\!Y+FFòsØ?rAp*Ñ.{©œCV̲is¿¦‹˜ó4)lÁ˜‹”‰õzªØÔ{âOr+:Ⴌ©Ñwöæ³ñ9°Ø©°k²ùè¶·2?rMдծ…b9ã‚nIJ.ÃqÂOï½\ˆª ÛUæÊïN+HÔsòs búsv­h0üÕP€ÿÆÿê??MÿëõߌlŒlÿûúovöÿ¶ÿÿÅúïÿÉ=@¨ÚŽÝFÏ2hU…yðØìœØÀâîõKïÀ¶Á{Nþ¦ äVÃç-ðºŠF¯P"Îì›w¿úV 8]õâ:Õʪʟö^µ*lyÑ'‰]AÈA£•B?r ¸lÂWpŒ¨ø§ÒÒ™¶˜žÙù‘eèóÝ8/"è-i+¿CbUgðn-!EÓ!ÜÀ&6›úä?nµ è¤ô¹Bo8œíì™s¨Ÿ!!g7¢KšúǪÌé‚ LB…š%ÓÜ"¡sdñõIኬS,uMô¶€<««õ”æÃh%hX´ð‡Ý¶QÝÝ>+IªïlÉ 2™3™Š|fýÝ0é9‡"\MB@ºôä7ÄN{o?rª??þ(¡èŠòlhw»&ù1y7Åo%)­ù¦»Q9Ëz?0ÚŒÍ‡Ü ó¨aI‘ÙHú±ÕRÎwùh³Ýë×ÏØÒ(•ÕµµµªøùLådå.§×}œËóÍ<5qõ=iu¬•=Pq¹‹Ãb¸Šl?rmjH¤Þ8µ…¡ò—áÚ`öNÆ"‹Ã§¨yy²ðýŸkŒÕÎBkÖ¢r˜µ‚_\ê<(OáÊøCÄ8ÐGà® cÉRü¹ôÑJü€àž¿ ,ÓÉ »—MâÒÅóÏF¿ñºÝÛ™§S‹vô†j³OÆtôDd~Ÿ*”l*³6ô¦(­„t¯o•h›zzÒà/›Ô;zòþ`<nÈ\è…ö§\ ­f[TÏð ­Žd—?rÐþÂ¥D7ôUUKì"î¤ô\8q¸4š0¤ºLµ¢"?nüFŸ$ÜæW£—£•CÄ¥Óa¥i1hìÜb–¸+šð]»s¿×´‹#Ÿ/ÏÉ%ÜtM*žÈÑ«n¶C¥>W¸SÏ¡ìIŸÂ®cKìS?nAO??[×Î,÷™"âõ?nþtÚOC¹ê¨†‰ÄAÆN³'ÃËLº0-´„ =¬þ·ì?0I1`——KWãÙ6ïBJt6$V¥!îoEï=Óäá*«âtœo7^Ûƒúñ×u²ù.?nü‹¡ß4m&þÃ÷æÉTa‘µ¬¶àé…žfôrƒ~øó—áË“!žsæÈ·’ÔŸ±ª…‘߯œÊµ¹¤É™QdR ë6ÝÍêM½xö™nš8îÔAî57µ–Ÿ3ౡ’?nrßåè8£¥UÛP^fWîÍØ·›L*vDzÍ«…ñÛ—¶)ON*ÒöWÑá˜`¾òyªÍCª5“üvð\k¥ãè"“R™ÈX´ÆüG”dIô™™µV·ƒpQp½xõìO??=ÿõå«7¿~÷êO/ŸE´n^ÎwvßZozß43”ár|•æYYƹ7,íµÃ;õèØVž\]ò«ÝŽ“†ÐœöM8uJkÚêØ—/l– ² >FQ4cãYv&Ò’Ë•Yϲ$!:¨í™ç¥‰ï'ÓËžKìœÜï±Õ?0ÌÊh Ÿu–4±­™—<ÓoĆ«ÚÄ25îPžªå2v¸£ÑFzQ™®k­ïËßò»°´á©Cá«­e"Êj£~áKÍ«uD‡“@­^2­LfxT;9‰M<¦GJȱL&ë²$TœœŒ“i!ªm™Ý¥·:Ûn¹F^+nþ“cp99é;??À6¹Ó¹åIÚW+³Êüªº?nÃ#}ç¥à²ë[¦Úq2^ß ºðFÐT€ö€i>PN…Ó7ë:°§èíù©/ǤA[ʇ¦©ehé°°‰åQsÜ[§-§»Ýg_Žpø÷0ÞÊ““yfÎã(MGð¯×#ôçÀ?0‘é$^Øð~¼Öô›æŠ~ö)ŽqÙ%?0êu®9—ûˆõ!Q0ÆÓï¥?0›Æ‚?r'„Å?r%$å<ìÝÝ@Ý7qx^ò?r@ÇŒVu}g¿ðÊ(ÍÙ}•kU–oÔvêTŸn³ÿÛ+‹Fê?nâK–þµ-½…YO?rо+yjStmñ×eºdC÷*±(¢û˜õ\ÌÍèÕËlÃ?rz œ"çU«¦Bå5?n–ú'¨'©n¸^–ê–EkQØ€÷ÎÐĆL??Vm;¬Gjày9G‹êÉQ×#Eäã9ì?0$Ù£†šíkPºç¡eûB[kyãÆ2íMIe—¿%‡ºùÝçó8O??`ÊëÇê­Ê‚kp„#¹?0T?rŠ~©?npš¾X‹²8V•Ð#™.Ð7]ï–d!0Ôa“ÎÚ`8(ó8жšGd&[SåUõ†¿È·®2ÏÓÁ¨ÄuÉ#ê«Bs˂è]6 L0<¼…®T?nÉáç-w û„ƒFÀt„aK®z› C‡’¢<¨éXä½HNß´ÛbÐóë-ÐO…¬¸6äK¥y,VY?n]K$0ƒ@3(ø.æAö¾,Žû2Õ ›ÍD +qÄ¥÷‚ßlÍ]„žœÄz~¤hŠN®çÈÊÔìñ£‚‰Ü„1Æ­dªFfÀ—@£ŽÑŸ ;÷S™cv?0‚AÑŒ–~ óÛrØŽ8±ÍNN†Q)’¸.kh pÉ|É KûŒê6Û>S¹Íᘣ¡ïå¶6ß Æ#4ÊÕf«*×Û"3Ð^=„þ…­Åà ÂÞò'˜ ôrx{?r'Èøä@G®T3~ÈÓL°èÍó¿¾yúËó§ EJ°?nÚp> ÚNf§ÃÑå??õgãñõÕ(5¼2±N«¹¾{ÍKð œê§Î¨øÎ\(i?0;¤ ý Œ+|Ó±ê—cCÐ|ØÄ‰\¨zLº÷9ʇ¬ÿgíjbåW2êè€Vè¯??ý?r¢J¥ë¬ú¿B?0‹XýîCÅŸ2Ïrãúè1ŒÑ$>¿<\=Ù]>IN¯‹ËËwƒ«ÅÕ¢rðŒEBâË”\b[Œ¨|c—t?r±kö‡lÔRÍÂðD¥¼´8§š©‚ÿ3°nãyÜÃãhÉ2XûÍ?nÕzºHb° ¡5ë+V„æ¾ymW¶¹ë¥`9´¦kã`Å¿ëg—Ÿ_Ñɘ$úTÑeŸŸ·ùtË–H?0 +v;Èx/Á1¯õ¦ó¢}i—ϳ ìÌ—œ`ÀӾЦë%kÈ^Âø% ÇúÜ%Ö˜è0ø :J\w4´H¶I~ùÅü|yEè=*oZÑ|=7aªnûoHÓܸÁãÃ*øÐ&“E•J~‹•ŸÊâ{Ypiý[Èš¿h7s~•a}”µ‚«ý1·Ê`l¶.¿ƒÀÓŸÁf~Åë•@É…hWÅhKØ€©ØxVe~•]%‰3U?r†®®Ò5Ï?nš;Ç}.þti¸Žk B †£¦äúx…ã˾?0¬Ý#¿'¡ýÒ?r”¥|œñHÉš—[®ß€sz1n”(Ê"éÂi,Ž5¹è²†?r¤âœ.+1€j Ù9„We8ÀIu0õB¬‘o &ÛŽÚ¡å±8:Y ôŽ?r²fñÍnwOí#4ŠZì9Ù8†Y(K2i?0 ‹2²®`¸f+tÑwl¸¦·l ×éêò³+©?0Þ½óžG䜗^°Ûyç»+ïÓ“d2Å&}0Á{?0²w}'×ÉEòŽÞÂÎO¬h?r~Ò Àµ$ÍïÖ¿ü´å3Εa¦ˆàg‡‹e‡‰{?0¥v>Ô/õºÂë¬âSžöubChKt??_Š•VÕtlß.j=Åu¯nK|¢+Vy¶y͹´é¦¡9l8°#ùéê€,"ÅÌž«°äô*èÙiûö(›"íÇžC£Þ»´ç/æC¦Ó^¦ÝîEfÖéFÈXw½ê€:9k•>‡-§] ¶`¾m_ë*Z­P̤–ÏØ"AžnUÕS;@"PÜwÔó‹TeOµ×lؼ§4>$>E•2v1×ih—!ëÒà‰¾T×Bò7ÈLµÛÁJËS:Z£'š€—'aÓ0ÑPaƒàÔ¤ö%ܘä¡Û |…˜#eWÖP è|ëLW@d/M»!)?0O臀ù–Ú+ÀÚu™É·8,<؇ÑTÍ:^ºj¤[öìR3¬¤˜ +u¶ôQs=m•?n˜;µ©©jšß7Þ}õÉÓX´€е??£4äÞO;$;¶©µ,³UåUê7s3刕Tš_€ç‘°ëØrÛCžhÕålêÒˆRH¨¶±™?rÐn$N_gúÌøéŒNr:aLtC–Is€ÈSk‚¡@$,Ì鸂&s>Å“—_øêù»-tZ©Z眊ãëéÑ&ì‹…Üá¿Å_àÿ³ÝârñvÈö̓ƙ׳a:6Ä#Ý|MùéxÆ$°GKÆíf»™©3VÎT’@ñÚÛ¢ïQ@–YÛTÁd\Zû Ne§5i=a©Õf*pÐuê!Ô¨0Ôp(äçÖt“œ;4M/BéEÀi‘Š?rq”{Ùf^¶M \I+6¡µ1ß—­>cù¬‡P-Î*˜cÅõyNf…Ÿ ®kÈœ•ÌyT9_OËÄÎ,ÖMõ„}FC¡:­Ž–^)¥UÊÒQݲ²›ò-[­¤Õ¶&FŠ8Y\¢R¬M+I²muu:¡7l{Ù'½gûŠßЛVå›dÕ·¼úÔü&Yõ6€™T_íÐ0Ë&´Lå-ãõ(Z×`Üó²m•¢ºUŸ›ê*r­yövæ×ªg keçÜs%ÛúñPìvÙ¹hˆ¾u: Ô¹‰??,=ÿõ':ÇRèÞ;ÎÑ;NO¡ütâW3{‘³*®~v6ž§µç>kת‡î‘‡îѧ“ü¡{ä¡òÇÜ£~Ÿ{d ЄæÇN\âÙ´B^ ëé‹s¶üg V_žÁ†œÍ_zïÙä‹ÓS2ËZïÉæÛéÖyOÖ”à=Öö+†ÇÔ¦]Õ­?rË:_Yó•¶:ô•;V$›VzÍ6—›9ɽn—Üì9‰Oͯ“›ÀIn.ou’Õž“±??®F y3f|+??t¶q»#2qÓ­ìHÞa¿k5›Yzš1å)èÖƒt¢ÎÏ'``óNw!7?rdFg幜+–M5Ë’IÀú'†TÑÌKr ¶´]–`P3•´be¬½uF ?rÃÔBÏGdƧ^¶š)"l½—“Éi幄?r??I5Ç?nË0“V²œÐ-t»$íNªoض”+0Ÿ­…cõŠäíÈ€ûñX±"^Ò-]Ñ’$9õبi˜0 3“ªG…«Ð4·]d®ö#™_îocXÔ»Ñ?r›ÌnÎ*ßàIh­8…nBÛ«Pl~ÇÂj=>PÆw„¾ƒf}ö2sÝäWcïÂà» 5ö?0åàA ¨Fö|`ù»?0)ŒP§“¤GÒ9" Bu¡ú”fùù醌%[vêÈ ±,}?0\~lû€ÔàäÁôÐpý¦Ñ¦7܆Ü{¡ž«;J> -õ±Ðê½ÔC+„ÒõÜ{¨(" ÔBèîHã‡jÚ<´mlU{H4ÃûXif^åy­5—9‡?r<ê2qñ9n°½À¨Ýå*gœJ&ç/–laéK阸*¸v2HäK6ôÁrIÍ+ÄeVñïTYLcHQÕ^¡yp²hæ±q¹^'m??v8áU¿¾ –¬xbº&V^A+Òi ôTÁd7£U°Žù¢õ4k‰’fú1?ròiÙ5 Ík+ZºÕÊ(”.•Jo²Rü‹Ï½#W½‚qôò»gx¥~R·\ã?n-&?r•×Ì*àhÕ#غíùû?nY¼äïÌÃ}´U –Ç`ª†âÛÏšßUWï¯=nk÷µŒªd{æÌÜc-µPëRFYw;â““xDI&ÚÙÅY¡€7|9=µé³1Ôqåm‹=7‡³ÃÝ.Šˆ÷i2µd’Ä6:ÿ¨FØö0¦®0I¡c<ÆÍ7ÎÕ:/ ¼t“Úcþ¹¡>Úíò?0»ToÞ`㎢¢EÈÌ+`ÿ8AÓ=þhÐ)t&QµaU‡ ÝOCô¥~0+OpàöÙóýöxraGêï <g¸¬Pº£x¦óõE­+¥#î]µZÂ?rœm‡~ê?nB:¢pØýÿF÷Í€`e÷%_8ä‡äö׿x×zÉ PÑg·ª'"èD:õ‘0&¸A¸Q1yÐÒ¨ˆs@5é¶®Öñ}&óµÒSçfÐû¦®uNÚEK©âáù¡ ãßý¬ýëOÙ¤<~G yáå?0Nyª¶\g–D|üªˆ›µá  ¹rÍy‡^6¨ñǬþzâ«–XÕï…Ö±‡?0hØýl. ÈìfÈš8˜µ«V;íf n¿¸a +#ŽÐR¢óDŸm©T)ëÝ®oŒ}œ±ov;Da?r À³>LI꜔œ±ñžTèÔöåçc„ZµKа·s…!×&hb‡<Œ({\|3ÒqRb]€"™bÒ“Á `DZ¶¦E¼Úöë!hs ½µ¥Ùsò|¸*T¶| L4övNxcïXÎxŒóƒÙÌogV¬Í‚E¹ˆ+:¶›šÉ7´`ùy¿5XÌ%çX¼dŽ8 ®éšÞw7­¦ec#€??‘ÊÜè°$ÓÌNr\€&tL—„íoÅòfï,£ÎÌ£bJ6žÉþt&I$1—òÊ{ÒÌ—°ñÞƒø,¤eIË$!h$ï7|·CÛï|•à›£V8¸àŠí/ö?n `ÏÆºO­DL¦1g|·ÃOšHÚó€E›‘ôþ}äØŽ3™ÝˆUf”N¡L??]qi¨ raþg@Õ¼k¶âù[µ-Š‘»âŽÙF/^ÿ|?0™]žf£7Z@j1ŠçÓ˯O¿½Ú-ŠûÏhCiúDßLí´‘??] Šž+Õ÷ó3&`ˆÙí^5CŸ??¸®Ž»»ÝWÓ$VP•ÀuS†¿Fá×???nà¡ç \òÿ ò ŸE?n??}Yî]¬9O:9IÚ k£WxCÂK6zºÝ–|€kÃu[Ô+ò†ËÀA· °,¼z=øë`²(ÏâËoœ~Y<ëI®ØègXFm~xÝçnìõ´Ñ u-JbÜöìv}o°ýFÕùúg%¤©Î??#ô8”…V"0ÔÛìvpwht˯_½Þý±Ìò·äZßí¬€ƒB?nÿ?nî¾îú?rì$P¢–ªÁÝËÑâúB¿z½¸î»ºe£[!}C¨uÁps‹²xôg®qŽlÕþ?r×ÉìD½`/ëÍ5×ñš–PÈ»8g“/¡ÈîUÌ÷–]C&ÌY€p½.v»‹³ |V9–¯™Üí?0Eå9û¶Óo‚½…à„7Šÿ§¶‰žD?0â??Øü‘{{Ab¡#ûýÞßY¥‚½Q†Ü2Wø%—l7ᎊÍ]Å1m«Ú‹Þ”‘h{ØÝô²<; ¼9ÞMÇkïÝ«YØé©!‡ŸWÙgÝNKGø©“°-ÀnönÐÒWý®ö¼¤}rÀÝ ¦Ò¡´„?nÌ:øAÛŒ#z‰Ç.üÒ߀6¤??6ÏT8ø¨ræRßöò=??”¯¸«‹£§ø½®?r#­ðk üÆ¢þ²ö“ˆ€æ+§^`ðsÏÞÇ{ ÃL𙡠$„ì¾íspå^(¤2™!³V¸aŒ·M‡cwõ°“ž¯—ÝE?r»Q·’ëg­šw;>3úîa–ádË[¶ÉÑ¥±¥ÁÛ.ª¸Ûíì.»0°|„¹{V¨Û_”:HíšÃ/õÊ6=»ÿùˆÎÐ Ûx Q2ŠËy4pwIBxw…û“Ë]€gâLzôˆ$!òR\Á4ùMŒ/ć9Ô} Mb³{¦_ú…Ýc™ûþw¢¾r¼¸ç²ˆX§·6ã5.!‘bmºOÙÑ=]´Sà9¾´°H1P¼Q­â±8æ!`z‹wG÷þÀY–ٶ⸧J%dçâìÉuD¥/p ‡EŠÜÿ²·ÌS·dˆIýþpÀ~¼‚··óÏA£Åšõˆ€ÒüŠÄ$Ê??ßKÅdÊëì—€r‹ ´¬.ÍŸ¿íký³¿õTkà¥ß¤·Cû0;Ó+K½¢“ŠFAŒò· Í`’þëƒ;!À÷0ì¾!”“!ÇÏ)^ÝÊŸ5.4Í],À{ìa…¿=V@9ã!ÔñÿäQW7Ý?0?r?0áb»J°;]1~ S-êñ8_ˆõ îšJxí°Ž&hxø•PÉxya"êvàË3ب€Qßtû3IlN¡,KXyª(<äiöï’*Vâ¹úf~m´ecdœ¡Û›¬¬=—Íçªû4БzÔöÓ4nòò Œg7ØDÁœÕ0± Þñëe6vïëL%×쟱M*Ù~ÃdKCçøŸ‡ñ H<\{¢8¬‰X¯&ÙÚðtÒüÖƒ°ïä@I¢@ÅXöÎX‚s©g™AïX‚OŸWqÏu[ù´«Kvm¦X­.ÌÌÉÐ5Lø,¶ZƒÑæ¬ÓÂgï;ð¶¡^Çc¡F)'¶%qQèÏìÞ(wtöð?0)ØÑ°ß[DMCÿà¿^žâYÀì^i±r=±—G?rýkŸ—`Œ£ý킆5ëð®ñ.È ÝIÔªwaëT 7(Ö•À‚)É¿Iws8£æT[ïÑ ¶¤‚©dB1%Oõ¿K6s‡ßg—QtÕsÿ_~6:û»ÿþ†ñù»ÛOø!þ;Áë­] øû%¿êcÅa´½äýÞ\_…óCL_^µ#3??™ÀÁ$Mö„ öIɃy„¯þ?n×ä~øK¶—Ž9žiòºî`ÀiF''¿Ú­*­žg£K pÅ~¿üæküýv|ŠåðûÕØ&¾ZBùçã/ om"s [ò…­öEqý%ür›øv™ç‹:Ëm¢ø:[^…xñð‚êm°näçÑÿþ_‘½‚nÔŸ¶[;dg²på›ûfáÍW¾·>0óá06í×?0 ‹ÛˆØ – f<­©Íé f|o%ƒ#·ê`б3,~iü……ádÖ¾Œ­ªKnUý8 B ˆ¢5´íÎzLþ!¬õ9Þ§TÏïYF62yødÊ:øÉ0°`ÅþlÉüdYŒzewyš‘—q vø3rž=%™Ý”ò“¿ÆX.ïb|ù9R32q1"c5Òq9Ïén$dÞ²$#¬ÉH•-0Ë‘äÚxåNF?nyº¼ùùó„!ó–%a`ÇËÈ‘5Éʑ0äÚr'#;<òæçò$óÆ‹Ÿ,ÉHÁ#kÃ.[`–#äºr;ÉÅw¾¦ÇfdàÏÈ‘µb“-0˵ñ“;°áÆ#OÅ&o`~ž|eIF?nY»lYî~ò„‘‚?r¿çÍ ‹‘Ÿ,ÝÈ•+a12qá–{9Ï/?rœ2Wì8eOF®n¤`Ã.§é8kK½Qò8¦dÇ0Òqâ^YN¼¦4—=)rVlx䔹~ÏžÂH‘9Œ0j`–=0ËÕ¹NSNÆó¾Œ0~ó9Ãv2N#ëùK‡¬?r‡‘+›c`5²eÏF6~Ïån$Ë\°ùá–%d[¦ô,‡c’¿0Rqà|îØ¦äŒ?r»ƒ0;?nu‰‡‘{wœß×????Èÿ ??iéÜ7Æž#Í®|»ùky ñ<?n”5–÷ø§¥ý2 ÷á‡6'‘ËbЂq3^ˆ$bºn‹ËËC\á½óÍJxA¿÷™é¶à/hhb-ñTÆ 0·7E=µÓÿðïÿG?04I;óv\[EÌ36µÕJ¹^ØÆìié|Hû¡Z£d¶KŸ”:½êñŸ?rë,kèà²×³¼ûýÿþSK›Ù4HÁs¸x“ÍÓù¦Y=_Üãhz÷Žo^-î]ÔWõ¼å?0asHëD9uO¶ºž³iÏt… Jû tL0èÃÏõùT|Hê÷ãrVÊ­Õ<²AÐ#B⪅ز* ʳD¬E¥bO"Ö¸°ã£-þÏíÔ9aÚù±YÂ??y‹6€ÄbpâŠev¨F»nY??Cß,“K.$¹Rö"wÊGmßŇæçzù±°·Ÿ”.É»TƖ˾XY@¤Rê‰K…Ô}-?rìAûa˜AÑé%*ŒP¢€Òrì?04›À6Õ‰ÕiâP 4í1@?0fB‡Ø6!›RoŸ62¥øãÒM›ÃIÀzËÉúD‰üLi+–´SîÀ(l$ž,Tã‚Æ~]:ñcúªnË>Æê/œ1ªÁ•yØ@QI\Ö«ªÜ¾+aìW%ˆ7Úˆ;Ç€;?0…íã «4¦tˆp_PnX7Ôঙ@a?nÀ;Äž`nî÷Æf€1\$P›ª8O8L̼ÿ¯g??¿üyþlöóûñcöêzó;‘:ò&Lx˜"U­è{?nÁû?rPÿ¹mê·ƒÅRÓ¸‡ —<ÚrŸÃ~ÏßS¤’©:ì???ntá?0÷1Ê"L¬ˆ¤PС\zŠÛÞû +#°•Võ4Z Ö‘~3gêIjÝ7lÑ{¡,´5m¿fC®Ôã©gÓLgÍ3W¡IƒÅ]‹Ÿ{4HÑ3j½ŠºÄðä§ÓyÆz?nÒ„?rm;‹Fì›ÎÀøÌN%ÀÏ…è °Æ7éÊû5`ÅåCä”fù?r.€œþ…ŠFiVCMœ‚#§tXqP§‰é*Iæû’jCãD º€Äzë‘ÿŠ¡…O‡Jï$gxn“²¡•Í^iuûÚŠ…?n–EÎ??ê7t‰X`Äìh B ÛÒq²œµ@¯ø;/-ȽVòön"<]ÿʰMÊwÃk±x¼àWŠH^J®ä_¾8®^??s?núÐ|‘;e?0ÛUó^.<\ÞM°UªÖÀ¥s(Íâ«C¥Ê\v›–’øa"¬X!x›Îðü8Q;{9+‘Œ F@Å3¾0®??†?06 Rm錩9Èn¼4æãæ¾}aÈõ©_?n"¡lrj•pd?0e™çF¸Ý¤%­÷müÀÆñ_ÊÈá;-×*›ˆôœGO:У)“M?0lïà\zŽ÷*ß”åóÑÚÒ¨Hí@¸ðñŠëAªW©dGªœ Ç`ÞÞ`°×DYø‘pew€8pgY¢ÁmŽ7*Vg,ù†ø'./tn6‹p¬öäÜsݸ8$Öã½?nCöÂ,ús;c˜+¨в?n×,±ÅãŽß…h¸È  eŸ˜žW7tí×}ˆ¥ßN‹\ýîIÁMi“yò ^ø{;éÚ98D|w£ÛÎ]<+biÇ(|ÀŸ”1ʧ1m»ò Ÿm‡ í43,¨yy3Ziž¥Ëy9ìgºeò£þz‰`Û÷1 ¥qMáý§ì?n»œ ®8jõÊsQ¯_5xb!'—•²äP«Åtwz´Ä¡ä$Q8bœlÙrœvù°ß½Ú±õ25ÃÂピfYÒ¯Ý9| |+ÂNã\ÛIFF:"%‡ €#øq¢ DhT°ÓwU­ËOX¨]´Û¡q§šEèÖBI7Ñhö¢ªA°vÛ+ˆ ±×̦iž??@ïõñÝAß,e—Bñ¹Ôƒâƒ`ËAM5*Ôf9„w(UMÐ,I7c¦­|ż¡lÞ[ᄎŽ×Aq¤G®æo`®¾hh8ÍÉÒ»-±²i"íÇêÞ½ó}¯„³’['ħe?r× ”¡ÜRAâhl‰]¢üô¨Ø¿–n‚Ì”ÛØJ]*Dì¡øh¾Ö]o¡Ž;#AU·xÝáØ’G Cñ@tï6ã=SìÖ⛕‹¸´¦zQâ»dÑvkmi!cvŒæ¥ÖÓ}hRXXŠJT?nDÚÄoðÚ$$Óe“cs:¢×ñÃ#AP)~Áæ8q=ú¯¬ûWâŸP$¢yçSôŠÇˆèÃEsƒ—Í/‚Ï<È EV 䘸‘öPU2\k,ôG"Ý;H/@=¡tî§k‡mÞ 8X†<9•ˆÈI3 ¡4!\(kç÷ò*öÿ“/è%ôMõ??&:¹@?róìlv?0”‘K}ŠŠ…èSq?0T‹ šÀ–´ Q_z·>«,,· \UŒÚ0©mB °Þ— uóón$J*º<.??ÈŽð±Á‹Å|Î/0nN\“^ýÂÏónZÝÌÉXM(ÀœNàÒ¶™e;}‰ßøå®ÛƒÀ64jåŽÏº¤°l[ÞVhe !?n+²`º¬T½–¤ÇÐ)[¹•ÌE»güÞñ´Š¢@e€ê¼Å(|[OíO€,×P¤°Éý þå92,ü äëÕ®óq…s^‡}1*+6,n¬“EàœŸÃþåCçŽp,Ü›Á… s?rÌÏ ÿzÚ,7¼\ ‡zßà½ã)‘¼¯¦…¦1šƒùU±-§ŽZÀ?rÙ;Áݶæs¤ òêO_>ü¦Sb.|ÖÆÍÑ,&?rÞ3³07˜ÞHŽM (3ªNüPJToY*)cu¢ä(=J¹®ã¨TÅe??!‹}Êù^¬ížž´@²=9†0Òk…ø†m•[­?rÁUxˆ0/ñ¸øÖÈ#—EµˆÕq4pÔ°.¡‹¢Ú”ë{¾¹W¬_àJºÇÑõAœ¾ßúwçÑÒ?0eÂÐÌJÓ‹\Öé€Ç‘ÒÌ$1R¹:ß§Ç?r¤Å‹^³ïôŒ¼]ɳD×»·BÀ#žÛeS1âQåa¿ìÞ›5ñ°ï÷üšÊ´ò·wm^îSLÈÒÜÅ9øÛÄ8ƒÄù|oõŽäDÌò½6ìX¥Åó™—¶«¯¶Š17ùXQ(dÒñÐÆ3BYtû)—PWhŒA௰´ä[Xù‘ÁS—7>óCÌ?0æñ/ý`üJVê»RY¯ù ‡D©&÷¯¾Ë“"–ý©UŸõØuÃÄx®¼iâ‰Áã¥#±°™w‡¿Þ˜°óŠ%cWa‹å³mlŸÝñ>‹ÛKƒÆ} tC^±0eÈpù]Å­uOÙ¶u°kmìÊîâd†¯Øê¥ÏV¶Å™ænK9ÓfÈYJQ) ,Õ.mhå.âî+Ñ'²eѶ3Cö ¥>—m³«×s³Ì~~q@8&V h^ºRiÎBT†˜fuw½Õ??DmXN 캰ÇÓs(¼]â??Pôþ³\Ç£à>RD?nÌSA)¦ÁÛëE\ô9ç‚f?? çp+$£ýò?rªÆ×¼/k?rÖÛЧ?rÌëÆÉ´'Ex`[¼bÙ_(ÖwÉ•±W?nXUãù(?0fø ë³¥C8 aîà­ŸÇô:ÐMééèNž™m0LØÁN¢²TÉð ïm÷o4{ƒÍ®cÃöŠîQW1&ñL-x‹ ?n;½žƒ6y»¼ ÷: ¶¨¡+ßdë`â¶ÁZ}w²fê_˜6S7Äjw³Ëè­¬•ÛîCª£^î?0üô{£!@r¨¿h³»Â…|Ãn™çÇÿ?nÿ]tÖ¢^ïHš©?r’Ú¾]£5Z%õP/¦Ã¾Ärm8«É*¦Š?rhŸQeLÚWr(/NÑf¢û&÷Y?r4^ãFc‚à Ñx(³S­Â×ja¬kÇÚÅ.¢›!V;M?n«ˆàm«øFWP º8ï°Vƒ¶[çclKÛ8ª_…c?rçT¼íŒoXÈ,¯§QÖŠê4Îx›“ b{Q#€Í¶¬Õf·­^”øúî’3NœÏÃZ•ó'­Ëé9œu?r–iA‡R¸CMb#áõT„fdw ú&mò[zOÓO©EøëùÉ*™f>^âaű®byô?0ºÊjÛÚÂØíz¬½û?0ÞawŸ¤ÃHlQѹ·ÝX:4Î}ŠÈŠÎ»sÇ.²Ø•û<ã#Ñ[wË6??âx &}¯eÓ¡wŸkÙœ­Ÿu8½‚Öãæ?n{ÌÖ^¼?0k±Ä :¹>ã×lerÊl÷¸ån¥éhúæDÉ­nb0øíÙ4=q—PÀ??3·21—ú²±—±/€[Nw+‹Ú@ÂF!û2FâR³ƒÅö|¦>èÆX8ú¶‡I[LB5Ç’??Û°Má.D‹··é4Ìïcö-râÊÏaÜçåç&œãgçÏx¬/ C/†DBx¨„úÇtÂØW?0ùÕÉ-€|e®¹æ ³_[á3®»F“šn/¼^k½€x:jBÚ¾ñâ©©ËĶסs ð‡«²¦PÑpŒÂT??ŸÛÎÒ:¶´,Yö(ÝúÒk„Þv­Ï¡ñ?0…±ìíÄ/y™EÈõ„_¨£èáþ0|¯€??òÚ<Ý%Pšv¤âx?r‹ª,)ú‰×f©]ÏÜ'??ß²–3Nbc†0³­óñ³ÁÓ­?n¼é,+ÁþeAI?0ÍTpßPäo(þ€ŠÃ¿ä¤@Áqµž—ðŠÕ¥Ç`+÷?n¶âÁ„Wê0`©Šåà`•–µl>&Gâ­ãx™_òxrêeØß‹µmC%°ƒÁôÚIÂ×òµš"ÀäH‡äf«ìÇï:b}üU#Ç_Çߢmw³Grø| § FCÓGƒ$Œ˜Ë~Ö]Q¤OÁðÓ4¬¦??¬B†µamNŠnXòyöÔí`dK5ä0ÁR$U ??‰è‹Ä*µ‰f[c×\Ä7¶BòÔàGË—Çluê`§^Ÿ ÌtuBß‚¨à_R«·.QÙ”.êØZm?0@®~?0}úÛñè*sÀÉï#±PA¯Cå½îì ïˆn¦ŒK}~Œ„›Z«*[zM§2±•¸uúîžÏAÑ£½+¿Q{é©~”¨3ùŽÔÏpý¿bµJbÅšƒUA¿+Ws¿‡Ý¦ögÇ*˜î<Ñ¢ïÓ¢6RåMẻ?nf(hGöe²ÊÛ¥ÌCÔ½pðɺ,ÉwŒ¬ûªp~J~2Ù ?0‹z.§f5dÖªíZ8³J`…¨€#®Y±Ð˜&þJ.N±2_ëCÔ‰íõ¸Ínà6êûzÓFü¶.É>°‡OÚ1|¶ µ7´¿á¿‘[!MÎ(¾ß¼90P#3Ñv3¡œüÐç-+­:-”%¡>е=:ç ë??@VF[Û™ïõ +#3z wÑ,‡»slȤ&¾Íˆ±±HºL0ô•̤ÙMlvî>ˆÞÉ•‹ÅÅ*3Qê«HåJÞ}*#>€>lÞD¡•k»Ë<6‡{÷¼å[7!á|¥à4)œzûn"?nõôŠq#Ã3î;ŇŠfÕÖ,«]x*8ÜÆj[ý¦•t–o9@œ$©Ü‡으ÅmÍ¿W@íé‹ú´è¹ßJÝAv^ž¢ÖK¸iü®´_°m f<«®^Ö”nòc¡Ž°íïY˜Hœúþ%ØgîF«)d›f { ¾ß¿µó÷ÌÐþþU-ýµ;A&`I¼ñ^š)‘O$•Ëßùü{/‡ø"Á}?nÿ¸²ÓÎ€ÔøýK‹ž¯¿ûÿMµÑòÉáñqt¾• —Ý.§7ÐÈÿ£sí{è\›³m@å^ß´eõàÏÔÂuÁêHúˆ©±hÎHÌkøÚäGö†^_]çÐt[T?0NÖŸ•Î’ã)ýÚæÉ2¾€°~À0˜PzjöHe±Ýµì³•pè¥EëVþ_Äóa6£•{ÖB·<8²T$„=²¢˜Uq÷O¯4uþÜg­,‹4ÖOºuQ•¨ëbó%w?n6q €Æc›¾•Eš T0£Šßî³…¬ÁבúÀ™Òÿ‡'YVc˜ÆÂžGC–·øݬ,b'Ìh¢Ïan´v™Ét!(o»´\-q"Mž¥%¡O½%) (Æf=8ªŽ$©ZЊaáÌk>Fd8ĽÑ`°dÎÑO¼8—°áE§øÚâ=0œjoÙºK˜^„æçúS~ËYÚ nÊæÜu~Zñå°!Ó={[Üä¦V”`@¢€WþÓÌw/8ÍÚÁé'Ÿ†?nø¿¦´gT5Ä­Öh_=5‡‚¯ü¦tHo7õ‚æ¼ãRfÇ4Ö÷œ¤‹¶*æ›â¼ÜLm?0…”¿LªÚ2w®Dôú¬Ú%’ ÅÉc©§ÇÀI½ïÞ`/-•+2.Ó©M%¯zŸà‘ñÅ ô)ì^"Û‰‹ÅçÐÐ÷á·FzÙ!6?0¶gèÇä?0‚s’j¢^*ÙMîíšð`mŒµ\Uņ_Ç óÈȬyL(žÝõÃ1G7ùOÚâ’Þe¼­s­W¾ïU®90¼ÇPÿ7Ëðþéü"Îá"y^÷PĈUÐ;^Ø‹J 9ÆÅr—l]è‰kX–yšymlÙí7¬†¦g—–òà™¹KNƒÒmÝ®îfIãE`ˇ#uîeÜÿ[Fp_!xînçÿ_n “<Ðé¿eçÆNeu›wНþâ`?n蚌ꜼØ1®÷–êÏuø^f‡»ÔÕXNÿÃÿö??Osø÷ÿY¼ÆêÖ•n›Ó8Òdø?r2WÍ6§ ë½?rê[hàÅ^¨®,Pñ…Rü9ÛÞî…:Âï“éA"ë*ÇŽóÛG±1„Õ[¼îƒûέô6àÕ{º°kð"êv‘p?0½r5îÈ‹?nÚy5s•±-µÌ4¾tªvÏì+#­t ÑYxk¶}õ‡pðð3K°k=„o:?røûe:cíöìa§¶,Gø—f??ž±ÓAˆþ6°›ÀÈju“E›ÖCÈ·­±XIO˜‰ª¾Ùy° *×ۇܼôﺰ˶n¼gã¯=0:‰Õi‡”Ë7ä©­103Oư7Xí»º\W#ó Ôm¼­²ìôIÜ¥Ï6®Z\цk&ŠR™ia7vkáT´k{e/äÉB 'ØÉí5o/xßGŸQç@õ´u;·r̺C øßïî··ÈôžGñk\”xöwö¥«Î^=³ÝK¥Ô:ƇAÇø20nàÑEæ– áaÊKž¸‘s¼$9þ{zϨhi &PoDd4¡Š\f‚e^œrBö‚rìΡ¡Úēڲ½ŠJ¤ÄUo\¶YnfÓÛ·S2æ³ãÛM:ÎlŰt,"…±÷/ è9ò¹bT»R A5æ• hve¯²²œæ8`‘œ»-â9nIŽ‚pöü™Óyð{‘ 8›¬Ih¾Žú¸‡Ah¾v/?rï¼<äSB0Šù“ØíséÌ £''瑎ž 'ÁùÙ°/d<ìeÄïbcëëˆRò <=¹í¨ñ©­Ç[p©8{ʱë\Ìl?r>¤R©@UkÔÞ^ÌŽsü0ó+C ùÜ+¥£2h±f¢#$ Î‡xsê.{ùegsçUX„áÁí/ÜÍ,¾IÍåôGî‹Ó/—×bÔõåüÆä× /—RÀ#[,‹Ù6ßÚ•½™=?nP?0Ë¦ÝØ+Âú§îKóúÚE`Ê‚­Núpã¾°+Xûk‰kHNŸ]Îfxp;Nðë3`ÂyGùÀ/tÂä8£IoSà?04~0;NkÑD™?nq`Û¿"¼·; ÚšÅfJø¯…èvh®£´^Y” ¡Ýª“––p“ÿ<“ŸÆÌk ˜c.ŠÏÚ8‘2)Ç%p.[—µûÕ÷ýÛÇ\pŸmœéq½h\n¦d»h`kÉ_ÊðÑåFÀ/¨§¶å¡Êlë×µëì{·t£©ß˜òuèÛ»F•ZÔõ²ru?ršh8&ËõC(€¶‹Û¼ÅŒ¶Î3ü‡ª^9(j{dôC#Q9ô£qJÎ¥^¶$´,NDÁ?râE¹°wžr°Š-‡Ð Só·©Y¶!“”s;r‹”°Õ §^¹ÝÍÝ1àrJ¹Ó=õíø˜â?rl+'Ìåà)]–5‡~qŽA\œË?nÔ‚??>úæ³7o†iRn]¾¨V%S«NÝ#¾–ÀU_—§f;{·WÌ‹\ݶr_’1À±9Td!-8Y 5¡jxªÓc å¾#×È`ߌËV½b???rŠaJèsƒžý}Ø??ϳ¿aQÁ/ø÷÷•å•??ùã÷®¶ÁÍÝšäËÅîôˆìª8ÇÆ¶ö5(wá¶*2bR»¼8 @ÐîB¶™üèY¾=„j´Dˆ$??¡w-Ì|K†”@˜&FÑD??è?nX.Z°P?r,4Á(㳟ê%†°wƒØ ¸ƒü±Ï¿®ÅáèÒµXfÎG“.©S_¸ìrv?r» ½?r§L'·ìó~«Œƒ'Ù‹“[ðg7‹s؃lõw¾„$ô=8²/æÚ‡á^ †×Ë€›œ‘ebWdÔX„ÂTŸë¤… ¯"^Â%¢¿+da¸—;ÚŸula.Áo€Oäü¶I§-€u—>²»–uõpßÉÛ?rUºSÛ5ÆXp£ÇÓp‡¨Ãí~áG£fSÃwg“×öë:`ñ(À;"HG±ò‘r½ùj<ü£?rFu0»Î`7sò!+Ñs‹È~åd³ÌZ‡îǶp<&÷'Ûeã ÃÏ7ìE²ë Æ???0qç€ÃDó¹ÿ,ž…rƒåÀWÆ–2Dúd+¥¾M‹¾Uzèõà~èá+íTMÝ8xcáÆß7ÌÀ¢F_üt/`°i&tÃ(ÏlêuÅJ9î0HƒLÁõsÐÌ›÷ª~–C¸û㇠lSRmèÀhì@Q…=éÞ|£Å+°‹\γ’D›‡NIæ=Dì3$qm£D´Æäò„ê÷ˆXxœµüñk’ñ ‚H•?rŸ!-Â`) ÂPŽä¿½Ç°³¾s‡Aæ7Úߟ޽¿‰Øð[H‘\‘£‚ÄÖl$­Öòð}_”Ù°Âå½·5G¬[ÿìëj&rû–,?0èâC>ñ®äàyØi¥. K?n¦›˜Â``Q¸ÆX#œN¬U1s¯4#“óž´…ñf§ˆD¨‚7gæÎñ‡âË>@j—\C~è’À›C„ãP™™^ð)™Ä[aÍt"œð¬Š1 Ôëwõ0XM÷è—D$õm¹q­­È#¹Ùùm…&ˆÆ6êá»zxEõlt3B¿?0n?0{QÅ×®XólmöµîG !^Ck.”šUá ,„ßž6C8RÂrüq£ÉŠ›8¶Ð€??AšÆC¿Ü@UZ2Àg‚ú+Ž­ ’ªQÊô˜âÒÁ,Ͳ?ráæãl"bN0•aaßV‡Zç(=lgNÙ» zhc©Ý"6ï8O+¨ÿš.ð9Ÿ—î!.Já·|ÖØÝ øöß¹:ØÞùS?ngœü`cm]æg5ÜmqVà0‹¬Êj“µ&§›¦iC¡FÅÏDW÷%Pç‚>j×äÞ53N(ÛmYC´”7bý!gÂVÎüžL†ÂøªsÊÀˆ7†ú§5îŠË’¯Àe¸ŸzXê¼ÝMޱ,µuâPI%O.€•óˆACÑ­ ìv¡èjÅÕ|ÍUÖ |^ÙK‡zTçnWn5Ë.áê`‘Ïè±rÔG¯Ü„úØ•ŒEß¹ÿ•ügjl‡k¸}Dµcbu‡¶uA-òÔ÷eü-=_>è¦k‰û8>RÁ?0ÄÁgš.ÁþÓŠè–ÕÆ$P°JV¦n°â(ªP“X¸©°AãÆ‚…2\Ô Iq?rÏ.56kT˳ÖV\@©Ä£?0CÌOÿFûyßÄ&÷cv*¶³ Ìëðƒ‹WVì’¸öˆ•ŠvÏMµî¢m¿óNÊ6wR®¬êböc"‹­iÓ*É»1Ú» ÞOX«ùrȉÁ×´_#£Ö=ŽrÄ'+_[mr§bJIz¼0³™‘ÄîÍXäœfn˜¡N¶e3ËÔ1ªwö÷+”œaÞÌ4ûVVú¸ûµ#”.üÈIÞq6lÒ³ðfñ^kù*U¶õ_ЩÅ.ˆíІ¶AuãçÁÍy: MÄ Kûö=¥F\JüqôB’~‚$¼ ­Âsv¯U[ûåØSh”Ø8Æwo£ØµÂdž+‰ìÜùñ×òRŽs=òPÎ\è`‡|4óB­GüIÖïYÖÌjâä/%X3_u6`;õ¦muزâi“`[ï[_(ªÓ#úÛ8×Ù÷$Û}ë6³â¬áºso›ÙŒ—/dÌ=…Ú¤˜“ü‚‡ìqµ™¸zagx÷??ÊUIP¿ú=42‡r,áæ,®šýÑ+ÿéjÀÏ+d ì~äEq ¥‰áCÛà[{??;ÐNÚŽÊf³z$¸å–Z¸!\5/1`sSý\¸¡BW@/×]8¬å?r~>'ò²;c6\OÀ¦èÛ·Æêb‰Ø6¶ùå ÅTÿnµÛÂ͵õ‹Bº”†¢®þ{m⫾a?0(È Ü´Õ59Dó1]/¾ñ-ÇW²-`Œ\È>aXºH×%ÆÏψ™+"kV?0–·ý¯Vˆpd剠³Ý›72lšG3ÆáV_šP C}k”þA­+J±z[B¹pW©À3ÈËûÞñ“·šG°T·õÀpÉ%›icÊÒÅ6u,Á??ìT3Ðâ¢Âu²¸(¶´Šæ+TÚü¾ëôJu—¡p¸Ã‹Â‡ÎHÔCâáà+e Ÿ¤‹å8ønSônd°ÞdÑ&Ž,Y’Õ¢Ó?nŠ$?0û¸Ðƒ=¿*Ûp=ÿmXÕ®ŠÛNÿuÑÞvßPo„,†NÑšVÓ°ÑÓÀ¹ø-™z2Þû󇿽—Éèê„*Ïù*yЏd“$ü¨¯â¾wÙ¾R;à8\È;í!É­Ån\ÐJm;‚bé;ÏŽ%Ë×yHRb3/¢qƒÒu»fwÒvRzëºA¡¶þ‰Ñ ˆyÓ,×S½¬úšÍΗ‹{ѳ$ŸŸë{wýPŽe}ÏW3†!_nçe^›wĤ@•«¹7ªUoÐ+¼º¶È|¬ÝJ ݤgÚXÿ» ãE* p_p¿°7.SSRp‘–¤º‚Ö®äE1HvLö~ùù±ÓÁk'·; Žm_„ßhïìÜÚÞ:H^Ùs7!Íì%ñ‹Ý??!ï0ÎÓ¹}éà‚s»ÜäLlÝ«å6¿ ´Ë^ZªfÎñ‹×º,<·OìSûåâyÌ·µO„k¦V:ˆv—=ŸÌŸwåë„ú?nr„l#iLf”a?0ÀSþ„Lð%]Ê&pÎÁâ½°MˆŒÕ¸ c!å’R(áÒØlR„rE(WH9LÁ(··ÀKã˰±Ëu^Á|øvjaµ¼fÓ§†LŸ?n¶{[;Ža{EJq>F×AuEÍ*$Ø5ên¯H#Ù™‰2Ùq coøg¨i/ÑÄâÂ8üsƒ4º\^hJs|h²Ò~e ;¢ n;bR«f™5nÁ™Ô/øwcÌ.“KìÕ­õúi®SW µ§¶ÆÌ@‚FåEÉ×ØÍ¼±¾Éµ…mfÇzN^V£± m•*Ät·g ¿”6ŒÏΈøð³›¯å %¯G¦Õ6ŽÜìm¯¤xñg"ƒ©†šå$GI³†Ûßð©hoÈDŽ{[Ù‹ÕN7sz–sZStM»-0û”.+©ÕΚ(à“)¢¢6ÛÁ=.–3sèÏjâZ訆¼2áÒ3á î£&Wwc•g†I?n¯*u¾¶Ž… ãÝ8và‹›Ó‹xí†æ³ç8ova,ƒ4L5ãŽòq•¦«U`¬çÙ?ršŸÝ3xhsF¾7YR5dÎbñ9´^¨HtEj??¥Û€à\† i“ˆGQæ „™é¸™Ïi¥V$N ì‹,S…Ó²_lg¯B«Ð[ö/\A8œË—”©ÖæÙ/E}Ípåæ¡vC’ƒó1¿Öù‚mmýT[r ±Ð-¨ôðØv“9uüᤳæÞ¾XwÐ.ÄöÃj™áo[ÓöŽì-A>%æU°8J tmúM°AVäò×ï¬Ú›á[¯«6(Õÿ/áÉ1Ü#£¹7'÷Ѳ.- ûî÷Æ>¢Šœ æóŒýÒ覥#WÈÄ'ëG†??4:?n…æÎ%Ðwc‡@Ô`'n?rl±šÍyÛ¯w^´[ “õ$–AÁè[€3àƒ%œ×Øx ärþp{ãån+-ÄF½ôᆧwT™á~M±Á Ñ(Yy¶¢¹6«JFƒ^‡Ñ-¥²[ŸTããë%ìþQªÜÀRh2Êýò»Ž +#å¹ÔÜh(*ÙrùA•LíÜaKhU4‹ã‡oYoèhúÎ(ŽÏÌwñ{¬?n6Äm¦ ‰­c¼|«ñwþV×.>öL¿Á÷ÓwñF‚\•σlÿÅò©íë !?rr˜ç¨îM[ýz'Ì(ï‡Íç_÷°I6q¥šXThqÆ‘ÛÚîDï%*AJzÚ>í”+M­¾òW )€(>«8ETTär¶7¦“L5BI6î)?r¿©}Äh¿*WðRÉß˶¡j(ähÞÖb€¹ªæ>vsÔåpýéñŸUûÅÇ•?n;ÏÖ\nhüXj?n??-õfǶ–|nV?nðO(Q¹²Gt çÜï ( Ï7?r¼Öf{ùÂJ/+ÒòäÓ£©°óÝêgP?0#”êêvP™›bÞDT‰Äi \µõÙƒƒ™Ú·wGÂo°éù<Œ]€9ÝZ!E—QºV@ o-Tpód??šKyV3?n A×ia¡¥tÒhãÔ¾½EÄUpT,^Ué䑽êëϰü¯±àcªœ\c¨ bp!‘AðÇ~oõÒI6½‚ôtê3´ì­Ü®rÉB”`¤Ìǘ:^Ö˜"]„Ť` k…ÝdjÆ;Ët«û:B×ÒÓ~F¿ŸßCîán?n€AÞ9}LæH:)/Kœ s³œßG‚çÝL“j²´¤í„ˆJEhC‰Ô9¬Lâ›"Œý2!'÷Æ7ÄÑàÄ-GµTÚ5;.??°Yg1œÅ Éa‰hÆxFÕºX+”ln‚VBß4UgߨmÆ6Ѩ\ÖL~¸^¬8c;(mcM4n"Z–‡1™wcªðÐ롊dôCPÓ§Dz½(ôRÆØAO°ÙÓ>øu¥[ÐLÂ¥‰bŸ9âRÅ?0m{ÑVib`??f½0„àŸ Øl2?n#jð†w(Ö),µÏ]·ïÞmÏã °­ÖÜ=óì®Ïf+<å:¸¦ËjiÖÅPšlÚ3Zé#"¢P+°5ÑìJêñB½×*V‡íºl¹¶TÀò÷¶Íô¾-ê-ÓÍÔr??`ûŠ—™Ø'3ù£‡ñ¡rÜ¥¤åÃ…_jlPQ“¿µSëéWÑz¿µÆÓæI„¢KéûDÄko'zû ‡%e„^b—qÞÕútÿõAU©'_s$"žÏ_<Ÿuýa99‡ÅoI¼‰|ƨÌçæWt[É¿®Ô^òCj#«Ö޾Q«dn(;p›†b³i<ï¬bÆAâQ£8Jéò(àȳÔÛxÐhïíŠ~BÌ|Ñòp??lP}Ú¼¬§öõêoTNsµŒüŒùózØÏJªCù8Áú:†''‹tLÎ’4¬P¾unÿ湡+øGh£Ýźé¤#QZë^Óûîʇ^w÷EåZ.bU@U,Ç+é3@ÅïºÅãÓÙÊûëónÙŽˆ½ /àÈ~LáêÛÑ<|¬\01­VÝâæ¾d¥ õSV#þRýda-'ДbÅjÏfU_¤wÓ¸ÀÞŠØÖõ²®‚·1À<0@bÛOÛfwãêÚ½†¼•‚ƒ‡ÔΞíU¸ž¡t%ÊcAhË®¾Ú¢ÿÝdì&Ýk’c鬛Yw6ù5öžûçgàäí 9‹†84á§t0ãg[çñ 4ÔœÊUSxr ’´e88›©QX(}û—À†•Y7`³ÏP²_ÕPðöuxYÏzLÄ7¼3ËT,x¸[˜?0Ó ?rñ¬¬Åd–æYèaÿÛ*à PhzMü³â F=Ö. .ý,Óª2?r”iº2?r”ùË4ª ûÉzíû –)ž¢ÊÔëôÛ”N¯Qj*vq£D(™^m@p]¸¹ÁÍV‰-•Ìލ¾èЗœ{ú1F“‰-™ðš j˜ÀÈÍû=ÔÖ¸M*£aÇ˶ ,û×»\{¿óœ|½ÛúŠºÔ>chw¥¶Õ7o¢ÒS63,¡¶2@aò»SÈŸÔÊ,-ã›àøQG1h(±¯à9µÎ–IF†ÇüßœlšL?nÕ}Ë&yÒ¡½MFmT/¢ÃØäúY¯é¿UÑh(¦ëÿã.:îƒÅ»?rOÌeŒ˜÷ %?r±3GÍG±þçnË”ò´qÚÉw?0º/m|kXüõ#F7®QwPÉÇ´à¬_.õÀöÖƒÂ~of~p¡´ ä'£êì·Ã??A¸¬lK±ŽÄÙ??ßYÝ|¹ÖâÌåêwñòú®7ç ^C½A =\÷$Úý?n¼Ú{H8ñ©Â Îr`è¼ÒÇÝ¥ÔêC3üXÑA‹§c???n‹??|8÷Uö?râw1‚5XÉw»Ð?r«ØúÀD)^pâ¼baƾÂkÒ4v¸ÂêvXh0ýÀѪó}žá “©Ç¥,H¡ßÌ{’fù†÷NíŽ-ÁVøÍᲆ}ÿœÈÉâ/ÿÞË«²ÜPðÒClm½_ÃC’&’I²ÿ¡‚\ƒI€þü{kȸC’·Q¦(©2$O4Gd—ô(0~‡¾/Ä{£ލDÂo7K˜6‰=·ü[ ÀŽ6í¨ôé"Ìyë&ª2#?0°??Â??–#)aÕ>Oôᢉ|lJ®ûE‚sÕ$.óKJ©M¾±;¶¿h]Ïc<«Æ,0(è…ݰÀÐ5ÅþBgsŠÝjû »r=k»¨ä;ïí&ŠŒn’*ÉØƒÖ+—Nþ+@?0Æ?n0E.ÝOuÚœ2°JàM?nXß½p?r¤×ø¬²C–ÅŒeø¦G–Ñ‚f1¹¿$3n.ÌfH`„Ý`ÄÒ©—3ÞǶ|]h\-¢¸•môĺYň-vpœzš5IìÔª¼ÂÛýЩZÅ¢KOŽ%ˆ4ì—‘Åò??üûÿ‘5Ù£þs??×os–ã!Ìkåf??O"ýåûÊ¥¶ŠÁn@‰YßKÜýØà©çÇ'kæ¾MÍôìÔ,Æâyj7 ÂØhUiôÃj"ØÕötalã©xlßž—I×HµELþ®¾ÒvϤÎÜMgZ¼1?r¸eSËÛúæÍM™aªEuF9eïêÞ#š…~0“Bhpð•¦Úé®@+«dܵ¹Ñ¶Hˆ©N˜CMx;å”íÔú$»;ü»c#.V >Ñ‘:%~N´ª“ð†™Y ŒŠu‰r¡'YŠ¡@õãJïЗV^Ã×­.5h^ÞÄ„ñÖY¬;P2hÊIû@IÒÃeuO¸ŽîÌ_t†ù,.OúÁ1ýî}ûÍKzõc˜¢ FúÛÐí½mj_•íà?r* f¥,ëõÀ?0<û©²‰óÏOý\êÀ›<b†×ØGÐà¬ci^6ís”ÛÊ­=öO‹³g1Ö¿¥ï?0o„e?n¶ßøìã#-&nˆdÈØc]¥…/,[»q¾KžJ×q·|ûÊ®Ëo‘¥ºÆ·y*F­ âQ·ÃÁ5¶n¢öd¡€ÛÛrÛð‚¨†t2COV¿âïÅj™fºU¾ ½ÄT?n¹vèl·¶I?0¯6ÆÝÄ­?0Èdq Wð³{;??!•°!éòÃCÑîWzû¼2kWœ]=뚃 (Õ²°—g‚q?0èx€œ¦Ü/J|³†²[¢$“qâîš?rè "kت×7ü/:w´ŒíñdéF‘Ì…lå•9^j)??B91Þ1÷Æ ©X´L’*çnšn#^Z9sp3W„Ü—ª¶Z¢ÚŸM¡+qðd𯖂Wzóñ0 l—ëá¦iÑÆ4üÞVø,øÖTV‚=S¾Z üà–Ol7‘waµŒ‘‹r†è…Ç6uIª;(¨ 6›x…¤Dö@ ,¾­¹:Úw\oÝß»L~Ìö¥îûÛAµ¹ly>­DS!äÉ1oúÜ4^¡Úˆ ??uÐPçùæ„std·z,²ÛH¢3¢#Q ìX Âä¾\È¢¨›ª{¸ ºÙ¶ðã~{A¸8õ‚PÛFSªÕ\–Ð ;X†@¾B²[·V{v¨®Yt¨:¤4sõû>Gþ„vlWır?nv—ñ{^p]Ý)u^y,A ìº°+Ltj&N‹ þɱ}jòªÂKºrRª×EˆdU ú½˜¼m¢iœö?0?ráȤ 'üƒãìeüí|g»LçMžu??Oý2)¬JJ˜8N2y—u‚X õ*D.D 3i‹6vô¤^¦µT˜:)ý¥¶x.ë-Yj Ê#U]½Á0­¾€9önã9Õ$£X¼èàBª¾2ÅÜñ¢¢%¿‰/×Éâ??°ÿ?r \-_öë=§c "‘zbÒòl)sì!ÜÁºRÚŠ+ÿ€2Χ°›Â8_úÑžç^Ùâ¥övÆvoÁíÂr³+µ³Ø5*ÂOv‘ïXGIà•Û­I›}]ËÅáñ*ħã¿É3ÚMxжSÎ]˜Âm²‚wÝ шš??q+?0¢p¹¸RÔ~šÉ/;es¥©9£ôì‚„>ôŠí•Å80yü(ÓÀŒÕ¿Þ)èùßËL¦Ø®h#*Â8ô¤i—ø›®‹Þÿ7¶0ûÕÌ]Ñá»g¼D|à¶6²Q„SÜØ·,kokªyrûÛ •,ýäÇ6“_3•``­ê߈dòþ¿fŸ}õÍwOß<ýøÇ§¾ýøùÄQ“â„&?nž†ÍèÝbø¾ÞF²e©#÷UÕP"ddå„ ¦—­\œæo‘–h+ƒ^OËt??l™ì=POÞ`ÍHÂփ͆à ^j´b¨þâ=1Sý·eº9í³¯»¸1üSÔ;”"®·©¶^›jj‡“#‹WÑ>ÏÒgYlcé1,ìZøt-£K–ôê§ð??¯úö{áÿl…¡ðï1ßÔÛÊú>ó??aîß@ÌëK·ÇèžÝŽ”@l¸×‰â°·Úœ=ÉÃ#¿Æ¾¥MÛ¿H‘Cׂ(…Eþt„‡ËJH¹_á–mž[ü×’5t }lùoàN?0»{˜\ÖÅ2¦ÃÄKò­Õ5tzRW.²X”ÑÎ-Y¹§Ùè CT„h@ûCcôèâÕ¬%ìèÐÒü"nFd}ãѯ??$ÜV*Æ(éU;¿‘g¥ß§=|%ï~jNoèÒj†/ÁÔÔOR•>/7ôDmãZE‡ ±.5Âk"CÏd\¦~tS+ø u=»¤Æêª ðÂÌŽ»Ê\oÖ¬°)ŠÆ?n-#>óù±=ØY©¨íþZ9YT§W…8oû¼Ä+GnÀ²¥f©,5å©B°ÖtÆw ¦[߃08´ýo.î¡TM$ú¶qŠÖ{`F·SºÐ]Ù ÝyUï`8èsÍ‚ë×#o,A΋½Ü!Ñ?nØaÙCÍ166úùÇbìˆ@¯ ýÏâ™Ö©¹`l¤–°µ~î“iŸÞ¶ *•.a2ª6ο[ Yî?n¨‘c(Èv û»ÚnñŠäŠ;?r’¾oì&¡%ÎbÂGbÂNÒ¿ÖÆ!R˜F¼â2Ñ©ópCø,J¬èĮ҂[¢Dýd ?nŽY:NQ®²MßOÂD‘œ_PÉ©Ö~7·ÛðÔ¿µI.¥Ûã”ߨX¸Gºµ³‚O*©ï"I·Fm¹!»àŽ_çÿÞ1ÛÍáaq‘-÷á¡€ äËßX-dìgÒd?n¯7x=ƒÏî@C³óX*¿Ç%0êf¥/ÃCh åZvv‡[㹪~ÕÅCý8•‘Ò¼ûÃͦ$xØ`F¬Y>„ÝP ÇmLß¿)@1‘Ï?0Fu`ÐQt–ÜÐ6A((Á'šcö«„ÔУz‹âi0â†[œ*JX‘yägÀ¡…„‰Z&¿ºa™¼T??` úÙp lm†=”–•'Nšžˆ‹idøº §ëgPlÓ‡4©ø‹s4}‹¦»àð·€ól€iHy´f„ßk4‡&íê[ê¿$Ø ßVlF߀o$L¤ü”x’Ü÷¡ñ´mتöÑ.~ýÄ_bw«ý@KðUÛ CáÆ2,¾{ÚìV²û(ÞÇáû«ÂµÉõÁÂ9ß @®ª+_û¿k¯P–E¸žX‰A¶¨ÔÄÌÅ)«á„‹½QÍ—Àw•'RàÀJà^o¢…I:+Ÿ/÷] Ø^EfNEÐ,a@v¹ñŶN~ÿoÖ?rÝÙ|Qmð«xUm”üõ·Ÿýý믞>øâ~üì ??]ʉöxÕï??þöégCÅe-Õr©•¥=2öõ«ÜÛÛäý›+5ÚB,Ä^ýÞ­Pe|ËUœñpM€Móøè¾skÑ«)EÈìÁ\£R,ÇE 7¡äÆÛ¨!??ÞèÁz¤±‡óÜØÃ5˜¸ß©—Ó6;HžoÞг:¯H©tk ÀjŠIÄ?r¨ÿ’äéYÿh›øù>Ž2iÞîÜF6z‹±uP¡]bŠq#¹¼±H6ÿ o]€U.6ò«J¤µÚš2kåºãI_e¯Ý†å;—·[\ºK¥(2±ü X[/N®ãŠzÁ2–ë³ÏHàØóÞ©»\ìô^¹çîMjøÿUbÚ]í*Ó/jðYóûÂûà@ ~þ”Šd“& ?0F_âŒa,æl£·É`´…(’®ðê¸uÐ}50^é^Í6£¦4‹[0œ~åT§^Ínçâ:éFåÛöåìvù¶‹'ø*ZÕ¿„Ûí®% Š£IBV²ö’,{ãÒø°›ä,PÈ´9GZþæ“äP`o1ÓO‹1Gt›ó¤„õºÕyÒ[ã^)ÃQð¾ü?0??.ëGØ ß—‹t¼›áq‡"ìæðÿßïZ|W<ƒ??ÇÆÎf»Öì)2;+ÍäL##-C©b æï}øA^ã×ñ‡ùŠSþ”_0üùñûðr¾ILBÄ ~¯*FÔ‘G­œß/nZu®aNÑÞº¡¿s–y¶· Œò—]±ÙLœƒj"Þ;b— 2q)P@zú8HÓ:Yaj§J*9Q©Knˆ:]v¿HÏRz1BëðèätzE«¢??FÚßÇQõP±.Ë›‡ÍÍí8£?0<ëý-¡C¤Œ¸l3ï³´ß±_vÃ}“#Ü´YêM¯«[8— D}ù$¼ t˜¹énÄݺ'å³øšY7é‚°´qAU­ˆFô);{Öà>l¿ŽÓ[%Ó[Ó„~NA¸IGq¯a;ƒ¤’| '®{.6Läü˜™±ËÑÅĘ‹‰°í¼²)¼n{6!Z!"ƒí—Å-ñ·¨ZÀŽz˜‚VïtÚøû`}Éžb}ŠžÚKëX{®+g-ô^·ü3??~f·`n¶ˆó³“"ÎÜr{z”oeñ¹Êg.­ªÙ•«½?0c¬ÚµÛ”XÒ¹‚ð•o$aC ÐeäçóÚnÈÄk>oì}è½sg»^îòîˆ)m²õ«QLËœ1 Ì‚½Å<³GªÊ-¤Kv$F_9Ž}Æ©ò¤ýrõkÊ³ìØ¹¤Ð2T_]å¸ÅC^£¢æ‡NyJ|cõÚp¹è²|£‰:ëÅq•;ç“ÎeXƒ2æ¾™S”GèNܸ²sRmífŽàC±9¦Ã_cµ­¾^‹ì9ä…¼8ùB²)?0F—‡É¬´qçõÆâOÞp½‘×¢q}±Ÿ©-{O4ªØó²cÅ¿8¹uxG?n“5Ž5—œYV2ú¨¤jâ!Ñ ³RhÇí>Òª.ýkJ1öy*k~®ª_4/•Ý:ÌŠþÜWË`[O¹üÉ9°+¨ÖÅêü€‹Hòl¬Œ¶¡Añi »«•°ñU•ŒãIºÚŽÅìkdâ¢ïéÔ¹Ï3¦Tƒ>8‰°áßΪþªÙ„…C°i”&Þv޳’¨×n½¬áÜaŠh R2ýÉÐý–W¬óö߀qñ nÚ5Wæ¯û¥‡Ú/<öBL­—ô6XeÞ«¶û Ý ºþ•ØKèñÅÈl\‰»??¨{£–Ö/>ÛáîÏ¢ÅìùÈS^7°;dzD¯ðÉÒ°£óÏmøyÓ¹3öµÙNöE|4 ̹+ì/e°xÃ×°áÿ±’éÿ [²fWʈñN 7ÞO.?0âÖM¶¶kîý…±@®š»coŒ½ éQȼìú^9·6ðvuéD“­,û¿¨·aS˜]Í’ä-&Ú õ°Ü5tô8éèu2;ã?0pªìÁFwgG8£hƒÕÇìØ^ËHÞ:6[ûÀð‰ÿ& î]z‡sr¥+ ñµ¾œt{iòFÆúÂìïZ'_ÊY0‘4 !ÝZåqeMØ'&wÔ"¾Ö³Î?nâ/7¸…N\Ÿn𺸽«­_ a‹¬,—´èË>+øAÞ°¥?r6sú1€ŽGðsÁƒðè%ñ3£!›>þó^4‡¼ßÅß6»½Wó¶|\x!ßy‹?0\iKR{ÒÑeÑ¿rduïc:ß} EðOjóèÕ‰g{5÷:›¶~3ñð¥™åãü 3b®µ5XQÇî'xìéÓÔ|›Â»zðóQyã¯(À©üÊß??â(è‘Jœ$À%ˆT™¯o’ì²Jå·ÕeU'e8I•»,ë²-°û\$4ŸJv0MÉUð}m¢ŸþðY|Ã79ñmYþiÇR2û¸;þg-.Š/¤a)K‹%aEÞ­âžc­(+†¡B°ï 6¯ø2 lQØy“˜É}-'}Ç9Æ|ÓÞ.ZžãVk|6vÓ9œÒ#_mœ9WÁCézš€Tü‚µÙ}eÓÙ´ËA{òöÏðÒV“ <øCèRó.±w$®%ÏÀýPß¼™þ~¦áRGo{~™!öˆ®Áà0~™œ??äøš„,AçRÒ¢¸N8å,ɘß«†š<ºQ~§@Ð2ñ‹¥·Y ;—gÖõ??kSu6ÝÞõöS‘Tk¼‚€Œ¯´¢…(£¿¦/þ˜6cNßVÚeߥ‚Š-ðXO8PV“Ÿ?r›s¯÷ÆÀÕÆ%åQ¾Þ$è¬N7‡ð’eÇîÕ¦¹ƒmÁŽ?0¥R•ßÜ>D$—ëeN?nß‹~‹p‰‹X£¸”²¡‹Pˆƒ©æe×µ’9 ]Ú4K©ëÑqTÈø°Ý$_âºñ´¦â®.jŒv)Ý«öÔš­ÜO_iý¬¤½4’¦¶1»¹ší3+øôZ…)ó4hK¡ƒ¶È5vë?nŒÔ²ƒ";×egÎÙ.NˆsÛð%Æš 3kx„ÛpûA»DìŠ+”«_±W.#Ø+GFUÆá„õZ/G¥þQµ1y£Î´Âɱg7ÐÜ"À³k³7‹Ã¡i¢¶à_ùlæKƒ!ÆpNá¾|Ãû½Í[Ò¤ª² èvZãá;šê½`A~Rè—¾.ENg<Øl GX­k!lluVé#Ö +#~A˜œšI•’ôZÊYb‹˜·”S??¼PX­ó¯Š¯Rïu²?n‰¬÷$Á8…ó”' ÁÇÁr†(zܽø52÷a´!hw.¥S}/Еl%#Ñ“M™h: A4Œ’4‹{Ë\aN¬ËóæX:æ?r7w£JaéÈ×MœS,´䣓 å~ϘsêëyU  †î$²PiTB«:wˆˆ©ßá$ÿÍÃ9ºr]ÎÉûoÞ‰v÷è¼£5>¦,ɦñéy$‡çƒPšÔ?nN¡paØ)(ÊÎ[Ù#„"U?0OwlUF”ß­¿.¹I,'¡À§i?0¨¿Ë$#g 1Õ±.ø£ZÉ„*NÚ˜>RmP×+èÔF£˜úþÆL°ñ"Ð*ú¶nÂ7õÇî‚ÆÑ¹Xf¹Í¹¥s?rúÂò d3·)L’¸$®ËÀƒö¶??Û?0BÛ³F–¬ËN]Õ.»?rÑ«Í0÷*Ö–`P\XÖtOÌth7•U&y#Ë$ÿ¬@¼m8© M™TÐbf?n Áv9¡IR޾«Ín ™øÛ®b´Ö5$MW¡ì¿ !kxÜ-ùó„n{yøß¸Jiß@ÙÕ??N¹$}r9~ÀJÃn²[ «ü¸ödȾrî\-Åçëù¼XDFrÏa6¡Œ«ðÍuµ&Sq*k‡Npõ:«`Ÿ8ÎQ8}¥82J­5Žfíþ†S¼¶óÊ®Áê;ÕF6t'0f˜EÏßÃvÔIXAW'GùÕé‘Äíø‰!{[ňŠÒ1ÙÀ¸,&?nÃ"p—. ÚÅî\,â" ›’…»Säû÷!ù¢ÀíØN(»±dC“¥’&( “ßó‘2s,$•‚¶ÂU>וÇío¥\.“wnÿ&]êtƒÈ,9Ž„ËC=?ru—tÄIt\˜Il^ŸB¬à\#ÒkÒ$º~NXz,—/—¾3Æó]$I\7Z…UïE°è''L?0Gíé??½a¥‹ÇljÝ1o}ç*¾á”RZNþ¸ÖX^W3E¹”kPð¬YŽ·sC†°Ìî>^ﳦ×ä|ɪ•à¯Jð»©Ý¿eZª j'%¤Å¸ª¿¯¸×¾1¡Æ"Ôªùíîæ¦-·[\ ÛP‹¶¬„?r½›û”lò î²7‘lÎ=[ÉŽò£âÉòžHÑu$øl®hx„É!ãG˜ŠÍ¦0Å?0‹Iý^dªàU60!:ž€ITekR…ÿ.ؘ(ŽÝ£yĤ•´îL=©^êºÓ¨ƒ·bÆ¥à­Ö#ßÖÛ‘Ûz{¶e¡?r²žt%ßD{Ÿ7o0‰I O6;;W[¹³­=†ËµS%¸œ3ë,²5BšéQ?rS‡lËj\4À°ðE¥«SUYm4îׯ֢Xj¶l«½Z»,ÌÜ*2âìU¬êD#Ŧf¦C}wi[´§€Èù¼55Ò#7Œ’$é9þ%×.ϦÓgv®}0 H^+˜8…È(&‘• ??I çóæhÒWmDª †P QW_mšP€%lr-Z_å†Ë „‘åÊ‹½k2ùM;žœ®Œ´„”-ízÕ;xg8´µÝÂBªSæ¾ëFÁ¬ºÝ¸)Ê›QÛµlè~ž7Am·:/$æ\ˆE}v€0òe\NZ¥cI®DyÌ1‹ÝlFÄŒz„Á=m´¶¡ÑÉ.‚>dµ—¨%1]6 :z‚|­5 a£œiË7Á^ÿtU–1´öµHçáÂ,\»ëüÙ`kÛ“®´«Ñ,cõOתoÞÀN•ê™THØÿˆãÁ*W'!›–å˜bf+‡²G>vB‡IWEAÆF¶^ã[)­_ÖCãêå+NÈQÀ¸hHc???nÅ,Fš`Óèãúd:ñÊìx1\lbfÌ‹­¸c±UÆ¿#(]¤çQ/:ôtÛol&„»Ê.ˆtåпyïÅ^Çwn»Mꈢ ‰ÖÄy¾»hÞ]&B¿¡?n_¡ÌJòõ6XZù¬=KºñJ.ÿ‚<ÛPªGl t2p,¡‹ñïÜ[¯-¸µ|ÓV*¬&…+`OP²m¼ª è¦PÓ4‰D™Iâ>ªiŒTˆO?r$<»ad§f¾‡li;{´I¦ÒO~;¦s¥%ÑröÚ?nùIK‹¶ qœ‚ÑGrã±¾Áé䬵bµK§ã™˜\>œ!ú¾îóB "ŸÛä…ÆK× F_€¤995‡ÍA‘`ѵv‹ïšÈyÊ4µ<c±YÙV¹­5.€*Ø€qãðùBÇY²`ʶî¯eöÅ­,¦h¢ÌÌnmqu®þ°²¹ÁˆCAs̸85hÆ•'(†3 0f­ˆ ??G¸Æì§t¨±öN;Çüô??<Â÷§ÒÉ;“WÚ^®ó]Š".iÙo z0N_aƒR0ædOe,…½®ê^:¶H¯÷óã#£í[#ÙW²Õi½¨æó¸˜ˆ&+£Ìvƒ "žï ç‚ôz…¡a6öö?nŸÁnšçpg;ñÐLíªÙq8Û÷Ãîèþ3-],:*’øx1ªCÏ@Ÿ)kJÃ`ØBvY‘U0'O—zófÈŸ¢lÈMuB»ÅîLÞªt ÎY1 ïL»Ö)áy x9oc{ò@|‘ˆ ÂjÎe]Ö<†}ïÛfÂ^±R´Ô=q¤>LT·7°¾—5Àu|.ÍÈ@°±8|±?n¨ñˆáœ¥$µÊU8°¥”#òWF×Í@Ôµà[fm,\ª¨”¬]Ö¸3øùÌ %låTžÚ©ÈÅHEž½¯é"E}Øf¸ÿùîêSÓ•' >b¸Ýë?07ê,¬OJ–âñŸ™«r’$æ+€S0¶ÍPˆ·&Üâ³D‰:üu›ÞM…Ï.fr¨Ã3cB6î‹þ .©ž`ƒÁÇ}¶‰ê¨ÐGÛËÄu²8àjªBûÈå\òÂ%½>Ù©ó—¿ЉJU–ÑA‹—a.àɧ°ð þÄI¾y?r¨“͆…šÁééÈ’{(`>yèc×h{m„Yuî;¶íï3Õ¼ÆÝeÛk¾@·–vµJêšZ5<`£ÇpRô» ªŠ6Ü5óUã´Kü›gU†ÒÝÈ»GÞwNÛ¨Ö¯±êÅ!‰Ë*{ƒÓ×Ξ­vBy¬kýÌQ»Y,0-|;_ ky…¡K ¾+ûm‰“Ó0¬V‰îL~F|ÿ®~þ¤ú·“¸1Ûê^íñ’~u—êÁ€÷—ʺ„ÉqDë3åœØ¹&š½Mªûf¤rýn•ÙŸo¤¾TŠ,ìœB-E*ô]cp?0ã¤R!e 4 MónÖ>õYKúÒÖNÙÎî!ª¼pY¼dcà\3el *ouu¡û2¨Î+vWõgæjUOõ[t¥‘húò5(VšÁSÉš’ñä„F-2ž_?r:& §%á´4¸uª&1¤‰¿h¸¿óËʸkn¥ÐÜù¾‘ƒ^ÃÝøä‘äÃÄc?rµhYÐ~"g]÷îmç¥A³¶0+,õP}¤k‘M記W²þYy‘4ɧžÏ-†±Ôëžlª3€Ñ˜h'ïŽØ°?rFÝ¢ªÃ‚??¹ÿa¸Xƒt7Í–D±ÄWMø“E•”xÖÐtSDOZ¨ÏlhB=–C"÷??4YÞ¤ ý³®GåÛ×?r#eæä–”®¥têƒlí:_¢'&DlßS} ]âEh`>âÌ|xÔá£piæ¿ÜÿpvÿCÔ0?0”f-¢«Q˜Š6Yå»™¹û¢}§a¼¥>«=bñ£žÀ?0Öµ½½¦`’àçùžÜT›Mf‚γ‰ïrÔúfaë²OÜqÔz”sqÝÐ/O#Ä£yÐç²dþ¡ýÐð\„QàÖË[D|*LðáÃWð£þTêT[š…$–+ªsqÇ»ÎjØS³å M«TYÿÌ–®Ú{ÉáÍýr¸„To¼Þ¤ÿÓ“xÃÅ ½'6s@Ó=ö-k ù$ÎGØÆŠÁ6ÆÔ²çÀ%ÍÀèL-dh¬Z›Ÿ<ùúeýMÛÜÀ*»Í*¶=èµÃ®›Ep?n[摃é?r—¢ù'ž—'™x7‡ý>|/ø4÷C€ïoԑƶ÷Ý45?0¼–v÷{/9n›­»4[!ç\›î!Ú?n§þu,±èöxÅ"P2gKgÉ¢dulŽÏ• k¿åã¬Û U«Ÿˤ1áoð!°¶PÀ¶˜WÙ)·?0¼‘X—‘‹RƒáÍÔò„”|oSô±ÈiŒ9ÙÃê«U‡Á„·—÷lZéÜ/P"ô«Ï"?0[4Æ_Ù*-it`Èu2äš)C??¬ò1ÀD¸TA©ZZb³×0l˜XV?r˜½½-³›†'áºqGöEJùúú#nŒÄ5yÛ§ìjíf³ëF‘öm" àxUðF5½÷+uɬrÿÀ.†¢x??e*9K â­¿AÆÆULT¢k ÝQl¨Kº?r¬”¦.ñ;#CÊ`³OÍsþ[j²o_s·È{ÆdŒX^m¿á€»µ W5¢Lܤϛ¬Á×VÔ°ï¢B¸mŒ®ÂèA€/‚ˆ¾fé1Ž?n œŽ!5ì°?rÁU§áíb“Õã*6ÕyJ 2®U¹þ¡"©m#¼‰dÎ3ò?r¡@¤ºZé4.N¤ò¢^oÊ/›Ý¶äÐroÞh€ÃÁÕeݴ弤Â]”à*ð9á]k?rCå¸0Å2?0¢ÉϼØQ±%y±Ô³7F*Së2CèϨ§Ýø÷Ø.äÞMøjê= r y7X¬?0Ã(ˆdHU8êî+ìâ¾ÁÇYž6Y±¯Ì kc(w›"x §9݃XTïs =»uaL;Òr)˘í˜Q ¾ð|˜Â?rÇîÀË'??x(îÜNäŸAR·Ç6³ˆý ímG‡Þ«‚.<íõÌOJœIíIÄ‘þ&ze‘Ù‚3uÔBÛö,}–ÉO^îÞäi)—üJ«Ä -\LNA»°!˜²^CÆúè¯UxqNEt_È*Zù/}ÖØ­:"__ku5§Õ1-š©ýÀÂV³7=\¿Õö 6ô_y[£Ù@µ¡-àëš,UÁð³Ìš1V›?roÈ¡nÆ€Ä?0?n&"³ÔOBIN=¨Y,‡ÃÈúƒÌšxÔØ&XÄÕíh9ã¿; JUe;ÛCU'#UQúqSÚ +#b½'þvC¿Êz-ß«-íZEܧ藯ü¦‹¹?n“¼X¸:|±RygW¢ ƒ}X»ïðd¥s|g§LFì2kw(£Œ¸ß¿¸“í“0h»ãq–°ä3‘‡¬@z7Û¾(ùè)µ05:|›œ‘ïfS'ÅlÍÊÆš¼þ»;?0;ÿð¶jSÂ!9H…¿—Ì`ÿÐg…^i–Gì{"-X/ÁÄí&"9‡R±Ñ7dàCŒ?r©ÔeH¯Bº±Icîy¿õ¥Vª¦€^i¯@ú¦µWØ“·ø÷¸CÔWŦ¥ ¨•?n??hé°$Æ®P-»3‹Õ©t` žä¸=PÚ­9Jmkör_Å,;i£pGvœ|Ý„sdÉD+Z6HCU|IË«Ät¾Pðý€—XïnÀ¼4€¢÷à•,’ÔÙ?0÷V/GZc>´¶Z0Å@ëÓsÐübãS焹Fì»cÓóm`G+ÁºLÖ†Á"i‹DÝæŠHçµC›e“¹7Ærº±sŒ ×=Ç©*ò²©úÕ<ü›Ô©ö¶V?n–Þõ²%{Ìü˜ãzpN¥÷®ÌHE¥Êf=Hmä„\Òüüñ£?n³ñ'úæ~Vã>QòXªF¯\RapÖX\ö5›‡yä-Üë‚€S~/êÁEq‹ÅÚÌ7‹-m­mã¥pK1†*!Úx·«l‰‡pŠ_Å|ôàN¥×Ñu´àPIáhR~ƒúKêÄ6Z;å1ñ½y3¿«¯töÁ ¥oGZÕÁ©e°ëòÐ`‡Z’OU`EÞÿÑÑÃÖãRɇGE·ðá~>zç™?rdp?rÔÚ†·ëÕÅüU£Lñx£ÂÎm³Ô +¼üЄtmN6jÔ'’C£Ã6© ]êuæôúö¬ŽãBÜ’yd’2Úd%„`úûe±6òf“ä¡=ìudÝÜ`+?nGk>¬”MeZ8o‚uåÈŠ†ýçÿÑ\ÙÁãn1Œ{Û*¾t:h½âÑ´è±fØùó;Ï*Á°&8*F¹D³çf¢äèyㆲ%g×vOÏc:CPø©e:z??^Š]ÀÝúž³«n:e7˜gæŸÏD¼¢¸A©S??IÀìƒæ 8ÑS«8XœœZQÊÐ(«RŠPzÄ’_2‹grÑÄñ†n®4Œú®¤gú¡"—9ßqEOÊWáO-Ä0k—ô‘O7¾rvÓ‡‘?r?r}˜ÃÀRÚÖ?rÿž¶Œheg|Ê=†à\¸x-žkM*l?rZ³?nVæM?rMïV0æüyc+??¢!«—<~ØÏ»I´~^B\eëLùœ‰ŽÍvÚ¸ƒÚ÷#;¢l®gŽôÍÚ*CkÎt·¨×Q<¦朱ŠhÂPúï‹ÍNWŒ§Ê÷á’õ–A \íœ+—h?0÷Ϧª3¸ŒjJ)€€aÐÈlc›­²lØ.ЫjR¬~»hØPI-Œ"bÿ%ô¼3ÃR™È¯bǦ-s}ƒ‡5HÜñ¤Ê·=²G ùy"??c,Ëù‰ä|é]†×ø€`b½ ??‰ÄlÅs£À äSŸ†QÆz8¨g`WùtÊ<9S¸z†Ïz|ž¸}@ÑiP…ý˜d¦úàQ8žŠMŠä=è§ÐÿŽEÿER¤*GWÉGNÚ¿b›½&¾¯¶»‚ßVÑöYzÛéZ5öO@›]íï¶ÀÛÈCŨ?0€+Þ¡˜¦ì½?rŒÎØÀŸ`‰åa‡&) Ü>€ tŠVGSZ¶{|ʆG!‰äSض§\Þ7ü;Å3&ð!nÛ9ænbO|1övqì®JƒÂØæVp öY+ßµé*ø!säó#“—&<ÚFkY{¿ pµ9’z€Ã'ßÝ ¾‚‚…$ЀÚ`Ú6@*¤ÑºݾKí2K猊îŠxòòþ{w(©–Bô´:Ô?n·i:ÚŠ$Á’U~äÕHØ¢Ò%2=Öî¹ètÛøu+Û{0Žƒ,ÖwO¶œ‹ÿS%aܑʡ~TÒùHúYþqã![÷{˜Œzu^Ž4똿驂…w~风¡5y»ÿ­Ç½îïv¼Ã@ÿ)ûLjD~Þ©½»wÆz¨æÚ.pC<ÀGP|8 Z¯çšÊ÷>è-;¹`äFzBuj†Í«eu¸¡ý(¾n–i»?01 ŠˆÅ¼S°H~ ˆÅCÌT~©Þ‹}ƒkŽÎ‰ ~½¥»é­¾qmx!ÜëÂ_¸’åeÞÖ ( v‡ ·fð7ÛØ íì´h›Yau®Ò·Šà8¾bÓøUÜ??NŽ?0'…lAø¢ÆÖ®—Û|bæéÜ­ÝšÞ[ÕðÕ¦SÚN¶Ú{‹îŽÛùÜø&AAøb³ü¥åß“ ßù\øNÚ?0P??¥¨‚ïð »NôŽb˜9hBmwƒnX ¿½J[J:)>¦ãïdÐWÃ’6 ]nÈ Hé½?r—×¾¥l'˜ÐW\›Z©³Í¿ÞZ8å¬êb×Îf¾'‰ŽzÇ´³V׬¯õš&ËÊpÁX­JÌGeÌ‹±›¹N3öKNH©‚"©ZÆ/@½·(‡A N­3P"K»Ԍ€ôSÇXTÜ{uuð~ Ù±d刞?n*’0ÅÕVÓïÀ:ÎñI01ŸS#󦑜ò¿Iç†Ð0SâË"ƒ*€‡\ú['{á©ð™D…³°øÖckRˆ¿‡Å¨'üJ0‹~GŽÁàJ.ªÑÃxèMYɳ넹—‚1h%”*BD/õ¢dÛÄ^ižqv…ÿñbáÈ‚aKƒ^˜\ÎIy$„ð€qÚzl‚Ša5Ä zY8¤úðøt»ü¬¾h†Ô€çØð‚ÂBËáu9F˜AÚIz‘ÕEä± fŽ¡8]S0̱k¹·l•—AÒÆɰòÞÌQɇ°v1G¾íùeL _fŒiòmÙ>ŠÒøk¿G¾™.üî”ʨ[?0ál“Æï|Jv£ÔKJx.-]†?nçðøè%s”‡]çœ!ü˜#C™âŒà1B³ö4#ÔÖsR÷(þœ¹é½é¬æùÁO?rY?0,»ûï?r6x}!é¶Ý¸… kÃ#†]?0àÆµl—g¨rÓUŽ1‚é‰üšZƃ˜`µb„Š˜Y¾`Ç™¸6”^ÚAff±Bº@Z))ö‰Ådõ.ÜŠÕN|a©˜f¶| u(L|Ý<æoQË#& éãÕ°ƒ³ß¡Ø«IT~œ‰{°|£j¯Cyô¾xà—uŒØ’‹Ù¾\—ã…K[=wU-ð3zm ÿ#Y”¨‡‡*džãe§îXm—?nãÛZ6¶"kñFÉ€¡Q¥¶žl†Ýhcâ`ˆ€ìˆ ¦G“?0ìv˜qž:µºPê™&f6Ú5æ¦ÿ¶¹CkkKñU±CpÓ#qÊgñGÁÖBÁUϘh‹ÕÜKØGO&ûOÚ2Îs­tf¥h½ûmSsZŒqÇvÌ\¿íÀô•sÉûEoÐ¥SÃFJ¥¨r¤Øí®ýrle’a³m2ÂÑ%S×1w·!yލÉO½S'¨l)‚&þ{s|XÜpôJÅBœ W>¯Æ6  =’DпÐ1­iˆÃéÖ­%Å­éã¥0âÇoè&-±ãl÷±%£™Ö¹èüŽ£??¥dÞ |³AN; …8cÒetq89‰CB1ŒŠ—E­/”`œnû8•"ûÙ¬ÅõXþ`³QXU·#@Û[c[áo¢•H’?0Ü]Já²ô2¶ ]å«ýš=•­Ü˜À)4|×0Ä„ªÕÊ«YEÖï§S…˜¾£z Ù0¦ÛsHæQôFgvY©Ôòë­¸Õ¶Á³}Ò?r…â_]u¯oò“BõoŸÂ 72"~–¾Êòè%y3vk?nZúwUQöµØº‚FO™md}ͽü´^kñ{Z}Ì,7tÎ+8]‘å²(OêpáA2B’[$#ó²ûmWìãñ¨Y¥ç^ÊèƨSCW¬†=¢”å@‹›eêüC^u¤mÿ=aóüŒ{\:EìêIám Cƒéx§Y(ÕÃqÐ\%׿T8ÂèadÀØÕ/eéôz?rhªl0×Ð÷ÆVR‡ŠÒ¢ë h†Ö|fÓÃüpŒyQD7k6\¶ëìœÚ€ïB§’ùÜÆâÌNB¶°UÔøu_Ä}@%1Ñ!$lŒÝDÓ¢?n_Ú«ì+>Õy3ÞÕˆƒ>Aá¡gn R£² «Uè6Coæ{’'x* dÈ9hI%G.Õ[zæPzƽµ`Ô%=±C°ŸB")„ƒâŒ_Œ9ø®[à’±T|Ä­7™I}?nܘ ˆé_¯ìaA‰;]W¿ˆK:ݾ¡ÇZ oJé–e•a!2+jÚCÅV×{+rþQµ³^¡Ë“'Ó‰&À*y}&hP*Güs=%nøQ\ð㢞??8q¬µ'Ã"ÀèÄõv ÈMS\i‡ç¬™~’voÄÞƒÌw½ ¿ÁÐ#2ø¨ ›Ýñö¹¶_+£Ù§N¥.Ðöø5ÝÓ½MO ¤ûeƒML.‘äKÔä¼Î¼Du+aÕŸ#¶-.>€&/b™x:WX‘h–¾xÚ??tQžRmJfŒ?0.\¶ß'™Ì˜èÉ+T\ >*Xªl“¤‘ µ¶qÚ¶¸ÅÈ ¥65œÍø!™ï±wÙ€Á vÙÄ•‰eM©šåP”¤µQ?rŽäЬ†#?næ†U€\{ ?0㟛bëËé~á›Ð6 ÷—–¿ÑÂG÷I¾éqt¦?0¼(î´ÉaÍ[›¸K›MóòQÛÜ?0âèþI6õ¼«Œ-yï6ÛÌ,zõݤÀÎÞÔ%:þ¹ázÛb×°À¦)FìÚ1JÇv·ñ‹÷Ï~~ut4‡þ ÿ/áãøâÙëûû÷ƒ -¦|?r0Ã0É»êÁïÆÚ?r ë•…&¯¢]V­žÙ÷žQ¡t×mq ¨¿„êx×äÙÕÁk ( o¿7ÊÁÏaûÀ‡ášÏ²‘?rG$+¤fñÂj‡0ö>ÚE¶·üdA™\ž¿³)6B¿…¸o‹ƒÃ˜Œ%¿‡l,½WàßKMdЛì{Ee5…w¿’uuûÄÉUw`^˜"ètj¯Î.$ø~³BsŠmC‡ý@㜭E9jÅ{=û‰â`I%W¿l0©”JØàh/^i7Ðx0@|ˆ²±jÆŽ¤òv¬_ümóõÅùùxSâ'˜²<ŒÞqÚ¬à-E3å xP/õû¡Ñ[dvÐ[^ÄhûZ6L?nõÒÜD™&Ç”’[¨G-b†q\85yÔ —y÷ o—ëâ>ú±.3Þ…QÍŠL{ª¸à +#+<”–FÉ: Àê'?r4KOÛÓ=W>ßìÆ5"PëEE¥±Ÿ:TÞ'ê(“Y]À€ó°\ƒkH ¢6¼æ„\Iú'IßkÔþ>¦Êýê¼hAÝWÝÜ”|}Âû‡ß#„÷úƒ|úM±Û>þœO?? öIpjÿ’OŸçS{ ‚ïèñóé|C >ÿ”Oúv_PíÁ“þ"îA.¸‡}ѬžÃ/(ûñv5µ܇ú ýjöD–ðýþ~Ô¼¬áׇØÞ> µÇÍ5?0x;„/hŒª@[ü`Œý´Å5ÿ€pÚªö ÆŸ?0ˆ]ºá`=âp[öC¨»˜Ú??çSÄ??À?rÂÇýøñÀC­ßã_hßá_¨9‡¿ÇÐä{øê¾±%JÓ€9pÁ??€þ?r?0þ Ý·ø7?0úK?0ô—?0ûðÿ„!*Ïà/âñçŸñ?n<ÿPàßáß??„>þñƒû€UÀ ~À¸à`¬ñaüëO÷ôóÆ4ÿ`üó÷e–èçŸøgl⃣û‚â½ý9´¯›“cüèÇÍüýߟ9þúËŸ¹'dÆž}Ýt®_5î.¾jNÜ_ŽàWüª‰eé:‡«?0÷‘ì+Uñq㎡Þñ}øÃõà/ÌÊ3ÇŸ4îgnúÉtö¸!.ï3 xåE󃬽à»`õ÷çÙrò;ó>œÂ®9kºWà½×’$Gá4üš;<˜­®×oÀ³xs?ro VÌ`LwØØW™ŠÙÆ›¥.à¥@€óf î?ržõ@j ¬:%½'<»˜Á*MÁ?rÃ|W»ß\ÖÕ¯ç YWU |0ìò9(Ù6fQÔîÚq¥Ï§3´Óä߸pB®×ü»¢ß¼_pŠ~¥´C£ÎRw¾L–C®O=bˆ¿±ˆ¬ŒüóÖª“ò ÇBú¤lùžxÖfÑ'ÖXÊ®³äõmãºü]“°”Aßͯd¸ì—2>ZÓ=8¯ËµF?n@Ö^•>ÑñP–¬HãlyKÊÒ{»ûüÎB¬„æVCAå#ÆŽ¿{ŠîÃλ_Áä&,7zDàÐùÜT×ï®ÕHÄïÐl9h¶i6¦q ÃÕònã“§ÜHÌÁ6óQ/S½˜âåföáB÷?nSA<²×lêÈB±¼Þs%Fúx÷d©þ'ퟭÜH¯RõΣêÅ{ÍŬêÏŽz½^¢ŸŠÇ€ÊøjÄœŒÜ!•Àœ,ÀxËÒNŠ0Ô·ÖQE¸N·‰WïêïR3ÚñÞ†ZÖ ?0¿ß½ ïÊÆÜÄ~‡t¡w›½±¯£.|†?n»©=¯Šm~¼—Våbõokú¯]ÓïÒ*`ëßÔœ¾Z÷[4õI^x÷½gJçÙ6‹â½%V»åžb’“œ…k* ½wç#{dѼ?rÔØûwÄ÷\†®Wõo¼¬r<®Þ¾Ýû·/vÐÜsÞ.ÿëuümðúÞÙʽ½c"ÎAûÚ2TɪpüoË¢ýûû????yß,õ¢É+ß“ÆÇ‚Dõ}6??f_‰©”Gæë®ª_‹ÞÞÀ?r”’òom@Wûßá?n© <®º‡V†TIk4›ÝuýN­PI]ë]ZÒµðê÷.-½„r\Cn™Y9f4:+b‰B”Bªã°[¨ô~÷;Nâµ¶…´Tü€–t??ê«þSPŒïY5*²êùÜô00:jëÏj±5Äo¦$ ]³kO4Í#DV-Z&W}#A ”îÌFè;oI(…Úú°¦J”@z…àÞ§ìk~×¾B@à8¥žKï6\*ëí±Ò­ý5ÇÐÃÞUuàëÔ:NÃvõ,QM[¹æɼÙ1L'‡xä6]˱Ë@½œo›åQÞB«[_­žßªîþب ^coõN`Äß7g>ñsïÙ––õv×–ß4›?rî¢ÁUH?n±²†?n5Ñ,·Ô¶é/‚k·»››N6tj`iDx@A´á%nâ¾ßÃßþÛ±¥çß°ûí ƒÐ Ô“”‘*d€HÿŸ?0´½í–ܶñ7x+Ó}üx Zî‘"'!õ‘å7%¶¥X²e??ŠâÃéf÷0â’-i2Óçì·ý°w°—·W²õCÐļ$ç¿>V Þ …BU¡Š+çÄ ×7‡WóšõC¾í"–µ/àƒã?0 qÖgŽÇp˜óà8BùfdLÏÅŒ¿èr®®ÂϦ|ã£9X…Àt':¨½vþ€ÝõÓ×ïLªäAp`€q™x&ëÝÓ"!€5›“IëÆö/Iø'Z hæe$÷-J&aë!»’ZöÉ«]È왉ÆEgÁF×øâólš7Ofÿû퀳0„iI¥£è4^òNèåUäcÝêêóñ­DÃ'‚¾A0û4Q‡ì”øMYT«„}¼Ÿ°jW??&/°¥¹WƒØžÄTNN>ýôþÃªŠ›ª}‚˜0¹r0Y@JN¾PªÄ}Y;E&ÆM+jËÎPj®ÚEÉ¡uþô§ ²|?r§šÉé¢L’?nkjDò[OÓrºÄæ«éßä”þ®ý­ÀøøˆÂ¥áÙÅû|j<5áÂå ·ñó¿Ÿza³–mÓugyÙþýÔ"á¼È…¬¡>Œ/ñ‡ðnî5ª?r •Ø¢‹æô{Îi¬-ÔØu‘ؾèuIÇ/Y$ŸÃŸb_á“ØwCq«xq«k‹Ûã,±w=ÉuW¾Q ø<˜C¹Íj3xðq”ÿ#Ë ¼=F ¨J??x˜1qCL¨4 ¢ÕR¹°t˘1gl©VS­- ]Ì|9G_TÓÍ>CuÆsynÁ<’‰µ äç5bÏ*Q{éÁ)-E6ýû)“Ö뜲01ü??ˆÓï]há-pYp8”MMãð¼höžïh´¹lr™ç*d·/Ï‹!>BÓYÇé§»¾ojU{|eu0òvSɳ=LV}/§<ï‹WÍcX›æqiÚ¾ÓÉÐÀê4ÊqÉÚ ›£’p£ÆÈs@3¾nQ´|)??­@úäÍ¬â¹ææZ‰;²M–¹7É¥¹¤($M7pD64¹q½»Ý88Ï!¢ÝÐ1¬9Çõec­¸HÚ\5ó¦„??Ûª˜Š´%Øv @*5ÏMŠlmÎU³;Õ9Q^3”Î*婎žÖŠÔ¾ +#g??nÙ TòD1›m¸K¦Ýî¾[$9aSóXà{LŸVÄ–O3ÛH,JýŠßŽ›Ôö[o }z…w|™¿0~jècr¢}8±…Å}ýüC¹Z¸½_!ŽííçÌUgù9éCÕò¿àèKÅ.CÞýfN½4«¸•¥lôŠAi¿]D\±/<¤þg²-‰ð=b?n!x#—Í5†šZúÄÎ hÓlêu¹Ùµì6g?nô.¥É^î}L»ºìíÜ~\ÔÞ–]œ@m¨rÆàªYLqÌ5Æ?r¨‡ÙcIjÊÈÁI,ðLY¢¿êe=‡?ràËo˜ûE¹?0s?r™-"_‘}Ëä1êtá7ÛJæçõWD É{Uà|¸—AÎcÛ½õ¡µsfÄ™ãåAtpköÓOàq!’¹!ªQ³Ú¹+¦½ÀSJ%sa|1¶É0:ÔÕd9ŒáãOEE’.†m“\ñxøFöÅ‹k9VG6 ¦;õKy¨–ïд8õ²ˆ\*¥.½ ²)ýËj×j??MT˜¾tò?r“¢Åu€(%ÝðIdnÁƒÛ–ê'NÑ~²8à}vw€cmæ^—øZ*Ëcä*ò±áâ>!A¬bëêê§ÖJC˜ëÕ??¥Å[ÙѤ¯¯®ˆÐÿY©jqÝfoô½Ófuahö%QŒ?nßKõÖÆn?r§aªqWh’E=Ñ"šü´Ã1¤*©ä_g…}Ç£¯¿á+??‰Çêd¾—+ q‰½5»apbŒ"ÌNîôMd5)½Õ'Wwœ÷Õç}uÓ¼¯®™÷Pt~ ÿ"éjñD$1?0@Kç ÙAX¥H6ZÊL‹2 ëæVîTê$—1ÉÊVN,Î.’pámwY©Ž¨ÑbI¾ S–ó…pÜ®jÂXùCgÓØW¼A©’··Œ¨¿X0š¥áé”9;qNVªË™îr9Y¥†æÿ»M*¹²âàË‚íW‘®ö¶Ò\tèîÒH~à羓ûB.‡àµÕ[ª]ÈËÎ,¼“Á/Ãgµ¦'ØÁƱ~ž7>Ûã`œýÚìý´NØŠmZw eÆüæعéï(´“•ü«H©ksî»GgPÒ_xFŽ3U;žmبùŽÈÏdžþdl]˜{(|òˆ¢lI*°hƒ§Î‘Z.œ ¹²_{ÿko¾®]¨„%èVYÁ2¿oÜw.Ý#ÏÝßNIùCx‘m©ólc%ï•nÔÆ´çBý–¼—kjR¶Vj»h}E l¡6òBHó—Ðá½Eâ–n€E¡[*‚¦v÷ë2¸åàPÅ.ÝÆ´Œ¨È!´Ùƒ’ñÛ³ºo~)‹ «êT~T•üÀk«·kK>UÍjÊ!>˜gù”ŒErª>ð?n{ªÊ>ùh9 ›GÕÄ ?nç3üë;uÐ/¨Wh©Ø™«·Å•ŠÍN²C•DüUÀßZ›³Õ?0mùiðÝ‘ÙPZ £¡þÉÓ`äßÉK‡OØ[å6œ?rüeîôEЧz‡ü‰ÖIìô<éœ'îÌx¼‘¹£oÑuÅ÷‡e˜Ë3:šFù @¦ñêc™è IÎHUi¿‰üöˆÃÐpx[Ö/‡:ºfEÑZð?r'ç‚tjàmaM´˜ÑÍù!íñ––¾oΔ:ϺÛ+?nøçºîXu-ÐM@ œÌé=†±ˆPÁtnÔÉçsD§a6íAº3ƒxqG–óTÈfä5Þyˆ×˜rpỒѤªÂ\h˜´ˆaqN??é{V)ÊSÎõ^dñΞÊÕ»²º[W«s*ŸRÓwhšRNŒòé§§H™¼]À!ÓOÖGL)n“ÅÄx!‘>`uÆËÅX´õ´©A(êêÜ”@?nºÜÓüÙ­vø›åÜK^O|àÄ÷ ž§¶Ð郕)Ã]Lœ ;ûo+†gj•…%”æ&.†ºìòƒ™løÞuYщV?rÏpßbtÙÄ’cq¤ÈæÁZ?0ç8Ç?nAöÐb=æÝr]5Mët•Æzw»×Bã©T Ç™{üh¥r+I¿®0v"Z?nBR×÷…–±Øyø  nfš¤mgù½÷´}=×VŸYáû°ý/·Å2ì;ú·„ ›•Žù†ì>ýtws¯«Aõ·Õ?r•…üÕîB±F t§x€ÐòÑy™E?rrL¹–¯ÉÀ©ÍÊd8­Ý´É>Ði˜Û<ž¶(·®¦ÞüÕÆ`åÄX k1=C-ùû9¾SòKHnnZǽ×ü³‡ì´qx?n]¢ñ„M¶ `vÈ«ý:áÝïÉ6—½t*æþÞ{(³‘”½\ žuK ÕûʘJõ¡½–ì=—˜)Âg®Ê—S…´ÊHU·™éÊKLJ÷¦÷ÞÞ§«åßN‚ÎÕµš½iÑÛÀK3P7E:»<ËMq}€EVIz-aBôŒñt¿.MiøežN]“’”ÎÎXTœ~¦Þ#ŽQ:G*|áØ??’??!QsSÅ?nî ÚÖœƒÜÌN$iÍý×€§Dʹ÷!vsª?rŒ$[s°‰@G[Z™Wl0Š”=?rT¾ë´rO:ü6!Ë'‹†[8ŨmÐdnåg7hì³QaG'˜Á]é›Õš£®r!äÌê´ZòæjÚ· Á±š6…LÂJ¨„uWj6oz?nvcÀ;®¨TÓY‹l —ñÖ\@ÚC²Ñ s3 :×샻îÈ1Õ²åöf$FJùk‹ ýÆUäÉYÑ&é¢kÍ`d©.‹z•νͫMPL .Nju3FøðLhA‹B&¥?n>Š{TŸÓËz4Òºiyòõ“ïuÒ¿¦??Ïž¾¢¿Ï??(5•š×¸÷k&º—:‘c1~뻆®aœö;v¡Ú\4a¹©÷I-ç ¡°„3q, _?nN×Qö‡ØTóœ¯!õ¹7¾öàá7??X‚5æÜÀ?nî¬5h”ØÔ9»etÇdðxä>÷??è#…ùRº/šÄÇë8/ƒâÙ]sôý?0º7Ç#8”–/‚ÃR,ôšüÞDš(Å᱃£¸ø6&×ð–µCk³ðtAب©µµªxo[´ÉÅ&??ôšº£ü›œÜyr›zz’ë\dmkû?r·ìΞi%°ÿÆ?0Ù”ýÔŸæN`¹§9ù©7pÿ¶í ®žK§8ÁŸ“<¢*6åiY•ýÅT)x†iÎ OŠ•>$'•Ųvó'Âó¢Õ¸O,ò¿«)†y*ö¹uyÓ©m.é•E|Ñ íÔ&×zš“RØñýG²nk§ÀÌ2GêáËŠÒÜ2åÔò?0{+kѪGõãùJ)¸ãÅ5¦ƒ®´:—C]ÿœTº[L‡hŽ*.Fßš¥ºüW–èðj“KÅ9$¹Å…Qà€?0ÆFè/¹9à MÒƒ~$;t%i%í§5#Ò¡Ô/WW»ÇÐÃ1ƒ_šÃDf{½g—³;DŽ_§ *D#7|qaí|éÐ?r@:O1æ¾s²SÓÈŸ ISw׀͢`£(‡è5?n­arà…mÜñ¨®B¨9†ð°@D¤Û©‹8àk)vŸCŸÍfkqv¬r¹:V¸é‹‚Îí¨VJø[²›MÔrðÛЬ´æFÎÿ†ÝÝ”#4@L3Û’-µxû¨‰ÖßZ ‚ò??¿Ùj,Ù8/?nÑèù·Þl8ó¹?nÛ•i«Ž­ÕOŸËsLȬF*Ô仦y§"Os1HðæñáðNËÓДìyJ}—Í|yÁŽï³IZIÉ"IM —Í&®ƒÂU#Ëå"ªÇ¹ìÔê:°Å®ô%åÇ'‚Ò??äð%ר Î|­R(ŬÔA–ÿe!ùU):qí@Bºë \”ãƒÕïbÀˆ* aÖ_7”ÔŒ3ï€ØÜY]Ä‹¢µ/‘¢Hº˜[(r^‹‹HC;WÊZìôVø ø*Ï‚ZùíîОÍf*Æz¨´m¹¸Ì•æÉ¼ÆY¾Ën/LÜ\1˜IBìƒf{œsƒ‘«@k¬6Œ˜À⽡z‘åökPÓTð"ÿ“Ýy87N¾c·}žh¯iºd9œôµ`ðM¿°Û¬¶îºeÏOz†­ï†PH&¬C &¹Æ´öúó½Ç/k‘Û“žÆ´ÉE¢9ñeÓB6•õbÚÀi%M§BFAómÙç•ÇSÞÝm‹ª"‰§65˜øÊú¯réçy4;'¹æPO|[SQÙt¤G³“â<;Úr}Ð×§ðôèdû1;â“8½?0ˆÈ×,H ƒ¦ =ª›ZÇ…D@´=šº±­IB‘•+ZçÙ‘kM[T9ĨCü:çôâ\^u‹Â~4¬šÒÚ&ˆiz8RZ¯'Í0žƒ†›|§´oJ;¢QÀ=Ê|¢Ýn4ŸDx«^æªæÃŠ³dí`¥ *U8ÕébV§žÕînÁ,w žÏ#¥Jþd0|??’é^Žìõó^S·$-æÉRæ}³k?0.«‡ïÿù ²t~ôB¿ÌS¤àA§üñÁd¹?n½¹;ÎßqUÀè†ÔŸ%«Åý”Ú!䬶¬U®ÚÈõž.¸«ç„=öÐsYñ]&Jæ'w¥)×{îpì”±ÚêÄ#ã ‡,ež ­Ô'ÀÞvcÇ&ɽµ•“ÄLÓNÏ‚¦WÚ£†m w¾³QATž?r~ <{5✦k—8ÁžuáÅ铹ûÂölœŸ½9Æ1´z]¬”û&ÏT¡ÃGâW‡é-ªm9]ûyr(7“¤~„Û¯TåZd”tÂH´õ‘È àX™r£bÅä™ »]Ó”(H…½ô”èÍd…#rgŽÈ[ÁNÿ§[}dÃÇ5ÙјoÔX2MÔF\R3´\àDÒ¨ 3 Øÿ°OápE`KÊ! I‰õÒ?rü•YÂÿÝjìidî¹.8*zøb;:'SÎþe¹Ò÷Tä¹:l,¸9ï²2Ú<ô‚õ3Ãzëœ?rzÌŒÉé/äOI!hÏ®‹–Èd)V~nÄy‚ót7»÷ð³_½j„|@Tza÷Ʀ.ÕjFA‹l§ñÑš:–ì{ȃ<~À°³|îÈ’F}‚¥WA“x¨qG£”éõ„(?rí#5OÛÇ*7UD³pCif¨=V??«íuÈFÏÉ×ypV5MíÝdÔ>Ñ›&d§ê mÿ\ªf+@N2pÀ6kœUQÏ7ì2…“`ìÕtHt`›6_tL+„uã4•$˹LO¯ý<`²¿ª×(^ã²"ïM‹aÌ~¬“pÛÙ2Q6«ýKó¼e"„±÷46Ü¿©6KÄtRoª*ßvÅB+eS£e”¥CèC;C¾“wšCD|ù..“¢½{G`ÉTRŽ¿ø¦•õà}õ»¨ÑD^]>Xu¾j°é5jæ­Vs&¡ÀÆRõYVªÒ“«sˆ–‘ÖƒMp~FˆÜ[,3#¯%ªìõ-lnÄ\@W6™¥¯u¥mгشBïøSªütFQÅØ‰LT­£” ®û€®ñÒm%„Þn’EŠòÅ‚Ž+T†óyã -d?nñäByÂuPƒ‹ C†¯TÆœK?n_LY²V?nd¾Ú¡fJqŒhÙ[ü¹°Še`»‹Šž’ÂùéΚQ-:í—" Û3·á¸ñ'ÖˆxôÛòÀÇaoã€Íå'BÞÚt(/©áB4qy(UÓ¦VxJ¬€|,àÌÆªës!o‘×)5¸»››ˆÈnÏtGzÙ©Ê[)I:túC‰*?nâÊ¡rÙR8ÖIV_ÿ˜üa³×Iw­ÑsyKÔæJÈÝ`m´Ó7‰à¢ùð‚¯#ûŽòÍ]2â½yÃLÖ\K—ÔëV:ÌsR/Ñ“¹„©žx…o[Y¡e(П4‰ñoTJo/°ví+JÊÅ^ú› ì¶Š¢æÀùßç§ðT?rB´?n6œbaÌÏß?núÚ2ŸUÈ;¶t€âõ3›é°ãlJ§ŠH%??ò­è²W æÍ¦Ô&¥V6\îT¼«èѧŸ>G{PÚ%˜Þ¶Æö?0¾þÍý·’ÛdÏ.—*264SNÃí_?në²âÓ-?rÅÊ3µBÑÝ‚ÿ¼á??0Õ ÙRq“ÎÞœ¹o¶1^âý·3ïíÁÛýž¯3šV¬åVÌÝ\à£Rã‹; ÌîåMb,º†=* v»d[möŽD°¼7Æû:ÒÖ5?nÐîòŒÜag£„5Ø­žØÝblC+ óÜ 9sÍ`Èáõ°?rálÝ•œf1 c@­ââÆ†ÉˆôÑ'gQId@ãÂθ^>«Á6íÝ::ã/ëñu–`‘ƒi9ojêX×OùOÑÚÍüù°…Ë>nÓv¿UÓºáYœNì5ðÆ­1?n°WæFÝr^Ç2°gÈ(0à!“’h1—´íyà°é°Bõ[>ÀãÃh§¶FµQ>€3†9B_—ãl“ùs—)%W9âmvý,>zqËÖ`UCØíÆÉ³~ëx¸è!ãZÈѵðÉ߲™¢¥# «â¨`–ùüÐÐ6é*û°ük 7šéQ³ï´ŠL÷}*0Q:quR?0È%l “ˆ¢b?0`ø$N Âaò»5¨9ÇŠ?n"n(¼F6‹;±  8ùô¸Ëßgh ôK» Ûò$[\ÖÌpÙ±–“Š?r’­ÄŸ=?0Œ{ß–›MÑ>‡Ï\„‚J.1¨©37^„Ó??ÉPb’Zx{ƒüL2Ø‹0x"?r)uJâò:®‚¯ÞS.o唉©!ùþÈ¢ê[ëœå}«]!Až•ÔÌC—øÃ·C®]ìzmí#8hd¤>òu·(ú%cv'×ê×xŒúÕâ;ž³»2rÊUcC•j†ãÐ\‘Ê?0W?0‡¤Ò „È&IÃm·Ù ”qºliSœÁ¢¬t š¢™9R%œø¨Ä¢vuT¨£ònÔÉœáZïDÆß·ƒFÌíé ­”NýJ?nUClÄŸ…Xp˜†¢“ 'ËZ?r)Ìɧ\1‚£n3d îg%ŠÜÉ%×Z9MçÒ6m8©ÀÉžë aƒ‚°Ê­À0% ׎åñ‰i‰ÉàRÑ–‘Ü_Nj7BÎXÖ †3ݽÁñT[ßÉJÅ£ÌtôÑ7¸)Æ÷¸zo-WJ¾o—D;lLšcUÉÎZCs®­Ýßý>im íâ`ü&wÉñZˆÌ«%Ù:M£¶À7Ìaû·J¾µŠn^§kT°¸,QŸ®sØ)¨q™úF€;¾j`£>ÿGBèqr{U•Wèê´Ÿ|^ò.£‡M„)öçÿ8mcµ_—>¢²nhô‚ «yvþ(¢ÙÌÎIUx–ø_Þœ¿™iêVÄkOØáô” O|?r”«ËÞ)é<¯8;²º??e‡8~ê|žÏ‘z„Ohx´’ÄÔ™èUï/sFúÁfƒ&½–;ÙËNy¦¾µÄÏ»MÑ9¢ÙÔcåyÉ<ž3ìûIVB©¿$¶¢?rf›Pùæo7›?rÊ¡aÑDÑ/«´²_@?0øðÔïÝ‘¹ÜÒ¿BVy®óÊ÷Î2gcÚ!Ïm;³õ#âm6¾•ÜšÚv&dâøx=Ìû…Bä©îü芿°4kÖö±Nçì:ó?? º\òñÓO/¼]A[Ú?n”:õ¿úßа-z¨+p¨/½Rf“ÐÓ¿&í1vÅÿ¾Êõl&éêK¶z³râ)u1Øýº¢??k7Îá(Š>E½tÏn‰¡Æh¤ñ4;kX‚K$È>W^L–ʪ¿Èqoѳ_\ÏÞåbx¾> Š«v1DxI]¢ãɰ"§˜æÌâëh8¸‰/Ê* ¼Øù³[Ù/Ìâ¼®.ŠUÇÓîXe|ļ¾Ñ0ÃÍW„¦Ó@óMÞá.^ÜΑ¯D;áÃEW>w¢G©³×ÿ±Œ•à¯ñþÀ7Ô­³r†°—²‘žÿAS”ÿÿ-K×èýD¤INZkl#úCÓù&ûÕ¤ßïò]í «‹ 40Èö‹t0ÎJƒ/.´á…Üv¸Aç?rYÕŸ‰xøDå„ ||—µFDLÄM¶c£ˆ¡Ï|Ük*xM_^è¹t§YoBµ;¿Zç×E؆ÀÅ×)« ­-T/8¹ÕÒh¼´ß5{˜º¥‰,ªÙŸ3ñU—’˜Vˆ¬ íyT¾Ëƒê“fð}T8»9eÎÈÂýƒ:—aw²qû—^RìÜ$™‚HÃ3‡Ïbx/Œ÷lé”é}±®£?n©Ah¡âÉ‘…dP\ ÈËAÊ—^lqÓ½ØßnP‚NMM,6Ê»5(Ì[m@E 7½±qU@æÞ"«éΡö–XAvÙûvI}`ÔØ!¡Åÿe‰«ïvóz‹ÉëÿÌÆÄrž_??inkÊ(…= rM\á4€É^êûP¿¥ƒ&@0nqÚÛ\¹cIÞêÀ˜Fn˜bwúè½ÙÉ\¶¸hqœãwÖàW‰’à¼éNEi×£\î¡PQøCeá.þ”|}û·Ÿ‚ýŒ´×jn¯×ÕJ?n<4–¡7à]}¶?0ÇSÚzd}Q…@9¤Š¨,axLǸ½ ú ¹ÖÃs’½–è8x!pQVärÄ–YO1µ³[5ÌÔ²??Tˆ½W¾Ã¦ËÂ}Öš'À /lH¬ºéôæÒ@òD6f1àwÖºZ9Û•¤'>ªoÆ´ÞQå O@P.‡C)'H€ÉKd7lgãv>®JvÙ­>ùõÎ×,‰ëVÁœË½]‹Ì?n>ºË,R˜møpÍyõðTóßè|Qla÷Ü"¢ú-\#-8¼x+Á𛦆§ŸùêøþÜS@#??Æy´X$ýèèäµ6G㿘ËZDÚº½v4=Û½:D>YªpÅ‚êÜH³Üš„??…p4¯®¾¿Eøš¤‡çô»zièƒÈÍ¡)I«j¦"hg«ꉓD›?r€¹S¥ZÒõó7ß­ÿ8ŸÏðg½¶Qœ[§Â‡ —\Ã`é™7âõ›Æ÷_o5=­/ÉäûQº??0AX4vc3ýÿÏÿ{Jž/¾øB©Æ¶Á¿q„Q‰‰-€ƒ,/ Ü9apieÍí kî૨ôÛÓ ¥Ú0"ÝÎÊ1G:^í.£µÑ¢„´µÌ:y(ÿZL??ãçbšò鬤¢Å˜Ç|EÄ/3ߪ¶·ÔmeÚl_ËZ¾¨Þü|c±ÿoŽÖQˆ|}Gm@ç<‘œÖÕF¤+nÝÇ¥f\ð­[¬Dº;¢ԛÅMqy ³áÝ߉UúÁÅR‡Ói_ê»r§Êˆ7U}‘ÖüÎUnËõ¥Ͻ†n#ÈZÕ†”khpAUß¶"©­‰BC?nÜ¥\)»í-»"×£~Ùº/ÆÀô…¶*¹)ØÈ5e)ωè`¹œÊ8HàçÂy–˜;׿íïõþcÇ–Óã¤B*n5ÛÿP€£e˜À¯€D†pý{¦—iz×äaƒœKlZtSçÎÓvsš'÷>”GîçÞü¡˜"o^wls3¥Rm™¡‹ìˆ}XÌL§æÙá÷±·†de‘¢¢ŒœzV}ÑÒøTÛ³<1ßÔC‘Mµî{©¬æ­CA%3èö›eè>YoÏ÷ªI4Ý[ºü–f×±­Š¾Ç9ªý¨Œ-õ^–ã½Å!yèR»¤öTWÉnT=vö)/ÅåÓÂJF7>Y“†Š¦ºùàjÚw¥Þk«ÜläcA?0ýÐqîìÆ&ªuǶ— ¾–‰Ž™lx‹$\]˜ÁK¡š3Ïôñÿúœ”³‘þÀ Ú??tnyX§:qI¬Ù²×gor??þ÷):qŠôºÑ/ÌøÅéÁZ¦¬8L?0Íž%‘§ð­"héd#žj'ä5c-.ù¢ƒ.Wh´Èxøç²ñ1"6 n`®0*kÆSxÇÐï||_hjüPc‘ÇÇäcxÃ]_ýÖ:ÍEšÄ…Ž®??BÔÛ‹¼¸&8oÝU«SøŒñ¡ÈÍò¶+åJ!ÐP¼šL?nÔ|»VM\îvuÎI4VEè.5óŽ'Ú"I6Ô+KÙpA_Q"PÍâ0$‘ûC³lÞ÷iK+Ÿ}BÚ€*µ,t@!”u2ÕËr*§Þs¡»?rs–CŽlð\Ê7_ó±Å”ÁE>¥öxy“Щ¼OmzèëSG˜B²ç¯mjA7שüC¨;x׺Ø9eÔØpE›bØ{©~ðáÁÎÃôW–Öfü\öE›MðýCdÎ̲Ô`Ò5ãލ1æ+­jŽHì>Œm«X@#ކ+»v¾éZø¦kg3‘7Ü|L=¼PnüÕ½Ñ)¸àîlp 4ÇÀøÁ[¦§¯9ý7ß“5~ÿ¸Ö/Ö/ùŠ~¿8Y²†þ-ôï¿÷ÿ¤ÿ¬WúWýâ ýûGýK_×ÅzM¿ëõŸgúÏò-™!ä¹àŽ~ µÎä§bóõÇm‚ãæ®]ljq;·¯Bï)Wx’Ó?rý˜5ʱn U1îø —œ5Õ?nq©~ëcÞ7‚,V>õôtaˆ¦òb1²QK=—iIpi›GZ)„³Œ_Ń#šÐôGÉEÑ‹£SJ`ú‡¸EùQ»«k¢£GT°Û50tâvrÝ"w Úo‡{ÚÕ»Â<5ÃTš—/(.ÐSüÇ%º¬~¡m_Á#ý/e·Ë«ê‚Jý Ó??ÐôÂáªñÎjÑaÆdÐPÚ•¤N'«ŒqÀ¦xžÀqËï@ª "Òk-2"â-6À|yæž ÿÒå{N7OH'ÙˆÕ@¡rŸ6§ÃfÇ” i@³u¹Ùµ…3á}ð]ƒëQ~ŸÛN˜¸‡SIômLÈÇ–üª°0¦ÚÚÑs7 íÍÿ[ã|[ù/Úìó}Xi9:Oet.qáš~¤‘yÕ¼´¼Ø!R±Óüeb3˜U;­µ—À+jfñ,v¥…ºAb0,Î!=îCyøÜÙ”î‡üB+žÂry±ô¹Cž½§›ùX#ا°§IoWìHíçA RST1ûÄëGåŠUä‹pcýÃm¤¸ÕáŽ|…°ŠX·ç9Ský@â7e¥Í}ÚÃ#ö%ÍÔ»Ÿ´ˆ‡æÞÆp2ß”4??þVN¥ßUÅSï;Í/æÍqpðTø§Ê=w)_!4žKZWÀØšì"êNçg1®îás÷)‰)ÁpeA¦ûU±íϦ8›\»Ya ÛPÐCÕ›ù1_´ù†mëý‡\n6@'Æ#…ŠïΊrø;£ï{ÍãQþâ^§"ªqŒÈÝÄi¬P©åÂ5GÅd×Ö°(¡1ž!¿¢§SS¨ßác×àê”sÕ·ÓG>r,|pfÙžµy7`í>¹ÈÅŽdÚÚàL{Çìxµp'=u¹„±L»[Ò„¦…ÔÍöC¹þÄ:¡U,2Œ€?rI˜\…ã!ü¨œ¸òD—s2üè`|›(h}‘5òaX’‚¥¬¬ƒ"Áµl÷Š_¦\SV¾ëK•r¶-DF¿iT°†&™œ?r 1E{I+ﯚ}JÉÄ sÂw?n깘â,4Miõi³þéÛ„X(Ý.65q%EîŽK„‹»¬~Ô»°*µ(×|¢Æâá^ˆÃªp*§n«•*µ<>é<ç0é'¥wôø>8Î|WÔ‹>Ü{'Á<²[ ã”ñ%Ú(FG?0SIGq~/p?0?0믻³ÞU÷¦"›„<§ëz©æà9Tò¨ þ Ž|Y‹GªÍ*³Âö¬”s\ñN¼kLÝò––ÀW¤½l¶ù¿vEZÃæ‹Ÿå¶-›–d»œhßèBÚ>NĬ|¯ZòôøX–Œ°?0<×Ñ1O¸kÿxÆÝa²m·ôlh…´…˰›‘"Ö¶úzn°›YÐ;{¶â‰­˜8I%HKØ¢ŒÊŽ9·_ê)Ç€I)±]¼ø-BþV$vQJPˆ9SnÍÐ^Ç Ý6ð×t üìÄ8]ï#^Ö{ã??¿µñ&„çÐý1ìHmƒ¼tY0AW~Z©Âúaß…a »3ïÌð{.iÕÚ¡¹'I•3±ªec®0ÖNc‹2¼Ë›öh‚Û2‹9UŸxáç+ÒçÖ:Ƽíh)»;‹?rU6WªÑo\`’ݰjw4P¦Å±íï­›¹ÒFƒi¤DH}ÆacAŽ_=éÓè®yÞsΤÔ9º›3À[«½C??ÔÆkrRYÕô®±f¿ÔÒô/ yä…dú•8A—øü¾ŽÅá51\¢W59âɰVà–«ò¸}üø€I¾  Ïr8Й‹ÇPá«Ü‚'ú#Ý%}ÔpßüJ…gÒŠ¾±çÌ?nkÍiÅ?rq!ÕP:îÅUæDî©móï.ÜA5ƒ¶˜·`€‚H݃ìÔX³/hdZî9©™Þ¸¹Ml ,¡ó§õ^^½ãÓh `˜ßÂÊËο%™o8äE}Èa8ç³Cˆî7ý[€{è¹1ØQ؆€EûÞ±e¨ywŽÎ5”]eìVIγê±u¬ÊYÉMê۬ʫŒ"¬1ßš$PïýíÂ?0$‹ Ø‚öqŠð–þœ–~{ï÷MÕœæ•mYg[¶S÷7ÝÛl½Ë*iô|?nV’›”r…™*âWςƄCTªÈ<9|n½™Õ–ªí ̽šEm\Ý×ÚÕýì$-@ʤŧ½äƒÚSŠöÆšBËZ[￘vzõé`ÓæôŸ#º-v’‰ LóÒši4Î ø–Zu–ÇÛdªýÙTá¢ëÆ‹ø¤FìwóXxÚ¬mô¨4Э'Æœ7^ž+æ?0…D~øse¢,ù«Oñ¸3Žö’M ž07•=.¢ +#GÑØ?n†hÜä1 ¯Œ Yñ(X9ÒâqKÕ ²9w@ÏÍŠ+Õ@Óþi¡äa˜‡AÖd€¤Çâ8iC}ÜßÙ??û¤)6#y…*öp܇A'_øþº\˜wm1Éô[˜‰)÷/æp>†çSÈ1ò$'ûfüáU³×'ªžÊM¯uD ᩹S~~¹ƒw‡E‘[ãµ_.U1„"è\ÕZª¨©µT™Júvxá¿ÜÔM[Ìô-¾n*§t¼-¦Ñ£k …ˆ»Ò„S‘¬·JZ¡ôb¨9µ„¾™¸u‘3”q’çBÄ#% …g. ·$°‹—»ã§‹,hFaôÆ|ˆk{ ÷(ºñà ӧY˜œ^SÎ#µâoæ³K ÁÍy|¦mbÎfAªØ÷ž=ðÎÇsóÒ¢vQ?0Ú•°À7¿ÒdJ…s!K5O§çåjUx×­Ln-–˜66tÉ6ûÚi;S™«K|J—ô`'uÒƒ‘&tÅî`D÷Æ"©Ô‡2id.„“¹úé÷H«\Š0à6é{‘ý‰ƒâ$H]áìÓçÂ¥[c??|(>ü¼MëÜ¥h•Œ-¤"XÏV4=¸ªðK3ÞXÖê—†D,¶w¸¢éšúµÑFŽNi¯ì¹NŸéà{ƒ„yß=—d½µÒâøÖ«YïØ›æ€™7¾íáXЦóIî¾5²Õe?n?0„NC¹p$$Îóß¹¦F˜?n¶='êNäÄÝ©üòÂËQ:§½a JÒ—¡œ>ŽìsAÔŸdÉ—ÐyÏ„1ÔŠ´ ž¾´Œ{SÝ ù«¹:Wý-påKµ¤ZÜä^ü˜«¢5ùsÃsé*¬Í¼aøKY»^y^yÑ Þ¦ZlI ´O9‰ßÞâÈèáÈ/×ã¼-]'ù!žp´ºž â"ºaËÍý+ ©Y4ªÓô"å??ª‘I®¾±Gi –C¬À§ýQΘõË“âM†laÜѨ B‚ù¼A máUT‘yc!ÞÍXjN o2r‹R9{¤âS ?rI-sÿRZÆôŠ<»7d« @ÁqŠŸ*dËgþÆ%v³ZV@‹ 66u/Wª ¸vÚåžÂúëg´£3MÐû[…_!;à¾3??8‹•žDÃ??“ùét$óZ­Zz·÷šøqœGBù±„À.ÖYrÞâgx¼MŠ °U6=eýÿD&ŒMðDõCô‘I½8>.ÓÙ¬öHßêªÖ^ÅÓ´‹ˆl“>ôiŸº0WM Ð}.xü}â6¢\D„(O\st¼6aëIÛ°@Ô+»RŒÔrc$$¤{)õØl6UñÜx?ršh.ô_¤B©+C‘ÄÒÕ$–*ß1ôØ9¬oßoáA ¿ÿ38EÇø?ný³ºŽX³06Ré5*k\Š| ¿Ð5¢wåf\΄=F®®M†-d™³›?n*Êr3÷²tÒ<#ýcåë³zݤñ뵡UÂàü•™²Â㊤>¶:fŠO ÆÜe}Ws§gAé§yËß%›’ÛLÌ5^›‡™??ŸóO_Xé±éK“¸7Ãõmƒ#[d¬(")Lpà¸cØ¿—œ‹…ïÁluÜB¤ã÷ÂÖ†5&çb(9µ.N𕊷pÅ?0!´”þ@¤ëÚ ??¾–¿é¼;ý*ˆ:lçöxÕï-w˜þ`Ї‹ÑµÕ?0Ÿ ¦„ååm›ë˜”ÇãêŠü>­Ž??1Îx^»Â‚í}Š}6Иp©ó0tVîY9£-åè)G0•¾©ݽ58eä™Ñ~†Tg ÚÊÈi??б™)‡èé„ӟ̉™²éÃæÙcóôà!묛ï×hh±W·rÊ_‡ˆÇÇ­Õ©ñt³«(¶|#!#„ÆŽ`*=›õ?rÕõ~[穵–ÏÇÐð «÷’ÄÝøïÐ4'À®8­Z’Y\±r’Ÿ¬äBnêõ—–n"É ¡ÁUfŒ}Šƒ¤aS …­7ؤ˜JšÄã{¯®®E/l›o˜îC¾ýªéB¥[æ9lvÎ{¬ý¬µ½ÂpáZ[üK"ä÷­_7XøÚ ¹q öoO#.&£©M—MíLyÐm6Öˆ°‚Áx2T—¹è…ϵTÙ³ÆÒ|t¦FЬ ˆÄÈÀ´ô??Ýâuö×L\LlÎx#R´w[ó³òÀš¼°w¼[NgbI›ãEÁÛ@[lÊŽÒ˜??v»gùcD½CÛ4:ªÀÏ¥Ña¤oˆ{—H'Í£j÷^Éßj€hù|­!l‚þÒpYVCšK¨HÒV¾§êš½ØÃt‡iò—¹š‚챯£#68‚«Ÿ#"ö~äYéLÙl‚+]cÿ"‡õ»Æað_GâE.È¿ròe.é ç_jŽƒ~ó"«"¤3æ’¡ºp'ÚeõM¶]t-à2GÕ‹‚>?nI•:ûðN]Z›òôu.,ÎÓ¯ó=àù"ôpA ñÅÜ0òö !SMà’ïªò™ßv¿Ý^-Ï~øú ˜ÏwŒ“úƒ¦$\YÄÃÝ¥¶ŸI£¶#Ý»rûªa¯ˆY„sPòÏ õaEÇx _ã¬Þ£?ryó8j*‘¨ë²½kbÙÀ@¬>»ŸÑEð‹ßiU¥—{a/Vó_I÷è­i„Î0“ãçÄ}P.] ÃÖÝ`@žà5 >«à«°lí½Áðβrp˜QÚyŸT­q²š¡+ÞjJëƒ?0mÌbu¯.ܵ¼~ L+Á·‹8]ËÎ|he‚^æYÂhå†JÝÛªÈß/uÒEÖ¬žTM]xQK‘ Wjîq>Ëßò!)¸J—9¤·Ã£Ÿšï} =ü—ÝÙ³Ú/¨Ùì¨eª”5z6ž~ÏVZB©ƒÒuv‰Î??~øUìAl™Ì̮٠vØkÆôÀ6Ï!q~3üÑo‘Ÿ«Ÿ ¤ÐÐ2ƒ??ãE«N罿o̳Umñ^ü: U§…Níúf‹?r<ßèCˆúè’ÕÓ‚cGÁœWƒoõ}ξw]íÔ×fB@çIRÔ¡ÈéÃ{_<¼wò§)%j—0—û·òÏ雨b> ÈÓngCuÓO³"Ø7ëó©ŒÚ—Â>êÒÍZD-½„%Ðäÿ#í8×ܶa¯âc–hQ–•^”[»[QÞKõJ+ù«¿Zò³ Líî& €?0—nzÝûÍ~ºÆ?0â"(=ñ(òFÐbf)þ¼‡NÁƒ?0äÏ™Ðx)ÙíºÍ£‡!'ùî²Ë8¥àÆs°ÕªÖŒF#Ûå7V‰ÒbD.­J/€€d>â=]ÙAÑ 9°ÎÜ­2«ìkÞÈš˜ï£Ï×xÙ)$ËLèEv¨¿ù8ùTóéÍmž­±Nþ´‰¸Ùè’ViEþ^˜pŸr–t &Ùyâ´\]´\ˆ©–r6L¥i§ò*¨=qå]áòe”|q˜|ý1]E Z72;¿ß9??õwʆ¶‚ÚjBoè÷N}ñ:S3N À†§7âe??S^TD½•@ºž+a×ÿI6¿y€Г #k+ÜLL6ãÝuMkgÔÔgbñq ôqK`_ìõ·v–+€ñþ9OV,—ádÇñYoù2)m:fx5½K“okÖê¡´³\ìõ£ë\Ä\̛⣣U?nÃOž‰Æ>,X^jÀ’›È‚ÖÜë¹ð·‰&.æx«'ØAaŒT·,ÞÄõº Är/ÉgœËÈ*&êQaþ9±|%ڥ˷¢]¸ôúY(¼þÿVÀ9Çv-\®*àU²Ý0<ðiFQ´süš®÷ñ;µ2u.'ê趤V¸Ö^ETÜÞ¢ÕÒ/‡}2×'‹ fØïWâå-É‹P}¥­i?rR'×-ÉÈWG/æð°ÜìÁ¨Òü|å÷/ñZóý9+²ïºà N­h¿<¬°•'¹Lhóɰã¤ï.Àª°v˜Ø@é{’AÔ‡èÎs=/^è…~•?nW/™X!{`?r+Kà¥KèŽ6Â]€Îx“У/„¥N“Anuˆ‰EÞÌ$+üL .ürŸ ýùt;Ž6e?0É+Â_Àô¥4;{AËI6Má=?n —ÕÔ!«ÙE7²É!Ú@ZXMEîycs ÎÆ´Õ hQ¨3Ò¨íh³èÆ$袱@ˤâ®òøé¹ÈÀ÷Ø1 Œv˜È–XA‡œ«ØvÅT`ÓW!˜9s7SªOLÞ%Ç/t{ÀCwûèàùÈæ®$øwÒÇÜ3àÕ3ù"Ôø'Od©ƒiᔘàƒÁ£+¨°;Têý‡Q‡=³Î2„„,0¥çóÙN±gÃŽJ®¨ß”k??rÂ.j7?n‘ÏG ÅŒ?0?nCûF…²‡ð¥ïWìÑã'OŸY¼k åô”+¯ßûnnA¦Rá`8:¥ÙYï<øÿ÷êø‹taK@4"Kîi79ˆƒ0¾ð‚??»´ôj<¼°›¿»¿cÙþù£¹¿pt·Ö%ÄÂ1q…û…G1c -cq›N\xð†iãC ÛÂæ[Ü®*ä†Ö…LwdòÂÎV°†]Ýeáå>^±°.Ü®i‘Hú°„y ¹á¨áU!'úI’–%ÄBÛnn*d¸EÕmíâ çÆ¾-äº=Wlá-B!˪Bb˜¥`uWzháR!?r‡ëîº$Ò4´#ÄÜ“·†þУBzxýGý4©D¾fe(+CM˜…¼°_dy\|nÖ!d‡ßÄåöƆÞkxð´ð5!æÎ‘%2Gx‡ÚÁë›B>4÷vuŸªD¾fe(þȪIHÆÁ!fÅ.uù/¬bÍmz…0ôŒ/´OHwG/BÎC mYxÜù=´°âIBnxš»êÂpZhŠÉÝÏïÙn~hîK*d†Yꇅ¡%Ì8pávM“ŒUÈÂ#Ä´p£…Ÿ†y ¹ha +#ÝxÑØÜê¿ÿýóçÕ¿=WUÈçÚ´ðêoäÞf úÅî§v÷& søª?na·ÐTHs-O!Î)äü®P8iéBn¸7|l8ÜHQ!9ìkxª‰7¼¯n*!7\ów•8C“Dnw_IB27„¼ßQ“£º?n)¬iÅÚ×Zh•ú–ßÞŸ¹gCn:ºDþ'îJøÛÖqüW©õ.1fwn9Š6Óc¯fÞþ¶y{ÅnÇŽÄ›ÄÎÊnÓnäùìƒ??Aˆ ¥\s¾#¦HðA@ð׿îöƒ“:áàëSü@Ο’Ï- ´ÿ·6ð1ØÃ诩)g3[]MÙœ06á lM¯ù—Y Gü2¢Ë—ïQˆ¯S2£Š4´)ÿ"÷’oV‰[_øš(NVƒ‹‰ q­ î âœäòà9űvw5:÷nß:Åï(F"c4ûîþyŽŽ­a^ˆf½·‚¬±®Û??„PãtUŽfÇ‚¯á>F1¿›ÞâÜÌ}…lÎ})Ž´!aäÊÜ& Ï8­¯EŒˆ?ršQO ‹M?rü{´Ž?0iiÒÅIe;\ª©*²¦{ïMFˆuE~Fv5*ö«ÎK2â1µ½ƒJÀ‹Û/yÊ!BÆéå‰?n‡×ÎÈÇtÖËͽßä˸¹Áæ5Ž=;Ãcú¸ÎŽ?r_Áåv¸±é_î??Ú¾ÑÑü¸^°ˆêÊ-5ãXôT¡¬µ—Û–Æ?n¡÷4šçUE×Èõ¨€¡foÑŸuB8BÔnìqžvz¹4Ä•³‹¨9û9)Öf^¬teìQ.R²F«òŸSg¨±µ‘çá^d$bŒÉÙD8Aªžµˆò¤YmHréé¡.ôäRä~þK)¢¬W¢Ò7/W€)SQR©QíÂ?nŒi80æräE¯˜D é«”ýܵ L“ƒÄl¶í¢èe>Üí=±vM•k´Õ•wŽiýƒßK^øCßÒx5â;‡›"Y\Á÷ÄU""¹xR\vg°o)ÒM)ó?0xŒk绣º Ž7“Rm&e-Y,›ÛJ$Ò‘Ö‚Í??ð#‰hE<*OÃéI‚æ&Öýl'ÝwCƨ,ü¡ñÌR½¦H5x®õÿûíÆhî&Ý[ß:ú¡-gt…=ï1ù‘Ö»Ò8ûGië”4A@Çx߉¬ò™Ó©Uÿ.ñ”(+¡#¡Þ\tŒ"Î+Ýn˼Ë55ÿ%­øI ¢^ÑüÅÛ©V¼¥?nÎȨñ|ûÄ©?0‚ÙvY#ô³ÝsCg2™´¦ï¿ÿ~\U~x(",؈¥|y\U7¼_Îd¿4~”¡Ô;ĬO{>îÈüÃ.5QHÖ‰² šˆVÄg.5& e'ôo~D(¦î{†–‘—¡*4­‹\sÕ2Ùq‘n®×€ÕpÄ‚~M–œ$ÐäxÕPq?r~ž6¤pÅp~ RäE>cF QšÍÃò%\OÌÆp©çG-Üà¨É?rŽ6¹ÁÑ}Ü ûzò‘t¡3î*¬uÁÂ{À`J9¶q^³óûØÆE@íò-?0—õŠ-XÝÃ\á?r¦hí)íU^‚ã¦T­4h´†Í!óU•h’"`î}T¸ºžÜK¯ØœX-`VˆXjà*ª]+Dó=ÅÇ\hnT¸Ó”g²™ÉTÍ…?nó Zµ¦¡=eÞC‘ç«`rɦªÿænÅ›ØBGÚ¦Û• Ãöh‡Ñ_Ê»‹Ñ‰ÚsLK½Ý„ö'?rGVÖ èkã4PÄ“?nñq²r0›ÑZ`ÚiÓÀTU[ìew_ųšÁyLæ3%SV¾™µìödb]¿í.PtYm„ÛÛÂÑË“p[" %ç~uµXN'›ùo¼”ýñ2ä˜z%16ùT^% 8ÏÚ¨àcŒëMˆŠXžÇ h¡€¿‘ §u•&QHÉ«$|":Ïß’ z®¢½ÐJïK¬1V¨Kp„•À2€‹ aìŸE‚üÙJi:­­0cØR?0Ø^Ö×ïÒiËâm¾£5ºp'²P³Óz™,úþÙÆwÏÅ|›xAJK$q{‰Ômâ2·é—ÅâŒÞö,I7Ý?0ýîe‘Œ’,'æYVÎK¬µ‘¥Ä˜a™ZØ&ð{Úòë¾t—k÷à¬$ÔŽ$ïÑÌ¥I”:ù‡ó–pÿE­„SQ9$ 98Û”õºA{JVªuàé'??äIŒÕŽžjˆT€Ø¸|Ⳏoè>ÆÖóxB®Ø{kÛ´#õÌú¡ üu%)pî`™'&ábNóäÀ4eÐ?0~Hº””.»I¢¤'U`±Õý?0Ü`€Ç?0ªí¡!oæÉ`ðm/1¦KÒ›u«ÿÇvLÒ]¢eÒsu:5a]×HWâ#Cc¡ÿôVó#¥‹|Õ8Ë¢D‚r"(eù¡áhü"÷‰Ü¦³Ü?r§ð2g2£ÆLô‘R€/8£Ù; M>e dˆÛIÕp‹n\‹ìu¾±ÏN—ä@|šHSoŠ›.M“îuvíÝáJizeéÖ`ø¢€v6­'†hµDzqÇ òOŸL?0ÇèÔ}=ñ­ˆŠV’&¥ƒtg«˜êE18Ahø`Ó0÷ô®õg´p(-Œ?n4wÏ*®JÆ#.O5ÙEêƒÛ‘½Yk\u<`è»ìùz¡qHÚÅ¥Ix“oE’žƒÀ·b ÕèrÝÆ°»Q/޲÷)û›dü!½X­n–Eu†¿&ìvÝê6 +¬L݃ˆtôÙ8??~~lÊ=24·ðŠRv+ô›îb'˜ôÛ[yòáà–Íø;´ML]Ð4o%Ô†ÇÎöv5(*2ÆxõúðøppRÁôpˆ??d$BΚ¤‹ïÙÎ`{Øeó’œþ§îÁ4ÑlñÞr@Öfw†®ñ¢õ5_y‰‹>g»ç'—þéNþ•aÇ–¬ï9”®üÃy_M¿¶‰óu§??T‰!#“Þº‚^/2fá¡ù±t)]žûé&í[†“},Áš‘F ŒýbÖ¢èîv«86™Q­=›¼"qıåòVUòÑ‚ÏÑÛ¼g/sb'Z?n ¢`Bä@.eû¥éßîÿ µCêLVÂæ¶Ûí³wå%6UEùíë\¹\xdtÎ壪BÒ%}KÐØCíR²  *Ù$hì[fÛÞÔ*‡Ûï^:üO¯I"÷ºª¨ï‡·Ãï¿%?0è?0%Có:À žô­3&~ëÀ·=*XdË.@WðÆU@÷äÞõ¹ôöYüKªÞb"–qÄ›öc™}AapF9Az$åW>âê"™ ¡é¤€ÑXމ {¶®=’½¼h…úN=õá tUSNÄ ¨IÂôV‘ù¾?nÅ‚]q¹e ý<8ÊšÚ4g 5½Ô–ÃhYmäÙ³ZЂ&n}Õ„‰??ÀÿüÓqú’q*çý$Ój«ðƒIw(Z‰«Áv[´Év©ò’Ë{z/¸ß²Œ·l)ªg\sœ¦(iå‹î·¼O—ñI³ÛÍ6b\Ýí?nõ=f??ÔYò¸>x&¨TŒØÍ]d)ñÞ>MÄ©nJ/gÅŒ·Ì¶ÌY´¯w8ÜVçÎtV†f…óò}jO‘KD#“&ül0ó~e‘FM©})'$xðr˜ØÆ)²û??OÛ^ò)7A4ºb’rCRíYd_›??¡þ¶‚Læ……kÞñî€ LÇX?rÒq¼U5H7£ˆ=1f w7èw×&C•^ŸïDEI¾!çÃ9d,x^3Þ4’â3Ô™37=x8­ŒÑ³è6²ÕD°Ñ{5gšd˜ñªSÃÛ??wÖĶC³õÃHˆf 2…&¿¨ç ë$Ói¼´£(mÝ>Udê´—…l̸Òß„åu£’êóC󃩣ˆVLÁuņ Ë £¶?n;’|óÈ5¶³llŬ"‹„ !i‰vJÊU3©.Þ*=P¶g¹YtR‚Ð-»ÔÆð4@‚w £Õ.,(“‹uîóàƒ?n„XE ¬}¤ÀE‰x‘¹räW¤¤Ùɯ¸^ñõ·Þ9Úoá¶”Z‰qÒzÕo¥pLc°lµŠ•Ñã×+Ÿ–•nÚI›ækÑ@Dê9[«­´:‹c‘7Š.t?r<Öt¡û2ØBŒ<æü>ö}&¬t5gY„5—)‚Ux ˜?naˆ™`p¯T/À˜``(æ (*ÿ= ¤9Øp ]nE¢gX„†Ë•O6^œR3#YøsL$;šr½¦Ì2?nwò/FlE£†£ÝpÞÄÒƒdÐxÎ;Ñ¢Üh²á †ež›-ä`P°À‹GЬ³Ù¬S„ö‘Üd²l--˜µÓ0û-??&³¦¸ÐW“¸?08?róz '$¸ù™¾™gKeò+?r‰ÃHÕ’äre1öÕŠ"Y›Q)GéRØ€¾4úþzpû({ÅKç+¾æì1²dðåg§$LJ¢4¢ù,ÙÞ¦´ibO¡3ùmISvºZfIjN†wë~$ƒä÷¿Ç5.ò\.*®…åx³6–!íAkøÇoŒ¶€þEïŽRñýÍjëö ¢íw@%SÖëYU"}¿Ä5Ð^óhúÀýO&æ'w°¥É’t— _¼½}^”ôêêQ˜Žeúf-Ófâ‚gt¸Ë Ždi{xKÁBUùwàÌ€¿owgkë«ú§ã£wQ± R)ák¼«™Xl' 'âÝ›exVãe2´äia´$¸/pX×ÒÚ0ÐRŸ|Kuš&øs9Q…þ—´5ÂAÐìÔ-,n$¬¯Íð¥SX?nfˆ/Kþ‹¢—ŸÏUûË%út»œ0?0jöcçyaØ´÷#žõz·¸–¯FËiìVø‘©f#æÒˆ™nÄÜÙÔ̸ö¼ÖæÎBìÒ觤Ë}+Å–Tl€=)‡º”µ¸—í^ÅÁâÊ›]K£d2Êô¿ËÅüɹ?0LÙøx±¶88Lúk¢hUˆÓˆ¸M4ÅÍÅÖ°Zûçµ?r ?0¶ü?n>CõÜâ•{VêŸEÌ¿“»ÈK2©Ã$úe·g¥S¦¯ÝI·¢œ’ãÞ¡gÕqÀØ$lLç&a#¶I؈aß_&<¥OÜ„°±4÷Ÿ¬±¿47ì/Ÿ³ð’tXäüÌ<”ËìÞÙàþaLq¿%þÒEüxÿ:ìbj¸Sƒ°ÆOÁÃrÇ??a\B¨÷cuN?n kÄo@ í…•íÆ˜†pñ»¿BH¥¡?0p·áš;,£HŽ8š-ñܯ›4-Þ‚“ô¼eþ´Ñi EmÔߨ¹·Qsü}<:ÇJ‰èРÞ:v?nÂìJXâ'{r9aü4Ÿ­,\DØO9kLÙ87 NåOáÕà“YžœóOô'N(9¡¤??Aˆ:áƒoý=OõG1áWeÁŽEçÐÈΊ)k¶:‰‘à ‚"?nuŠmÄÌS¼Ö³Z€ËÃb•¬†ÜÞ¨Úß…œ^Œ `¯|uüßÿöVY‹T9KÚ…«‚¤(ß5Ôv¨Ð¤ß¶"y ‡êú™7|µIAMµüa²t‘û‚v© Äªh¸“,!#×9Bº §‚~1æSeO¾w(K}ßœ}AP5 Þ·ß&Ô.ÈÁPŸŠˆ€IF×ÛÙŒ)xD¦ŸÅøøðý>p¤)!÷…òÐm#=ŠèŠ CÙÅo]ƸITbM8C‘LçÆÞ’&iÌk®‚ðRåŸþ=­âßa®Í??hEõ¬øJ‚³”_ñœÌWõ%Ö˜³ÕdÈæº)‡ßQ1")3ã1•„°ðÇ':C=§[q©&¤ó¼Œ<™[Ï?rö;„"¸Ïê›H4zpÑׯ’ˆe<-aîn…¸Í„à]/2?0:¯ƒù´¾àj7úg6üäSbo´—ï ’ÁíFÔ`$‘82Š(X’ÓpOK"ÝœK7!;›Ë} CŸj |7猆`§»5Ý´¹Á+öŽÞÓYm¶é«åu*hÂ늩ÐnÏØƒAÏ\î^žSÕófs[ËÛn–'’/^BTŸÎUŸ 뎙ÔuóÎ:…N†˜×÷n,âqãtŽâ݃?0EòÊÑìqýì_wO—;jŸks׬.øH°»à F\äKQb|yÞÜ©Ò:Úïº]¡ŸZ}bêóvê <Œ|ç9#°¥DÉf¾Ù«ótn¨cMÀ¤ à V©=R8޽Ô[€{bšU˜ˆ_L6?rÀÝfc}Ÿ+.óM•†jûZ;ʉi#³åN“!¢’Öð°°R6¼!AÕ™ I¼¬Q 7.VY¾)‘K‘º!õS³a°ç›CÓ‹ÂåÎ&ûÕÄw#'.~Æ{9ï^Þs¦9K^ePYÐ ¡lÝÀÏ{mË!Qx æQ–ÆC^UaHL˜ÊÂ$.L6cö.«Ñ?nŽ?niÅ4?rµ´Uòî¡J^úMtò9îmºœ5oèÓEUÍ-rÛDº2Jy>*æÝDø¿ž‡iÕ…WíVB¸˜l¡i1Ï&QìhQlòrÝ^R»ìZ†â²ñJq¬7Š+î䏸¦>'&a{?rRš](?0½-ÛC12bÒ`\ÓÂ?nK×Ò¡}Ä$çP½+ú(B‡ºu^_b÷e¦R¯¶Ò¶rÞŽðÆBUõ ªÇl"gØ8C’…éî~0”ö ÷¼&‹=ïBqNpöSxr…ãºÙ`fz»;x.3eönAÎkNzü?nx¿d£ ;É‚®ãÓôN\<»ú}ÂöÊ1Ƹœ})¨¶b\„ŠùøÑäEt9üXúˆ¶·Q³:´µY—?nÁ'î3hίo°6:??-4.™žÐUy¼Ô½q’íîèгÜfÃîÁ·»¢6r1??›òïù,ƒ`ƒµQ^¯žN¿eVÅ+ßÔÏðüˆ&´|Ý–¥|¯ç‚mÕ= ê VJÕ߬D9¸ˆ‡ùý-]ô+Шø k";Žöç}R¤»‘ËÉõ«l¿(™Hc*¯U´hY‘vm­Ö¶((í~“¶´'ÚªÒÅé$ÑÛ¹Beˆ ~3Ï•«??M´~Ÿtýg÷8éÍ©p]dV»¡Çë‡ó º(3ÉEs嫨áiÎÔ­”dbrú˫уúìlj\F3ÝQ?0õ3?n̯õ¦ÉŒëÄó‘¦~ÊzZ¿IÄÉðöy</›™ñ¢¥ø¼^=yX–£¯¡ôϬu{F®™¾ù>,N¨6`…Ÿ-=v¨!áãß?r9ïþÈÜU-¶ÄÀQ¹=ƒÂÌœ¼”Íl‡ñç+éd/^ôÎèPË4Ô×þ¼Šy&e±m*ŒmÁðMõœ°nQÍú:)çµöC‰ßFR¿Ó9 ©eL÷ˤ0áIT#-S&Dº¥R›ÏâÛòh[°Ëu_Ëp÷ìÀ*¸¶%3Ú˜ªS6ØøN½ÝkÜ ;"w[¨ê¿LRqà‹ Éü;×ÙÂëŒì§Z›ÞXÂwSâ^Ìô(WèÖCrµJŽŠ…qîyL‡[çÖŠÜôóbŽV×e çdÅ…~üÎýZþòm%­°¸ñ[¦”ºÎÃQØ»ÏQ¾/Qñ6ÇS¼x¬ ïÌ5ÄVŽå¼pÃ…×EÌrÜžìô¼«fí=>)trK^+eòß–bW펅n+ÚÛ›Ÿ!ùrV(Ä+?r_83óîmã… LॠLâ‘ Lá¡ L㦠ÌàÌ¥\;»±yæKä:•Ï»ÛÚ+ëq4Ï|ÈxNRô¯Þ½ïpó_ü£Ð6%§ INØ"±BÇA°Ñ*ÆëàºIÀ=0Îà?0ú»LÈ]èë8¸à\8îg1NÞß6¿Ñ²Öoã7aq7iq“7eqS7mqÓ7cq3`¼ O¾Vü„ ù1QÈOÆGsØ!„1Syæ¼øiŽÛ?0/¦?ns?n¡ßa{B×Ãöø8ðð›¯&ñÜÌž8ú’ɨZ«HíÙ€ÿhººåXA4ø*^båDß\íƒ$©SŒC!€:3Õ¿]V¶Ê˜îOàëúèÎmVKÓ%à÷Hˆ6©ÿˆ7¡f“‚3èûOóÞÚ!ÔÿøÐ»ÉB­j.C@¯Œú|C??)=ö6¥'_6AÕ;)7[Üì<[»‹%D8û{Š‹âø9˜.¹Œ‘XŸ[Á+.ƒw&Â{þì[þþ¹¹doáÙ–[bv° ®¼=Ü\;°­UÈõ÷Ó£p‡dk?nÓ¢GÊ‹¿?nbŽàLœÍâˆsÝ ??}ïÔŒ’lÀ??ÿŠ>…K0165Ö`Ô‡bSñwb¹Ò£ðpnø\T†íÔ³aDvÓøæþÿß®·¶k.8ÚçÓ{ƒÃŽåqj>Zyþr¸\šŒkÓe¨jSP½ÒPZPJȲ†rb H3Ñ$Ô?n­ ‰Ž*„匞xn¬Ä²@%¡ïiÅbÉD¥lP‹0Ò#UÈU½_-z+ôˆiÂüS£wbê‘ QÅ‹( ‹µÞÐ/¾G¿I1c©¡O'NY«è 5Ukã%´åAú^<ôd4´7¬á#fÃ- "Œ®ìý†¦§>£e†a0`œV0¼˜ 4b`²h;Hî¶“8¾C9"/¦ËD”„Ó¢<]a_âã(/_?rB5½lMCÛôÿj mµ`80ÃMž0H0ñùwÛý9$¡ Nû®œÅ]SïÝHáQX´NÒß"EEåà"Q–4~š­*üƒÂ€ˆÌ=(‘Ÿ¡6]ÃIÑQ@€W$Ý¡SZTø|c>„3VÁ?rM 7çÛnžh¶Ÿ=AøV¢`­¾¹ßÖ/øvCÆhŒÃh¿ùЍö3Ç)Ìc©0nZb|r¼ É'+æ@;?r¢¸VÐ Xjš§#䇒Û¿º~Ãôœð­Üàƒ‚??54€W»Å¡†×‚Š»Z‘Hà­HÉ‘:®?n%“x{ƒ\¦Cï`,L@Ù'°ÖxÑy¾ʈ-ÖŠdë#üsS˜Û棑Ýæ®ùèäàЈ]ù´œ/‚m¶!³BhøZ"Ì(7l|l…_Â(Îp¯xGèœGˆ¢8lT’š¶¡E¼6”°)p[4qíR0Ç×/èšå»›]²ˆ Ћfm–b`Ì?n²e%…bv±È??J*üpvÐëߡùáð;zNÖø)–›×éÜð¥ÆŒäü;R«Å+ÔÈ{³ƒ…Wð>c~bUü¶ßË]"°£$7#×f»[uŸÑñ¯1›ýªòÀ¥ù®²³\áYeƒÌžà ©jô¸‚rb)ˆ[aC¢øÛmåS‘??Éáû¥í•Ö":‹†ù\I(º`‰ô$QÌÀãîàYržŒE "NÔÅZ—¬ûmHþïX"Ü3áT]c)¹`=²Éz9ñe“ÃêDPluGœ6¢{äÊ‚'_ƒQX—Ic}ˆ]tà5à܂ͮ|Z\¸žðNÍÛa9í5¶»+Øœ*ÌóÆ²Ÿzla^ð¸PÄSe‰gX^¶¼Êÿšûõ¶q¤ÁW‘Ñ3n"‚dËI÷ü‘‚Ö—³Ï's©½YZ¢lvÓ¤š¤’xbýO°°/°/¶O²U…"ˆ¤¬Ì±ß¤;Q(î«PG4¨7 –Ž ŽÕ=IV(µÓÒÑ8œ»¬!Mëkb@‡¡ð@!äØÂ  L¬ÜøR7.«ø?0#‚ à_•Y* r’çè.7èT.Qæ"À–·bû|˜¥s8sþê9†’óÌ@n 3ÔúÏœçQXF•kGriBN2“1=y ج°ÞS@Ô2‘Î9ºŒÌ£´’¶ +‘ÎDÚF£ ª :6LúðL‚¢IQ4¢Yq6.@g»$V|-b;Di”‚*,Tà³ù¶óAíQIÑŸeº0..­åÈ܃£Ë«-1EoM +#*âø~õÉ´fÑÕš9»Œ«Ú¬tꚪéÆìå\ÚM¡Ö_•?n×OêZ¹³5»¡YèÐ28² C>¢ö$ïƒpü]Ƨ·£ ™¶±“‹¾1 ª¶J"°B¡X6ÁÒCÊŸü cp”^© PªÊ¸¨ùÅŠ3™™ðà…ÔˆYÆe¨)‘¢€ncó‘@Ò¤ÄY¬[Y®˜M‚Â6 ºžv^HK÷Ek”m1樜ˆþª©#K eò½áÖÒÜÛ5övݥعPÏõÖ°˜€#«çSŸ÷èÈ|ó4$††% C­^³îúñŽY¿¯B“sj–¿¢BÃQ¤~3o³DgREm«QîÏ™jࢡ檔ãß‚‚hàö ÏäÆÓñ‘Ÿþxfl»òùv «0‚,.Þ¯mxç$Óâ¢6Ñx¥R#×i7Ú‰«›Ñv×Õ®èF»ç =éFûÊA{Øöµƒö¬3¡R-îˆÉL „}¡ €çñy²^D…sÙ]»².t餞°Cý)(Õ6÷ÈÓzÚÖ麃ü‘8XÝq¢6öY!!–èþ<‰ZGØšâ!Ùè–plÁö?0œóì+ :æ/tò¤ƒÒaÀåÒBA嵇^í¿è¡K;ðò??û??Ù/ÿð­O"Êh#m*UÛ¤ËÛΧ¯}Ñr¯ÄAÒã£#Q™‹éžòí럊J˜Ù@ê(|V)ˆ??Æz«q­§1Iõ÷A,7¿ "v(¤Ê!IÇúÉl©¾û—–Š˜€·‹pl¹ü„¶`ß»·Ï[ôík€yTÐqŠë4Ŧþ3¦Žˆýˆ„ŒÐFÏŸ‰¹8hwªBQ??/wZ%Óeõpì™Þ!³?0¸/†:sìîd>ÎpC{Ó!ìÅ8Ø<`jº™X¡ê˜øeeþðç"^²u+%B¾'<ˆ¯Ü•$”ò7TãVͽ–^¯’,\D‹ï)®t¦7ÆÊ$©n-f¢Ÿ)¢IP•@ÂM´a¡„÷ºxæ0Ä”¯Ë˜!€¡Zœ¿Lßrìñ Âx‡o_•á1NñU”­Kïùfú}“iäQäî¯:êò)bÛû“ßÈÇ"™QQ±×Nsˆþý3‰r7ùD/]ñU—úëN®7?n¸’Ú??Ÿ€lš_:ÓäQWšwµØ‹IM3çvŽâŒ¸ÀƒókÃ¥>S™F»*>ûùó™ÝÓ}XÝp˜ß›{­â&êÎ(RÄj¦!$ŒO["­æ QY_¸c"ì`pïSùi¸;žL_K®¾ÅÞYwšf¶lªfímL³ê8Ž+Ž^jO¸@&ŸuÔø”ìÛyÂäW4ÇîÕZ{lõI±«“×-‘ü&¿,mö‚^´K§§^Gé"Â}äwùÍÍîxû²Ûä^s.&M?r߇É:Â9+Ù~F‰\Mç!––ÊÌš¡ù¡9®-8Bͪ-ÔÎ:ð•R,—û&¡EŒ”JÜÕëíç®^~¯ª\ØjsÌáa÷°È¶‡hk>î?nùÙ‹Úá!®Éê Öx¶GjšÇÂxÉn “Îë|+Õ$<$6©Y?rã‚~ƒ<é ”µc¹Âe™¿BtÐy3s‚iJ??´âîGÆó–ÀØ&ûœ5gd{¯9´T;7‹xïudÇŽ‘í³†»Ëwн~Ç~Lwixý¾} Éx 1>O2g?rq-Rýf™0xÅø^H™«ƒ޼*„ŸÑ*h‘ÐfÂ2|@/¾HƒDµ]V¥ò±?nÕ¼»BÛaýtæ~×y§Þ˜Þ½ZÀR•Èÿõ„Kæ×3 r.@Î 7î±Y??ä»kâ¼¹à˜aÚ–Ë䤵À¬Ÿ÷CIyõu%(£°Ó?näay`Ù`\%ª?0X}Ô´½ú­éÕ}ŸZCQíÌ…¼¹U,…ìÎù $Ë a Û1$dRB$÷Š_€í¬I®k9ƒr?nÔŽ§[ â úÓý|,ÌÏ×ZƒH¡á±1V^Ý9ÿ Ç#Šf~쉷ÂGh#iÊ>±SÑP[*û¢GZ§??ëSŠšßU9©ØãŽÄíܘçmCàOÿ†?0¼ˆýó3°wÀïÀúòHåš-âgz…ÛlØ>b’¶ „Ñhã¾’G?rf´ˆØ¸fÔ?0Ye"6¦+JÌÑbXÃkœ¡øF4óI·ó¿ÿ}<›Yd?rý#Hà-ˆÎ¥oàE‡ÓÀÏ“ÚÏÕlÝ•=:ZíÛ2Xè¢o?nQgÇNÉ7 Ñ®ŠkäÁ•:„%¸ŸôÃÙݳ~Y=W®#Ý*A.sÇÇ4??‚R'4hK²K3×#µÐ¡™È‹:á/oJ§\G;¥4A›ßÓXÇNf%g6¶ë­Í)Z¯¿æ*&ç?ncJ àKa_7§QÌ'õ¦QÚ>íÓèwÞþè}áÍÍѰ ÐCš´+™Å¤K¨*™M±N¦[§CÞ6òºM»Ko¨¶ª¶¦y?r¡ð„ø•´x­õÚáò?rïÈÙÿ88ë/Üò†ƒÈíÝÀk1¿cLÌcncÏØé©áðæ~ã¯ûóêñ©)vK#`Ù:µ&tTÈG˜d;òô÷ÃP%?0óŸ+  ý©^ýñ³{5懄3Óq<áÃßÑ!tt†˜BŸÅÐGY»JÖèãIˆS#HðP’qgÛ‡lOZ„ƃ+Ââ^ØÓ@ ± 3,ÍïÞÉ`z€ÆfNÏîÔ îµB-ø'Ç,ÖNùf‹ìM'ÍKЗ£×VäÿþoŸ6ºÈS$ç 2&ý⑉±-zsÓ,ó”š} O*™™`nÂ~“Üà¶ðÈœ.÷³3zÝ~Ìq䕤Õ;‚-l`+è»wj3n¦nOlÝb)<c{mìœDá[Q¢¾N«™‹ÕÕË„‰›‚EYËÂû—ÚàÙ󰼆çEÉ£ã“{•+}Ñú%¬“ÊRþ"ûiDQý~ºù@¦xÔßPHÚ<°æû-­a™=C‡ÁHÙôÌå¯[æ4Q ¦ÜO O`ЖH/Naþ§s¼JÓÅ~Pc ºæaD]X f'Ç|”Àmö\Qƒþ\£0­µ)¦É쯔êb)Ä”ãºV«ÀVûo¾µ¥xöó by®céª:jV¶Rû£Ë@bñ ô?0úpð7x1|‡†ràçäá3ø÷îý“þÌïÿ÷FsïÙ}|`üy}ïé1î??{¨wïQàÞâ +#|ùózyŸËp¹„˜ã{€Ÿûç³øC¸<룩T“m6Ò·r|ØÔê¡2‰ñ=ô˜È£1rWALãÇÔýû_ÿ×4íkŒdä1GõD??~ПÎÉäÅ2ì-ÃÐ5s¶‚ñ öÝ’U‘P(–ï¯bÜ??a„UT—ŸNÜžÔˆ6)_€nMÉx6áèö4#ýävôýî­è€b<À1~ v®Æ ?n ’hYŠÆÑš0(0È:‘w#¬lv°Äoåa‚Ç0›Ÿé¼Uø™Íuó#ÙÙ?0Ã(?0 ì‹Ö _'6ÍUœ® ¡øâV—æÒ¸C6±« Iì;EæøØ;Q§îb?r‰lëEÎëÁ<Îç¹±Ñø-TYˆ¹üš/ˆÛ¨(âùÀ–ÏÌ|0úQ OX3OÇ?? 1J•äôO<"´…Q¯"ÿ<…˜Œ Õ„®=B&²"õ=…|bþÌcš°&}î‘öpªN½„›zÎ2]Ö{9¶¢Å¦büâ6§¸%íÂÛ³˜5ÐÎê’@¯ÊuÓ\lNl3úµ™‘Ųù<¢p[6£F£šøi;ñ‘G·èIƒèIMôM;ÑKôy´ˆ×Wmtï6èÞ­é>÷éZ¿±²V™Ï¶‚Û˜¥cð»Îí£7†0ªÊå1¢mx 䤰i??xi)®JüG xc¯¹&2M\æ7ƒeÛâ[TQç=äEmåך[k^Oý¼ÚszJ!nf坿œ“ «3zégä Ø|#°G@¿oí`ta³]p |ÛÜ^xýƒQuô?05l·&¼Ñ01?nÔÔ¾ó¨Q\½2’ØDZ|zþ&Å„] ¥ÿg¯m|œ³:§«U9A™ˆ?rïtL–â+¯Ä·Uâ7„¿inLЇ;¤ÿî¶v¶•5…†mùýj¶©÷cŽä`]„·^í8VÕêèÐüU!Œj^Y~ù<çåë ß¹6ÄF¼ŒYÓ½¥ø„Ü;¿¦_¯$îY‚Ka!u®¼J[„}³·â~æNÓß³­àÀ6|šá(?nÔ¥ªeõ>|ø0¬,<Ê0Ë/ŽÎÃ"žŠkèÿG«!5;ÍqóØûQ:Kû•_Z†mÌéŠcð».ëk/ŒjÍþmJ§®<²dàÛ’ùÅ#“Gd^côÿ §7Z§fb†ŽˆzZ“â̬8?0>Ø‚{Rr_G*ñ4¡ŸÓŸÓ›Þc:”öF½úóÄ~ÞíÝ Î€ÿôÚ?? ¥¿zø§ãsƒü}ñ0[¢dp6*­ª-@p³·Ìò^yõ „kN1Ù’"Ë0¤#ç´ŠÒÁÛSÁ®®ÇŸ.³u>'ƒE|—BáÙ½Œ,`ƒ}Æ'ed™|ÍþO{y¼9d€ddòÆâaY†óËz:,pn.òð–Ø|d«Þ‘ à«°(ñs™gW½y¯Î³0_ …BqÂ'âiZFP²'ÆT¤Wf=Ó‹½¸dDŒ…*¨1¢‘=,Hø°øb8$dà‹\ç7ý (#Ë(Ž{_¬ã‹ß å?n5R?núêùRØžEÏ^@³÷Hõ~„¿ÏAc„Ði†ªßÆïñ.ó×lÝ»ZeÏX??í…Ô8P,dž€ç–‡èl?03zs\ ŒêAˆôHM<$Àto²ì§0Çž¥Áì?nS”YÖƒƒn/à,ærøs?nÊÄñÕúŠ2ï!± Ô?r"dÓ®ø” åŠÓì?n®ØZ¢´ì}È3ü¼ŒRî ˆ±ƒÍ+ËP8æ—’ù/›â‘,?r¨rÁnÁ6€ÕhGÓ²O`¹ÂžÁ´xø!*  ƲÓHÂÌÁ4Y³Ê¬ )Ìž^FQY(ÃYI]ÎJŠÌ“Ë?r]ж<Ôg_Ê i3MÐ3³°áqžT¢BcøkÌ7 è–S§%‹š‡RÉ,•o }“¨‰'XD9zÝbóG&/-õÁ ¢7@Á¨?rý˜|迟.Á®BËgµâ<‡¶Pi½R‘+Ì©ñâCÑÑŸ`Ä4²PÑÚ `Ä<ò ŒÈNZ†Ü̸ÚÀ°Œ¯"SC#ŽTk,Ñ¢zÌÙNØópe’ü°5ÙÒàÌËùºªFá‚Çš­Ðöò2Óâqn^Z´ó?rZI™ Çh‡M;¥ÕªIsìÁí7к{|¼ú( 3òïò8L`æ€e¤­ðÍ;`º?r‡†??2µBÓ"Ø6ø?r›'Úš×^0NŽïÿaôÕI…öp>V¥öBPD?n’‡ê™OЩ¿A¤ú4?n œÚBYä?0mëSŸGEŒF[pFæä¯Âòòá9,Áèr° óªüøôõ3 Ý?nC}æE¾¼Š‹$ú?0³¼Ò7ÙwÄ×?r‘oèm, 1pxßÅ xj]1Aüý¢U·y,±¨#ÌÄ¢T8ã2“??…ɪçb;¥¤E¿5nH[Õ)|-,?néS—þ€ca s¢Dö9Ÿä¬ñôáYX¨-žð6Í™°C]Çž¯¡aš†UÓŒ®Ý…[ü«ŠFˆE™’ºâ??®TÙûÿÐ^„tn¹Hl/m!™6HVsÞÜ*þièñâ-m¦k wUa­é hŠðM¦Å2ʇt…”cŸZЊÔU7¼gFÊý«K]_`Ÿ@É:‹Ý†µ‘®ÐAí¶‡èö~Zf8߬f™_rbH:÷û2º?nDqµˆÞ¹QEÀ]?0‹w¡ËÍœÞÈùl…Ùmøãx“Dö¨èU²aÖêBû;_µ\áM8§·ÿ‘çðßœx#t÷·HîU'Š#ê´^Þ6}§se++@é³?r„ŠYäDõ„Ä^RÍjw¯%êv(íb;´¼ô§¦èM¿½ZǧÂÿî†Iw7ŒŸm³!(càTò?0¯r¬°ÛŒñï©+!“i]KúL??#%]oËè#9÷"ÄøØ%Õ~C5t{ç8taÊôPøGÐ-5ÍÊú¦:nO|Š>¿Âd›¤…-ZL§Ñ¦Õ]FoéïsË'…ÞÑäðÐ?rñY§¾ÓìŽeß8/MìtoTr¹ÿ9… fÏNòÆ~>r5:ú‘rÞ©Twqº’XÈ6>`•U®‘?r¨2ÀÈä?rké òÉÝF7hö :Ôv"w‹\?0tczó—ì£@“LdÎE‰;IÓü"NŠ–¥ƒÑð«èJ¨*.© ƒ #E-Ò,ÅMºv+X1ö8'«mµ :Ú²ÒÎ?nJ’nÄÊI=ÆgÇ$-> eÖòÆJäWQÒ©N p^'ÐVq\~·>??]Åóv¶í›ŒŽ‡øN†ç³‡0œƒH}º‚Èq©±…×·ñ:5ž=6bÐ)…¦ü;®üCÅ®-… “cNØA2¾pÝŠf]9ŽF4ê}¨ÂeD^?0ƒÏ[¤‘B2ØUd?rPPé<„Æ,Æ™ª9ˆ° ÖßD›”Ë u«ì]˜¢÷3<Ó>‹ÍúYŒgfŸ€5)ÆçWÇï!¶!6wêîw"‘.„šeÑ£t’m®FÌ´6>XRd(ކ´È[?n“ûx"?rQEÄúâžgëÂúÐõ}‘‚áû"ú?0„ÞBqgòs³ Èɉ‡N»‰±‘‹ÛõU«(Iho#Zئ`§uÄÍMFýþÚhû×…p`‡‡^pÓ¼ãw›XvØÅ©ý¶<çÔ7½??ÝKJ㦗ŠG-ls<8ÎÍr]è-²“‚˜ ÁâÌ7¥ˆŒÄ€ÕmF³Ó’Å-.`¾òv}óxl”)q ªÅ–ÿ ¨”Àu)\ûŠh´ESwsréØsáGGT???nêró`¦¥5¼`o ¢\¹zo (U:¾Ö9Éâ'ä[''µãÉcàZÀ0JIÅÉÖð 9̺z?rEîòJ7??¾«H¼+?rLª îÐçÏl e“Í :»Wqùn%àë¦cÈ(R¥¬¿ìf.›»º_¥´zf3¼ïÓ§øZ]¶?0™Ì(¿¦¥ ?0 ´ˆáüÊðtÌ…Ä9ð4ÎQÐâb˜?r#ºµ²kר%hü¡^©÷vß…„ž+ñIҾȄœAv8ŸÛÃh“h©@Fþü³4É|x?nÓð5CañlÙSüãʉ>©ÞôãÙˆŽÈlï©ÒÙ3†5sƒ•éöØ?0ËI㸖‘âS&7%æ—¼JÕk?r€…ë´êü?r3p™tA¦&W&Žå%p7Ý â€Y)Ò «užŒóÚ]î?n‡Ð›:ßÀÉ]žíÖ`Œ7Ýkh •˜÷.?n*Ø>UÀ¶n0¾¶'Á3VÍUÜÎ_§åVŽÝšá9T3XIuìØº¸pŒÓ©xBìÚ®!­Ê!4šTÛH´ %ƒó 3óÆ€ˆ>á÷¡SƒÔA?0òº¨/V!Òì^¤ÚGP1l¿¥¸»P +#]Þ¯¡ :õÀòÖX<»OpÁG??*å'âc­r÷ñs¥wFÇÇ}ñ{ÀUK§¡IUˆ+"‰:¯WîÀ­JÄŒíÏ;¥KŸ~½ º ‹¢ZÃÚŒC«ÀåÞ›¯’xL¼?rg1GŽ …Ȫ”$WD$†U‘í-#èIòLO²R­yÓ(¥ýöJ+²ƒ‹øÕáa;Է܈Ì.µc£´í˨½j²Îó-ï¢u·*/#Ý•'G;š×Gj¢ÆZí•?n&ˆW¤[úÚ-¡[±2LääÁV¿ÃézlðîÁS"SóežHÄ«—§oøžÐ²€]fä0?r/¡«>],¡!LyNÈœ+ ±0(• +2zkQ…NÔZW“å![^ŒjœIñ r=³ÁÁ$°D‹~ßuøRôÑSÖSýÄœœçzt'kylåU8yA¼Z´n>Ç*ÖƒÑĘ¡#¥>¾èÕ-’C,3|&ð¢&Q_ÇV´Ë>û´½ØµL©Þ=>$©þ’„Q¿<’½§‡®÷t~ÕÁm:šô/"neâ.èåªPÅÒ¸Ël‘ÖP§¹Î&Æ¡/PA£º[’åÌ:Öǹîë9ç?r+¨š×¡W4#x¬Í"•©õÙfãØÁݨK½ÈYÂ×%p&ú#´åzs3óz]!ºªd-LAFDÏ`§ƒTL?n[ I¾RÁÌmbN þ‘wV“äÄóþƒ%ý㥢qûàîÉôBÍ ¾{òŽyœ üM ý‚Ⱥ`P}1H·¦Àf#%K?rÒ»åE±\[|µLfõdËa²1§ÖÏi‹tµ»=K²° œBÈ£R…:ˆ¶giU¥f„KVÞ¡§0&+:¿¥}Ì?n4D˜—·J•&°¯@õ¤‰TÂð.D®%A~ïˆfÄNÃß:ì2Ž»e1Ò–í‹ #TÚ.„ÁñÚò„T^IZ‰HµëÏD4t˜’ŽINÄÛ?rBæ??P㘖6}ÉàÂh&·DÔs³Îæçs\»Ù{]Ü(ÈxHs7CQv©`‡AeDn%v&$ÿÁ’‡¶ää†N1™œ³¤ãÆuçŠÖsÖÓe ÇQ-ð;]‘j4ñ›xUX¨•èT±_B‚ƒãÏH#` ”YJî|qãFgºT¾C‹RúÖ‘¨¼„Î’ݲ?0†O;Éœ‘ërp¹Ý^ž1tµNÊx•à„R‹‘!©¼ÚnpaUƇÕE\À&zÍbÌ’„󸼆½¢á¬cCÃNnb׫‹NU¼íÀEç†yÕx´Ç nÀ?nédWr*hœ‡WAbwÚÙ¹µ+nùš9šÓÆ2MÓ.§žÉ!òÉãO?n¥;Ü„l¼U.Œ’VA,ÞàŠ'ÂBª´ñ€îËKy+js5-7 7F¥vFQí¶U76NäUOÔ¾æà¹lH›W¾YõV?r£³ž³£)õÉB°ËlꈞF9Î…¨,}‹÷UŽâ‚«Ab"lxãðn ‘¶w µ.¦Aè7¦ó–öÇ€t<(ß?0™.ùÞ¨’Œ±E·ç‘ºÊa/D9æ©qkb4F€™‡ÀÇǬ¸T±u¨›ÃçüVt—PÜcÑÏ1ëú…Š2÷óf‡V?r=ì1¾ma™x!”ûœ»7–H<¶v×µl²(6R5G\á²ÐªÜHÙFnè4¼û A³cÝrލe«®$îäaç^ ê­tKŒ«0%‘QK~n¿UËÃÏÜ/Ðü~8XÈöDÜ`—äa«2”ËÖ®éî 5—Ú†nfÙ\ú÷˺Âv3_{{ÓBnö—šåLº…g×êÒ»¾«µ¿p³(EsîmgïÛÌ‘œqé¬)µO³'Ù Date: Sun, 31 Aug 2025 02:46:20 -0400 Subject: [PATCH 079/188] Bug: Fix OSM Download Time & ETA (#1194) fix osm download time --- sunnypilot/mapd/mapd_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py index 9ebf07a7f4..1211c1ecc6 100755 --- a/sunnypilot/mapd/mapd_manager.py +++ b/sunnypilot/mapd/mapd_manager.py @@ -6,11 +6,11 @@ This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ import json -import time import platform import os import glob import shutil +from datetime import datetime from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper, config_realtime_process @@ -56,7 +56,7 @@ def cleanup_old_osm_data(files_to_remove: list[str]) -> None: def request_refresh_osm_location_data(nations: list[str], states: list[str] = None) -> None: - params.put("OsmDownloadedDate", str(time.monotonic())) + params.put("OsmDownloadedDate", str(datetime.now().timestamp())) params.put_bool("OsmDbUpdatesCheck", False) osm_download_locations = { From 43c12ae7b3106e668efd41a7c306d0e8c7f816e8 Mon Sep 17 00:00:00 2001 From: Kumar <36933347+rav4kumar@users.noreply.github.com> Date: Sun, 31 Aug 2025 01:30:20 -0700 Subject: [PATCH 080/188] =?UTF-8?q?Visual:=20=F0=9F=8C=88=20road=20(#1067)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 🌈 * Update selfdrive/ui/qt/onroad/model.cc Co-authored-by: royjr * ui: enhance rainbow mode description and add colorized text rendering --------- Co-authored-by: royjr Co-authored-by: DevTekVE --- common/params_keys.h | 1 + .../qt/offroad/settings/visuals_panel.cc | 7 ++++ selfdrive/ui/sunnypilot/qt/onroad/model.cc | 40 ++++++++++++++++++- selfdrive/ui/sunnypilot/qt/widgets/controls.h | 21 ++++++++++ 4 files changed, 68 insertions(+), 1 deletion(-) diff --git a/common/params_keys.h b/common/params_keys.h index 0b08d8c0cb..142b3422cb 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -156,6 +156,7 @@ inline static std::unordered_map keys = { {"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}}, {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index 818da9b75e..dd2f05416d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -28,6 +28,13 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { "../assets/offroad/icon_monitoring.png", false, }, + { + "RainbowMode", + tr("Enable Tesla Rainbow Mode"), + RainbowizeWords(tr("A beautiful rainbow effect on the path the model wants to take.")) + "
" + tr("It")+ " " + tr("does not") + " " + tr("affect driving in any way.") + "", + "../assets/offroad/icon_monitoring.png", + false, + }, }; // Add regular toggles first diff --git a/selfdrive/ui/sunnypilot/qt/onroad/model.cc b/selfdrive/ui/sunnypilot/qt/onroad/model.cc index af0177c344..5d92838f8a 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/model.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/model.cc @@ -48,5 +48,43 @@ void ModelRendererSP::drawPath(QPainter &painter, const cereal::ModelDataV2::Rea painter.drawPolygon(right_blindspot_vertices); } } - ModelRenderer::drawPath(painter, model, surface_rect.height()); + + bool rainbow = Params().getBool("RainbowMode"); + //float v_ego = sm["carState"].getCarState().getVEgo(); + + if (rainbow) { + // Simple time-based animation + float time_offset = std::chrono::duration_cast( + std::chrono::steady_clock::now().time_since_epoch()).count() / 1000.0f; + + // simple linear gradient from bottom to top + QLinearGradient bg(0, surface_rect.height(), 0, 0); + + // evenly spaced colors across the spectrum + // The animation shifts the entire spectrum smoothly + float animation_speed = 40.0f; // speed vroom vroom + float hue_offset = fmod(time_offset * animation_speed, 360.0f); + + // 6-8 color stops for smooth transitions more color makes it laggy + const int num_stops = 7; + for (int i = 0; i < num_stops; i++) { + float position = static_cast(i) / (num_stops - 1); + + float hue = fmod(hue_offset + position * 360.0f, 360.0f); + float saturation = 0.9f; + float lightness = 0.6f; + + // Alpha fades out towards the far end of the path + float alpha = 0.8f * (1.0f - position * 0.3f); + + QColor color = QColor::fromHslF(hue / 360.0f, saturation, lightness, alpha); + bg.setColorAt(position, color); + } + + painter.setBrush(bg); + painter.drawPolygon(track_vertices); + } else { + // Normal path rendering + ModelRenderer::drawPath(painter, model, surface_rect.height()); + } } diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index a840febfe7..03cb461385 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -750,3 +750,24 @@ public: setFixedSize(400, 100); } }; + +inline QString RainbowizeWords(const QString &text) { + const QStringList colors = { + "#FF6F61", // soft coral red + "#FFA177", // warm peach + "#FFD966", // soft golden yellow + "#88D498", // mint green + "#6EC6FF", // sky blue + "#A78BFA", // soft lavender + "#F78FB3" // rose pink + }; + + QString result; + QStringList words = text.split(' '); + + for (int i = 0; i < words.size(); ++i) { + result += QString("%2 ").arg(colors[i % colors.size()]).arg(words[i].toHtmlEscaped()); + } + + return result.trimmed(); + } From 1fc707158481402e559f6307063f09fe15ad1cc6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 09:58:22 -0400 Subject: [PATCH 081/188] off --- .../tests/test_speed_limit_controller.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 679029c69a..287f0266a9 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -66,6 +66,13 @@ class TestSpeedLimitController: assert v_cruise_slc == V_CRUISE_UNSET assert self.slc.state not in ACTIVE_STATES + def test_long_disabled(self): + for v_ego in np.linspace(0, 100, 101): + for _ in range(int(10. / DT_MDL)): + v_cruise_slc = self.slc.update(False, v_ego, 0, 50 * CV.MPH_TO_MS, 50 * CV.MPH_TO_MS, 0, Source.none, self.events_sp) + assert v_cruise_slc == V_CRUISE_UNSET + assert self.slc.state == SpeedLimitControlState.inactive + def test_speed_limit_at_initial_max_set_speed(self): v_cruise_slc = V_CRUISE_UNSET speed_limit = 50 * CV.MPH_TO_MS From db2edc944dffb6eb94740f6150d9a3a51ba7ddc0 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 10:01:02 -0400 Subject: [PATCH 082/188] no warning in this PR --- .../speed_limit_controller.py | 20 ------------------- .../tests/test_speed_limit_controller.py | 3 --- 2 files changed, 23 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index b9bd6dfb74..8d6c0ddb50 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -63,14 +63,7 @@ class SpeedLimitController: self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) - self.warning_type = self.params.get("SpeedLimitWarningType", return_default=True) - self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) - self.warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) self.engage_type = self.read_engage_type_param() - self.v_cruise_rounded = 0. - self.v_cruise_prev_rounded = 0. - self.speed_limit_offsetted_rounded = 0. - self.speed_limit_warning_offsetted_rounded = 0. self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH # Mapping functions to state transitions @@ -117,10 +110,6 @@ class SpeedLimitController: def speed_limit_offset(self) -> float: return self.get_offset(self.offset_type, self.offset_value) - @property - def speed_limit_warning_offset(self) -> float: - return self.get_offset(self.warning_offset_type, self.warning_offset_value) - @property def speed_limit(self) -> float: return self._speed_limit @@ -166,9 +155,6 @@ class SpeedLimitController: self.enabled = self.params.get_bool("SpeedLimitControl") self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) - self.warning_type = self.params.get("SpeedLimitWarningType", return_default=True) - self.warning_offset_type = OffsetType(self.params.get("SpeedLimitWarningOffsetType", return_default=True)) - self.warning_offset_value = self.params.get("SpeedLimitWarningValueOffset", return_default=True) self.is_metric = self.params.get_bool("IsMetric") self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH self.engage_type = self.read_engage_type_param() @@ -210,12 +196,6 @@ class SpeedLimitController: self.update_v_cruise_setpoint_prev() # always for Engage.auto self.op_engaged_prev = self.op_engaged - self.v_cruise_rounded = int(round(self.v_cruise_setpoint * self.speed_factor)) - self.v_cruise_prev_rounded = int(round(self.v_cruise_setpoint_prev * self.speed_factor)) - self.speed_limit_offsetted_rounded = 0 if self._speed_limit == 0 else int(round((self._speed_limit + self.speed_limit_offset) * self.speed_factor)) - self.speed_limit_warning_offsetted_rounded = 0 if self._speed_limit == 0 else \ - int(round((self._speed_limit + self.speed_limit_warning_offset) * self.speed_factor)) - def transition_state_from_inactive(self) -> None: # if it's a new session, wait for 2 seconds after long engaged before transitioning ot preActive if (self.frame - self.last_op_engaged_frame) * DT_MDL > 2. and not self._session_ended: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 287f0266a9..5e3ec887d4 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -47,9 +47,6 @@ class TestSpeedLimitController: self.params.put_bool("IsMetric", False) self.params.put("SpeedLimitOffsetType", 0) self.params.put("SpeedLimitValueOffset", 0) - self.params.put("SpeedLimitWarningType", 0) - self.params.put("SpeedLimitWarningOffsetType", 0) - self.params.put("SpeedLimitWarningValueOffset", 0) def test_disabled(self): self.params.put_bool("SpeedLimitControl", False) From 25668fcf33ee406942539071a7f5fc7739212169 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 10:02:12 -0400 Subject: [PATCH 083/188] no speed factor engage type yet --- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 8d6c0ddb50..2eebacb37e 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -63,8 +63,6 @@ class SpeedLimitController: self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) - self.engage_type = self.read_engage_type_param() - self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH # Mapping functions to state transitions self._state_transition_strategy = { @@ -156,8 +154,6 @@ class SpeedLimitController: self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) self.is_metric = self.params.get_bool("IsMetric") - self.speed_factor = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH - self.engage_type = self.read_engage_type_param() @staticmethod def read_engage_type_param() -> Engage: From 3d065a066ca81b9831c39094dda52686500fafe5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 10:11:01 -0400 Subject: [PATCH 084/188] wide open --- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 2eebacb37e..c207036bd6 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -94,11 +94,11 @@ class SpeedLimitController: @property def is_enabled(self) -> bool: - return self._state in ENABLED_STATES and self.enabled + return self._state in ENABLED_STATES @property def is_active(self) -> bool: - return self._state in ACTIVE_STATES and self.enabled + return self._state in ACTIVE_STATES @property def speed_limit_offseted(self) -> float: From 3fe2ec883f48de214f736a0dce02176d2ff026e3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 10:11:41 -0400 Subject: [PATCH 085/188] no --- .../lib/speed_limit_controller/speed_limit_controller.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index c207036bd6..219ddb135a 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -178,10 +178,6 @@ class SpeedLimitController: # Update current velocity offset (error) self.v_offset = self.speed_limit_offseted - self.v_ego - # Track the time op becomes active to prevent going to tempInactive right away after - # op enabling since controlsd will change the cruise speed every time on enabling and this will - # cause a temp inactive transition if the controller is updated before controlsd sets actual cruise - # speed. if not self.op_engaged_prev and self.op_engaged: self.last_op_engaged_frame = self.frame @@ -189,7 +185,7 @@ class SpeedLimitController: self.speed_limit_changed = self._speed_limit != self.speed_limit_prev self.v_cruise_setpoint_changed = self.v_cruise_setpoint != self.v_cruise_setpoint_prev self.speed_limit_prev = self._speed_limit - self.update_v_cruise_setpoint_prev() # always for Engage.auto + self.update_v_cruise_setpoint_prev() self.op_engaged_prev = self.op_engaged def transition_state_from_inactive(self) -> None: From e4ae2a7774c9a3ec23c719588b507aeff3375541 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 12:06:01 -0400 Subject: [PATCH 086/188] introduce disabled, no longer transitions at inactive --- cereal/custom.capnp | 13 +++++---- .../speed_limit_controller.py | 28 +++++++++---------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index f5cbd10156..a95d73ff85 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -147,12 +147,13 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { } enum SpeedLimitControlState { - inactive @0; # No speed limit set or not enabled by parameter. - tempInactive @1; # User wants to ignore speed limit until it changes. - preActive @2; - adapting @3; # Reducing speed to match new speed limit. - active @4; # Cruising at speed limit. - pending @5; # Awaiting new speed limit. + disabled @0; + inactive @1; # No speed limit set or not enabled by parameter. + tempInactive @2; # User wants to ignore speed limit until it changes. + preActive @3; + pending @4; # Awaiting new speed limit. + adapting @5; # Reducing speed to match new speed limit. + active @6; # Cruising at speed limit. } } diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 219ddb135a..03a2742d8e 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -56,9 +56,8 @@ class SpeedLimitController: self.last_valid_speed_limit_offsetted = 0. self._distance = 0. self._source = Source.none - self._state = SpeedLimitControlState.inactive - self._state_prev = SpeedLimitControlState.inactive - self._session_ended = False + self._state = SpeedLimitControlState.disabled + self._state_prev = SpeedLimitControlState.disabled self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) @@ -66,6 +65,7 @@ class SpeedLimitController: # Mapping functions to state transitions self._state_transition_strategy = { + SpeedLimitControlState.disabled: self.transition_state_from_disabled, SpeedLimitControlState.inactive: self.transition_state_from_inactive, SpeedLimitControlState.preActive: self.transition_state_from_preactive, SpeedLimitControlState.pending: self.transition_state_from_pending, @@ -75,6 +75,7 @@ class SpeedLimitController: # Solution functions mapped to respective states self.acceleration_solutions = { + SpeedLimitControlState.disabled: self.get_current_acceleration_as_target, SpeedLimitControlState.inactive: self.get_current_acceleration_as_target, SpeedLimitControlState.preActive: self.get_current_acceleration_as_target, SpeedLimitControlState.pending: self.get_current_acceleration_as_target, @@ -188,12 +189,15 @@ class SpeedLimitController: self.update_v_cruise_setpoint_prev() self.op_engaged_prev = self.op_engaged - def transition_state_from_inactive(self) -> None: - # if it's a new session, wait for 2 seconds after long engaged before transitioning ot preActive - if (self.frame - self.last_op_engaged_frame) * DT_MDL > 2. and not self._session_ended: + def transition_state_from_disabled(self) -> None: + # Wait 2 seconds after long engaged before starting fresh session + if (self.frame - self.last_op_engaged_frame) * DT_MDL > 2.: self._state = SpeedLimitControlState.preActive self.initial_max_set = False + def transition_state_from_inactive(self) -> None: + pass + def transition_state_from_preactive(self) -> None: if self.initial_max_set_confirmed(): self.initial_max_set = True @@ -204,10 +208,9 @@ class SpeedLimitController: self._state = SpeedLimitControlState.active else: self._state = SpeedLimitControlState.pending - elif (self.frame - self.last_op_engaged_frame) * DT_MDL > PRE_ACTIVE_GUARD_PERIOD and not self._session_ended: - # # If the initial max set speed isn't reached within the allocated period, permanently disable for this session + elif (self.frame - self.last_op_engaged_frame) * DT_MDL > PRE_ACTIVE_GUARD_PERIOD: + # Timeout - session ended self._state = SpeedLimitControlState.inactive - self._session_ended = True def transition_state_from_pending(self) -> None: if self._speed_limit > 0: @@ -219,25 +222,22 @@ class SpeedLimitController: def transition_state_from_adapting(self) -> None: if self.detect_manual_cruise_change(): self._state = SpeedLimitControlState.inactive - self._session_ended = True elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: self._state = SpeedLimitControlState.active def transition_state_from_active(self) -> None: if self.detect_manual_cruise_change(): self._state = SpeedLimitControlState.inactive - self._session_ended = True elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: self._state = SpeedLimitControlState.adapting def state_control(self) -> None: self._state_prev = self._state - # If op is disabled or SLC is disabled, go inactive and reset session tracker + # If op is disabled or SLC is disabled, go to disabled state (not inactive) if not self.op_engaged or not self.enabled: - self._state = SpeedLimitControlState.inactive + self._state = SpeedLimitControlState.disabled # Changed from inactive self.initial_max_set = False - self._session_ended = False return self._state_transition_strategy[self._state]() From dcf0dd5d80e038c6cb60672abe07185407954f56 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 12:12:22 -0400 Subject: [PATCH 087/188] fix tests --- .../tests/test_speed_limit_controller.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 5e3ec887d4..9d1dd07f56 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -32,7 +32,7 @@ class TestSpeedLimitController: return CI def reset_state(self): - self.slc.state = SpeedLimitControlState.inactive + self.slc.state = SpeedLimitControlState.disabled self.slc.frame = -1 def setup_method(self): @@ -54,7 +54,7 @@ class TestSpeedLimitController: for _ in range(int(10. / DT_MDL)): v_cruise_slc = self.slc.update(True, v_ego, 0, 50 * CV.MPH_TO_MS, 50 * CV.MPH_TO_MS, 0, Source.none, self.events_sp) assert v_cruise_slc == V_CRUISE_UNSET - assert self.slc.state == SpeedLimitControlState.inactive + assert self.slc.state == SpeedLimitControlState.disabled def test_no_speed_limit(self): for v_ego in np.linspace(0, 100, 101): @@ -68,7 +68,7 @@ class TestSpeedLimitController: for _ in range(int(10. / DT_MDL)): v_cruise_slc = self.slc.update(False, v_ego, 0, 50 * CV.MPH_TO_MS, 50 * CV.MPH_TO_MS, 0, Source.none, self.events_sp) assert v_cruise_slc == V_CRUISE_UNSET - assert self.slc.state == SpeedLimitControlState.inactive + assert self.slc.state == SpeedLimitControlState.disabled def test_speed_limit_at_initial_max_set_speed(self): v_cruise_slc = V_CRUISE_UNSET From 800920f9bc76c34d20a142ceae09db0941768d48 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 31 Aug 2025 23:19:53 -0400 Subject: [PATCH 088/188] no more tempinactive --- cereal/custom.capnp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index a95d73ff85..4844b700a2 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -149,11 +149,10 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { enum SpeedLimitControlState { disabled @0; inactive @1; # No speed limit set or not enabled by parameter. - tempInactive @2; # User wants to ignore speed limit until it changes. - preActive @3; - pending @4; # Awaiting new speed limit. - adapting @5; # Reducing speed to match new speed limit. - active @6; # Cruising at speed limit. + preActive @2; + pending @3; # Awaiting new speed limit. + adapting @4; # Reducing speed to match new speed limit. + active @5; # Cruising at speed limit. } } From 67fd6c80dd64d0c0168c6109c1d0a0e6dae54472 Mon Sep 17 00:00:00 2001 From: commaci-public <60409688+commaci-public@users.noreply.github.com> Date: Mon, 1 Sep 2025 09:37:18 -0700 Subject: [PATCH 089/188] [bot] Update Python packages (#36090) * Update Python packages * no more stall --------- Co-authored-by: Vehicle Researcher Co-authored-by: Adeeb Shihadeh --- cereal/log.capnp | 2 +- opendbc_repo | 2 +- panda | 2 +- selfdrive/pandad/pandad.cc | 1 - tinygrad_repo | 2 +- uv.lock | 172 +++++++++++++++++++++---------------- 6 files changed, 101 insertions(+), 80 deletions(-) diff --git a/cereal/log.capnp b/cereal/log.capnp index e756843562..b98c1f5242 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -585,7 +585,6 @@ struct PandaState @0xa7649e2575e4591e { heartbeatLost @22 :Bool; interruptLoad @25 :Float32; fanPower @28 :UInt8; - fanStallCount @34 :UInt8; spiErrorCount @33 :UInt16; @@ -714,6 +713,7 @@ struct PandaState @0xa7649e2575e4591e { usbPowerModeDEPRECATED @12 :PeripheralState.UsbPowerModeDEPRECATED; safetyParamDEPRECATED @20 :Int16; safetyParam2DEPRECATED @26 :UInt32; + fanStallCountDEPRECATED @34 :UInt8; } struct PeripheralState { diff --git a/opendbc_repo b/opendbc_repo index 0fe56bc289..ac6122e272 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 0fe56bc289d72d2f278e9c085f2ada6ecb13e688 +Subproject commit ac6122e272e0dc040d5abf3bde6fca4e034a7ef7 diff --git a/panda b/panda index 3dc2138623..819fa5854e 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 3dc21386239e3073a623156b75901aa302340d6c +Subproject commit 819fa5854e2e75da7f982f7d06be69c61793d6e1 diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index faaeb15319..8289df5491 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -152,7 +152,6 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setHarnessStatus(cereal::PandaState::HarnessStatus(health.car_harness_status_pkt)); ps.setInterruptLoad(health.interrupt_load_pkt); ps.setFanPower(health.fan_power); - ps.setFanStallCount(health.fan_stall_count); ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt)); ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); diff --git a/tinygrad_repo b/tinygrad_repo index e146418f65..965ea59b16 160000 --- a/tinygrad_repo +++ b/tinygrad_repo @@ -1 +1 @@ -Subproject commit e146418f6566301ffe8ebea093c0081409241c8d +Subproject commit 965ea59b16679793b8f48368ac24c4a0ef587e71 diff --git a/uv.lock b/uv.lock index 95a60715b5..e1678029cf 100644 --- a/uv.lock +++ b/uv.lock @@ -510,27 +510,27 @@ wheels = [ [[package]] name = "fonttools" -version = "4.59.1" +version = "4.59.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/7f/29c9c3fe4246f6ad96fee52b88d0dc3a863c7563b0afc959e36d78b965dc/fonttools-4.59.1.tar.gz", hash = "sha256:74995b402ad09822a4c8002438e54940d9f1ecda898d2bb057729d7da983e4cb", size = 3534394, upload-time = "2025-08-14T16:28:14.266Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/a5/fba25f9fbdab96e26dedcaeeba125e5f05a09043bf888e0305326e55685b/fonttools-4.59.2.tar.gz", hash = "sha256:e72c0749b06113f50bcb80332364c6be83a9582d6e3db3fe0b280f996dc2ef22", size = 3540889, upload-time = "2025-08-27T16:40:30.97Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/62/9667599561f623d4a523cc9eb4f66f3b94b6155464110fa9aebbf90bbec7/fonttools-4.59.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4909cce2e35706f3d18c54d3dcce0414ba5e0fb436a454dffec459c61653b513", size = 2778815, upload-time = "2025-08-14T16:26:28.484Z" }, - { url = "https://files.pythonhosted.org/packages/8f/78/cc25bcb2ce86033a9df243418d175e58f1956a35047c685ef553acae67d6/fonttools-4.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efbec204fa9f877641747f2d9612b2b656071390d7a7ef07a9dbf0ecf9c7195c", size = 2341631, upload-time = "2025-08-14T16:26:30.396Z" }, - { url = "https://files.pythonhosted.org/packages/a4/cc/fcbb606dd6871f457ac32f281c20bcd6cc77d9fce77b5a4e2b2afab1f500/fonttools-4.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39dfd42cc2dc647b2c5469bc7a5b234d9a49e72565b96dd14ae6f11c2c59ef15", size = 5022222, upload-time = "2025-08-14T16:26:32.447Z" }, - { url = "https://files.pythonhosted.org/packages/61/96/c0b1cf2b74d08eb616a80dbf5564351fe4686147291a25f7dce8ace51eb3/fonttools-4.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b11bc177a0d428b37890825d7d025040d591aa833f85f8d8878ed183354f47df", size = 4966512, upload-time = "2025-08-14T16:26:34.621Z" }, - { url = "https://files.pythonhosted.org/packages/a4/26/51ce2e3e0835ffc2562b1b11d1fb9dafd0aca89c9041b64a9e903790a761/fonttools-4.59.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b9b4c35b3be45e5bc774d3fc9608bbf4f9a8d371103b858c80edbeed31dd5aa", size = 5001645, upload-time = "2025-08-14T16:26:36.876Z" }, - { url = "https://files.pythonhosted.org/packages/36/11/ef0b23f4266349b6d5ccbd1a07b7adc998d5bce925792aa5d1ec33f593e3/fonttools-4.59.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:01158376b8a418a0bae9625c476cebfcfcb5e6761e9d243b219cd58341e7afbb", size = 5113777, upload-time = "2025-08-14T16:26:39.002Z" }, - { url = "https://files.pythonhosted.org/packages/d0/da/b398fe61ef433da0a0472cdb5d4399124f7581ffe1a31b6242c91477d802/fonttools-4.59.1-cp311-cp311-win32.whl", hash = "sha256:cf7c5089d37787387123f1cb8f1793a47c5e1e3d1e4e7bfbc1cc96e0f925eabe", size = 2215076, upload-time = "2025-08-14T16:26:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/94/bd/e2624d06ab94e41c7c77727b2941f1baed7edb647e63503953e6888020c9/fonttools-4.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:c866eef7a0ba320486ade6c32bfc12813d1a5db8567e6904fb56d3d40acc5116", size = 2262779, upload-time = "2025-08-14T16:26:43.483Z" }, - { url = "https://files.pythonhosted.org/packages/ac/fe/6e069cc4cb8881d164a9bd956e9df555bc62d3eb36f6282e43440200009c/fonttools-4.59.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:43ab814bbba5f02a93a152ee61a04182bb5809bd2bc3609f7822e12c53ae2c91", size = 2769172, upload-time = "2025-08-14T16:26:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/b9/98/ec4e03f748fefa0dd72d9d95235aff6fef16601267f4a2340f0e16b9330f/fonttools-4.59.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4f04c3ffbfa0baafcbc550657cf83657034eb63304d27b05cff1653b448ccff6", size = 2337281, upload-time = "2025-08-14T16:26:47.921Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b1/890360a7e3d04a30ba50b267aca2783f4c1364363797e892e78a4f036076/fonttools-4.59.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d601b153e51a5a6221f0d4ec077b6bfc6ac35bfe6c19aeaa233d8990b2b71726", size = 4909215, upload-time = "2025-08-14T16:26:49.682Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/2490599550d6c9c97a44c1e36ef4de52d6acf742359eaa385735e30c05c4/fonttools-4.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c735e385e30278c54f43a0d056736942023c9043f84ee1021eff9fd616d17693", size = 4951958, upload-time = "2025-08-14T16:26:51.616Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/bd053f6f7634234a9b9805ff8ae4f32df4f2168bee23cafd1271ba9915a9/fonttools-4.59.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1017413cdc8555dce7ee23720da490282ab7ec1cf022af90a241f33f9a49afc4", size = 4894738, upload-time = "2025-08-14T16:26:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a1/3cd12a010d288325a7cfcf298a84825f0f9c29b01dee1baba64edfe89257/fonttools-4.59.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5c6d8d773470a5107052874341ed3c487c16ecd179976d81afed89dea5cd7406", size = 5045983, upload-time = "2025-08-14T16:26:56.153Z" }, - { url = "https://files.pythonhosted.org/packages/a2/af/8a2c3f6619cc43cf87951405337cc8460d08a4e717bb05eaa94b335d11dc/fonttools-4.59.1-cp312-cp312-win32.whl", hash = "sha256:2a2d0d33307f6ad3a2086a95dd607c202ea8852fa9fb52af9b48811154d1428a", size = 2203407, upload-time = "2025-08-14T16:26:58.165Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f2/a19b874ddbd3ebcf11d7e25188ef9ac3f68b9219c62263acb34aca8cde05/fonttools-4.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b9e4fa7eaf046ed6ac470f6033d52c052481ff7a6e0a92373d14f556f298dc0", size = 2251561, upload-time = "2025-08-14T16:27:00.646Z" }, - { url = "https://files.pythonhosted.org/packages/0f/64/9d606e66d498917cd7a2ff24f558010d42d6fd4576d9dd57f0bd98333f5a/fonttools-4.59.1-py3-none-any.whl", hash = "sha256:647db657073672a8330608970a984d51573557f328030566521bc03415535042", size = 1130094, upload-time = "2025-08-14T16:28:12.048Z" }, + { url = "https://files.pythonhosted.org/packages/f8/53/742fcd750ae0bdc74de4c0ff923111199cc2f90a4ee87aaddad505b6f477/fonttools-4.59.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:511946e8d7ea5c0d6c7a53c4cb3ee48eda9ab9797cd9bf5d95829a398400354f", size = 2774961, upload-time = "2025-08-27T16:38:47.536Z" }, + { url = "https://files.pythonhosted.org/packages/57/2a/976f5f9fa3b4dd911dc58d07358467bec20e813d933bc5d3db1a955dd456/fonttools-4.59.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5e2682cf7be766d84f462ba8828d01e00c8751a8e8e7ce12d7784ccb69a30d", size = 2344690, upload-time = "2025-08-27T16:38:49.723Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/b7eefc274fcf370911e292e95565c8253b0b87c82a53919ab3c795a4f50e/fonttools-4.59.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5729e12a982dba3eeae650de48b06f3b9ddb51e9aee2fcaf195b7d09a96250e2", size = 5026910, upload-time = "2025-08-27T16:38:51.904Z" }, + { url = "https://files.pythonhosted.org/packages/69/95/864726eaa8f9d4e053d0c462e64d5830ec7c599cbdf1db9e40f25ca3972e/fonttools-4.59.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c52694eae5d652361d59ecdb5a2246bff7cff13b6367a12da8499e9df56d148d", size = 4971031, upload-time = "2025-08-27T16:38:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/24/4c/b8c4735ebdea20696277c70c79e0de615dbe477834e5a7c2569aa1db4033/fonttools-4.59.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f1bbc23ba1312bd8959896f46f667753b90216852d2a8cfa2d07e0cb234144", size = 5006112, upload-time = "2025-08-27T16:38:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/3b/23/f9ea29c292aa2fc1ea381b2e5621ac436d5e3e0a5dee24ffe5404e58eae8/fonttools-4.59.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a1bfe5378962825dabe741720885e8b9ae9745ec7ecc4a5ec1f1ce59a6062bf", size = 5117671, upload-time = "2025-08-27T16:38:58.984Z" }, + { url = "https://files.pythonhosted.org/packages/ba/07/cfea304c555bf06e86071ff2a3916bc90f7c07ec85b23bab758d4908c33d/fonttools-4.59.2-cp311-cp311-win32.whl", hash = "sha256:e937790f3c2c18a1cbc7da101550a84319eb48023a715914477d2e7faeaba570", size = 2218157, upload-time = "2025-08-27T16:39:00.75Z" }, + { url = "https://files.pythonhosted.org/packages/d7/de/35d839aa69db737a3f9f3a45000ca24721834d40118652a5775d5eca8ebb/fonttools-4.59.2-cp311-cp311-win_amd64.whl", hash = "sha256:9836394e2f4ce5f9c0a7690ee93bd90aa1adc6b054f1a57b562c5d242c903104", size = 2265846, upload-time = "2025-08-27T16:39:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3d/1f45db2df51e7bfa55492e8f23f383d372200be3a0ded4bf56a92753dd1f/fonttools-4.59.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82906d002c349cad647a7634b004825a7335f8159d0d035ae89253b4abf6f3ea", size = 2769711, upload-time = "2025-08-27T16:39:04.423Z" }, + { url = "https://files.pythonhosted.org/packages/29/df/cd236ab32a8abfd11558f296e064424258db5edefd1279ffdbcfd4fd8b76/fonttools-4.59.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a10c1bd7644dc58f8862d8ba0cf9fb7fef0af01ea184ba6ce3f50ab7dfe74d5a", size = 2340225, upload-time = "2025-08-27T16:39:06.143Z" }, + { url = "https://files.pythonhosted.org/packages/98/12/b6f9f964fe6d4b4dd4406bcbd3328821c3de1f909ffc3ffa558fe72af48c/fonttools-4.59.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:738f31f23e0339785fd67652a94bc69ea49e413dfdb14dcb8c8ff383d249464e", size = 4912766, upload-time = "2025-08-27T16:39:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/82bde2f2d2c306ef3909b927363170b83df96171f74e0ccb47ad344563cd/fonttools-4.59.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ec99f9bdfee9cdb4a9172f9e8fd578cce5feb231f598909e0aecf5418da4f25", size = 4955178, upload-time = "2025-08-27T16:39:10.094Z" }, + { url = "https://files.pythonhosted.org/packages/92/77/7de766afe2d31dda8ee46d7e479f35c7d48747e558961489a2d6e3a02bd4/fonttools-4.59.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0476ea74161322e08c7a982f83558a2b81b491509984523a1a540baf8611cc31", size = 4897898, upload-time = "2025-08-27T16:39:12.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/77/ce0e0b905d62a06415fda9f2b2e109a24a5db54a59502b769e9e297d2242/fonttools-4.59.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95922a922daa1f77cc72611747c156cfb38030ead72436a2c551d30ecef519b9", size = 5049144, upload-time = "2025-08-27T16:39:13.84Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ea/870d93aefd23fff2e07cbeebdc332527868422a433c64062c09d4d5e7fe6/fonttools-4.59.2-cp312-cp312-win32.whl", hash = "sha256:39ad9612c6a622726a6a130e8ab15794558591f999673f1ee7d2f3d30f6a3e1c", size = 2206473, upload-time = "2025-08-27T16:39:15.854Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/e44bad000c4a4bb2e9ca11491d266e857df98ab6d7428441b173f0fe2517/fonttools-4.59.2-cp312-cp312-win_amd64.whl", hash = "sha256:980fd7388e461b19a881d35013fec32c713ffea1fc37aef2f77d11f332dfd7da", size = 2254706, upload-time = "2025-08-27T16:39:17.893Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/d2f7be3c86708912c02571db0b550121caab8cd88a3c0aacb9cfa15ea66e/fonttools-4.59.2-py3-none-any.whl", hash = "sha256:8bd0f759020e87bb5d323e6283914d9bf4ae35a7307dafb2cbd1e379e720ad37", size = 1132315, upload-time = "2025-08-27T16:40:28.984Z" }, ] [[package]] @@ -855,7 +855,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.5" +version = "3.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -868,25 +868,25 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/59/c3e6453a9676ffba145309a73c462bb407f4400de7de3f2b41af70720a3c/matplotlib-3.10.6.tar.gz", hash = "sha256:ec01b645840dd1996df21ee37f208cd8ba57644779fa20464010638013d3203c", size = 34804264, upload-time = "2025-08-30T00:14:25.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, - { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, - { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, - { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, - { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, - { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/5d3665aa44c49005aaacaa68ddea6fcb27345961cd538a98bb0177934ede/matplotlib-3.10.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:905b60d1cb0ee604ce65b297b61cf8be9f4e6cfecf95a3fe1c388b5266bc8f4f", size = 8257527, upload-time = "2025-08-30T00:12:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/8c/af/30ddefe19ca67eebd70047dabf50f899eaff6f3c5e6a1a7edaecaf63f794/matplotlib-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bac38d816637343e53d7185d0c66677ff30ffb131044a81898b5792c956ba76", size = 8119583, upload-time = "2025-08-30T00:12:47.236Z" }, + { url = "https://files.pythonhosted.org/packages/d3/29/4a8650a3dcae97fa4f375d46efcb25920d67b512186f8a6788b896062a81/matplotlib-3.10.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:942a8de2b5bfff1de31d95722f702e2966b8a7e31f4e68f7cd963c7cd8861cf6", size = 8692682, upload-time = "2025-08-30T00:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/b793b9cb061cfd5d42ff0f69d1822f8d5dbc94e004618e48a97a8373179a/matplotlib-3.10.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3276c85370bc0dfca051ec65c5817d1e0f8f5ce1b7787528ec8ed2d524bbc2f", size = 9521065, upload-time = "2025-08-30T00:12:50.602Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c5/53de5629f223c1c66668d46ac2621961970d21916a4bc3862b174eb2a88f/matplotlib-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9df5851b219225731f564e4b9e7f2ac1e13c9e6481f941b5631a0f8e2d9387ce", size = 9576888, upload-time = "2025-08-30T00:12:52.92Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/0a18d6d7d2d0a2e66585032a760d13662e5250c784d53ad50434e9560991/matplotlib-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:abb5d9478625dd9c9eb51a06d39aae71eda749ae9b3138afb23eb38824026c7e", size = 8115158, upload-time = "2025-08-30T00:12:54.863Z" }, + { url = "https://files.pythonhosted.org/packages/07/b3/1a5107bb66c261e23b9338070702597a2d374e5aa7004b7adfc754fbed02/matplotlib-3.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:886f989ccfae63659183173bb3fced7fd65e9eb793c3cc21c273add368536951", size = 7992444, upload-time = "2025-08-30T00:12:57.067Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1a/7042f7430055d567cc3257ac409fcf608599ab27459457f13772c2d9778b/matplotlib-3.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ca662df6a80bd426f871105fdd69db7543e28e73a9f2afe80de7e531eb2347", size = 8272404, upload-time = "2025-08-30T00:12:59.112Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5d/1d5f33f5b43f4f9e69e6a5fe1fb9090936ae7bc8e2ff6158e7a76542633b/matplotlib-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1678bb61d897bb4ac4757b5ecfb02bfb3fddf7f808000fb81e09c510712fda75", size = 8128262, upload-time = "2025-08-30T00:13:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/67/c3/135fdbbbf84e0979712df58e5e22b4f257b3f5e52a3c4aacf1b8abec0d09/matplotlib-3.10.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:56cd2d20842f58c03d2d6e6c1f1cf5548ad6f66b91e1e48f814e4fb5abd1cb95", size = 8697008, upload-time = "2025-08-30T00:13:03.24Z" }, + { url = "https://files.pythonhosted.org/packages/9c/be/c443ea428fb2488a3ea7608714b1bd85a82738c45da21b447dc49e2f8e5d/matplotlib-3.10.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:662df55604a2f9a45435566d6e2660e41efe83cd94f4288dfbf1e6d1eae4b0bb", size = 9530166, upload-time = "2025-08-30T00:13:05.951Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/48441422b044d74034aea2a3e0d1a49023f12150ebc58f16600132b9bbaf/matplotlib-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08f141d55148cd1fc870c3387d70ca4df16dee10e909b3b038782bd4bda6ea07", size = 9593105, upload-time = "2025-08-30T00:13:08.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/c3/994ef20eb4154ab84cc08d033834555319e4af970165e6c8894050af0b3c/matplotlib-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:590f5925c2d650b5c9d813c5b3b5fc53f2929c3f8ef463e4ecfa7e052044fb2b", size = 8122784, upload-time = "2025-08-30T00:13:10.367Z" }, + { url = "https://files.pythonhosted.org/packages/57/b8/5c85d9ae0e40f04e71bedb053aada5d6bab1f9b5399a0937afb5d6b02d98/matplotlib-3.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:f44c8d264a71609c79a78d50349e724f5d5fc3684ead7c2a473665ee63d868aa", size = 7992823, upload-time = "2025-08-30T00:13:12.24Z" }, + { url = "https://files.pythonhosted.org/packages/12/bb/02c35a51484aae5f49bd29f091286e7af5f3f677a9736c58a92b3c78baeb/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f2d684c3204fa62421bbf770ddfebc6b50130f9cad65531eeba19236d73bb488", size = 8252296, upload-time = "2025-08-30T00:14:19.49Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/41701e3092005aee9a2445f5ee3904d9dbd4a7df7a45905ffef29b7ef098/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6f4a69196e663a41d12a728fab8751177215357906436804217d6d9cf0d4d6cf", size = 8116749, upload-time = "2025-08-30T00:14:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/16/53/8d8fa0ea32a8c8239e04d022f6c059ee5e1b77517769feccd50f1df43d6d/matplotlib-3.10.6-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d6ca6ef03dfd269f4ead566ec6f3fb9becf8dab146fb999022ed85ee9f6b3eb", size = 8693933, upload-time = "2025-08-30T00:14:22.942Z" }, ] [[package]] @@ -990,6 +990,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/a7/aad060393123cfb383956dca68402aff3db1e1caffd5764887ed5153f41b/ml_dtypes-0.5.3.tar.gz", hash = "sha256:95ce33057ba4d05df50b1f3cfefab22e351868a843b3b15a46c65836283670c9", size = 692316, upload-time = "2025-07-29T18:39:19.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/f1/720cb1409b5d0c05cff9040c0e9fba73fa4c67897d33babf905d5d46a070/ml_dtypes-0.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a177b882667c69422402df6ed5c3428ce07ac2c1f844d8a1314944651439458", size = 667412, upload-time = "2025-07-29T18:38:25.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d5/05861ede5d299f6599f86e6bc1291714e2116d96df003cfe23cc54bcc568/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9849ce7267444c0a717c80c6900997de4f36e2815ce34ac560a3edb2d9a64cd2", size = 4964606, upload-time = "2025-07-29T18:38:27.045Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/72992b68de367741bfab8df3b3fe7c29f982b7279d341aa5bf3e7ef737ea/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f5ae0309d9f888fd825c2e9d0241102fadaca81d888f26f845bc8c13c1e4ee", size = 4938435, upload-time = "2025-07-29T18:38:29.193Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/d27a930bca31fb07d975a2d7eaf3404f9388114463b9f15032813c98f893/ml_dtypes-0.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:58e39349d820b5702bb6f94ea0cb2dc8ec62ee81c0267d9622067d8333596a46", size = 206334, upload-time = "2025-07-29T18:38:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d8/6922499effa616012cb8dc445280f66d100a7ff39b35c864cfca019b3f89/ml_dtypes-0.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:66c2756ae6cfd7f5224e355c893cfd617fa2f747b8bbd8996152cbdebad9a184", size = 157584, upload-time = "2025-07-29T18:38:32.187Z" }, + { url = "https://files.pythonhosted.org/packages/0d/eb/bc07c88a6ab002b4635e44585d80fa0b350603f11a2097c9d1bfacc03357/ml_dtypes-0.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:156418abeeda48ea4797db6776db3c5bdab9ac7be197c1233771e0880c304057", size = 663864, upload-time = "2025-07-29T18:38:33.777Z" }, + { url = "https://files.pythonhosted.org/packages/cf/89/11af9b0f21b99e6386b6581ab40fb38d03225f9de5f55cf52097047e2826/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1db60c154989af253f6c4a34e8a540c2c9dce4d770784d426945e09908fbb177", size = 4951313, upload-time = "2025-07-29T18:38:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a9/b98b86426c24900b0c754aad006dce2863df7ce0bb2bcc2c02f9cc7e8489/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b255acada256d1fa8c35ed07b5f6d18bc21d1556f842fbc2d5718aea2cd9e55", size = 4928805, upload-time = "2025-07-29T18:38:38.29Z" }, + { url = "https://files.pythonhosted.org/packages/50/c1/85e6be4fc09c6175f36fb05a45917837f30af9a5146a5151cb3a3f0f9e09/ml_dtypes-0.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:da65e5fd3eea434ccb8984c3624bc234ddcc0d9f4c81864af611aaebcc08a50e", size = 208182, upload-time = "2025-07-29T18:38:39.72Z" }, + { url = "https://files.pythonhosted.org/packages/9e/17/cf5326d6867be057f232d0610de1458f70a8ce7b6290e4b4a277ea62b4cd/ml_dtypes-0.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:8bb9cd1ce63096567f5f42851f5843b5a0ea11511e50039a7649619abfb4ba6d", size = 161560, upload-time = "2025-07-29T18:38:41.072Z" }, +] + [[package]] name = "mouseinfo" version = "0.1.3" @@ -1164,27 +1185,28 @@ wheels = [ [[package]] name = "onnx" -version = "1.18.0" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ml-dtypes" }, { name = "numpy" }, { name = "protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/60/e56e8ec44ed34006e6d4a73c92a04d9eea6163cc12440e35045aec069175/onnx-1.18.0.tar.gz", hash = "sha256:3d8dbf9e996629131ba3aa1afd1d8239b660d1f830c6688dd7e03157cccd6b9c", size = 12563009, upload-time = "2025-05-12T22:03:09.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/bf/b0a63ee9f3759dcd177b28c6f2cb22f2aecc6d9b3efecaabc298883caa5f/onnx-1.19.0.tar.gz", hash = "sha256:aa3f70b60f54a29015e41639298ace06adf1dd6b023b9b30f1bca91bb0db9473", size = 11949859, upload-time = "2025-08-27T02:34:27.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/3a/a336dac4db1eddba2bf577191e5b7d3e4c26fcee5ec518a5a5b11d13540d/onnx-1.18.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:735e06d8d0cf250dc498f54038831401063c655a8d6e5975b2527a4e7d24be3e", size = 18281831, upload-time = "2025-05-12T22:02:06.429Z" }, - { url = "https://files.pythonhosted.org/packages/02/3a/56475a111120d1e5d11939acbcbb17c92198c8e64a205cd68e00bdfd8a1f/onnx-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73160799472e1a86083f786fecdf864cf43d55325492a9b5a1cfa64d8a523ecc", size = 17424359, upload-time = "2025-05-12T22:02:09.866Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/5eb5e9ef446ed9e78c4627faf3c1bc25e0f707116dd00e9811de232a8df5/onnx-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acafb3823238bbe8f4340c7ac32fb218689442e074d797bee1c5c9a02fdae75", size = 17586006, upload-time = "2025-05-12T22:02:13.217Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4e/70943125729ce453271a6e46bb847b4a612496f64db6cbc6cb1f49f41ce1/onnx-1.18.0-cp311-cp311-win32.whl", hash = "sha256:4c8c4bbda760c654e65eaffddb1a7de71ec02e60092d33f9000521f897c99be9", size = 15734988, upload-time = "2025-05-12T22:02:16.561Z" }, - { url = "https://files.pythonhosted.org/packages/44/b0/435fd764011911e8f599e3361f0f33425b1004662c1ea33a0ad22e43db2d/onnx-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5810194f0f6be2e58c8d6dedc6119510df7a14280dd07ed5f0f0a85bd74816a", size = 15849576, upload-time = "2025-05-12T22:02:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f0/9e31f4b4626d60f1c034f71b411810bc9fafe31f4e7dd3598effd1b50e05/onnx-1.18.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa1b7483fac6cdec26922174fc4433f8f5c2f239b1133c5625063bb3b35957d0", size = 15822961, upload-time = "2025-05-12T22:02:22.735Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fe/16228aca685392a7114625b89aae98b2dc4058a47f0f467a376745efe8d0/onnx-1.18.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:521bac578448667cbb37c50bf05b53c301243ede8233029555239930996a625b", size = 18285770, upload-time = "2025-05-12T22:02:26.116Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/ba50a903a9b5e6f9be0fa50f59eb2fca4a26ee653375408fbc72c3acbf9f/onnx-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4da451bf1c5ae381f32d430004a89f0405bc57a8471b0bddb6325a5b334aa40", size = 17421291, upload-time = "2025-05-12T22:02:29.645Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/25ec2ba723ac62b99e8fed6d7b59094dadb15e38d4c007331cc9ae3dfa5f/onnx-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99afac90b4cdb1471432203c3c1f74e16549c526df27056d39f41a9a47cfb4af", size = 17584084, upload-time = "2025-05-12T22:02:32.789Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4d/2c253a36070fb43f340ff1d2c450df6a9ef50b938adcd105693fee43c4ee/onnx-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ee159b41a3ae58d9c7341cf432fc74b96aaf50bd7bb1160029f657b40dc69715", size = 15734892, upload-time = "2025-05-12T22:02:35.527Z" }, - { url = "https://files.pythonhosted.org/packages/e8/92/048ba8fafe6b2b9a268ec2fb80def7e66c0b32ab2cae74de886981f05a27/onnx-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:102c04edc76b16e9dfeda5a64c1fccd7d3d2913b1544750c01d38f1ac3c04e05", size = 15850336, upload-time = "2025-05-12T22:02:38.545Z" }, - { url = "https://files.pythonhosted.org/packages/a1/66/bbc4ffedd44165dcc407a51ea4c592802a5391ce3dc94aa5045350f64635/onnx-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:911b37d724a5d97396f3c2ef9ea25361c55cbc9aa18d75b12a52b620b67145af", size = 15823802, upload-time = "2025-05-12T22:02:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/db/5c/b959b17608cfb6ccf6359b39fe56a5b0b7d965b3d6e6a3c0add90812c36e/onnx-1.19.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:206f00c47b85b5c7af79671e3307147407991a17994c26974565aadc9e96e4e4", size = 18312580, upload-time = "2025-08-27T02:33:03.081Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ee/ac052bbbc832abe0debb784c2c57f9582444fb5f51d63c2967fd04432444/onnx-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4d7bee94abaac28988b50da675ae99ef8dd3ce16210d591fbd0b214a5930beb3", size = 18029165, upload-time = "2025-08-27T02:33:05.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/8687ba0948d46fd61b04e3952af9237883bbf8f16d716e7ed27e688d73b8/onnx-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7730b96b68c0c354bbc7857961bb4909b9aaa171360a8e3708d0a4c749aaadeb", size = 18202125, upload-time = "2025-08-27T02:33:09.325Z" }, + { url = "https://files.pythonhosted.org/packages/e2/16/6249c013e81bd689f46f96c7236d7677f1af5dd9ef22746716b48f10e506/onnx-1.19.0-cp311-cp311-win32.whl", hash = "sha256:7cb7a3ad8059d1a0dfdc5e0a98f71837d82002e441f112825403b137227c2c97", size = 16332738, upload-time = "2025-08-27T02:33:12.448Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/34a1e2166e418c6a78e5c82e66f409d9da9317832f11c647f7d4e23846a6/onnx-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:d75452a9be868bd30c3ef6aa5991df89bbfe53d0d90b2325c5e730fbd91fff85", size = 16452303, upload-time = "2025-08-27T02:33:15.176Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/639664626e5ba8027860c4d2a639ee02b37e9c322215c921e9222513c3aa/onnx-1.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:23c7959370d7b3236f821e609b0af7763cff7672a758e6c1fc877bac099e786b", size = 16425340, upload-time = "2025-08-27T02:33:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/0d/94/f56f6ca5e2f921b28c0f0476705eab56486b279f04e1d568ed64c14e7764/onnx-1.19.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:61d94e6498ca636756f8f4ee2135708434601b2892b7c09536befb19bc8ca007", size = 18322331, upload-time = "2025-08-27T02:33:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/c8/00/8cc3f3c40b54b28f96923380f57c9176872e475face726f7d7a78bd74098/onnx-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:224473354462f005bae985c72028aaa5c85ab11de1b71d55b06fdadd64a667dd", size = 18027513, upload-time = "2025-08-27T02:33:23.44Z" }, + { url = "https://files.pythonhosted.org/packages/61/90/17c4d2566fd0117a5e412688c9525f8950d467f477fbd574e6b32bc9cb8d/onnx-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae475c85c89bc4d1f16571006fd21a3e7c0e258dd2c091f6e8aafb083d1ed9b", size = 18202278, upload-time = "2025-08-27T02:33:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6e/a9383d9cf6db4ac761a129b081e9fa5d0cd89aad43cf1e3fc6285b915c7d/onnx-1.19.0-cp312-cp312-win32.whl", hash = "sha256:323f6a96383a9cdb3960396cffea0a922593d221f3929b17312781e9f9b7fb9f", size = 16333080, upload-time = "2025-08-27T02:33:28.559Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2e/3ff480a8c1fa7939662bdc973e41914add2d4a1f2b8572a3c39c2e4982e5/onnx-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:50220f3499a499b1a15e19451a678a58e22ad21b34edf2c844c6ef1d9febddc2", size = 16453927, upload-time = "2025-08-27T02:33:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/ad500945b1b5c154fe9d7b826b30816ebd629d10211ea82071b5bcc30aa4/onnx-1.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:efb768299580b786e21abe504e1652ae6189f0beed02ab087cd841cb4bb37e43", size = 16426022, upload-time = "2025-08-27T02:33:33.515Z" }, ] [[package]] @@ -4605,28 +4627,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.10" +version = "0.12.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, - { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, - { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, - { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, - { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, - { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, - { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, - { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, - { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" }, + { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" }, + { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" }, + { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" }, + { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" }, + { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" }, + { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" }, + { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" }, ] [[package]] @@ -4640,15 +4662,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.35.1" +version = "2.35.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/72/75/6223b9ffa0bf5a79ece08055469be73c18034e46ed082742a0899cc58351/sentry_sdk-2.35.1.tar.gz", hash = "sha256:241b41e059632fe1f7c54ae6e1b93af9456aebdfc297be9cf7ecfd6da5167e8e", size = 343145, upload-time = "2025-08-26T08:23:32.429Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/79/0ecb942f3f1ad26c40c27f81ff82392d85c01d26a45e3c72c2b37807e680/sentry_sdk-2.35.2.tar.gz", hash = "sha256:e9e8f3c795044beb59f2c8f4c6b9b0f9779e5e604099882df05eec525e782cc6", size = 343377, upload-time = "2025-09-01T11:00:58.633Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/62/1f/5feb6c42cc30126e9574eabc28139f8c626b483a47c537f648d133628df0/sentry_sdk-2.35.1-py2.py3-none-any.whl", hash = "sha256:13b6d6cfdae65d61fe1396a061cf9113b20f0ec1bcb257f3826b88f01bb55720", size = 363887, upload-time = "2025-08-26T08:23:30.335Z" }, + { url = "https://files.pythonhosted.org/packages/c0/91/a43308dc82a0e32d80cd0dfdcfca401ecbd0f431ab45f24e48bb97b7800d/sentry_sdk-2.35.2-py2.py3-none-any.whl", hash = "sha256:38c98e3cbb620dd3dd80a8d6e39c753d453dd41f8a9df581b0584c19a52bc926", size = 363975, upload-time = "2025-09-01T11:00:56.574Z" }, ] [[package]] From ebc117af1ddacb9e4a0a82af20cddf43423f9874 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 1 Sep 2025 22:29:00 -0400 Subject: [PATCH 090/188] clean --- .../speed_limit_controller.py | 46 ++++++++----------- 1 file changed, 18 insertions(+), 28 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 03a2742d8e..391cb8e512 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -56,7 +56,7 @@ class SpeedLimitController: self.last_valid_speed_limit_offsetted = 0. self._distance = 0. self._source = Source.none - self._state = SpeedLimitControlState.disabled + self.state = SpeedLimitControlState.disabled self._state_prev = SpeedLimitControlState.disabled self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise @@ -83,23 +83,13 @@ class SpeedLimitController: SpeedLimitControlState.active: self.get_active_state_target_acceleration, } - @property - def state(self) -> SpeedLimitControlState: - return self._state - - @state.setter - def state(self, value) -> None: - if value != self._state: - debug(f'Speed Limit Controller state: {description_for_state(value)}') - self._state = value - @property def is_enabled(self) -> bool: - return self._state in ENABLED_STATES + return self.state in ENABLED_STATES @property def is_active(self) -> bool: - return self._state in ACTIVE_STATES + return self.state in ACTIVE_STATES @property def speed_limit_offseted(self) -> float: @@ -192,7 +182,7 @@ class SpeedLimitController: def transition_state_from_disabled(self) -> None: # Wait 2 seconds after long engaged before starting fresh session if (self.frame - self.last_op_engaged_frame) * DT_MDL > 2.: - self._state = SpeedLimitControlState.preActive + self.state = SpeedLimitControlState.preActive self.initial_max_set = False def transition_state_from_inactive(self) -> None: @@ -203,44 +193,44 @@ class SpeedLimitController: self.initial_max_set = True if self._speed_limit > 0: if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self._state = SpeedLimitControlState.adapting + self.state = SpeedLimitControlState.adapting else: - self._state = SpeedLimitControlState.active + self.state = SpeedLimitControlState.active else: - self._state = SpeedLimitControlState.pending + self.state = SpeedLimitControlState.pending elif (self.frame - self.last_op_engaged_frame) * DT_MDL > PRE_ACTIVE_GUARD_PERIOD: # Timeout - session ended - self._state = SpeedLimitControlState.inactive + self.state = SpeedLimitControlState.inactive def transition_state_from_pending(self) -> None: if self._speed_limit > 0: if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self._state = SpeedLimitControlState.adapting + self.state = SpeedLimitControlState.adapting else: - self._state = SpeedLimitControlState.active + self.state = SpeedLimitControlState.active def transition_state_from_adapting(self) -> None: if self.detect_manual_cruise_change(): - self._state = SpeedLimitControlState.inactive + self.state = SpeedLimitControlState.inactive elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: - self._state = SpeedLimitControlState.active + self.state = SpeedLimitControlState.active def transition_state_from_active(self) -> None: if self.detect_manual_cruise_change(): - self._state = SpeedLimitControlState.inactive + self.state = SpeedLimitControlState.inactive elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: - self._state = SpeedLimitControlState.adapting + self.state = SpeedLimitControlState.adapting def state_control(self) -> None: - self._state_prev = self._state + self._state_prev = self.state # If op is disabled or SLC is disabled, go to disabled state (not inactive) if not self.op_engaged or not self.enabled: - self._state = SpeedLimitControlState.disabled # Changed from inactive + self.state = SpeedLimitControlState.disabled # Changed from inactive self.initial_max_set = False return - self._state_transition_strategy[self._state]() + self._state_transition_strategy[self.state]() def get_current_acceleration_as_target(self) -> float: return self.a_ego @@ -256,7 +246,7 @@ class SpeedLimitController: def update_events(self, events_sp: EventsSP) -> None: if self.is_active: - if self._state == SpeedLimitControlState.preActive: + if self.state == SpeedLimitControlState.preActive: events_sp.add(EventNameSP.speedLimitPreActive) elif self._state_prev not in ACTIVE_STATES: events_sp.add(EventNameSP.speedLimitActive) From 7824e732727768465ca53c07de11010a3dd86a14 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 1 Sep 2025 22:29:26 -0400 Subject: [PATCH 091/188] rename --- .../speed_limit_controller/speed_limit_controller.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 391cb8e512..86a3355e51 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -92,7 +92,7 @@ class SpeedLimitController: return self.state in ACTIVE_STATES @property - def speed_limit_offseted(self) -> float: + def speed_limit_final(self) -> float: return self._speed_limit + self.speed_limit_offset @property @@ -116,8 +116,8 @@ class SpeedLimitController: if self.is_active: # If we have a current valid speed limit, use it if self._speed_limit > 0: - self.last_valid_speed_limit_offsetted = self.speed_limit_offseted - return self.speed_limit_offseted + self.last_valid_speed_limit_offsetted = self.speed_limit_final + return self.speed_limit_final # If no current speed limit but we have a last valid one, use that if self.last_valid_speed_limit_offsetted > 0: @@ -156,7 +156,7 @@ class SpeedLimitController: def detect_manual_cruise_change(self) -> bool: # If cruise speed changed and it's not what SLC would set if self.v_cruise_setpoint_changed: - expected_cruise = self.speed_limit_offseted + expected_cruise = self.speed_limit_final return abs(self.v_cruise_setpoint - expected_cruise) > CRUISE_SPEED_TOLERANCE return False @@ -167,7 +167,7 @@ class SpeedLimitController: self.a_ego = a_ego # Update current velocity offset (error) - self.v_offset = self.speed_limit_offseted - self.v_ego + self.v_offset = self.speed_limit_final - self.v_ego if not self.op_engaged_prev and self.op_engaged: self.last_op_engaged_frame = self.frame @@ -237,7 +237,7 @@ class SpeedLimitController: def get_adapting_state_target_acceleration(self) -> float: if self._distance > 0: - return (self.speed_limit_offseted ** 2 - self.v_ego ** 2) / (2. * self._distance) + return (self.speed_limit_final ** 2 - self.v_ego ** 2) / (2. * self._distance) return self.v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) From 8174b2da514c3409f48ecebf99bbc1124b338f79 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 1 Sep 2025 23:49:47 -0400 Subject: [PATCH 092/188] offset default > off --- .../lib/speed_limit_controller/__init__.py | 4 -- .../lib/speed_limit_controller/common.py | 2 +- .../speed_limit_controller.py | 9 ++-- .../tests/test_speed_limit_controller.py | 47 +++++++++++++++++-- 4 files changed, 47 insertions(+), 15 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py index 9b31b07d24..8781782f67 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py @@ -6,10 +6,6 @@ DEBUG = True PARAMS_UPDATE_PERIOD = 3. # secs. Time between parameter updates. PRE_ACTIVE_GUARD_PERIOD = 5. # secs. Time to wait after activation before considering temp deactivation signal. -# Lookup table for speed limit percent offset depending on speed. -LIMIT_PERC_OFFSET_V = [0.1, 0.05, 0.038] # 55, 105, 135 km/h -LIMIT_PERC_OFFSET_BP = [13.9, 27.8, 36.1] # 50, 100, 130 km/h - # Constants for Limit controllers. LIMIT_ADAPT_ACC = -1. # m/s^2 Ideal acceleration for the adapting (braking) phase when approaching speed limits. LIMIT_MIN_ACC = -1.5 # m/s^2 Maximum deceleration allowed for limit controllers to provide. diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py index 4a460b3335..414981a80b 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py @@ -20,6 +20,6 @@ class Engage(IntEnum): class OffsetType(IntEnum): - default = 0 + off = 0 fixed = 1 percentage = 2 diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 86a3355e51..862dc71d2e 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -11,9 +11,8 @@ from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V, \ - PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, \ - CRUISE_SPEED_TOLERANCE +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, \ + SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Engage, OffsetType from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.helpers import description_for_state, debug @@ -127,8 +126,8 @@ class SpeedLimitController: return V_CRUISE_UNSET def get_offset(self, offset_type: OffsetType, offset_value: int) -> float: - if offset_type == OffsetType.default: - return float(np.interp(self._speed_limit, LIMIT_PERC_OFFSET_BP, LIMIT_PERC_OFFSET_V) * self._speed_limit) + if offset_type == OffsetType.off: + return 0 elif offset_type == OffsetType.fixed: return offset_value * (CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS) elif offset_type == OffsetType.percentage: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 9d1dd07f56..543c02cd59 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -18,6 +18,13 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import S from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +SPEED_LIMITS = { + 'residential': 25 * CV.MPH_TO_MS, # 25 mph + 'city': 35 * CV.MPH_TO_MS, # 35 mph + 'highway': 65 * CV.MPH_TO_MS, # 65 mph + 'freeway': 80 * CV.MPH_TO_MS, # 80 mph +} + class TestSpeedLimitController: @@ -30,10 +37,23 @@ class TestSpeedLimitController: sunnypilot_interfaces.setup_interfaces(CI, self.params) return CI + def teardown_method(self, method): + self.reset_state() def reset_state(self): + self.reset_custom_params() self.slc.state = SpeedLimitControlState.disabled self.slc.frame = -1 + self.slc.last_op_engaged_frame = 0.0 + self.slc.op_engaged = False + self.slc.op_engaged_prev = False + self.slc.initial_max_set = False + self.slc._speed_limit = 0. + self.slc.speed_limit_prev = 0. + self.slc.last_valid_speed_limit_offsetted = 0. + self.slc._distance = 0. + self.slc._source = Source.none + self.events_sp.clear() def setup_method(self): self.params = Params() @@ -48,13 +68,30 @@ class TestSpeedLimitController: self.params.put("SpeedLimitOffsetType", 0) self.params.put("SpeedLimitValueOffset", 0) + def test_initial_state(self): + assert self.slc.state == SpeedLimitControlState.disabled + assert not self.slc.is_enabled + assert not self.slc.is_active + assert self.slc.final_cruise_speed == V_CRUISE_UNSET + def test_disabled(self): self.params.put_bool("SpeedLimitControl", False) - for v_ego in np.linspace(0, 100, 101): - for _ in range(int(10. / DT_MDL)): - v_cruise_slc = self.slc.update(True, v_ego, 0, 50 * CV.MPH_TO_MS, 50 * CV.MPH_TO_MS, 0, Source.none, self.events_sp) - assert v_cruise_slc == V_CRUISE_UNSET - assert self.slc.state == SpeedLimitControlState.disabled + for _ in range(int(10. / DT_MDL)): + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.disabled + + def test_transition_disabled_to_preactive(self): + for _ in range(int(3. / DT_MDL)): + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.preActive + assert self.slc.is_enabled and not self.slc.is_active + + def test_preactive_to_active_with_max_speed_confirmation(self): + self.slc.state = SpeedLimitControlState.preActive + v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.active + assert self.slc.is_enabled and self.slc.is_active + assert v_cruise_slc == SPEED_LIMITS['city'] def test_no_speed_limit(self): for v_ego in np.linspace(0, 100, 101): From b325fb2a9e23ec0d9665b99b284f92a4fc65e35a Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 00:54:59 -0400 Subject: [PATCH 093/188] new tests, fixes controller --- .../speed_limit_controller.py | 31 ++--- .../tests/test_speed_limit_controller.py | 106 +++++++++++++----- 2 files changed, 98 insertions(+), 39 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 862dc71d2e..9dec7d290d 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -38,6 +38,7 @@ class SpeedLimitController: self.CP = CP self.frame = -1 self.last_op_engaged_frame = 0.0 + self.last_preactive_frame = 0.0 self.is_metric = self.params.get_bool("IsMetric") self.enabled = self.params.get_bool("SpeedLimitControl") self.op_engaged = False @@ -47,11 +48,9 @@ class SpeedLimitController: self.v_offset = 0. self.v_cruise_setpoint = 0. self.v_cruise_setpoint_prev = 0. - self.v_cruise_setpoint_changed = False self.initial_max_set = False self._speed_limit = 0. self.speed_limit_prev = 0. - self.speed_limit_changed = False self.last_valid_speed_limit_offsetted = 0. self._distance = 0. self._source = Source.none @@ -125,6 +124,14 @@ class SpeedLimitController: # Fallback return V_CRUISE_UNSET + @property + def v_cruise_setpoint_changed(self) -> bool: + return self.v_cruise_setpoint != self.v_cruise_setpoint_prev + + @property + def speed_limit_changed(self) -> bool: + return self._speed_limit != self.speed_limit_prev + def get_offset(self, offset_type: OffsetType, offset_value: int) -> float: if offset_type == OffsetType.off: return 0 @@ -135,9 +142,6 @@ class SpeedLimitController: else: raise NotImplementedError("Offset not supported") - def update_v_cruise_setpoint_prev(self) -> None: - self.v_cruise_setpoint_prev = self.v_cruise_setpoint - def update_params(self) -> None: if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0: self.enabled = self.params.get_bool("SpeedLimitControl") @@ -171,16 +175,12 @@ class SpeedLimitController: if not self.op_engaged_prev and self.op_engaged: self.last_op_engaged_frame = self.frame - # Update change tracking variables - self.speed_limit_changed = self._speed_limit != self.speed_limit_prev - self.v_cruise_setpoint_changed = self.v_cruise_setpoint != self.v_cruise_setpoint_prev - self.speed_limit_prev = self._speed_limit - self.update_v_cruise_setpoint_prev() - self.op_engaged_prev = self.op_engaged + if not self._state_prev == SpeedLimitControlState.preActive and self.state == SpeedLimitControlState.preActive: + self.last_preactive_frame = self.frame def transition_state_from_disabled(self) -> None: # Wait 2 seconds after long engaged before starting fresh session - if (self.frame - self.last_op_engaged_frame) * DT_MDL > 2.: + if (self.frame - self.last_op_engaged_frame) * DT_MDL >= 2.: self.state = SpeedLimitControlState.preActive self.initial_max_set = False @@ -197,7 +197,7 @@ class SpeedLimitController: self.state = SpeedLimitControlState.active else: self.state = SpeedLimitControlState.pending - elif (self.frame - self.last_op_engaged_frame) * DT_MDL > PRE_ACTIVE_GUARD_PERIOD: + elif (self.frame - self.last_preactive_frame) * DT_MDL >= PRE_ACTIVE_GUARD_PERIOD: # Timeout - session ended self.state = SpeedLimitControlState.inactive @@ -265,6 +265,11 @@ class SpeedLimitController: self.state_control() self.update_events(events_sp) + # Update change tracking variablesZ + self.speed_limit_prev = self._speed_limit + self.v_cruise_setpoint_prev = self.v_cruise_setpoint + self.op_engaged_prev = self.op_engaged + self.frame += 1 return self.final_cruise_speed diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 543c02cd59..578544ee90 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -4,7 +4,7 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import numpy as np +import pytest from opendbc.car.car_helpers import interfaces from opendbc.car.toyota.values import CAR as TOYOTA @@ -13,8 +13,9 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitControlState, REQUIRED_INITIAL_MAX_SET_SPEED +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, OffsetType +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitControlState, REQUIRED_INITIAL_MAX_SET_SPEED, \ + PRE_ACTIVE_GUARD_PERIOD from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP @@ -93,29 +94,82 @@ class TestSpeedLimitController: assert self.slc.is_enabled and self.slc.is_active assert v_cruise_slc == SPEED_LIMITS['city'] - def test_no_speed_limit(self): - for v_ego in np.linspace(0, 100, 101): - for _ in range(int(10. / DT_MDL)): - v_cruise_slc = self.slc.update(True, v_ego, 0, 50 * CV.MPH_TO_MS, 0, 0, Source.none, self.events_sp) - assert v_cruise_slc == V_CRUISE_UNSET - assert self.slc.state not in ACTIVE_STATES + def test_preactive_timeout_to_inactive(self): + self.slc.state = SpeedLimitControlState.preActive + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) - def test_long_disabled(self): - for v_ego in np.linspace(0, 100, 101): - for _ in range(int(10. / DT_MDL)): - v_cruise_slc = self.slc.update(False, v_ego, 0, 50 * CV.MPH_TO_MS, 50 * CV.MPH_TO_MS, 0, Source.none, self.events_sp) - assert v_cruise_slc == V_CRUISE_UNSET - assert self.slc.state == SpeedLimitControlState.disabled + for _ in range(int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)): + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.inactive - def test_speed_limit_at_initial_max_set_speed(self): - v_cruise_slc = V_CRUISE_UNSET - speed_limit = 50 * CV.MPH_TO_MS - offset = 0 + def test_preactive_to_pending_no_speed_limit(self): + self.slc.state = SpeedLimitControlState.preActive + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, Source.none, self.events_sp) + assert self.slc.state == SpeedLimitControlState.pending + assert self.slc.is_enabled and not self.slc.is_active - for source in (Source.car_state, Source.map_data): - self.reset_state() - for _ in range(int(10. / DT_MDL)): - v_cruise_slc = self.slc.update(True, 40 * CV.MPH_TO_MS, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, source, self.events_sp) - offset = self.slc.get_offset(self.slc.offset_type, self.slc.offset_value) - assert self.slc.state in ACTIVE_STATES - assert v_cruise_slc == speed_limit + offset + def test_pending_to_active_when_speed_limit_available(self): + self.slc.state = SpeedLimitControlState.pending + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.active + + def test_pending_to_adapting_when_below_speed_limit(self): + self.slc.state = SpeedLimitControlState.pending + _ = self.slc.update(True, SPEED_LIMITS['city'] + 5, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.adapting + assert self.slc.is_enabled and self.slc.is_active + + def test_active_to_adapting_transition(self): + self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + + _ = self.slc.update(True, SPEED_LIMITS['city'] + 2, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.adapting + + def test_adapting_to_active_transition(self): + self.slc.state = SpeedLimitControlState.adapting + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.active + + def test_manual_cruise_change_detection(self): + self.slc.state = SpeedLimitControlState.active + expected_cruise = SPEED_LIMITS['highway'] + self.slc.v_cruise_setpoint_prev = expected_cruise + + different_cruise = SPEED_LIMITS['highway'] + 5 + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, different_cruise, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.inactive + + @pytest.mark.parametrize("offset_type, offset_value, speed_limit, expected_offset", [ + (OffsetType.fixed, 5, SPEED_LIMITS['city'], 5 * CV.MPH_TO_MS), # 5 MPH fixed offset + (OffsetType.percentage, 10, SPEED_LIMITS['city'], 0.1 * SPEED_LIMITS['city']), # 10% offset + (OffsetType.off, 0, SPEED_LIMITS['city'], 0), # Off + (OffsetType.fixed, 10, SPEED_LIMITS['highway'], 10 * CV.MPH_TO_MS), # Different speed, fixed offset + (OffsetType.percentage, 5, SPEED_LIMITS['highway'], 0.05 * SPEED_LIMITS['highway']), # Different speed, percentage + ]) + def test_offset_calculations(self, offset_type, offset_value, speed_limit, expected_offset): + self.slc._speed_limit = speed_limit + actual_offset = self.slc.get_offset(offset_type, offset_value) + assert actual_offset == pytest.approx(expected_offset, rel=0.01) + + def test_rapid_speed_limit_changes(self): + self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + speed_limits = [SPEED_LIMITS['city'], SPEED_LIMITS['highway'], SPEED_LIMITS['residential']] + + for i, speed_limits in enumerate(speed_limits): + _ = self.slc.update(True, speed_limits, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limits, 0, Source.car_state, self.events_sp) + assert self.slc.state in ACTIVE_STATES + + def test_invalid_speed_limits_handling(self): + self.slc.state = SpeedLimitControlState.active + self.slc.last_valid_speed_limit_offsetted = SPEED_LIMITS['city'] + + invalid_limits = [-10, 0, 200 * CV.MPH_TO_MS] + + for invalid_limit in invalid_limits: + v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, invalid_limit, 0, Source.car_state, self.events_sp) + assert isinstance(v_cruise_slc, (int, float)) + assert v_cruise_slc == V_CRUISE_UNSET or v_cruise_slc > 0 From 157181bc8602fd7873badb5b2dc69b6f546dbfd3 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 01:02:30 -0400 Subject: [PATCH 094/188] more tests --- .../tests/test_speed_limit_controller.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 578544ee90..3cfa764df4 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -165,6 +165,7 @@ class TestSpeedLimitController: def test_invalid_speed_limits_handling(self): self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED self.slc.last_valid_speed_limit_offsetted = SPEED_LIMITS['city'] invalid_limits = [-10, 0, 200 * CV.MPH_TO_MS] @@ -173,3 +174,13 @@ class TestSpeedLimitController: v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, invalid_limit, 0, Source.car_state, self.events_sp) assert isinstance(v_cruise_slc, (int, float)) assert v_cruise_slc == V_CRUISE_UNSET or v_cruise_slc > 0 + + def test_stale_data_handling(self): + self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + old_speed_limit = SPEED_LIMITS['city'] + self.slc.last_valid_speed_limit_offsetted = old_speed_limit + + v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, Source.car_state, self.events_sp) + assert self.slc.state in ACTIVE_STATES + assert v_cruise_slc == old_speed_limit From 0adb5570b9680d0ff7f756fde763ff0d22cff6ce Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 01:05:00 -0400 Subject: [PATCH 095/188] not really needed yet --- .../lib/speed_limit_controller/helpers.py | 23 ------------------- .../speed_limit_controller.py | 3 +-- 2 files changed, 1 insertion(+), 25 deletions(-) delete mode 100644 sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py deleted file mode 100644 index 53c9bfb5a5..0000000000 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import DEBUG, SpeedLimitControlState -from openpilot.common.swaglog import cloudlog - - -def debug(msg): - if not DEBUG: - return - cloudlog.debug(msg) - - -def description_for_state(speed_limit_control_state): - if speed_limit_control_state == SpeedLimitControlState.inactive: - return 'INACTIVE' - if speed_limit_control_state == SpeedLimitControlState.preActive: - return 'PRE_ACTIVE' - if speed_limit_control_state == SpeedLimitControlState.pending: - return 'PENDING' - if speed_limit_control_state == SpeedLimitControlState.adapting: - return 'ADAPTING' - if speed_limit_control_state == SpeedLimitControlState.active: - return 'ACTIVE' - - return '' diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 9dec7d290d..e428da002b 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -15,7 +15,6 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import P SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Engage, OffsetType -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.helpers import description_for_state, debug from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.selfdrive.modeld.constants import ModelConstants @@ -71,7 +70,7 @@ class SpeedLimitController: SpeedLimitControlState.active: self.transition_state_from_active, } - # Solution functions mapped to respective states + # Solution functions mapped to respective states self.acceleration_solutions = { SpeedLimitControlState.disabled: self.get_current_acceleration_as_target, SpeedLimitControlState.inactive: self.get_current_acceleration_as_target, From db9e410bee694b42353be464cf012690dfbddf9b Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 01:05:04 -0400 Subject: [PATCH 096/188] lint --- .../tests/test_speed_limit_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 3cfa764df4..41ddef217f 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -159,8 +159,8 @@ class TestSpeedLimitController: self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED speed_limits = [SPEED_LIMITS['city'], SPEED_LIMITS['highway'], SPEED_LIMITS['residential']] - for i, speed_limits in enumerate(speed_limits): - _ = self.slc.update(True, speed_limits, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limits, 0, Source.car_state, self.events_sp) + for _, speed_limit in enumerate(speed_limits): + _ = self.slc.update(True, speed_limit, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, Source.car_state, self.events_sp) assert self.slc.state in ACTIVE_STATES def test_invalid_speed_limits_handling(self): From 6acb23ba306767d733db5f7650087bf3a34894d7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 01:07:27 -0400 Subject: [PATCH 097/188] fix --- .../speed_limit_controller.py | 23 ++++++++++--------- .../tests/test_speed_limit_controller.py | 6 ++--- 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index e428da002b..08d84a1f12 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -28,9 +28,10 @@ class SpeedLimitController: _speed_limit: float _distance: float _source: Source - _v_ego: float - _a_ego: float - _v_offset: float + v_ego: float + a_ego: float + v_offset: float + last_valid_speed_limit_final: float def __init__(self, CP): self.params = Params() @@ -50,7 +51,7 @@ class SpeedLimitController: self.initial_max_set = False self._speed_limit = 0. self.speed_limit_prev = 0. - self.last_valid_speed_limit_offsetted = 0. + self.last_valid_speed_limit_final = 0. self._distance = 0. self._source = Source.none self.state = SpeedLimitControlState.disabled @@ -113,23 +114,23 @@ class SpeedLimitController: if self.is_active: # If we have a current valid speed limit, use it if self._speed_limit > 0: - self.last_valid_speed_limit_offsetted = self.speed_limit_final + self.last_valid_speed_limit_final = self.speed_limit_final return self.speed_limit_final # If no current speed limit but we have a last valid one, use that - if self.last_valid_speed_limit_offsetted > 0: - return self.last_valid_speed_limit_offsetted + if self.last_valid_speed_limit_final > 0: + return self.last_valid_speed_limit_final # Fallback return V_CRUISE_UNSET @property def v_cruise_setpoint_changed(self) -> bool: - return self.v_cruise_setpoint != self.v_cruise_setpoint_prev + return bool(self.v_cruise_setpoint != self.v_cruise_setpoint_prev) @property def speed_limit_changed(self) -> bool: - return self._speed_limit != self.speed_limit_prev + return bool(self._speed_limit != self.speed_limit_prev) def get_offset(self, offset_type: OffsetType, offset_value: int) -> float: if offset_type == OffsetType.off: @@ -153,13 +154,13 @@ class SpeedLimitController: return Engage.auto def initial_max_set_confirmed(self) -> bool: - return abs(self.v_cruise_setpoint - REQUIRED_INITIAL_MAX_SET_SPEED) <= CRUISE_SPEED_TOLERANCE + return bool(abs(self.v_cruise_setpoint - REQUIRED_INITIAL_MAX_SET_SPEED) <= CRUISE_SPEED_TOLERANCE) def detect_manual_cruise_change(self) -> bool: # If cruise speed changed and it's not what SLC would set if self.v_cruise_setpoint_changed: expected_cruise = self.speed_limit_final - return abs(self.v_cruise_setpoint - expected_cruise) > CRUISE_SPEED_TOLERANCE + return bool(abs(self.v_cruise_setpoint - expected_cruise) > CRUISE_SPEED_TOLERANCE) return False diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 41ddef217f..686d8a47ab 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -51,7 +51,7 @@ class TestSpeedLimitController: self.slc.initial_max_set = False self.slc._speed_limit = 0. self.slc.speed_limit_prev = 0. - self.slc.last_valid_speed_limit_offsetted = 0. + self.slc.last_valid_speed_limit_final = 0. self.slc._distance = 0. self.slc._source = Source.none self.events_sp.clear() @@ -166,7 +166,7 @@ class TestSpeedLimitController: def test_invalid_speed_limits_handling(self): self.slc.state = SpeedLimitControlState.active self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED - self.slc.last_valid_speed_limit_offsetted = SPEED_LIMITS['city'] + self.slc.last_valid_speed_limit_final = SPEED_LIMITS['city'] invalid_limits = [-10, 0, 200 * CV.MPH_TO_MS] @@ -179,7 +179,7 @@ class TestSpeedLimitController: self.slc.state = SpeedLimitControlState.active self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED old_speed_limit = SPEED_LIMITS['city'] - self.slc.last_valid_speed_limit_offsetted = old_speed_limit + self.slc.last_valid_speed_limit_final = old_speed_limit v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, Source.car_state, self.events_sp) assert self.slc.state in ACTIVE_STATES From 19bb39f09a59236da615f560fb32dabd9d5bacb6 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 01:14:37 -0400 Subject: [PATCH 098/188] some more tests --- .../tests/test_speed_limit_controller.py | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 686d8a47ab..dd1b1f4f6f 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -184,3 +184,31 @@ class TestSpeedLimitController: v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, Source.car_state, self.events_sp) assert self.slc.state in ACTIVE_STATES assert v_cruise_slc == old_speed_limit + + def test_different_speed_limit_sources(self): + self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + + for source in (Source.car_state, Source.map_data): + v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, source, self.events_sp) + assert v_cruise_slc != V_CRUISE_UNSET + + def test_distance_based_adapting(self): + self.slc.state = SpeedLimitControlState.adapting + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + + distance = 100.0 + current_speed = SPEED_LIMITS['highway'] + target_speed = SPEED_LIMITS['city'] + + v_cruise_slc = self.slc.update(True, current_speed, 0, REQUIRED_INITIAL_MAX_SET_SPEED, target_speed, distance, Source.map_data, self.events_sp) + assert self.slc.state == SpeedLimitControlState.adapting + assert v_cruise_slc == target_speed # TODO-SP: assert expected accel, need to enable self.acceleration_solutions + + def test_long_disengaged_to_disabled(self): + self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + + v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + assert self.slc.state == SpeedLimitControlState.disabled + assert v_cruise_slc == V_CRUISE_UNSET From 28544dc80326a9a91abe3c57d42fb93afd7f55a7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 01:16:50 -0400 Subject: [PATCH 099/188] wrap --- .../tests/test_speed_limit_controller.py | 22 +++++++++---------- 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index dd1b1f4f6f..2d96c80751 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -69,6 +69,10 @@ class TestSpeedLimitController: self.params.put("SpeedLimitOffsetType", 0) self.params.put("SpeedLimitValueOffset", 0) + def initialize_active_state(self, v_cruise_setpoint): + self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint_prev = v_cruise_setpoint + def test_initial_state(self): assert self.slc.state == SpeedLimitControlState.disabled assert not self.slc.is_enabled @@ -120,8 +124,7 @@ class TestSpeedLimitController: assert self.slc.is_enabled and self.slc.is_active def test_active_to_adapting_transition(self): - self.slc.state = SpeedLimitControlState.active - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) _ = self.slc.update(True, SPEED_LIMITS['city'] + 2, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) assert self.slc.state == SpeedLimitControlState.adapting @@ -155,8 +158,7 @@ class TestSpeedLimitController: assert actual_offset == pytest.approx(expected_offset, rel=0.01) def test_rapid_speed_limit_changes(self): - self.slc.state = SpeedLimitControlState.active - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) speed_limits = [SPEED_LIMITS['city'], SPEED_LIMITS['highway'], SPEED_LIMITS['residential']] for _, speed_limit in enumerate(speed_limits): @@ -164,8 +166,7 @@ class TestSpeedLimitController: assert self.slc.state in ACTIVE_STATES def test_invalid_speed_limits_handling(self): - self.slc.state = SpeedLimitControlState.active - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) self.slc.last_valid_speed_limit_final = SPEED_LIMITS['city'] invalid_limits = [-10, 0, 200 * CV.MPH_TO_MS] @@ -176,8 +177,7 @@ class TestSpeedLimitController: assert v_cruise_slc == V_CRUISE_UNSET or v_cruise_slc > 0 def test_stale_data_handling(self): - self.slc.state = SpeedLimitControlState.active - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) old_speed_limit = SPEED_LIMITS['city'] self.slc.last_valid_speed_limit_final = old_speed_limit @@ -186,8 +186,7 @@ class TestSpeedLimitController: assert v_cruise_slc == old_speed_limit def test_different_speed_limit_sources(self): - self.slc.state = SpeedLimitControlState.active - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) for source in (Source.car_state, Source.map_data): v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, source, self.events_sp) @@ -206,8 +205,7 @@ class TestSpeedLimitController: assert v_cruise_slc == target_speed # TODO-SP: assert expected accel, need to enable self.acceleration_solutions def test_long_disengaged_to_disabled(self): - self.slc.state = SpeedLimitControlState.active - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) assert self.slc.state == SpeedLimitControlState.disabled From fa26dda544617ba43ff94fbd7ecd3ec93f0086be Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 11:14:01 -0400 Subject: [PATCH 100/188] more --- .../controls/lib/speed_limit_controller/speed_limit_resolver.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index 887f419bf4..ead614e611 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -6,7 +6,6 @@ from openpilot.common.gps import get_gps_location_service from openpilot.common.params import Params from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Policy -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.helpers import debug class SpeedLimitResolver: @@ -63,7 +62,6 @@ class SpeedLimitResolver: gps_fix_age = time.monotonic() - gps_data.unixTimestampMillis * 1e-3 if gps_fix_age > LIMIT_MAX_MAP_DATA_AGE: - debug(f'SL: Ignoring map data as is too old. Age: {gps_fix_age}') return speed_limit = map_data.speedLimit if map_data.speedLimitValid else 0. From e1ac6fef51527ddb2dad38884c1144120a6c6793 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 11:23:44 -0400 Subject: [PATCH 101/188] more --- .../controls/lib/speed_limit_controller/speed_limit_resolver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index ead614e611..64a78e4d36 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -93,7 +93,6 @@ class SpeedLimitResolver: self.distance = self._distance_solutions[source] if source else 0. self.source = source or Source.none - debug(f'SL: *** Speed Limit set: {self.speed_limit}, distance: {self.distance}, source: {self.source}') return self.speed_limit, self.distance, self.source def _get_source_solution_according_to_policy(self) -> Source | None: From 87beff9cad126e59648e76a09abc6090e04f3368 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 21:53:05 -0400 Subject: [PATCH 102/188] use vCruiseCluster for set speed --- sunnypilot/selfdrive/controls/lib/longitudinal_planner.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 9224f9f358..c7e16ed976 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -53,7 +53,8 @@ class LongitudinalPlannerSP: # Speed Limit Control _speed_limit, _distance, _source = self.resolver.resolve(v_ego, sm) - v_cruise_slc = self.slc.update(sm['carControl'].longActive, v_ego, a_ego, v_cruise, _speed_limit, _distance, _source, self.events_sp) + v_cruise_slc = self.slc.update(sm['carControl'].longActive, v_ego, a_ego, sm['carState'].vCruiseCluster, + _speed_limit, _distance, _source, self.events_sp) v_cruise_final = min(v_cruise, v_cruise_slc) From 711a43082a6d822aadfb9f083ad2dab4447b4d35 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 22:09:26 -0400 Subject: [PATCH 103/188] init better --- .../tests/test_speed_limit_controller.py | 66 +++++++++++-------- 1 file changed, 39 insertions(+), 27 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 2d96c80751..fcc49cc0a4 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -29,33 +29,6 @@ SPEED_LIMITS = { class TestSpeedLimitController: - def _setup_platform(self, car_name): - CarInterface = interfaces[car_name] - CP = CarInterface.get_non_essential_params(car_name) - CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) - CI = CarInterface(CP, CP_SP) - - sunnypilot_interfaces.setup_interfaces(CI, self.params) - - return CI - def teardown_method(self, method): - self.reset_state() - - def reset_state(self): - self.reset_custom_params() - self.slc.state = SpeedLimitControlState.disabled - self.slc.frame = -1 - self.slc.last_op_engaged_frame = 0.0 - self.slc.op_engaged = False - self.slc.op_engaged_prev = False - self.slc.initial_max_set = False - self.slc._speed_limit = 0. - self.slc.speed_limit_prev = 0. - self.slc.last_valid_speed_limit_final = 0. - self.slc._distance = 0. - self.slc._source = Source.none - self.events_sp.clear() - def setup_method(self): self.params = Params() self.reset_custom_params() @@ -63,14 +36,42 @@ class TestSpeedLimitController: CI = self._setup_platform(TOYOTA.TOYOTA_RAV4_TSS2_2022) self.slc = SpeedLimitController(CI.CP) + def teardown_method(self, method): + self.reset_state() + + def _setup_platform(self, car_name): + CarInterface = interfaces[car_name] + CP = CarInterface.get_non_essential_params(car_name) + CP_SP = CarInterface.get_non_essential_params_sp(CP, car_name) + CI = CarInterface(CP, CP_SP) + sunnypilot_interfaces.setup_interfaces(CI, self.params) + return CI + def reset_custom_params(self): self.params.put_bool("SpeedLimitControl", True) self.params.put_bool("IsMetric", False) self.params.put("SpeedLimitOffsetType", 0) self.params.put("SpeedLimitValueOffset", 0) + def reset_state(self): + self.slc.state = SpeedLimitControlState.disabled + self.slc.frame = -1 + self.slc.last_op_engaged_frame = 0 + self.slc.op_engaged = False + self.slc.op_engaged_prev = False + self.slc.initial_max_set = False + self.slc._speed_limit = 0. + self.slc.speed_limit_prev = 0. + self.slc.last_valid_speed_limit_offsetted = 0. + self.slc._distance = 0. + self.slc._source = Source.none + self.slc.v_cruise_setpoint = 0. + self.slc.v_cruise_setpoint_prev = 0. + self.events_sp.clear() + def initialize_active_state(self, v_cruise_setpoint): self.slc.state = SpeedLimitControlState.active + self.slc.v_cruise_setpoint = v_cruise_setpoint self.slc.v_cruise_setpoint_prev = v_cruise_setpoint def test_initial_state(self): @@ -210,3 +211,14 @@ class TestSpeedLimitController: v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) assert self.slc.state == SpeedLimitControlState.disabled assert v_cruise_slc == V_CRUISE_UNSET + + def test_maintain_states_with_no_changes(self): + test_states = [ + SpeedLimitControlState.preActive, + SpeedLimitControlState.pending, + SpeedLimitControlState.active, + SpeedLimitControlState.adapting + ] + + for state in test_states: + self.slc.state = state From 0e400245488c9093dcf98369a4d3b9cc05c365d2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 22:13:06 -0400 Subject: [PATCH 104/188] finish it up --- .../tests/test_speed_limit_controller.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index fcc49cc0a4..7c09495f7a 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -19,6 +19,8 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import S from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +ALL_STATES = tuple(SpeedLimitControlState.schema.enumerants.values()) + SPEED_LIMITS = { 'residential': 25 * CV.MPH_TO_MS, # 25 mph 'city': 35 * CV.MPH_TO_MS, # 35 mph @@ -213,6 +215,7 @@ class TestSpeedLimitController: assert v_cruise_slc == V_CRUISE_UNSET def test_maintain_states_with_no_changes(self): + """Test that states are maintained when no significant changes occur""" test_states = [ SpeedLimitControlState.preActive, SpeedLimitControlState.pending, @@ -222,3 +225,17 @@ class TestSpeedLimitController: for state in test_states: self.slc.state = state + self.slc.op_engaged = True + if state in [SpeedLimitControlState.pending, SpeedLimitControlState.active, SpeedLimitControlState.adapting]: + self.slc.initial_max_set = True + + initial_state = state + + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED,SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + + assert self.slc.state in ALL_STATES # Sanity check + + if initial_state == SpeedLimitControlState.preActive: + assert self.slc.state in [SpeedLimitControlState.preActive, SpeedLimitControlState.active] + elif initial_state in ACTIVE_STATES: + assert self.slc.state in ACTIVE_STATES From 8b5290b462a7252601ecf9c5b1d1d6e4a4442bff Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 2 Sep 2025 22:15:13 -0400 Subject: [PATCH 105/188] no --- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 08d84a1f12..1aec76cc98 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -223,9 +223,9 @@ class SpeedLimitController: def state_control(self) -> None: self._state_prev = self.state - # If op is disabled or SLC is disabled, go to disabled state (not inactive) + # If op is disabled or SLC is disabled, go to disabled state if not self.op_engaged or not self.enabled: - self.state = SpeedLimitControlState.disabled # Changed from inactive + self.state = SpeedLimitControlState.disabled self.initial_max_set = False return From 9447aa0e3d0339cb8636541a6e7e0022cda79994 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Wed, 3 Sep 2025 05:49:12 -0700 Subject: [PATCH 106/188] modeld: turn desires (#1182) * Add modelDataV2SP and lane turn logic implementation Note: still need to hook up to other modeld's create unit test, fix stuff, and do the UI for it * add unit tests for lane turn logic * Add lane turn desire controls to models panel * use `events_sp` instead of `events` * integrate modelDataV2SP messaging to the other modeld controllers * move this to that * use min for general population here, on custom branches, change this to max :) * Update events.py Co-authored-by: royjr * Update events.py Co-authored-by: royjr * refactor lane turn value control into one method * Update selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc * add integration tests for lane turn desire * 10 updates is possibly more representative of real life * real objects ofc * desc: add toggle description for clarity --------- Co-authored-by: royjr --- cereal/custom.capnp | 11 +- cereal/log.capnp | 2 +- cereal/services.py | 1 + common/params_keys.h | 4 +- selfdrive/controls/lib/desire_helper.py | 22 +++- selfdrive/modeld/modeld.py | 5 +- selfdrive/selfdrived/selfdrived.py | 12 +- .../qt/offroad/settings/models_panel.cc | 41 ++++++- .../qt/offroad/settings/models_panel.h | 4 +- sunnypilot/modeld/modeld.py | 5 +- sunnypilot/modeld_v2/modeld.py | 5 +- .../controls/lib/lane_turn_desire.py | 45 +++++++ .../lib/tests/test_lane_turn_desire.py | 113 ++++++++++++++++++ sunnypilot/selfdrive/selfdrived/events.py | 17 ++- 14 files changed, 274 insertions(+), 13 deletions(-) create mode 100644 sunnypilot/selfdrive/controls/lib/lane_turn_desire.py create mode 100644 sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index fdb89da84c..5468e0f39d 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -172,6 +172,8 @@ struct OnroadEventSP @0xda96579883444c35 { experimentalModeSwitched @14; wrongCarModeAlertOnly @15; pedalPressedAlertOnly @16; + laneTurnLeft @17; + laneTurnRight @18; } } @@ -258,9 +260,16 @@ struct LiveMapDataSP @0xf416ec09499d9d19 { roadName @5 :Text; } -struct CustomReserved9 @0xa1680744031fdb2d { +struct ModelDataV2SP @0xa1680744031fdb2d { + laneTurnDirection @0 :TurnDirection; } + enum TurnDirection { + none @0; + turnLeft @1; + turnRight @2; + } + struct CustomReserved10 @0xcb9fd56c7057593a { } diff --git a/cereal/log.capnp b/cereal/log.capnp index 15795f8c38..474b4777cb 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -2631,7 +2631,7 @@ struct Event { backupManagerSP @113 :Custom.BackupManagerSP; carStateSP @114 :Custom.CarStateSP; liveMapDataSP @115 :Custom.LiveMapDataSP; - customReserved9 @116 :Custom.CustomReserved9; + modelDataV2SP @116 :Custom.ModelDataV2SP; customReserved10 @136 :Custom.CustomReserved10; customReserved11 @137 :Custom.CustomReserved11; customReserved12 @138 :Custom.CustomReserved12; diff --git a/cereal/services.py b/cereal/services.py index 373e865e34..b48290bc77 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -88,6 +88,7 @@ _services: dict[str, tuple] = { "carControlSP": (True, 100., 10), "carStateSP": (True, 100., 10), "liveMapDataSP": (True, 1., 1), + "modelDataV2SP": (True, 20.), # debug "uiDebug": (True, 0., 1), diff --git a/common/params_keys.h b/common/params_keys.h index 142b3422cb..e0af53b299 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -195,10 +195,12 @@ inline static std::unordered_map keys = { {"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}}, {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, - // model panel params + // sunnypilot model params {"LagdToggle", {PERSISTENT | BACKUP, BOOL, "1"}}, {"LagdToggleDelay", {PERSISTENT | BACKUP, FLOAT, "0.2"}}, {"LagdValueCache", {PERSISTENT, FLOAT, "0.2"}}, + {"LaneTurnDesire", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"LaneTurnValue", {PERSISTENT | BACKUP, FLOAT, "19.0"}}, // mapd {"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}}, diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 2adfa65f6d..93088182f9 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -1,7 +1,8 @@ -from cereal import log +from cereal import log, custom from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeController, AutoLaneChangeMode +from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController LaneChangeState = log.LaneChangeState LaneChangeDirection = log.LaneChangeDirection @@ -30,6 +31,12 @@ DESIRES = { }, } +TURN_DESIRES = { + custom.TurnDirection.none: log.Desire.none, + custom.TurnDirection.turnLeft: log.Desire.turnLeft, + custom.TurnDirection.turnRight: log.Desire.turnRight, +} + class DesireHelper: def __init__(self): @@ -41,13 +48,21 @@ class DesireHelper: self.prev_one_blinker = False self.desire = log.Desire.none self.alc = AutoLaneChangeController(self) + self.lane_turn_controller = LaneTurnController(self) + self.lane_turn_direction = custom.TurnDirection.none def update(self, carstate, lateral_active, lane_change_prob): self.alc.update_params() + self.lane_turn_controller.update_params() v_ego = carstate.vEgo one_blinker = carstate.leftBlinker != carstate.rightBlinker below_lane_change_speed = v_ego < LANE_CHANGE_SPEED_MIN + # Lane turn controller update + self.lane_turn_controller.update_lane_turn(blindspot_left=carstate.leftBlindspot, blindspot_right=carstate.rightBlindspot, + left_blinker=carstate.leftBlinker, right_blinker=carstate.rightBlinker, v_ego=v_ego) + self.lane_turn_direction = self.lane_turn_controller.get_turn_direction() + if not lateral_active or self.lane_change_timer > LANE_CHANGE_TIME_MAX or self.alc.lane_change_set_timer == AutoLaneChangeMode.OFF: self.lane_change_state = LaneChangeState.off self.lane_change_direction = LaneChangeDirection.none @@ -106,7 +121,10 @@ class DesireHelper: self.prev_one_blinker = one_blinker - self.desire = DESIRES[self.lane_change_direction][self.lane_change_state] + if self.lane_turn_direction != custom.TurnDirection.none: + self.desire = TURN_DESIRES[self.lane_turn_direction] + else: + self.desire = DESIRES[self.lane_change_direction][self.lane_change_state] # Send keep pulse once per second during LaneChangeStart.preLaneChange if self.lane_change_state in (LaneChangeState.off, LaneChangeState.laneChangeStarting): diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index caf342e88b..5f9ccf5072 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -216,7 +216,7 @@ def main(demo=False): cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") # messaging - pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) + pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]) sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) publish_state = PublishState() @@ -333,6 +333,7 @@ def main(demo=False): modelv2_send = messaging.new_message('modelV2') drivingdata_send = messaging.new_message('drivingModelData') posenet_send = messaging.new_message('cameraOdometry') + mdv2sp_send = messaging.new_message('modelDataV2SP') action = get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego) prev_action = action @@ -347,6 +348,7 @@ def main(demo=False): DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction + mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction @@ -354,6 +356,7 @@ def main(demo=False): pm.send('modelV2', modelv2_send) pm.send('drivingModelData', drivingdata_send) pm.send('cameraOdometry', posenet_send) + pm.send('modelDataV2SP', mdv2sp_send) last_vipc_frame_id = meta_main.frame_id diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 249621d6fc..c56c7fe65f 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -86,7 +86,7 @@ class SelfdriveD(CruiseHelper): # TODO: de-couple selfdrived with card/conflate on carState without introducing controls mismatches self.car_state_sock = messaging.sub_sock('carState', timeout=20) - ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ignore = self.sensor_packets + self.gps_packets + ['alertDebug'] + ['modelDataV2SP'] if SIMULATION: ignore += ['driverCameraState', 'managerState'] if REPLAY: @@ -95,7 +95,8 @@ class SelfdriveD(CruiseHelper): self.sm = messaging.SubMaster(['deviceState', 'pandaStates', 'peripheralState', 'modelV2', 'liveCalibration', 'carOutput', 'driverMonitoringState', 'longitudinalPlan', 'livePose', 'liveDelay', 'managerState', 'liveParameters', 'radarState', 'liveTorqueParameters', - 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback'] + \ + 'controlsState', 'carControl', 'driverAssistance', 'alertDebug', 'userBookmark', 'audioFeedback', + 'modelDataV2SP'] + \ self.camera_packets + self.sensor_packets + self.gps_packets, ignore_alive=ignore, ignore_avg_freq=ignore, ignore_valid=ignore, frequency=int(1/DT_CTRL)) @@ -300,6 +301,13 @@ class SelfdriveD(CruiseHelper): LaneChangeState.laneChangeFinishing): self.events.add(EventName.laneChange) + # Handle lane turn + lane_turn_direction = self.sm['modelDataV2SP'].laneTurnDirection + if lane_turn_direction == custom.TurnDirection.turnLeft: + self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnLeft) + elif lane_turn_direction == custom.TurnDirection.turnRight: + self.events_sp.add(custom.OnroadEventSP.EventName.laneTurnRight) + for i, pandaState in enumerate(self.sm['pandaStates']): # All pandas must match the list of safetyConfigs, and if outside this list, must be silent or noOutput if i < len(self.CP.safetyConfigs): diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index baba7d3a17..9e79aa220a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -101,9 +101,32 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { QString policyType = tr("Policy Model"); policyFrame = createModelDetailFrame(this, policyType, policyProgressBar); list->addItem(policyFrame); - list->addItem(horizontal_line()); + // Lane Turn Desire toggle + lane_turn_desire_toggle = new ParamControlSP("LaneTurnDesire", tr("Use Lane Turn Desires"), + "If you’re driving at 20 mph (32 km/h) or below and have your blinker on, " + "the car will plan a turn in that direction at the nearest drivable path. " + "This prevents situations (like at red lights) where the car might plan the wrong turn direction.", + "../assets/offroad/icon_shell.png"); + list->addItem(lane_turn_desire_toggle); + + // Lane Turn Value control + int max_value_mph = 20; + bool is_metric_initial = params.getBool("IsMetric"); + const float K = 1.609344f; + int per_value_change_scaled = is_metric_initial ? static_cast(std::round((1.0f / K) * 100.0f)) : 100; // 100 -> 1 mph + lane_turn_value_control = new OptionControlSP("LaneTurnValue", tr("Adjust Lane Turn Speed"), + tr("Set the maximum speed for lane turn desires. Default is 19 %1.").arg(is_metric_initial ? "km/h" : "mph"), + "", {5 * 100, max_value_mph * 100}, per_value_change_scaled, false, nullptr, true, true); + lane_turn_value_control->showDescription(); + list->addItem(lane_turn_value_control); + + // Show based on toggle + refreshLaneTurnValueControl(); + connect(lane_turn_desire_toggle, &ParamControlSP::toggleFlipped, this, &ModelsPanel::refreshLaneTurnValueControl); + connect(lane_turn_value_control, &OptionControlSP::updateLabels, this, &ModelsPanel::refreshLaneTurnValueControl); + // LiveDelay toggle lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png"); lagd_toggle_control->showDescription(); @@ -159,6 +182,19 @@ QFrame* ModelsPanel::createModelDetailFrame(QWidget *parent, QString &typeName, return frame; } +void ModelsPanel::refreshLaneTurnValueControl() { + if (!lane_turn_value_control) return; + float stored_mph = QString::fromStdString(params.get("LaneTurnValue")).toFloat(); + bool is_metric = params.getBool("IsMetric"); + QString unit = is_metric ? "km/h" : "mph"; + float display_value = stored_mph; + if (is_metric) { + display_value = stored_mph * 1.609344f; + } + lane_turn_value_control->setLabel(QString::number(static_cast(std::round(display_value))) + " " + unit); + lane_turn_value_control->setVisible(params.getBool("LaneTurnDesire")); +} + /** * @brief Updates the UI with bundle download progress information * Reads status from modelManagerSP cereal message and displays status for all models @@ -439,6 +475,9 @@ void ModelsPanel::updateLabels() { delay_control->setLabel(QString::number(value, 'f', 2) + "s"); } + // Update lane turn desire label and visibility + refreshLaneTurnValueControl(); + clearModelCacheBtn->setValue(QString::number(calculateCacheSize(), 'f', 2) + " MB"); } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h index 93edc4de1a..1906ebd2a0 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -37,6 +37,7 @@ private: void updateLabels(); void handleCurrentModelLblBtnClicked(); void handleBundleDownloadProgress(); + void refreshLaneTurnValueControl(); void showResetParamsDialog(); QProgressBar* createProgressBar(QWidget *parent); QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar); @@ -81,5 +82,6 @@ private: Params params; ButtonControlSP *clearModelCacheBtn; ButtonControlSP *refreshAvailableModelsBtn; - + ParamControlSP *lane_turn_desire_toggle; + OptionControlSP *lane_turn_value_control; }; diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index b94e166bc6..3d11ed23f4 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -177,7 +177,7 @@ def main(demo=False): cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") # messaging - pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) + pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]) sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) publish_state = PublishState() @@ -304,6 +304,7 @@ def main(demo=False): modelv2_send = messaging.new_message('modelV2') drivingdata_send = messaging.new_message('drivingModelData') posenet_send = messaging.new_message('cameraOdometry') + mdv2sp_send = messaging.new_message('modelDataV2SP') action = model.get_action_from_model(model_output, prev_action, long_delay + DT_MDL) fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio, meta_main.timestamp_eof, model_execution_time, live_calib_seen, @@ -316,6 +317,7 @@ def main(demo=False): DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction + mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction @@ -323,6 +325,7 @@ def main(demo=False): pm.send('modelV2', modelv2_send) pm.send('drivingModelData', drivingdata_send) pm.send('cameraOdometry', posenet_send) + pm.send('modelDataV2SP', mdv2sp_send) last_vipc_frame_id = meta_main.frame_id diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 1a420e6f1a..bf89bc98d6 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -202,7 +202,7 @@ def main(demo=False): cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})") # messaging - pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry"]) + pm = PubMaster(["modelV2", "drivingModelData", "cameraOdometry", "modelDataV2SP"]) sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "carControl", "liveDelay"]) publish_state = PublishState() @@ -322,6 +322,7 @@ def main(demo=False): modelv2_send = messaging.new_message('modelV2') drivingdata_send = messaging.new_message('drivingModelData') posenet_send = messaging.new_message('cameraOdometry') + mdv2sp_send = messaging.new_message('modelDataV2SP') action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego) prev_action = action @@ -336,6 +337,7 @@ def main(demo=False): DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob) modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction + mdv2sp_send.modelDataV2SP.laneTurnDirection = DH.lane_turn_direction drivingdata_send.drivingModelData.meta.laneChangeState = DH.lane_change_state drivingdata_send.drivingModelData.meta.laneChangeDirection = DH.lane_change_direction @@ -343,6 +345,7 @@ def main(demo=False): pm.send('modelV2', modelv2_send) pm.send('drivingModelData', drivingdata_send) pm.send('cameraOdometry', posenet_send) + pm.send('modelDataV2SP', mdv2sp_send) last_vipc_frame_id = meta_main.frame_id diff --git a/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py b/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py new file mode 100644 index 0000000000..00ce026abb --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/lane_turn_desire.py @@ -0,0 +1,45 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from cereal import custom + +from openpilot.common.constants import CV +from openpilot.common.params import Params + +LANE_CHANGE_SPEED_MIN = 20 * CV.MPH_TO_MS + + +class LaneTurnController: + def __init__(self, desire_helper): + self.DH = desire_helper + self.turn_direction = custom.TurnDirection.none + self.params = Params() + self.lane_turn_value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS + self.param_read_counter = 0 + self.enabled = self.params.get_bool("LaneTurnDesire") + + def read_params(self): + self.enabled = self.params.get_bool("LaneTurnDesire") + value = float(self.params.get("LaneTurnValue", return_default=True)) * CV.MPH_TO_MS + self.lane_turn_value = min(float(LANE_CHANGE_SPEED_MIN), value) + + def update_params(self) -> None: + if self.param_read_counter % 50 == 0: + self.read_params() + self.param_read_counter += 1 + + def update_lane_turn(self, blindspot_left: bool, blindspot_right: bool, left_blinker: bool, right_blinker: bool, v_ego: float) -> None: + if left_blinker and not right_blinker and v_ego < self.lane_turn_value and not blindspot_left: + self.turn_direction = custom.TurnDirection.turnLeft + elif right_blinker and not left_blinker and v_ego < self.lane_turn_value and not blindspot_right: + self.turn_direction = custom.TurnDirection.turnRight + else: + self.turn_direction = custom.TurnDirection.none + + def get_turn_direction(self): + if not self.enabled: + return custom.TurnDirection.none + return self.turn_direction diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py b/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py new file mode 100644 index 0000000000..5633ed6efc --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/tests/test_lane_turn_desire.py @@ -0,0 +1,113 @@ +import pytest +from cereal import log +from openpilot.common.params import Params + +from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper +from openpilot.sunnypilot.selfdrive.controls.lib.lane_turn_desire import LaneTurnController, LANE_CHANGE_SPEED_MIN +from openpilot.sunnypilot.selfdrive.controls.lib.auto_lane_change import AutoLaneChangeMode + + +class TurnDirection: + none = 0 + turnLeft = 1 + turnRight = 2 + + +@pytest.mark.parametrize("left_blinker,right_blinker,v_ego,blindspot_left,blindspot_right,expected", [ + (True, False, 5, False, False, TurnDirection.turnLeft), + (False, True, 6, False, False, TurnDirection.turnRight), + (True, False, 9, False, False, TurnDirection.none), + (True, False, 7, True, False, TurnDirection.none), + (False, True, 6, False, True, TurnDirection.none), + (False, False, 5, False, False, TurnDirection.none), + (True, True, 5, False, False, TurnDirection.none), +]) +def test_lane_turn_desire_conditions(left_blinker, right_blinker, v_ego, blindspot_left, blindspot_right, expected): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = True + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + controller.update_lane_turn(blindspot_left, blindspot_right, left_blinker, right_blinker, v_ego) + assert controller.get_turn_direction() == expected + + +def test_lane_turn_desire_disabled(): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = False + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + controller.update_lane_turn(False, False, True, False, 7) + assert controller.get_turn_direction() == TurnDirection.none + + +def test_lane_turn_overrides_lane_change(): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = True + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + # left turn desire + controller.update_lane_turn(False, False, True, False, 5) + assert controller.get_turn_direction() == TurnDirection.turnLeft + # right turn desire + controller.update_lane_turn(False, False, False, True, 6) + assert controller.get_turn_direction() == TurnDirection.turnRight + # no turn + controller.update_lane_turn(False, False, False, False, 7) + assert controller.get_turn_direction() == TurnDirection.none + + +@pytest.mark.parametrize("v_ego,expected", [ + (8.93, TurnDirection.turnLeft), # just below threshold + (8.96, TurnDirection.none), # above threshold + (8.95, TurnDirection.none), # just above threshold +]) +def test_lane_turn_desire_speed_boundary(v_ego, expected): + dh = DesireHelper() + controller = LaneTurnController(dh) + controller.enabled = True + controller.lane_turn_value = LANE_CHANGE_SPEED_MIN + controller.turn_direction = TurnDirection.none + controller.update_lane_turn(False, True, True, False, v_ego) + assert controller.get_turn_direction() == expected + + +class DummyCarState: + def __init__(self, vEgo=0, leftBlinker=False, rightBlinker=False, leftBlindspot=False, rightBlindspot=False, + steeringPressed=False, steeringTorque=0, brakePressed=False): + self.vEgo = vEgo + self.leftBlinker = leftBlinker + self.rightBlinker = rightBlinker + self.leftBlindspot = leftBlindspot + self.rightBlindspot = rightBlindspot + self.steeringPressed = steeringPressed + self.steeringTorque = steeringTorque + self.brakePressed = brakePressed + +@pytest.fixture +def set_lane_turn_params(): + params = Params() + params.put("LaneTurnDesire", True) + params.put("LaneTurnValue", 20.0) + +@pytest.mark.parametrize("carstate, lateral_active, lane_change_prob, expected_desire", [ + # Lane turn desire overrides lane change desire + (DummyCarState(vEgo=5, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnLeft), + (DummyCarState(vEgo=7, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False), True, 1.0, log.Desire.turnRight), + # Lane change desire only (no turn desires) + (DummyCarState(vEgo=9, leftBlinker=True, rightBlinker=False, leftBlindspot=False, rightBlindspot=False, + steeringPressed=True, steeringTorque=1), True, 1.0, log.Desire.laneChangeLeft), + (DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=True, leftBlindspot=False, rightBlindspot=False, + steeringPressed=True, steeringTorque=-1), True, 1.0, log.Desire.laneChangeRight), + # No desire (inactive) + (DummyCarState(vEgo=9, leftBlinker=False, rightBlinker=False), False, 1.0, log.Desire.none), + (DummyCarState(vEgo=4, leftBlinker=False, rightBlinker=False), True, 1.0, log.Desire.none), # No blinkers? no desire! +]) +def test_desire_helper_integration(carstate, lateral_active, lane_change_prob, expected_desire, set_lane_turn_params): + dh = DesireHelper() + dh.alc.lane_change_set_timer = AutoLaneChangeMode.NUDGE + for _ in range(10): + dh.update(carstate, lateral_active, lane_change_prob) + assert dh.desire == expected_desire # The first four tests were unit tests to test the controller, where this tests the integration in desire helpers diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index d81345a14e..7c723397fb 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -132,6 +132,21 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { EventNameSP.pedalPressedAlertOnly: { ET.WARNING: NoEntryAlert("Pedal Pressed") - } + }, + EventNameSP.laneTurnLeft: { + ET.WARNING: Alert( + "Turning Left", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), + }, + + EventNameSP.laneTurnRight: { + ET.WARNING: Alert( + "Turning Right", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 1.), + } } From 288a5e14daf5bdb3948cf4de8aab96c62f1b9cca Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Wed, 3 Sep 2025 17:18:18 +0200 Subject: [PATCH 107/188] bugfix: streamline LiveDelay parameter loading with safe handling (#1204) --- .../qt/offroad/settings/models_panel.cc | 49 ++++++++++--------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index 9e79aa220a..02a01a4b63 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -48,6 +48,25 @@ static const QString progressStyleError = progressStyleActive + " background-color: transparent;" "}"; +std::optional safeParamEventLoad(Params& params, const std::string& paramName) { + std::string raw = params.get(paramName); + if (raw.empty()) { + return std::nullopt; + } + + try { + AlignedBuffer alignedBuf; + auto buf = alignedBuf.align(raw.data(), raw.size()); + + capnp::FlatArrayMessageReader msg(kj::ArrayPtr(buf.begin(), buf.size())); + return msg.getRoot(); + } + catch (const kj::Exception& e) { + qInfo() << "Invalid param" << QString::fromStdString(paramName) << ":" << e.getDescription().cStr(); + return std::nullopt; + } +} + ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(50, 20, 50, 20); @@ -134,17 +153,10 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { // Software delay control int liveDelayMaxInt = 30; - std::string liveDelayBytes = params.get("LiveDelay"); - if (!liveDelayBytes.empty()) { - capnp::FlatArrayMessageReader msg(kj::ArrayPtr( - reinterpret_cast(liveDelayBytes.data()), - liveDelayBytes.size() / sizeof(capnp::word))); - auto event = msg.getRoot(); - if (event.hasLiveDelay()) { - auto liveDelay = event.getLiveDelay(); - float lateralDelay = liveDelay.getLateralDelay(); - liveDelayMaxInt = static_cast(lateralDelay * 100.0f) + 20; - } + if (const auto event = safeParamEventLoad(params, "LiveDelay"); event && event->hasLiveDelay()) { + auto liveDelay = event->getLiveDelay(); + float lateralDelay = liveDelay.getLateralDelay(); + liveDelayMaxInt = static_cast(lateralDelay * 100.0f) + 20; } delay_control = new OptionControlSP("LagdToggleDelay", tr("Adjust Software Delay"), tr("Adjust the software delay when Live Learning Steer Delay is toggled off." @@ -437,18 +449,11 @@ void ModelsPanel::updateLabels() { "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."); bool lagdEnabled = params.getBool("LagdToggle"); if (lagdEnabled) { - std::string liveDelayBytes = params.get("LiveDelay"); - if (!liveDelayBytes.empty()) { - capnp::FlatArrayMessageReader msg(kj::ArrayPtr( - reinterpret_cast(liveDelayBytes.data()), - liveDelayBytes.size() / sizeof(capnp::word))); - auto event = msg.getRoot(); - if (event.hasLiveDelay()) { - auto liveDelay = event.getLiveDelay(); - float lateralDelay = liveDelay.getLateralDelay(); - desc += QString("

%1 %2 s") + if (const auto event = safeParamEventLoad(params, "LiveDelay"); event && event->hasLiveDelay()) { + auto liveDelay = event->getLiveDelay(); + float lateralDelay = liveDelay.getLateralDelay(); + desc += QString("

%1 %2 s") .arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3)); - } } } else { std::string carParamsBytes = params.get("CarParamsPersistent"); From 355499a8deec8f54fa1668c88a88853e71c4d9a0 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:31:48 -0700 Subject: [PATCH 108/188] feat(esim): hotswap (#36096) feat(esim): device hw reboot modem --- system/hardware/tici/hardware.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 6a9b98af82..13a30ae011 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -498,6 +498,14 @@ class Tici(HardwareBase): os.system(f"sudo cp {tf.name} {dest}") os.system(f"sudo nmcli con load {dest}") + def reboot_modem(self): + modem = self.get_modem() + for state in (0, 1): + try: + modem.Command(f'AT+CFUN={state}', math.ceil(TIMEOUT), dbus_interface=MM_MODEM, timeout=TIMEOUT) + except Exception: + pass + def get_networks(self): r = {} From 6a4f685d0491d089151cc93e3267cf6c258b9f65 Mon Sep 17 00:00:00 2001 From: Trey Moen <50057480+greatgitsby@users.noreply.github.com> Date: Wed, 3 Sep 2025 11:34:03 -0700 Subject: [PATCH 109/188] feat(esim): bootstrap (#36094) * bootstrap * more * fix * simple * moar * clarify --------- Co-authored-by: Comma Device --- system/hardware/base.py | 7 +++++++ system/hardware/esim.py | 27 ++++++++++++++++++++++++++- system/hardware/tici/esim.py | 29 +++++++++++++++++++++++++++++ system/hardware/tici/hardware.py | 2 +- 4 files changed, 63 insertions(+), 2 deletions(-) diff --git a/system/hardware/base.py b/system/hardware/base.py index b457ea4e17..ce97bf294d 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -65,6 +65,10 @@ class ThermalConfig: return ret class LPABase(ABC): + @abstractmethod + def bootstrap(self) -> None: + pass + @abstractmethod def list_profiles(self) -> list[Profile]: pass @@ -89,6 +93,9 @@ class LPABase(ABC): def switch_profile(self, iccid: str) -> None: pass + def is_comma_profile(self, iccid: str) -> bool: + return any(iccid.startswith(prefix) for prefix in ('8985235',)) + class HardwareBase(ABC): @staticmethod def get_cmdline() -> dict[str, str]: diff --git a/system/hardware/esim.py b/system/hardware/esim.py index 58ead6593f..909ad41e03 100755 --- a/system/hardware/esim.py +++ b/system/hardware/esim.py @@ -3,10 +3,32 @@ import argparse import time from openpilot.system.hardware import HARDWARE +from openpilot.system.hardware.base import LPABase + + +def bootstrap(lpa: LPABase) -> None: + print('┌──────────────────────────────────────────────────────────────────────────────â”') + print('│ WARNING, PLEASE READ BEFORE PROCEEDING │') + print('│ │') + print('│ this is an irreversible operation that will remove the comma-provisioned │') + print('│ profile. │') + print('│ │') + print('│ after this operation, you must purchase a new eSIM from comma in order to │') + print('│ use the comma prime subscription again. │') + print('└──────────────────────────────────────────────────────────────────────────────┘') + print() + for severity in ('sure', '100% sure'): + print(f'are you {severity} you want to proceed? (y/N) ', end='') + confirm = input() + if confirm != 'y': + print('aborting') + exit(0) + lpa.bootstrap() if __name__ == '__main__': parser = argparse.ArgumentParser(prog='esim.py', description='manage eSIM profiles on your comma device', epilog='comma.ai') + parser.add_argument('--bootstrap', action='store_true', help='bootstrap the eUICC (required before downloading profiles)') parser.add_argument('--backend', choices=['qmi', 'at'], default='qmi', help='use the specified backend, defaults to qmi') parser.add_argument('--switch', metavar='iccid', help='switch to profile') parser.add_argument('--delete', metavar='iccid', help='delete profile (warning: this cannot be undone)') @@ -16,7 +38,10 @@ if __name__ == '__main__': mutated = False lpa = HARDWARE.get_sim_lpa() - if args.switch: + if args.bootstrap: + bootstrap(lpa) + mutated = True + elif args.switch: lpa.switch_profile(args.switch) mutated = True elif args.delete: diff --git a/system/hardware/tici/esim.py b/system/hardware/tici/esim.py index b489286f50..391ba45531 100644 --- a/system/hardware/tici/esim.py +++ b/system/hardware/tici/esim.py @@ -40,6 +40,7 @@ class TiciLPA(LPABase): self._process_notifications() def download_profile(self, qr: str, nickname: str | None = None) -> None: + self._check_bootstrapped() msgs = self._invoke('profile', 'download', '-a', qr) self._validate_successful(msgs) new_profile = next((m for m in msgs if m['payload']['message'] == 'es8p_meatadata_parse'), None) @@ -54,6 +55,7 @@ class TiciLPA(LPABase): self._validate_successful(self._invoke('profile', 'nickname', iccid, nickname)) def switch_profile(self, iccid: str) -> None: + self._check_bootstrapped() self._validate_profile_exists(iccid) latest = self.get_active_profile() if latest and latest.iccid == iccid: @@ -61,6 +63,33 @@ class TiciLPA(LPABase): self._validate_successful(self._invoke('profile', 'enable', iccid)) self._process_notifications() + def bootstrap(self) -> None: + """ + find all comma-provisioned profiles and delete them. they conflict with user-provisioned profiles + and must be deleted. + + **note**: this is a **very** destructive operation. you **must** purchase a new comma SIM in order + to use comma prime again. + """ + if self._is_bootstrapped(): + return + + for p in self.list_profiles(): + if self.is_comma_profile(p.iccid): + self._disable_profile(p.iccid) + self.delete_profile(p.iccid) + + def _disable_profile(self, iccid: str) -> None: + self._validate_successful(self._invoke('profile', 'disable', iccid)) + self._process_notifications() + + def _check_bootstrapped(self) -> None: + assert self._is_bootstrapped(), 'eUICC is not bootstrapped, please bootstrap before performing this operation' + + def _is_bootstrapped(self) -> bool: + """ check if any comma provisioned profiles are on the eUICC """ + return not any(self.is_comma_profile(iccid) for iccid in (p.iccid for p in self.list_profiles())) + def _invoke(self, *cmd: str): proc = subprocess.Popen(['sudo', '-E', 'lpac'] + list(cmd), stdout=subprocess.PIPE, stderr=subprocess.PIPE, env=self.env) try: diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 13a30ae011..0f50acdc38 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -487,7 +487,7 @@ class Tici(HardwareBase): # eSIM prime dest = "/etc/NetworkManager/system-connections/esim.nmconnection" - if sim_id.startswith('8985235') and not os.path.exists(dest): + if self.get_sim_lpa().is_comma_profile(sim_id) and not os.path.exists(dest): with open(Path(__file__).parent/'esim.nmconnection') as f, tempfile.NamedTemporaryFile(mode='w') as tf: dat = f.read() dat = dat.replace("sim-id=", f"sim-id={sim_id}") From a5044302a290cbb31a2e69a3a0e5bb769193d5b8 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Wed, 3 Sep 2025 16:06:41 -0700 Subject: [PATCH 110/188] auto source: auto source --- tools/auto_source.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tools/auto_source.py b/tools/auto_source.py index 401929a9ad..bef6a43e53 100755 --- a/tools/auto_source.py +++ b/tools/auto_source.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import sys -from openpilot.tools.lib.logreader import LogReader +from openpilot.tools.lib.logreader import LogReader, ReadMode def main(): @@ -9,7 +9,7 @@ def main(): sys.exit(1) log_path = sys.argv[1] - lr = LogReader(log_path, sort_by_time=True) + lr = LogReader(log_path, default_mode=ReadMode.AUTO, sort_by_time=True) print("\n".join(lr.logreader_identifiers)) From 0593667601a86a9ae39a4127ac048a518d9235e8 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 4 Sep 2025 14:44:42 +0200 Subject: [PATCH 111/188] bugfix: improve error handling in model fetching process (SUN-87) (#1205) * bugfix: improve error handling in model fetching process * cleanup * bugfix: refine error handling in model fetching process --- sunnypilot/models/fetcher.py | 44 +++++++++++++++++++++++++----------- 1 file changed, 31 insertions(+), 13 deletions(-) diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 358c65fe34..e681d150b3 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -8,6 +8,7 @@ See the LICENSE.md file in the root directory for more details. import time import requests +from requests.exceptions import (SSLError, RequestException, HTTPError) from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog from sunnypilot.models.helpers import is_bundle_version_compatible @@ -122,19 +123,36 @@ class ModelFetcher: self.model_cache = ModelCache(params) self.model_parser = ModelParser() - def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle]: - """Fetches fresh model data from remote and updates cache""" + def _fetch_and_cache_models(self) -> list[custom.ModelManagerSP.ModelBundle] | None: + """Fetches fresh model data from remote and updates cache. + Returns None on transport errors. Raises on 404 and other fatal HTTP errors. + """ try: response = requests.get(self.MODEL_URL, timeout=10) - response.raise_for_status() - json_data = response.json() + # Explicitly handle 404 differently + if response.status_code == 404: + cloudlog.error(f"Models URL returned 404 Not Found: {self.MODEL_URL}") + raise HTTPError(f"404 Not Found: {self.MODEL_URL}", response=response) + + # Raise for any other 4xx/5xx + response.raise_for_status() + + json_data = response.json() self.model_cache.set(json_data) cloudlog.debug("Successfully updated models cache") return self.model_parser.parse_models(json_data) - except Exception: - cloudlog.exception("Error fetching models") - raise + + except ConnectionError as e: + cloudlog.warning(f"DNS/connection error while fetching models: {e}") + except SSLError as e: + cloudlog.warning(f"SSL error while fetching models: {e}") + except RequestException as e: + cloudlog.exception(f"Request transport error while fetching models: {e}") + except Exception as e: + cloudlog.exception(f"Unexpected error fetching models: {e}") + + return None def get_available_bundles(self) -> list[custom.ModelManagerSP.ModelBundle]: """Gets the list of available models, with smart cache handling""" @@ -144,12 +162,12 @@ class ModelFetcher: cloudlog.debug("Using valid cached models data") return self.model_parser.parse_models(cached_data) - try: - return self._fetch_and_cache_models() - except Exception: - if not cached_data: - cloudlog.exception("Failed to fetch fresh data and no cache available") - raise + fetched_bundles = self._fetch_and_cache_models() + if fetched_bundles is not None: + return fetched_bundles + + if not cached_data: + cloudlog.warning("Failed to fetch fresh data and no cache available") cloudlog.warning("Failed to fetch fresh data. Using expired cache as fallback") return self.model_parser.parse_models(cached_data) From 8ccb777192535be10e072eba45db4c05c134fc5a Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 4 Sep 2025 14:45:03 +0200 Subject: [PATCH 112/188] bugfix: improve exception handling for sunnylinkd (SUN-89) (#1207) * bugfix: improve exception handling for WebSocket connections in sunnylinkd * bugfix: enhance exception handling for WebSocket connections in sunnylinkd * bugfix: improve OSError handling in sunnylinkd for better error reporting --- sunnypilot/sunnylink/athena/sunnylinkd.py | 37 +++++++++++++++-------- 1 file changed, 25 insertions(+), 12 deletions(-) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index 90eae1dfe8..a9204d54e6 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -3,6 +3,7 @@ from __future__ import annotations import base64 +import errno import gzip import os import ssl @@ -17,7 +18,7 @@ from openpilot.common.swaglog import cloudlog from openpilot.system.athena.athenad import ws_send, jsonrpc_handler, \ recv_queue, UploadQueueCache, upload_queue, cur_upload_items, backoff, ws_manage, log_handler, start_local_proxy_shim, upload_handler from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, - create_connection) + create_connection, WebSocketConnectionClosedException) import cereal.messaging as messaging from sunnypilot.sunnylink.api import SunnylinkApi @@ -107,10 +108,13 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: except WebSocketTimeoutException: ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping if ns_since_last_ping > SUNNYLINK_RECONNECT_TIMEOUT_S * 1e9: - cloudlog.exception("sunnylinkd.ws_recv.timeout") + cloudlog.warning("sunnylinkd.ws_recv.timeout") end_event.set() - except Exception: - cloudlog.exception("sunnylinkd.ws_recv.exception") + except Exception as e: + if isinstance(e, WebSocketConnectionClosedException): + cloudlog.warning(f"sunnylinkd.ws_recv.{type(e).__name__}") + else: + cloudlog.exception("sunnylinkd.ws_recv.exception") end_event.set() @@ -137,11 +141,15 @@ def ws_queue(end_event: threading.Event) -> None: sunnylink_api.resume_queued(timeout=29) resume_requested = True tries = 0 - except Exception: - cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception") + except Exception as e: + if isinstance(e, (ConnectionError, TimeoutError)): + cloudlog.warning(f"sunnylinkd.ws_queue.resume_queued.{type(e).__name__}") + else: + cloudlog.exception("sunnylinkd.ws_queue.resume_queued.exception") + resume_requested = False tries += 1 - time.sleep(backoff(tries)) # Wait for the backoff time before the next attempt + time.sleep(backoff(tries)) if end_event.is_set(): cloudlog.debug("end_event is set, exiting ws_queue thread") @@ -252,14 +260,19 @@ def main(exit_event: threading.Event = None): handle_long_poll(ws, exit_event) except (KeyboardInterrupt, SystemExit): break - except (ConnectionError, TimeoutError, WebSocketException): + except Exception as e: conn_retries += 1 params.remove("LastSunnylinkPingTime") - except Exception: - cloudlog.exception("sunnylinkd.main.exception") - conn_retries += 1 - params.remove("LastSunnylinkPingTime") + if isinstance(e, (ConnectionError, TimeoutError, WebSocketException)): + cloudlog.warning(f"sunnylinkd.main.{type(e).__name__}") + elif isinstance(e, OSError): + name = errno.errorcode.get(e.errno or -1, "UNKNOWN") + msg = f"sunnylinkd.main.OSError.{name} ({e.errno})" + is_expected_error = e.errno in (errno.ENETDOWN, errno.ENETRESET, errno.ENETUNREACH) + cloudlog.warning(msg) if is_expected_error else cloudlog.exception(msg) + else: + cloudlog.exception("sunnylinkd.main.exception") time.sleep(backoff(conn_retries)) From 0871abcf55947d9567da152d42424cc28fca6742 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 4 Sep 2025 14:45:37 +0200 Subject: [PATCH 113/188] bugfix: fix fetching params for sunnylink and backup (#1177) * Hotfix for the params stuff until I rework this properly and leverage the new data types * Revert "Hotfix for the params stuff until I rework this properly and leverage the new data types" This reverts commit c6031b29d92d3ff5b679ffce3ba53611bb2dba0e. * refactor: enhance getParams function to support JSON and bytes types with optional compression * refactor: add TODO for enhancing server support of metadata in sunnylinkd.py * lint and clean * refactor: update value handling in getParams to return decoded values for JSON serialization * refactor: simplify params_dict initialization by removing type hint * refactor: update response handling in getParams to include JSON serialization of params * refactor: update response handling in getParams to include JSON serialization of params * Add to dic types * refactor: extract get_param_as_byte function for improved parameter handling and fix backup inconsistencies * cleanup * ensure error propagates on backup fail --- sunnypilot/sunnylink/athena/sunnylinkd.py | 25 +++++++++++++++-------- sunnypilot/sunnylink/backups/manager.py | 10 ++++++--- sunnypilot/sunnylink/utils.py | 15 +++++++++++++- 3 files changed, 37 insertions(+), 13 deletions(-) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index a9204d54e6..363fa1defc 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -5,6 +5,7 @@ from __future__ import annotations import base64 import errno import gzip +import json import os import ssl import threading @@ -22,7 +23,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce import cereal.messaging as messaging from sunnypilot.sunnylink.api import SunnylinkApi -from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready +from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai') HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) @@ -179,16 +180,22 @@ def getParamsAllKeys() -> list[str]: @dispatcher.add_method def getParams(params_keys: list[str], compression: bool = False) -> str | dict[str, str]: + params = Params() + try: - params = Params() - params_dict: dict[str, bytes] = {key: params.get(key) or b'' for key in params_keys} + param_keys_validated = [key for key in params_keys if key in getParamsAllKeys()] + params_dict: dict[str, list[dict[str, str | bool | int ]]] = {"params": [ + { + "key": key, + "value": base64.b64encode(gzip.compress(get_param_as_byte(key)) if compression else get_param_as_byte(key)).decode('utf-8'), + "type": int(params.get_type(key).value), + "is_compressed": compression + } for key in param_keys_validated + ]} - # Compress the values before encoding to base64 as output from params.get is bytes and same for compression - if compression: - params_dict = {key: gzip.compress(value) for key, value in params_dict.items()} - - # Last step is to encode the values to base64 and decode to utf-8 for JSON serialization - return {key: base64.b64encode(value).decode('utf-8') for key, value in params_dict.items()} + response = {str(param.get('key')): str(param.get('value')) for param in params_dict.get("params", [])} + response |= {"params": json.dumps(params_dict.get("params", []))} # Upcoming for settings v1 + return response except Exception as e: cloudlog.exception("sunnylinkd.getParams.exception", e) diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index f98088a1fb..315300c73c 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/sunnypilot/sunnylink/backups/manager.py @@ -20,6 +20,7 @@ from openpilot.system.version import get_version from cereal import messaging, custom from sunnypilot.sunnylink.api import SunnylinkApi from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder +from sunnypilot.sunnylink.utils import get_param_as_byte class OperationType(Enum): @@ -74,7 +75,7 @@ class BackupManagerSP: config_data = {} params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)] for param in params_to_backup: - value = str(self.params.get(param)).encode('utf-8') + value = get_param_as_byte(param) if value is not None: config_data[param] = base64.b64encode(value).decode('utf-8') return config_data @@ -113,6 +114,7 @@ class BackupManagerSP: payload = json.loads(json.dumps(backup_info.to_dict(), cls=SnakeCaseEncoder)) self._update_progress(75.0, OperationType.BACKUP) + cloudlog.debug(f"Uploading backup with payload: {json.dumps(payload)}") # Upload to sunnylink result = self.api.api_get( f"backup/{self.device_id}", @@ -124,9 +126,11 @@ class BackupManagerSP: if result: self.backup_status = custom.BackupManagerSP.Status.completed self._update_progress(100.0, OperationType.BACKUP) + cloudlog.info("Backup successfully created and uploaded") else: self.backup_status = custom.BackupManagerSP.Status.failed self.last_error = "Failed to upload backup" + cloudlog.error(result) self._report_status() return bool(self.backup_status == custom.BackupManagerSP.Status.completed) @@ -264,8 +268,8 @@ class BackupManagerSP: # Check for backup command if self.params.get_bool("BackupManager_CreateBackup"): try: - await self.create_backup() - reset_progress = True + if await self.create_backup(): + reset_progress = True finally: self.params.remove("BackupManager_CreateBackup") diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 35714eafe3..569afd26b6 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -1,5 +1,6 @@ +import json from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID -from openpilot.common.params import Params +from openpilot.common.params import Params, ParamKeyType from openpilot.system.version import is_prebuilt @@ -55,3 +56,15 @@ def get_api_token(): sunnylink_api = SunnylinkApi(sunnylink_dongle_id) token = sunnylink_api.get_token() print(f"API Token: {token}") + + +def get_param_as_byte(param_name: str) -> bytes: + params = Params() + param = params.get(param_name) + param_type = params.get_type(param_name) + + if param_type == ParamKeyType.BYTES: + return bytes(param) + elif param_type == ParamKeyType.JSON: + return json.dumps(param).encode('utf-8') + return str(param).encode('utf-8') From 0cd2bbf6c0065992a4eddec6f2cdbaa6f3a73829 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 10:08:51 -0400 Subject: [PATCH 114/188] [bot] Update Python packages (#1201) Update Python packages Co-authored-by: github-actions[bot] --- opendbc_repo | 2 +- panda | 2 +- uv.lock | 236 ++++++++++++++++++++++++++++----------------------- 3 files changed, 131 insertions(+), 109 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 004fa8df07..42bbd450b9 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 004fa8df07479ceb205691e0689b42180270c45b +Subproject commit 42bbd450b90b01c4f7f82fef324bea06341b1a54 diff --git a/panda b/panda index f10ddc6a89..7eab6fd61b 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit f10ddc6a89953440a15deec6352fff1d406a627a +Subproject commit 7eab6fd61bae085e0fd44cccb79dc6451163029e diff --git a/uv.lock b/uv.lock index 7010cdfb83..e6bfbb2048 100644 --- a/uv.lock +++ b/uv.lock @@ -510,27 +510,27 @@ wheels = [ [[package]] name = "fonttools" -version = "4.59.1" +version = "4.59.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/11/7f/29c9c3fe4246f6ad96fee52b88d0dc3a863c7563b0afc959e36d78b965dc/fonttools-4.59.1.tar.gz", hash = "sha256:74995b402ad09822a4c8002438e54940d9f1ecda898d2bb057729d7da983e4cb", size = 3534394, upload-time = "2025-08-14T16:28:14.266Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/a5/fba25f9fbdab96e26dedcaeeba125e5f05a09043bf888e0305326e55685b/fonttools-4.59.2.tar.gz", hash = "sha256:e72c0749b06113f50bcb80332364c6be83a9582d6e3db3fe0b280f996dc2ef22", size = 3540889, upload-time = "2025-08-27T16:40:30.97Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/34/62/9667599561f623d4a523cc9eb4f66f3b94b6155464110fa9aebbf90bbec7/fonttools-4.59.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4909cce2e35706f3d18c54d3dcce0414ba5e0fb436a454dffec459c61653b513", size = 2778815, upload-time = "2025-08-14T16:26:28.484Z" }, - { url = "https://files.pythonhosted.org/packages/8f/78/cc25bcb2ce86033a9df243418d175e58f1956a35047c685ef553acae67d6/fonttools-4.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efbec204fa9f877641747f2d9612b2b656071390d7a7ef07a9dbf0ecf9c7195c", size = 2341631, upload-time = "2025-08-14T16:26:30.396Z" }, - { url = "https://files.pythonhosted.org/packages/a4/cc/fcbb606dd6871f457ac32f281c20bcd6cc77d9fce77b5a4e2b2afab1f500/fonttools-4.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39dfd42cc2dc647b2c5469bc7a5b234d9a49e72565b96dd14ae6f11c2c59ef15", size = 5022222, upload-time = "2025-08-14T16:26:32.447Z" }, - { url = "https://files.pythonhosted.org/packages/61/96/c0b1cf2b74d08eb616a80dbf5564351fe4686147291a25f7dce8ace51eb3/fonttools-4.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b11bc177a0d428b37890825d7d025040d591aa833f85f8d8878ed183354f47df", size = 4966512, upload-time = "2025-08-14T16:26:34.621Z" }, - { url = "https://files.pythonhosted.org/packages/a4/26/51ce2e3e0835ffc2562b1b11d1fb9dafd0aca89c9041b64a9e903790a761/fonttools-4.59.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b9b4c35b3be45e5bc774d3fc9608bbf4f9a8d371103b858c80edbeed31dd5aa", size = 5001645, upload-time = "2025-08-14T16:26:36.876Z" }, - { url = "https://files.pythonhosted.org/packages/36/11/ef0b23f4266349b6d5ccbd1a07b7adc998d5bce925792aa5d1ec33f593e3/fonttools-4.59.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:01158376b8a418a0bae9625c476cebfcfcb5e6761e9d243b219cd58341e7afbb", size = 5113777, upload-time = "2025-08-14T16:26:39.002Z" }, - { url = "https://files.pythonhosted.org/packages/d0/da/b398fe61ef433da0a0472cdb5d4399124f7581ffe1a31b6242c91477d802/fonttools-4.59.1-cp311-cp311-win32.whl", hash = "sha256:cf7c5089d37787387123f1cb8f1793a47c5e1e3d1e4e7bfbc1cc96e0f925eabe", size = 2215076, upload-time = "2025-08-14T16:26:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/94/bd/e2624d06ab94e41c7c77727b2941f1baed7edb647e63503953e6888020c9/fonttools-4.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:c866eef7a0ba320486ade6c32bfc12813d1a5db8567e6904fb56d3d40acc5116", size = 2262779, upload-time = "2025-08-14T16:26:43.483Z" }, - { url = "https://files.pythonhosted.org/packages/ac/fe/6e069cc4cb8881d164a9bd956e9df555bc62d3eb36f6282e43440200009c/fonttools-4.59.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:43ab814bbba5f02a93a152ee61a04182bb5809bd2bc3609f7822e12c53ae2c91", size = 2769172, upload-time = "2025-08-14T16:26:45.729Z" }, - { url = "https://files.pythonhosted.org/packages/b9/98/ec4e03f748fefa0dd72d9d95235aff6fef16601267f4a2340f0e16b9330f/fonttools-4.59.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4f04c3ffbfa0baafcbc550657cf83657034eb63304d27b05cff1653b448ccff6", size = 2337281, upload-time = "2025-08-14T16:26:47.921Z" }, - { url = "https://files.pythonhosted.org/packages/8b/b1/890360a7e3d04a30ba50b267aca2783f4c1364363797e892e78a4f036076/fonttools-4.59.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d601b153e51a5a6221f0d4ec077b6bfc6ac35bfe6c19aeaa233d8990b2b71726", size = 4909215, upload-time = "2025-08-14T16:26:49.682Z" }, - { url = "https://files.pythonhosted.org/packages/8a/ec/2490599550d6c9c97a44c1e36ef4de52d6acf742359eaa385735e30c05c4/fonttools-4.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c735e385e30278c54f43a0d056736942023c9043f84ee1021eff9fd616d17693", size = 4951958, upload-time = "2025-08-14T16:26:51.616Z" }, - { url = "https://files.pythonhosted.org/packages/d1/40/bd053f6f7634234a9b9805ff8ae4f32df4f2168bee23cafd1271ba9915a9/fonttools-4.59.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1017413cdc8555dce7ee23720da490282ab7ec1cf022af90a241f33f9a49afc4", size = 4894738, upload-time = "2025-08-14T16:26:53.836Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a1/3cd12a010d288325a7cfcf298a84825f0f9c29b01dee1baba64edfe89257/fonttools-4.59.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5c6d8d773470a5107052874341ed3c487c16ecd179976d81afed89dea5cd7406", size = 5045983, upload-time = "2025-08-14T16:26:56.153Z" }, - { url = "https://files.pythonhosted.org/packages/a2/af/8a2c3f6619cc43cf87951405337cc8460d08a4e717bb05eaa94b335d11dc/fonttools-4.59.1-cp312-cp312-win32.whl", hash = "sha256:2a2d0d33307f6ad3a2086a95dd607c202ea8852fa9fb52af9b48811154d1428a", size = 2203407, upload-time = "2025-08-14T16:26:58.165Z" }, - { url = "https://files.pythonhosted.org/packages/8e/f2/a19b874ddbd3ebcf11d7e25188ef9ac3f68b9219c62263acb34aca8cde05/fonttools-4.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b9e4fa7eaf046ed6ac470f6033d52c052481ff7a6e0a92373d14f556f298dc0", size = 2251561, upload-time = "2025-08-14T16:27:00.646Z" }, - { url = "https://files.pythonhosted.org/packages/0f/64/9d606e66d498917cd7a2ff24f558010d42d6fd4576d9dd57f0bd98333f5a/fonttools-4.59.1-py3-none-any.whl", hash = "sha256:647db657073672a8330608970a984d51573557f328030566521bc03415535042", size = 1130094, upload-time = "2025-08-14T16:28:12.048Z" }, + { url = "https://files.pythonhosted.org/packages/f8/53/742fcd750ae0bdc74de4c0ff923111199cc2f90a4ee87aaddad505b6f477/fonttools-4.59.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:511946e8d7ea5c0d6c7a53c4cb3ee48eda9ab9797cd9bf5d95829a398400354f", size = 2774961, upload-time = "2025-08-27T16:38:47.536Z" }, + { url = "https://files.pythonhosted.org/packages/57/2a/976f5f9fa3b4dd911dc58d07358467bec20e813d933bc5d3db1a955dd456/fonttools-4.59.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5e2682cf7be766d84f462ba8828d01e00c8751a8e8e7ce12d7784ccb69a30d", size = 2344690, upload-time = "2025-08-27T16:38:49.723Z" }, + { url = "https://files.pythonhosted.org/packages/c1/8f/b7eefc274fcf370911e292e95565c8253b0b87c82a53919ab3c795a4f50e/fonttools-4.59.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5729e12a982dba3eeae650de48b06f3b9ddb51e9aee2fcaf195b7d09a96250e2", size = 5026910, upload-time = "2025-08-27T16:38:51.904Z" }, + { url = "https://files.pythonhosted.org/packages/69/95/864726eaa8f9d4e053d0c462e64d5830ec7c599cbdf1db9e40f25ca3972e/fonttools-4.59.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c52694eae5d652361d59ecdb5a2246bff7cff13b6367a12da8499e9df56d148d", size = 4971031, upload-time = "2025-08-27T16:38:53.676Z" }, + { url = "https://files.pythonhosted.org/packages/24/4c/b8c4735ebdea20696277c70c79e0de615dbe477834e5a7c2569aa1db4033/fonttools-4.59.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f1bbc23ba1312bd8959896f46f667753b90216852d2a8cfa2d07e0cb234144", size = 5006112, upload-time = "2025-08-27T16:38:55.69Z" }, + { url = "https://files.pythonhosted.org/packages/3b/23/f9ea29c292aa2fc1ea381b2e5621ac436d5e3e0a5dee24ffe5404e58eae8/fonttools-4.59.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a1bfe5378962825dabe741720885e8b9ae9745ec7ecc4a5ec1f1ce59a6062bf", size = 5117671, upload-time = "2025-08-27T16:38:58.984Z" }, + { url = "https://files.pythonhosted.org/packages/ba/07/cfea304c555bf06e86071ff2a3916bc90f7c07ec85b23bab758d4908c33d/fonttools-4.59.2-cp311-cp311-win32.whl", hash = "sha256:e937790f3c2c18a1cbc7da101550a84319eb48023a715914477d2e7faeaba570", size = 2218157, upload-time = "2025-08-27T16:39:00.75Z" }, + { url = "https://files.pythonhosted.org/packages/d7/de/35d839aa69db737a3f9f3a45000ca24721834d40118652a5775d5eca8ebb/fonttools-4.59.2-cp311-cp311-win_amd64.whl", hash = "sha256:9836394e2f4ce5f9c0a7690ee93bd90aa1adc6b054f1a57b562c5d242c903104", size = 2265846, upload-time = "2025-08-27T16:39:02.453Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3d/1f45db2df51e7bfa55492e8f23f383d372200be3a0ded4bf56a92753dd1f/fonttools-4.59.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82906d002c349cad647a7634b004825a7335f8159d0d035ae89253b4abf6f3ea", size = 2769711, upload-time = "2025-08-27T16:39:04.423Z" }, + { url = "https://files.pythonhosted.org/packages/29/df/cd236ab32a8abfd11558f296e064424258db5edefd1279ffdbcfd4fd8b76/fonttools-4.59.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a10c1bd7644dc58f8862d8ba0cf9fb7fef0af01ea184ba6ce3f50ab7dfe74d5a", size = 2340225, upload-time = "2025-08-27T16:39:06.143Z" }, + { url = "https://files.pythonhosted.org/packages/98/12/b6f9f964fe6d4b4dd4406bcbd3328821c3de1f909ffc3ffa558fe72af48c/fonttools-4.59.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:738f31f23e0339785fd67652a94bc69ea49e413dfdb14dcb8c8ff383d249464e", size = 4912766, upload-time = "2025-08-27T16:39:08.138Z" }, + { url = "https://files.pythonhosted.org/packages/73/78/82bde2f2d2c306ef3909b927363170b83df96171f74e0ccb47ad344563cd/fonttools-4.59.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ec99f9bdfee9cdb4a9172f9e8fd578cce5feb231f598909e0aecf5418da4f25", size = 4955178, upload-time = "2025-08-27T16:39:10.094Z" }, + { url = "https://files.pythonhosted.org/packages/92/77/7de766afe2d31dda8ee46d7e479f35c7d48747e558961489a2d6e3a02bd4/fonttools-4.59.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0476ea74161322e08c7a982f83558a2b81b491509984523a1a540baf8611cc31", size = 4897898, upload-time = "2025-08-27T16:39:12.087Z" }, + { url = "https://files.pythonhosted.org/packages/c5/77/ce0e0b905d62a06415fda9f2b2e109a24a5db54a59502b769e9e297d2242/fonttools-4.59.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95922a922daa1f77cc72611747c156cfb38030ead72436a2c551d30ecef519b9", size = 5049144, upload-time = "2025-08-27T16:39:13.84Z" }, + { url = "https://files.pythonhosted.org/packages/d9/ea/870d93aefd23fff2e07cbeebdc332527868422a433c64062c09d4d5e7fe6/fonttools-4.59.2-cp312-cp312-win32.whl", hash = "sha256:39ad9612c6a622726a6a130e8ab15794558591f999673f1ee7d2f3d30f6a3e1c", size = 2206473, upload-time = "2025-08-27T16:39:15.854Z" }, + { url = "https://files.pythonhosted.org/packages/61/c4/e44bad000c4a4bb2e9ca11491d266e857df98ab6d7428441b173f0fe2517/fonttools-4.59.2-cp312-cp312-win_amd64.whl", hash = "sha256:980fd7388e461b19a881d35013fec32c713ffea1fc37aef2f77d11f332dfd7da", size = 2254706, upload-time = "2025-08-27T16:39:17.893Z" }, + { url = "https://files.pythonhosted.org/packages/65/a4/d2f7be3c86708912c02571db0b550121caab8cd88a3c0aacb9cfa15ea66e/fonttools-4.59.2-py3-none-any.whl", hash = "sha256:8bd0f759020e87bb5d323e6283914d9bf4ae35a7307dafb2cbd1e379e720ad37", size = 1132315, upload-time = "2025-08-27T16:40:28.984Z" }, ] [[package]] @@ -846,7 +846,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.5" +version = "3.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -859,25 +859,25 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a0/59/c3e6453a9676ffba145309a73c462bb407f4400de7de3f2b41af70720a3c/matplotlib-3.10.6.tar.gz", hash = "sha256:ec01b645840dd1996df21ee37f208cd8ba57644779fa20464010638013d3203c", size = 34804264, upload-time = "2025-08-30T00:14:25.137Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, - { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, - { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, - { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, - { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, - { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, - { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, - { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, - { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, - { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, - { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, - { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, - { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, - { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, - { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, - { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, - { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/5d3665aa44c49005aaacaa68ddea6fcb27345961cd538a98bb0177934ede/matplotlib-3.10.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:905b60d1cb0ee604ce65b297b61cf8be9f4e6cfecf95a3fe1c388b5266bc8f4f", size = 8257527, upload-time = "2025-08-30T00:12:45.31Z" }, + { url = "https://files.pythonhosted.org/packages/8c/af/30ddefe19ca67eebd70047dabf50f899eaff6f3c5e6a1a7edaecaf63f794/matplotlib-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bac38d816637343e53d7185d0c66677ff30ffb131044a81898b5792c956ba76", size = 8119583, upload-time = "2025-08-30T00:12:47.236Z" }, + { url = "https://files.pythonhosted.org/packages/d3/29/4a8650a3dcae97fa4f375d46efcb25920d67b512186f8a6788b896062a81/matplotlib-3.10.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:942a8de2b5bfff1de31d95722f702e2966b8a7e31f4e68f7cd963c7cd8861cf6", size = 8692682, upload-time = "2025-08-30T00:12:48.781Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/b793b9cb061cfd5d42ff0f69d1822f8d5dbc94e004618e48a97a8373179a/matplotlib-3.10.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3276c85370bc0dfca051ec65c5817d1e0f8f5ce1b7787528ec8ed2d524bbc2f", size = 9521065, upload-time = "2025-08-30T00:12:50.602Z" }, + { url = "https://files.pythonhosted.org/packages/f7/c5/53de5629f223c1c66668d46ac2621961970d21916a4bc3862b174eb2a88f/matplotlib-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9df5851b219225731f564e4b9e7f2ac1e13c9e6481f941b5631a0f8e2d9387ce", size = 9576888, upload-time = "2025-08-30T00:12:52.92Z" }, + { url = "https://files.pythonhosted.org/packages/fc/8e/0a18d6d7d2d0a2e66585032a760d13662e5250c784d53ad50434e9560991/matplotlib-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:abb5d9478625dd9c9eb51a06d39aae71eda749ae9b3138afb23eb38824026c7e", size = 8115158, upload-time = "2025-08-30T00:12:54.863Z" }, + { url = "https://files.pythonhosted.org/packages/07/b3/1a5107bb66c261e23b9338070702597a2d374e5aa7004b7adfc754fbed02/matplotlib-3.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:886f989ccfae63659183173bb3fced7fd65e9eb793c3cc21c273add368536951", size = 7992444, upload-time = "2025-08-30T00:12:57.067Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1a/7042f7430055d567cc3257ac409fcf608599ab27459457f13772c2d9778b/matplotlib-3.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ca662df6a80bd426f871105fdd69db7543e28e73a9f2afe80de7e531eb2347", size = 8272404, upload-time = "2025-08-30T00:12:59.112Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5d/1d5f33f5b43f4f9e69e6a5fe1fb9090936ae7bc8e2ff6158e7a76542633b/matplotlib-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1678bb61d897bb4ac4757b5ecfb02bfb3fddf7f808000fb81e09c510712fda75", size = 8128262, upload-time = "2025-08-30T00:13:01.141Z" }, + { url = "https://files.pythonhosted.org/packages/67/c3/135fdbbbf84e0979712df58e5e22b4f257b3f5e52a3c4aacf1b8abec0d09/matplotlib-3.10.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:56cd2d20842f58c03d2d6e6c1f1cf5548ad6f66b91e1e48f814e4fb5abd1cb95", size = 8697008, upload-time = "2025-08-30T00:13:03.24Z" }, + { url = "https://files.pythonhosted.org/packages/9c/be/c443ea428fb2488a3ea7608714b1bd85a82738c45da21b447dc49e2f8e5d/matplotlib-3.10.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:662df55604a2f9a45435566d6e2660e41efe83cd94f4288dfbf1e6d1eae4b0bb", size = 9530166, upload-time = "2025-08-30T00:13:05.951Z" }, + { url = "https://files.pythonhosted.org/packages/a9/35/48441422b044d74034aea2a3e0d1a49023f12150ebc58f16600132b9bbaf/matplotlib-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08f141d55148cd1fc870c3387d70ca4df16dee10e909b3b038782bd4bda6ea07", size = 9593105, upload-time = "2025-08-30T00:13:08.356Z" }, + { url = "https://files.pythonhosted.org/packages/45/c3/994ef20eb4154ab84cc08d033834555319e4af970165e6c8894050af0b3c/matplotlib-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:590f5925c2d650b5c9d813c5b3b5fc53f2929c3f8ef463e4ecfa7e052044fb2b", size = 8122784, upload-time = "2025-08-30T00:13:10.367Z" }, + { url = "https://files.pythonhosted.org/packages/57/b8/5c85d9ae0e40f04e71bedb053aada5d6bab1f9b5399a0937afb5d6b02d98/matplotlib-3.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:f44c8d264a71609c79a78d50349e724f5d5fc3684ead7c2a473665ee63d868aa", size = 7992823, upload-time = "2025-08-30T00:13:12.24Z" }, + { url = "https://files.pythonhosted.org/packages/12/bb/02c35a51484aae5f49bd29f091286e7af5f3f677a9736c58a92b3c78baeb/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f2d684c3204fa62421bbf770ddfebc6b50130f9cad65531eeba19236d73bb488", size = 8252296, upload-time = "2025-08-30T00:14:19.49Z" }, + { url = "https://files.pythonhosted.org/packages/7d/85/41701e3092005aee9a2445f5ee3904d9dbd4a7df7a45905ffef29b7ef098/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6f4a69196e663a41d12a728fab8751177215357906436804217d6d9cf0d4d6cf", size = 8116749, upload-time = "2025-08-30T00:14:21.344Z" }, + { url = "https://files.pythonhosted.org/packages/16/53/8d8fa0ea32a8c8239e04d022f6c059ee5e1b77517769feccd50f1df43d6d/matplotlib-3.10.6-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d6ca6ef03dfd269f4ead566ec6f3fb9becf8dab146fb999022ed85ee9f6b3eb", size = 8693933, upload-time = "2025-08-30T00:14:22.942Z" }, ] [[package]] @@ -981,6 +981,27 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] +[[package]] +name = "ml-dtypes" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/a7/aad060393123cfb383956dca68402aff3db1e1caffd5764887ed5153f41b/ml_dtypes-0.5.3.tar.gz", hash = "sha256:95ce33057ba4d05df50b1f3cfefab22e351868a843b3b15a46c65836283670c9", size = 692316, upload-time = "2025-07-29T18:39:19.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/f1/720cb1409b5d0c05cff9040c0e9fba73fa4c67897d33babf905d5d46a070/ml_dtypes-0.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a177b882667c69422402df6ed5c3428ce07ac2c1f844d8a1314944651439458", size = 667412, upload-time = "2025-07-29T18:38:25.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/d5/05861ede5d299f6599f86e6bc1291714e2116d96df003cfe23cc54bcc568/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9849ce7267444c0a717c80c6900997de4f36e2815ce34ac560a3edb2d9a64cd2", size = 4964606, upload-time = "2025-07-29T18:38:27.045Z" }, + { url = "https://files.pythonhosted.org/packages/db/dc/72992b68de367741bfab8df3b3fe7c29f982b7279d341aa5bf3e7ef737ea/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f5ae0309d9f888fd825c2e9d0241102fadaca81d888f26f845bc8c13c1e4ee", size = 4938435, upload-time = "2025-07-29T18:38:29.193Z" }, + { url = "https://files.pythonhosted.org/packages/81/1c/d27a930bca31fb07d975a2d7eaf3404f9388114463b9f15032813c98f893/ml_dtypes-0.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:58e39349d820b5702bb6f94ea0cb2dc8ec62ee81c0267d9622067d8333596a46", size = 206334, upload-time = "2025-07-29T18:38:30.687Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d8/6922499effa616012cb8dc445280f66d100a7ff39b35c864cfca019b3f89/ml_dtypes-0.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:66c2756ae6cfd7f5224e355c893cfd617fa2f747b8bbd8996152cbdebad9a184", size = 157584, upload-time = "2025-07-29T18:38:32.187Z" }, + { url = "https://files.pythonhosted.org/packages/0d/eb/bc07c88a6ab002b4635e44585d80fa0b350603f11a2097c9d1bfacc03357/ml_dtypes-0.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:156418abeeda48ea4797db6776db3c5bdab9ac7be197c1233771e0880c304057", size = 663864, upload-time = "2025-07-29T18:38:33.777Z" }, + { url = "https://files.pythonhosted.org/packages/cf/89/11af9b0f21b99e6386b6581ab40fb38d03225f9de5f55cf52097047e2826/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1db60c154989af253f6c4a34e8a540c2c9dce4d770784d426945e09908fbb177", size = 4951313, upload-time = "2025-07-29T18:38:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/d8/a9/b98b86426c24900b0c754aad006dce2863df7ce0bb2bcc2c02f9cc7e8489/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b255acada256d1fa8c35ed07b5f6d18bc21d1556f842fbc2d5718aea2cd9e55", size = 4928805, upload-time = "2025-07-29T18:38:38.29Z" }, + { url = "https://files.pythonhosted.org/packages/50/c1/85e6be4fc09c6175f36fb05a45917837f30af9a5146a5151cb3a3f0f9e09/ml_dtypes-0.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:da65e5fd3eea434ccb8984c3624bc234ddcc0d9f4c81864af611aaebcc08a50e", size = 208182, upload-time = "2025-07-29T18:38:39.72Z" }, + { url = "https://files.pythonhosted.org/packages/9e/17/cf5326d6867be057f232d0610de1458f70a8ce7b6290e4b4a277ea62b4cd/ml_dtypes-0.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:8bb9cd1ce63096567f5f42851f5843b5a0ea11511e50039a7649619abfb4ba6d", size = 161560, upload-time = "2025-07-29T18:38:41.072Z" }, +] + [[package]] name = "mouseinfo" version = "0.1.3" @@ -1155,27 +1176,28 @@ wheels = [ [[package]] name = "onnx" -version = "1.18.0" +version = "1.19.0" source = { registry = "https://pypi.org/simple" } dependencies = [ + { name = "ml-dtypes" }, { name = "numpy" }, { name = "protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3d/60/e56e8ec44ed34006e6d4a73c92a04d9eea6163cc12440e35045aec069175/onnx-1.18.0.tar.gz", hash = "sha256:3d8dbf9e996629131ba3aa1afd1d8239b660d1f830c6688dd7e03157cccd6b9c", size = 12563009, upload-time = "2025-05-12T22:03:09.626Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5b/bf/b0a63ee9f3759dcd177b28c6f2cb22f2aecc6d9b3efecaabc298883caa5f/onnx-1.19.0.tar.gz", hash = "sha256:aa3f70b60f54a29015e41639298ace06adf1dd6b023b9b30f1bca91bb0db9473", size = 11949859, upload-time = "2025-08-27T02:34:27.107Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ed/3a/a336dac4db1eddba2bf577191e5b7d3e4c26fcee5ec518a5a5b11d13540d/onnx-1.18.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:735e06d8d0cf250dc498f54038831401063c655a8d6e5975b2527a4e7d24be3e", size = 18281831, upload-time = "2025-05-12T22:02:06.429Z" }, - { url = "https://files.pythonhosted.org/packages/02/3a/56475a111120d1e5d11939acbcbb17c92198c8e64a205cd68e00bdfd8a1f/onnx-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73160799472e1a86083f786fecdf864cf43d55325492a9b5a1cfa64d8a523ecc", size = 17424359, upload-time = "2025-05-12T22:02:09.866Z" }, - { url = "https://files.pythonhosted.org/packages/cf/03/5eb5e9ef446ed9e78c4627faf3c1bc25e0f707116dd00e9811de232a8df5/onnx-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acafb3823238bbe8f4340c7ac32fb218689442e074d797bee1c5c9a02fdae75", size = 17586006, upload-time = "2025-05-12T22:02:13.217Z" }, - { url = "https://files.pythonhosted.org/packages/b0/4e/70943125729ce453271a6e46bb847b4a612496f64db6cbc6cb1f49f41ce1/onnx-1.18.0-cp311-cp311-win32.whl", hash = "sha256:4c8c4bbda760c654e65eaffddb1a7de71ec02e60092d33f9000521f897c99be9", size = 15734988, upload-time = "2025-05-12T22:02:16.561Z" }, - { url = "https://files.pythonhosted.org/packages/44/b0/435fd764011911e8f599e3361f0f33425b1004662c1ea33a0ad22e43db2d/onnx-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5810194f0f6be2e58c8d6dedc6119510df7a14280dd07ed5f0f0a85bd74816a", size = 15849576, upload-time = "2025-05-12T22:02:19.569Z" }, - { url = "https://files.pythonhosted.org/packages/6c/f0/9e31f4b4626d60f1c034f71b411810bc9fafe31f4e7dd3598effd1b50e05/onnx-1.18.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa1b7483fac6cdec26922174fc4433f8f5c2f239b1133c5625063bb3b35957d0", size = 15822961, upload-time = "2025-05-12T22:02:22.735Z" }, - { url = "https://files.pythonhosted.org/packages/a7/fe/16228aca685392a7114625b89aae98b2dc4058a47f0f467a376745efe8d0/onnx-1.18.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:521bac578448667cbb37c50bf05b53c301243ede8233029555239930996a625b", size = 18285770, upload-time = "2025-05-12T22:02:26.116Z" }, - { url = "https://files.pythonhosted.org/packages/1e/77/ba50a903a9b5e6f9be0fa50f59eb2fca4a26ee653375408fbc72c3acbf9f/onnx-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4da451bf1c5ae381f32d430004a89f0405bc57a8471b0bddb6325a5b334aa40", size = 17421291, upload-time = "2025-05-12T22:02:29.645Z" }, - { url = "https://files.pythonhosted.org/packages/11/23/25ec2ba723ac62b99e8fed6d7b59094dadb15e38d4c007331cc9ae3dfa5f/onnx-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99afac90b4cdb1471432203c3c1f74e16549c526df27056d39f41a9a47cfb4af", size = 17584084, upload-time = "2025-05-12T22:02:32.789Z" }, - { url = "https://files.pythonhosted.org/packages/6a/4d/2c253a36070fb43f340ff1d2c450df6a9ef50b938adcd105693fee43c4ee/onnx-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ee159b41a3ae58d9c7341cf432fc74b96aaf50bd7bb1160029f657b40dc69715", size = 15734892, upload-time = "2025-05-12T22:02:35.527Z" }, - { url = "https://files.pythonhosted.org/packages/e8/92/048ba8fafe6b2b9a268ec2fb80def7e66c0b32ab2cae74de886981f05a27/onnx-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:102c04edc76b16e9dfeda5a64c1fccd7d3d2913b1544750c01d38f1ac3c04e05", size = 15850336, upload-time = "2025-05-12T22:02:38.545Z" }, - { url = "https://files.pythonhosted.org/packages/a1/66/bbc4ffedd44165dcc407a51ea4c592802a5391ce3dc94aa5045350f64635/onnx-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:911b37d724a5d97396f3c2ef9ea25361c55cbc9aa18d75b12a52b620b67145af", size = 15823802, upload-time = "2025-05-12T22:02:42.037Z" }, + { url = "https://files.pythonhosted.org/packages/db/5c/b959b17608cfb6ccf6359b39fe56a5b0b7d965b3d6e6a3c0add90812c36e/onnx-1.19.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:206f00c47b85b5c7af79671e3307147407991a17994c26974565aadc9e96e4e4", size = 18312580, upload-time = "2025-08-27T02:33:03.081Z" }, + { url = "https://files.pythonhosted.org/packages/2c/ee/ac052bbbc832abe0debb784c2c57f9582444fb5f51d63c2967fd04432444/onnx-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4d7bee94abaac28988b50da675ae99ef8dd3ce16210d591fbd0b214a5930beb3", size = 18029165, upload-time = "2025-08-27T02:33:05.771Z" }, + { url = "https://files.pythonhosted.org/packages/5c/c9/8687ba0948d46fd61b04e3952af9237883bbf8f16d716e7ed27e688d73b8/onnx-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7730b96b68c0c354bbc7857961bb4909b9aaa171360a8e3708d0a4c749aaadeb", size = 18202125, upload-time = "2025-08-27T02:33:09.325Z" }, + { url = "https://files.pythonhosted.org/packages/e2/16/6249c013e81bd689f46f96c7236d7677f1af5dd9ef22746716b48f10e506/onnx-1.19.0-cp311-cp311-win32.whl", hash = "sha256:7cb7a3ad8059d1a0dfdc5e0a98f71837d82002e441f112825403b137227c2c97", size = 16332738, upload-time = "2025-08-27T02:33:12.448Z" }, + { url = "https://files.pythonhosted.org/packages/6a/28/34a1e2166e418c6a78e5c82e66f409d9da9317832f11c647f7d4e23846a6/onnx-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:d75452a9be868bd30c3ef6aa5991df89bbfe53d0d90b2325c5e730fbd91fff85", size = 16452303, upload-time = "2025-08-27T02:33:15.176Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b7/639664626e5ba8027860c4d2a639ee02b37e9c322215c921e9222513c3aa/onnx-1.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:23c7959370d7b3236f821e609b0af7763cff7672a758e6c1fc877bac099e786b", size = 16425340, upload-time = "2025-08-27T02:33:17.78Z" }, + { url = "https://files.pythonhosted.org/packages/0d/94/f56f6ca5e2f921b28c0f0476705eab56486b279f04e1d568ed64c14e7764/onnx-1.19.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:61d94e6498ca636756f8f4ee2135708434601b2892b7c09536befb19bc8ca007", size = 18322331, upload-time = "2025-08-27T02:33:20.373Z" }, + { url = "https://files.pythonhosted.org/packages/c8/00/8cc3f3c40b54b28f96923380f57c9176872e475face726f7d7a78bd74098/onnx-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:224473354462f005bae985c72028aaa5c85ab11de1b71d55b06fdadd64a667dd", size = 18027513, upload-time = "2025-08-27T02:33:23.44Z" }, + { url = "https://files.pythonhosted.org/packages/61/90/17c4d2566fd0117a5e412688c9525f8950d467f477fbd574e6b32bc9cb8d/onnx-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae475c85c89bc4d1f16571006fd21a3e7c0e258dd2c091f6e8aafb083d1ed9b", size = 18202278, upload-time = "2025-08-27T02:33:26.103Z" }, + { url = "https://files.pythonhosted.org/packages/bc/6e/a9383d9cf6db4ac761a129b081e9fa5d0cd89aad43cf1e3fc6285b915c7d/onnx-1.19.0-cp312-cp312-win32.whl", hash = "sha256:323f6a96383a9cdb3960396cffea0a922593d221f3929b17312781e9f9b7fb9f", size = 16333080, upload-time = "2025-08-27T02:33:28.559Z" }, + { url = "https://files.pythonhosted.org/packages/a7/2e/3ff480a8c1fa7939662bdc973e41914add2d4a1f2b8572a3c39c2e4982e5/onnx-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:50220f3499a499b1a15e19451a678a58e22ad21b34edf2c844c6ef1d9febddc2", size = 16453927, upload-time = "2025-08-27T02:33:31.177Z" }, + { url = "https://files.pythonhosted.org/packages/57/37/ad500945b1b5c154fe9d7b826b30816ebd629d10211ea82071b5bcc30aa4/onnx-1.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:efb768299580b786e21abe504e1652ae6189f0beed02ab087cd841cb4bb37e43", size = 16426022, upload-time = "2025-08-27T02:33:33.515Z" }, ] [[package]] @@ -1467,11 +1489,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.3.8" +version = "4.4.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, + { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, ] [[package]] @@ -4508,26 +4530,26 @@ wheels = [ [[package]] name = "raylib" -version = "5.5.0.2" +version = "5.5.0.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8c/35/9bf3a2af73c55fd4310dcaec4f997c739888e0db9b4dfac71b7680810852/raylib-5.5.0.2.tar.gz", hash = "sha256:83c108ae3b4af40b53c93d1de2afbe309e986dd5efeb280ebe2e61c79956edb0", size = 181172, upload-time = "2024-11-26T11:12:02.791Z" } +sdist = { url = "https://files.pythonhosted.org/packages/86/77/be23455a3ad6588860daa7cea0c27e762858d7e3a6dc81b5b7fc2bb972a1/raylib-5.5.0.3.tar.gz", hash = "sha256:f7cfabe7400bf334fc953df6ab99c7435cd4b2251c495040a48d22d44544080a", size = 184322, upload-time = "2025-09-03T16:04:22.577Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/c4/ce21721b474eb8f65379f7315b382ccfe1d5df728eea4dcf287b874e7461/raylib-5.5.0.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:37eb0ec97fc6b08f989489a50e09b5dde519e1bb8eb17e4033ac82227b0e5eda", size = 1703742, upload-time = "2024-11-26T11:09:31.115Z" }, - { url = "https://files.pythonhosted.org/packages/23/61/138e305c82549869bb8cd41abe75571559eafbeab6aed1ce7d8fbe3ffd58/raylib-5.5.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bb9e506ecd3dbec6dba868eb036269837a8bde68220690842c3238239ee887ef", size = 1247449, upload-time = "2024-11-26T11:09:34.182Z" }, - { url = "https://files.pythonhosted.org/packages/85/e0/dc638c42d1a505f0992263d48e1434d82c21afdf376b06f549d2e281dfd4/raylib-5.5.0.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:70aa8bed67875a8cf25191f35263ef92d646bdfcb1f507915c81562a321f4931", size = 2184315, upload-time = "2024-11-26T11:09:36.715Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1a/49db57283a28fdc1ff0e4604911b7fff085128c2ac8bdd9efa8c5c47439d/raylib-5.5.0.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0365e8c578f72f598795d9377fc70342f0d62aa193c2f304ca048b3e28866752", size = 2278139, upload-time = "2024-11-26T11:09:39.475Z" }, - { url = "https://files.pythonhosted.org/packages/f0/8a/e1a690ab6889d4cb67346a2d32bad8b8e8b0f85ec826b00f76b0ad7e6ad6/raylib-5.5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:5219be70e7fca03e9c4fddebf7e60e885d77137125c7a13f3800a947f8562a13", size = 1693944, upload-time = "2024-11-26T11:09:41.596Z" }, - { url = "https://files.pythonhosted.org/packages/69/2b/49bfa6833ad74ddf318d54ecafe73d535f583531469ecbd5b009d79667d1/raylib-5.5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5233c529d9a0cfd469d88239c2182e55c5215a7755d83cc3d611148d3b9c9e67", size = 1706157, upload-time = "2024-11-26T11:09:43.6Z" }, - { url = "https://files.pythonhosted.org/packages/58/9c/8a3f4de0c81ad1228bf26410cfe3ecdc73011c59f18e542685ffc92c0120/raylib-5.5.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1f76204ffbc492722b571b12dbdc0dca89b10da76ddf48c12a3968d2db061dff", size = 1248027, upload-time = "2025-01-04T20:21:46.269Z" }, - { url = "https://files.pythonhosted.org/packages/7f/16/63baf1aae94832b9f5d15cafcee67bb6dd07a20cf64d40bac09903b79274/raylib-5.5.0.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f8cc2e39f1d6b29211a97ec0ac818a5b04c43a40e747e4b4622101d48c711f9e", size = 2195374, upload-time = "2024-11-26T11:09:46.114Z" }, - { url = "https://files.pythonhosted.org/packages/70/bd/61a006b4e3ce4a6ca974cb0ceeb19f3816815ebabac650e9bf82767e65f6/raylib-5.5.0.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f12da578a28da7f48481f46323e5aab8dd25461982b0e80d045782d6e69649f5", size = 2299593, upload-time = "2024-11-26T11:09:48.963Z" }, - { url = "https://files.pythonhosted.org/packages/f4/4f/59d554cc495bea8235b17cebfc76ed57aaa602c613b870159e31282fd4c1/raylib-5.5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:b40234bbad9523fd6a2049640c76a98b4d6f0b8f4bd19bd33eaee55faf5e050d", size = 1696780, upload-time = "2024-11-26T11:09:50.787Z" }, - { url = "https://files.pythonhosted.org/packages/4a/22/2e02e3738ad041f5ec2830aecdfab411fc2960bfc3400e03b477284bfaf7/raylib-5.5.0.2-pp311-pypy311_pp73-macosx_10_13_x86_64.whl", hash = "sha256:bc45fe1c0aac50aa319a9a66d44bb2bd0dcd038a44d95978191ae7bfeb4a06d8", size = 1216231, upload-time = "2025-02-12T04:21:59.38Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7d/b29afedc4a706b12143f74f322cb32ad5a6f43e56aaca2a9fb89b0d94eee/raylib-5.5.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.whl", hash = "sha256:2242fd6079da5137e9863a447224f800adef6386ca8f59013a5d62cc5cadab2b", size = 1394928, upload-time = "2025-02-12T04:22:03.021Z" }, - { url = "https://files.pythonhosted.org/packages/b6/fa/2daf36d78078c6871b241168a36156169cfc8ea089faba5abe8edad304be/raylib-5.5.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e475a40764c9f83f9e66406bd86d85587eb923329a61ade463c3c59e1e880b16", size = 1564224, upload-time = "2025-02-12T04:22:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/17/f6/2d41282332286fcef2cd782580c5190513a5229c98e0e1d4569c00740d4f/raylib-5.5.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:07e43f2130017da557957dded9fd53abd81d7c0bc4d8f274f2f77027012026f7", size = 1640623, upload-time = "2025-09-03T16:02:36.43Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3a/801c147ef5232ad87184357655330914e99d4a8b4ef3b0cc7c97dca9f935/raylib-5.5.0.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:299a0eb585255ff301c5625a139fceeb051bdd0610adcf573079e28c1b4573b1", size = 1256123, upload-time = "2025-09-03T16:08:15.313Z" }, + { url = "https://files.pythonhosted.org/packages/52/e5/975d330602020eb51f7f3d71e9138886a67bb0f49da490d52324efb5449a/raylib-5.5.0.3-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:02ed5e8aceea781a9b1ef7d2cbef5e972e471ac094da2d9d2ee0d659b6f8a3d3", size = 2187927, upload-time = "2025-09-03T16:08:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/91/ca/f64a6e4f5a8231e80f6378fa495ed5f31cd60f9d09bc24adbc00553fd9b7/raylib-5.5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee9c6d5c1c10fd2a5969f3bbffa959ffa350d2ba4ff09e9aa46abc1bc0e610df", size = 2187545, upload-time = "2025-09-03T16:02:38.7Z" }, + { url = "https://files.pythonhosted.org/packages/36/46/d9957fcb5755aeb8f391db5147b126564f7f0242b1c780014d48cf7b71c8/raylib-5.5.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:995661b7987cac76e4b06f2561d465416c0232210aa3a13e721f018ad8be35df", size = 1705384, upload-time = "2025-09-03T16:02:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/c3/12/8f4f08416de3a01c6c6c4ee9fd29a298a4f0c1de602f00e4add91a4b2bd3/raylib-5.5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0efb1feac28512fc1097a360ef9b7f17625088bcbeeeb5275913f93358d41241", size = 1644996, upload-time = "2025-09-03T16:02:43.667Z" }, + { url = "https://files.pythonhosted.org/packages/8f/37/37f1a5e8f778f9d6715932a497136ff0bba06e3a304543c82503ad6be2dd/raylib-5.5.0.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:d620275ffaa279e4f6bb88be4fa1a50a5ebc6a94c74e411cf15c7474dfb06027", size = 1257758, upload-time = "2025-09-03T16:08:20.835Z" }, + { url = "https://files.pythonhosted.org/packages/38/9b/f79aaa3a3e043d2661cdea073cedd6ce9961aef59d9a7ad6a913d8b7bf30/raylib-5.5.0.3-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:7b1a7b2f5739407889bc38a68c9bcc91a30591f6d2f0837caad708e4113d9e0f", size = 2198841, upload-time = "2025-09-03T16:08:24.218Z" }, + { url = "https://files.pythonhosted.org/packages/bf/5c/ee254f409700dc2d0ffd417f01c4c0bc14a6c50f5fa8ffefa3259e06f5b0/raylib-5.5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:791beba74c173ea856cbefaa9e7c5ad94948b81c532c68eaea32ad225285cff6", size = 2205544, upload-time = "2025-09-03T16:02:45.978Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ed/5932409cb851039df540e3cd888a498b37b43fc7286c080f15af299de877/raylib-5.5.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf2f89735591c5f1a983e35ff0018b6a11c32b9a7e088d529e562e81ff52dd3c", size = 1708191, upload-time = "2025-09-03T16:02:48.452Z" }, + { url = "https://files.pythonhosted.org/packages/8d/47/a6661a5ef715966f740e05b52e5b9d6381de8077b64642315de9d0e4b80c/raylib-5.5.0.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8778876dcc5895d5f360ad95c15231c8d0a0af43baf436c052f2a50cb4fb9788", size = 1218941, upload-time = "2025-09-03T16:03:10.234Z" }, + { url = "https://files.pythonhosted.org/packages/3e/74/8c28931789a4670da3389e270f92b27adef41a9b84504ae0a9d3940cee55/raylib-5.5.0.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9eeedc8e6d7ad94f62ec33b89dd95e6873c4c0ed61173cb12e68938e66e68ae", size = 1433740, upload-time = "2025-09-03T16:03:12.288Z" }, + { url = "https://files.pythonhosted.org/packages/d7/35/dbc962cdb5cff97f111df58fa0a45f4c9af223409defb9e66034ec7aa007/raylib-5.5.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:415f5f0a0105ba32d3e433b7c1be8181bc4eec1a9af7ecead1af133bf9436af0", size = 1572295, upload-time = "2025-09-03T16:03:14.646Z" }, ] [[package]] @@ -4594,28 +4616,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.10" +version = "0.12.11" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } +sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, - { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, - { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, - { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, - { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, - { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, - { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, - { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, - { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, - { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, - { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, - { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, - { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, - { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, - { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, - { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, - { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, + { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" }, + { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" }, + { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" }, + { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" }, + { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" }, + { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" }, + { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" }, + { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" }, + { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" }, + { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" }, + { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" }, + { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" }, ] [[package]] @@ -4629,15 +4651,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.35.0" +version = "2.36.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/31/83/055dc157b719651ef13db569bb8cf2103df11174478649735c1b2bf3f6bc/sentry_sdk-2.35.0.tar.gz", hash = "sha256:5ea58d352779ce45d17bc2fa71ec7185205295b83a9dbb5707273deb64720092", size = 343014, upload-time = "2025-08-14T17:11:20.223Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/ac/52fcbba981793d3c90807b79cf6fa130cd25a54d152e653da3ed6d5defef/sentry_sdk-2.36.0.tar.gz", hash = "sha256:af9260e8155e41e8217615a453828e98aa40740865ac4b16b1ccb6a63b4b2e31", size = 343655, upload-time = "2025-09-04T07:56:37.688Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/36/3d/742617a7c644deb0c1628dcf6bb2d2165ab7c6aab56fe5222758994007f8/sentry_sdk-2.35.0-py2.py3-none-any.whl", hash = "sha256:6e0c29b9a5d34de8575ffb04d289a987ff3053cf2c98ede445bea995e3830263", size = 363806, upload-time = "2025-08-14T17:11:18.29Z" }, + { url = "https://files.pythonhosted.org/packages/cd/17/41ea723cb40f036d699cd954e2894fe7a044b0fd9a0e6bd881b1c9dda14e/sentry_sdk-2.36.0-py2.py3-none-any.whl", hash = "sha256:0f95586a141068d215376e5bf8ebd279e126f7f42805e9570190ef82a7e232b3", size = 364905, upload-time = "2025-09-04T07:56:36.159Z" }, ] [[package]] @@ -4710,22 +4732,22 @@ wheels = [ [[package]] name = "siphash24" -version = "1.7" +version = "1.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0e/be/f0a0ffbb00c51c5633b41459b5ce9b017c025a9256b4403e648c18e70850/siphash24-1.7.tar.gz", hash = "sha256:6e90fee5f199ea25b4e7303646b31872a437174fe885a93dbd4cf7784eb48164", size = 19801, upload-time = "2024-10-15T13:41:51.924Z" } +sdist = { url = "https://files.pythonhosted.org/packages/67/a2/e049b6fccf7a94bd1b2f68b3059a7d6a7aea86a808cac80cb9ae71ab6254/siphash24-1.8.tar.gz", hash = "sha256:aa932f0af4a7335caef772fdaf73a433a32580405c41eb17ff24077944b0aa97", size = 19946, upload-time = "2025-09-02T20:42:04.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4e/67/4ffd23a848739966e1b314ef99f6410035bccee00be14261313787b8f506/siphash24-1.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de75488e93f1cd12c8d5004efd1ebd958c0265205a9d73e8dd8b071900838841", size = 80493, upload-time = "2024-10-15T13:41:14.727Z" }, - { url = "https://files.pythonhosted.org/packages/56/bd/ec198a8c7aef65e967ae84f633bd9950d784c9e527d738c9a3e4bccc34a5/siphash24-1.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffca9908450f9f346e97a223185fcd16217d67d84c6f246f3080c4224f41a514", size = 75350, upload-time = "2024-10-15T13:41:16.262Z" }, - { url = "https://files.pythonhosted.org/packages/50/5a/77838c916bd15addfc2e51286db4c442cb12e25eb4f8d296c394c2280240/siphash24-1.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8ff44ce166452993fea267ea1b2fd089d8e7f103b13d360da441f12b0df121d", size = 100567, upload-time = "2024-10-15T13:41:17.435Z" }, - { url = "https://files.pythonhosted.org/packages/f0/aa/736a0a2efae9a6f69ac1ee4d28c2274fcad2150349fac752d6c525c4e06e/siphash24-1.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4062548dcb1eef13bbe0356d6f8675bfe4571ef38d7103445daa82ba167240d1", size = 105630, upload-time = "2024-10-15T13:41:18.578Z" }, - { url = "https://files.pythonhosted.org/packages/79/52/1afbd70142d3db093d49197e3abe15ca2f1a14678299327ba776944b4771/siphash24-1.7-cp311-cp311-win32.whl", hash = "sha256:7b4ea29376b688fbcc3d25707c15a9dfe7b4ebbc4322878d75bb77e199210a39", size = 67648, upload-time = "2024-10-15T13:41:19.606Z" }, - { url = "https://files.pythonhosted.org/packages/b5/1d/bedcd04c2d1d199c9f6b3e61a6caae0e17257696c9f49594e49856b17a99/siphash24-1.7-cp311-cp311-win_amd64.whl", hash = "sha256:ec06104e6ef1e512ee30f1b8aeae2b83c0f55f12a94042f0df5a87d43a1f4c52", size = 80046, upload-time = "2024-10-15T13:41:20.654Z" }, - { url = "https://files.pythonhosted.org/packages/3e/62/93e552af9535a416f684327f870143ee42fc9e816091672467cdfd62cce6/siphash24-1.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:76a64ff0cdd192e4d0a391956d9b121c56ed56e773c5ab7eb7c3e035fd16e8cb", size = 82084, upload-time = "2024-10-15T13:41:21.776Z" }, - { url = "https://files.pythonhosted.org/packages/59/3e/b0791ab53aa9ac191b71a021eab2e75baa7c27d7feb7ec148d7961d148ba/siphash24-1.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ca649bc7437d614f758891deade3b187832792a853269219e77f10509f82fe", size = 76233, upload-time = "2024-10-15T13:41:22.787Z" }, - { url = "https://files.pythonhosted.org/packages/29/4c/4c1b809bf302e9b60f3ec09ba115b2a4ac1ff6755735ee8884924fcdb45e/siphash24-1.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc37dd0aed23f76bd257fbd2953fd5d954b329d7463c6ff57263a2699c52dde6", size = 98188, upload-time = "2024-10-15T13:41:24.327Z" }, - { url = "https://files.pythonhosted.org/packages/96/bf/e6b49f8ff88130bd224f291ea77d30fdde4df5f6572c519aca5d8fc8a27c/siphash24-1.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea490a200891905856b6ad0f9c56d4ec787876220bcb34c49441b2566b97887", size = 102946, upload-time = "2024-10-15T13:41:25.633Z" }, - { url = "https://files.pythonhosted.org/packages/3d/75/45c831626013950fb2ea715c218c3397e5cf2328a67208bf5d8ff69aa9e6/siphash24-1.7-cp312-cp312-win32.whl", hash = "sha256:69eb8c2c112a738875bb283cd53ef5e86874bc5aed17f3020b38e9174208fb79", size = 68323, upload-time = "2024-10-15T13:41:27.349Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d3/39190c40a68defd19b99c1082dd7455543a52283803bfa111b0e45fae968/siphash24-1.7-cp312-cp312-win_amd64.whl", hash = "sha256:7459569ea4669b6feeaf7d299fc5157cc5c69ca1231dc0decb7a7da2397c782e", size = 81000, upload-time = "2024-10-15T13:41:28.364Z" }, + { url = "https://files.pythonhosted.org/packages/82/23/f53f5bd8866c6ea3abe434c9f208e76ea027210d8b75cd0e0dc849661c7a/siphash24-1.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4662ac616bce4d3c9d6003a0d398e56f8be408fc53a166b79fad08d4f34268e", size = 76930, upload-time = "2025-09-02T20:41:00.869Z" }, + { url = "https://files.pythonhosted.org/packages/0b/25/aebf246904424a06e7ffb7a40cfa9ea9e590ea0fac82e182e0f5d1f1d7ef/siphash24-1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:53d6bed0951a99c6d2891fa6f8acfd5ca80c3e96c60bcee99f6fa01a04773b1c", size = 74315, upload-time = "2025-09-02T20:41:02.38Z" }, + { url = "https://files.pythonhosted.org/packages/59/3f/7010407c3416ef052d46550d54afb2581fb247018fc6500af8c66669eff2/siphash24-1.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d114c03648630e9e07dac2fe95442404e4607adca91640d274ece1a4fa71123e", size = 99756, upload-time = "2025-09-02T20:41:03.902Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/09c734833e69badd7e3faed806b4372bd6564ae0946bd250d5239885914f/siphash24-1.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88c1a55ff82b127c5d3b96927a430d8859e6a98846a5b979833ac790682dd91b", size = 104044, upload-time = "2025-09-02T20:41:05.505Z" }, + { url = "https://files.pythonhosted.org/packages/24/30/56a26d9141a34433da221f732599e2b23d2d70a966c249a9f00feb9a2915/siphash24-1.8-cp311-cp311-win32.whl", hash = "sha256:9430255e6a1313470f52c07c4a4643c451a5b2853f6d4008e4dda05cafb6ce7c", size = 62196, upload-time = "2025-09-02T20:41:07.299Z" }, + { url = "https://files.pythonhosted.org/packages/47/b2/11b0ae63fd374652544e1b12f72ba2cc3fe6c93c1483bd8ff6935b0a8a4b/siphash24-1.8-cp311-cp311-win_amd64.whl", hash = "sha256:1e4b37e4ef0b4496169adce2a58b6c3f230b5852dfa5f7ad0b2d664596409e47", size = 77162, upload-time = "2025-09-02T20:41:08.878Z" }, + { url = "https://files.pythonhosted.org/packages/7f/82/ce3545ce8052ac7ca104b183415a27ec3335e5ed51978fdd7b433f3cfe5b/siphash24-1.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5ed437c6e6cc96196b38728e57cd30b0427df45223475a90e173f5015ef5ba", size = 78136, upload-time = "2025-09-02T20:41:10.083Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/896c3b91bc9deb78c415448b1db67343917f35971a9e23a5967a9d323b8a/siphash24-1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4ef78abdf811325c7089a35504df339c48c0007d4af428a044431d329721e56", size = 74588, upload-time = "2025-09-02T20:41:11.251Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/8dad3f5601db485ba862e1c1f91a5d77fb563650856a6708e9acb40ee53c/siphash24-1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:065eff55c4fefb3a29fd26afb2c072abf7f668ffd53b91d41f92a1c485fcbe5c", size = 98655, upload-time = "2025-09-02T20:41:12.45Z" }, + { url = "https://files.pythonhosted.org/packages/e3/cc/e0c352624c1f2faad270aeb5cce6e173977ef66b9b5e918aa6f32af896bf/siphash24-1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6fa84ebfd47677262aa0bcb0f5a70f796f5fc5704b287ee1b65a3bd4fb7a5d", size = 103217, upload-time = "2025-09-02T20:41:13.746Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f6/0b1675bea4d40affcae642d9c7337702a4138b93c544230280712403e968/siphash24-1.8-cp312-cp312-win32.whl", hash = "sha256:6582f73615552ca055e51e03cb02a28e570a641a7f500222c86c2d811b5037eb", size = 63114, upload-time = "2025-09-02T20:41:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/3d/39/afefef85d72ed8b5cf1aa9283f712e3cd43c9682fabbc809dec54baa8452/siphash24-1.8-cp312-cp312-win_amd64.whl", hash = "sha256:44ea6d794a7cbe184e1e1da2df81c5ebb672ab3867935c3e87c08bb0c2fa4879", size = 76232, upload-time = "2025-09-02T20:41:16.112Z" }, ] [[package]] From ee0fb6bf8ecc1b470317eebd2e09ca0c2ec6d9ee Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 4 Sep 2025 14:19:31 -0400 Subject: [PATCH 115/188] Revert "[bot] Update Python packages" (#1211) Revert "[bot] Update Python packages (#1201)" This reverts commit 0cd2bbf6c0065992a4eddec6f2cdbaa6f3a73829. --- opendbc_repo | 2 +- panda | 2 +- uv.lock | 236 +++++++++++++++++++++++---------------------------- 3 files changed, 109 insertions(+), 131 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 42bbd450b9..004fa8df07 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 42bbd450b90b01c4f7f82fef324bea06341b1a54 +Subproject commit 004fa8df07479ceb205691e0689b42180270c45b diff --git a/panda b/panda index 7eab6fd61b..f10ddc6a89 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 7eab6fd61bae085e0fd44cccb79dc6451163029e +Subproject commit f10ddc6a89953440a15deec6352fff1d406a627a diff --git a/uv.lock b/uv.lock index e6bfbb2048..7010cdfb83 100644 --- a/uv.lock +++ b/uv.lock @@ -510,27 +510,27 @@ wheels = [ [[package]] name = "fonttools" -version = "4.59.2" +version = "4.59.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0d/a5/fba25f9fbdab96e26dedcaeeba125e5f05a09043bf888e0305326e55685b/fonttools-4.59.2.tar.gz", hash = "sha256:e72c0749b06113f50bcb80332364c6be83a9582d6e3db3fe0b280f996dc2ef22", size = 3540889, upload-time = "2025-08-27T16:40:30.97Z" } +sdist = { url = "https://files.pythonhosted.org/packages/11/7f/29c9c3fe4246f6ad96fee52b88d0dc3a863c7563b0afc959e36d78b965dc/fonttools-4.59.1.tar.gz", hash = "sha256:74995b402ad09822a4c8002438e54940d9f1ecda898d2bb057729d7da983e4cb", size = 3534394, upload-time = "2025-08-14T16:28:14.266Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f8/53/742fcd750ae0bdc74de4c0ff923111199cc2f90a4ee87aaddad505b6f477/fonttools-4.59.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:511946e8d7ea5c0d6c7a53c4cb3ee48eda9ab9797cd9bf5d95829a398400354f", size = 2774961, upload-time = "2025-08-27T16:38:47.536Z" }, - { url = "https://files.pythonhosted.org/packages/57/2a/976f5f9fa3b4dd911dc58d07358467bec20e813d933bc5d3db1a955dd456/fonttools-4.59.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8e5e2682cf7be766d84f462ba8828d01e00c8751a8e8e7ce12d7784ccb69a30d", size = 2344690, upload-time = "2025-08-27T16:38:49.723Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8f/b7eefc274fcf370911e292e95565c8253b0b87c82a53919ab3c795a4f50e/fonttools-4.59.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5729e12a982dba3eeae650de48b06f3b9ddb51e9aee2fcaf195b7d09a96250e2", size = 5026910, upload-time = "2025-08-27T16:38:51.904Z" }, - { url = "https://files.pythonhosted.org/packages/69/95/864726eaa8f9d4e053d0c462e64d5830ec7c599cbdf1db9e40f25ca3972e/fonttools-4.59.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c52694eae5d652361d59ecdb5a2246bff7cff13b6367a12da8499e9df56d148d", size = 4971031, upload-time = "2025-08-27T16:38:53.676Z" }, - { url = "https://files.pythonhosted.org/packages/24/4c/b8c4735ebdea20696277c70c79e0de615dbe477834e5a7c2569aa1db4033/fonttools-4.59.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1f1bbc23ba1312bd8959896f46f667753b90216852d2a8cfa2d07e0cb234144", size = 5006112, upload-time = "2025-08-27T16:38:55.69Z" }, - { url = "https://files.pythonhosted.org/packages/3b/23/f9ea29c292aa2fc1ea381b2e5621ac436d5e3e0a5dee24ffe5404e58eae8/fonttools-4.59.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1a1bfe5378962825dabe741720885e8b9ae9745ec7ecc4a5ec1f1ce59a6062bf", size = 5117671, upload-time = "2025-08-27T16:38:58.984Z" }, - { url = "https://files.pythonhosted.org/packages/ba/07/cfea304c555bf06e86071ff2a3916bc90f7c07ec85b23bab758d4908c33d/fonttools-4.59.2-cp311-cp311-win32.whl", hash = "sha256:e937790f3c2c18a1cbc7da101550a84319eb48023a715914477d2e7faeaba570", size = 2218157, upload-time = "2025-08-27T16:39:00.75Z" }, - { url = "https://files.pythonhosted.org/packages/d7/de/35d839aa69db737a3f9f3a45000ca24721834d40118652a5775d5eca8ebb/fonttools-4.59.2-cp311-cp311-win_amd64.whl", hash = "sha256:9836394e2f4ce5f9c0a7690ee93bd90aa1adc6b054f1a57b562c5d242c903104", size = 2265846, upload-time = "2025-08-27T16:39:02.453Z" }, - { url = "https://files.pythonhosted.org/packages/ba/3d/1f45db2df51e7bfa55492e8f23f383d372200be3a0ded4bf56a92753dd1f/fonttools-4.59.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:82906d002c349cad647a7634b004825a7335f8159d0d035ae89253b4abf6f3ea", size = 2769711, upload-time = "2025-08-27T16:39:04.423Z" }, - { url = "https://files.pythonhosted.org/packages/29/df/cd236ab32a8abfd11558f296e064424258db5edefd1279ffdbcfd4fd8b76/fonttools-4.59.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a10c1bd7644dc58f8862d8ba0cf9fb7fef0af01ea184ba6ce3f50ab7dfe74d5a", size = 2340225, upload-time = "2025-08-27T16:39:06.143Z" }, - { url = "https://files.pythonhosted.org/packages/98/12/b6f9f964fe6d4b4dd4406bcbd3328821c3de1f909ffc3ffa558fe72af48c/fonttools-4.59.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:738f31f23e0339785fd67652a94bc69ea49e413dfdb14dcb8c8ff383d249464e", size = 4912766, upload-time = "2025-08-27T16:39:08.138Z" }, - { url = "https://files.pythonhosted.org/packages/73/78/82bde2f2d2c306ef3909b927363170b83df96171f74e0ccb47ad344563cd/fonttools-4.59.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ec99f9bdfee9cdb4a9172f9e8fd578cce5feb231f598909e0aecf5418da4f25", size = 4955178, upload-time = "2025-08-27T16:39:10.094Z" }, - { url = "https://files.pythonhosted.org/packages/92/77/7de766afe2d31dda8ee46d7e479f35c7d48747e558961489a2d6e3a02bd4/fonttools-4.59.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:0476ea74161322e08c7a982f83558a2b81b491509984523a1a540baf8611cc31", size = 4897898, upload-time = "2025-08-27T16:39:12.087Z" }, - { url = "https://files.pythonhosted.org/packages/c5/77/ce0e0b905d62a06415fda9f2b2e109a24a5db54a59502b769e9e297d2242/fonttools-4.59.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95922a922daa1f77cc72611747c156cfb38030ead72436a2c551d30ecef519b9", size = 5049144, upload-time = "2025-08-27T16:39:13.84Z" }, - { url = "https://files.pythonhosted.org/packages/d9/ea/870d93aefd23fff2e07cbeebdc332527868422a433c64062c09d4d5e7fe6/fonttools-4.59.2-cp312-cp312-win32.whl", hash = "sha256:39ad9612c6a622726a6a130e8ab15794558591f999673f1ee7d2f3d30f6a3e1c", size = 2206473, upload-time = "2025-08-27T16:39:15.854Z" }, - { url = "https://files.pythonhosted.org/packages/61/c4/e44bad000c4a4bb2e9ca11491d266e857df98ab6d7428441b173f0fe2517/fonttools-4.59.2-cp312-cp312-win_amd64.whl", hash = "sha256:980fd7388e461b19a881d35013fec32c713ffea1fc37aef2f77d11f332dfd7da", size = 2254706, upload-time = "2025-08-27T16:39:17.893Z" }, - { url = "https://files.pythonhosted.org/packages/65/a4/d2f7be3c86708912c02571db0b550121caab8cd88a3c0aacb9cfa15ea66e/fonttools-4.59.2-py3-none-any.whl", hash = "sha256:8bd0f759020e87bb5d323e6283914d9bf4ae35a7307dafb2cbd1e379e720ad37", size = 1132315, upload-time = "2025-08-27T16:40:28.984Z" }, + { url = "https://files.pythonhosted.org/packages/34/62/9667599561f623d4a523cc9eb4f66f3b94b6155464110fa9aebbf90bbec7/fonttools-4.59.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4909cce2e35706f3d18c54d3dcce0414ba5e0fb436a454dffec459c61653b513", size = 2778815, upload-time = "2025-08-14T16:26:28.484Z" }, + { url = "https://files.pythonhosted.org/packages/8f/78/cc25bcb2ce86033a9df243418d175e58f1956a35047c685ef553acae67d6/fonttools-4.59.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:efbec204fa9f877641747f2d9612b2b656071390d7a7ef07a9dbf0ecf9c7195c", size = 2341631, upload-time = "2025-08-14T16:26:30.396Z" }, + { url = "https://files.pythonhosted.org/packages/a4/cc/fcbb606dd6871f457ac32f281c20bcd6cc77d9fce77b5a4e2b2afab1f500/fonttools-4.59.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:39dfd42cc2dc647b2c5469bc7a5b234d9a49e72565b96dd14ae6f11c2c59ef15", size = 5022222, upload-time = "2025-08-14T16:26:32.447Z" }, + { url = "https://files.pythonhosted.org/packages/61/96/c0b1cf2b74d08eb616a80dbf5564351fe4686147291a25f7dce8ace51eb3/fonttools-4.59.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b11bc177a0d428b37890825d7d025040d591aa833f85f8d8878ed183354f47df", size = 4966512, upload-time = "2025-08-14T16:26:34.621Z" }, + { url = "https://files.pythonhosted.org/packages/a4/26/51ce2e3e0835ffc2562b1b11d1fb9dafd0aca89c9041b64a9e903790a761/fonttools-4.59.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:5b9b4c35b3be45e5bc774d3fc9608bbf4f9a8d371103b858c80edbeed31dd5aa", size = 5001645, upload-time = "2025-08-14T16:26:36.876Z" }, + { url = "https://files.pythonhosted.org/packages/36/11/ef0b23f4266349b6d5ccbd1a07b7adc998d5bce925792aa5d1ec33f593e3/fonttools-4.59.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:01158376b8a418a0bae9625c476cebfcfcb5e6761e9d243b219cd58341e7afbb", size = 5113777, upload-time = "2025-08-14T16:26:39.002Z" }, + { url = "https://files.pythonhosted.org/packages/d0/da/b398fe61ef433da0a0472cdb5d4399124f7581ffe1a31b6242c91477d802/fonttools-4.59.1-cp311-cp311-win32.whl", hash = "sha256:cf7c5089d37787387123f1cb8f1793a47c5e1e3d1e4e7bfbc1cc96e0f925eabe", size = 2215076, upload-time = "2025-08-14T16:26:41.196Z" }, + { url = "https://files.pythonhosted.org/packages/94/bd/e2624d06ab94e41c7c77727b2941f1baed7edb647e63503953e6888020c9/fonttools-4.59.1-cp311-cp311-win_amd64.whl", hash = "sha256:c866eef7a0ba320486ade6c32bfc12813d1a5db8567e6904fb56d3d40acc5116", size = 2262779, upload-time = "2025-08-14T16:26:43.483Z" }, + { url = "https://files.pythonhosted.org/packages/ac/fe/6e069cc4cb8881d164a9bd956e9df555bc62d3eb36f6282e43440200009c/fonttools-4.59.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:43ab814bbba5f02a93a152ee61a04182bb5809bd2bc3609f7822e12c53ae2c91", size = 2769172, upload-time = "2025-08-14T16:26:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/b9/98/ec4e03f748fefa0dd72d9d95235aff6fef16601267f4a2340f0e16b9330f/fonttools-4.59.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4f04c3ffbfa0baafcbc550657cf83657034eb63304d27b05cff1653b448ccff6", size = 2337281, upload-time = "2025-08-14T16:26:47.921Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b1/890360a7e3d04a30ba50b267aca2783f4c1364363797e892e78a4f036076/fonttools-4.59.1-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:d601b153e51a5a6221f0d4ec077b6bfc6ac35bfe6c19aeaa233d8990b2b71726", size = 4909215, upload-time = "2025-08-14T16:26:49.682Z" }, + { url = "https://files.pythonhosted.org/packages/8a/ec/2490599550d6c9c97a44c1e36ef4de52d6acf742359eaa385735e30c05c4/fonttools-4.59.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c735e385e30278c54f43a0d056736942023c9043f84ee1021eff9fd616d17693", size = 4951958, upload-time = "2025-08-14T16:26:51.616Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/bd053f6f7634234a9b9805ff8ae4f32df4f2168bee23cafd1271ba9915a9/fonttools-4.59.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1017413cdc8555dce7ee23720da490282ab7ec1cf022af90a241f33f9a49afc4", size = 4894738, upload-time = "2025-08-14T16:26:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a1/3cd12a010d288325a7cfcf298a84825f0f9c29b01dee1baba64edfe89257/fonttools-4.59.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5c6d8d773470a5107052874341ed3c487c16ecd179976d81afed89dea5cd7406", size = 5045983, upload-time = "2025-08-14T16:26:56.153Z" }, + { url = "https://files.pythonhosted.org/packages/a2/af/8a2c3f6619cc43cf87951405337cc8460d08a4e717bb05eaa94b335d11dc/fonttools-4.59.1-cp312-cp312-win32.whl", hash = "sha256:2a2d0d33307f6ad3a2086a95dd607c202ea8852fa9fb52af9b48811154d1428a", size = 2203407, upload-time = "2025-08-14T16:26:58.165Z" }, + { url = "https://files.pythonhosted.org/packages/8e/f2/a19b874ddbd3ebcf11d7e25188ef9ac3f68b9219c62263acb34aca8cde05/fonttools-4.59.1-cp312-cp312-win_amd64.whl", hash = "sha256:0b9e4fa7eaf046ed6ac470f6033d52c052481ff7a6e0a92373d14f556f298dc0", size = 2251561, upload-time = "2025-08-14T16:27:00.646Z" }, + { url = "https://files.pythonhosted.org/packages/0f/64/9d606e66d498917cd7a2ff24f558010d42d6fd4576d9dd57f0bd98333f5a/fonttools-4.59.1-py3-none-any.whl", hash = "sha256:647db657073672a8330608970a984d51573557f328030566521bc03415535042", size = 1130094, upload-time = "2025-08-14T16:28:12.048Z" }, ] [[package]] @@ -846,7 +846,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.6" +version = "3.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -859,25 +859,25 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a0/59/c3e6453a9676ffba145309a73c462bb407f4400de7de3f2b41af70720a3c/matplotlib-3.10.6.tar.gz", hash = "sha256:ec01b645840dd1996df21ee37f208cd8ba57644779fa20464010638013d3203c", size = 34804264, upload-time = "2025-08-30T00:14:25.137Z" } +sdist = { url = "https://files.pythonhosted.org/packages/43/91/f2939bb60b7ebf12478b030e0d7f340247390f402b3b189616aad790c366/matplotlib-3.10.5.tar.gz", hash = "sha256:352ed6ccfb7998a00881692f38b4ca083c691d3e275b4145423704c34c909076", size = 34804044, upload-time = "2025-07-31T18:09:33.805Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/80/d6/5d3665aa44c49005aaacaa68ddea6fcb27345961cd538a98bb0177934ede/matplotlib-3.10.6-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:905b60d1cb0ee604ce65b297b61cf8be9f4e6cfecf95a3fe1c388b5266bc8f4f", size = 8257527, upload-time = "2025-08-30T00:12:45.31Z" }, - { url = "https://files.pythonhosted.org/packages/8c/af/30ddefe19ca67eebd70047dabf50f899eaff6f3c5e6a1a7edaecaf63f794/matplotlib-3.10.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7bac38d816637343e53d7185d0c66677ff30ffb131044a81898b5792c956ba76", size = 8119583, upload-time = "2025-08-30T00:12:47.236Z" }, - { url = "https://files.pythonhosted.org/packages/d3/29/4a8650a3dcae97fa4f375d46efcb25920d67b512186f8a6788b896062a81/matplotlib-3.10.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:942a8de2b5bfff1de31d95722f702e2966b8a7e31f4e68f7cd963c7cd8861cf6", size = 8692682, upload-time = "2025-08-30T00:12:48.781Z" }, - { url = "https://files.pythonhosted.org/packages/aa/d3/b793b9cb061cfd5d42ff0f69d1822f8d5dbc94e004618e48a97a8373179a/matplotlib-3.10.6-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a3276c85370bc0dfca051ec65c5817d1e0f8f5ce1b7787528ec8ed2d524bbc2f", size = 9521065, upload-time = "2025-08-30T00:12:50.602Z" }, - { url = "https://files.pythonhosted.org/packages/f7/c5/53de5629f223c1c66668d46ac2621961970d21916a4bc3862b174eb2a88f/matplotlib-3.10.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9df5851b219225731f564e4b9e7f2ac1e13c9e6481f941b5631a0f8e2d9387ce", size = 9576888, upload-time = "2025-08-30T00:12:52.92Z" }, - { url = "https://files.pythonhosted.org/packages/fc/8e/0a18d6d7d2d0a2e66585032a760d13662e5250c784d53ad50434e9560991/matplotlib-3.10.6-cp311-cp311-win_amd64.whl", hash = "sha256:abb5d9478625dd9c9eb51a06d39aae71eda749ae9b3138afb23eb38824026c7e", size = 8115158, upload-time = "2025-08-30T00:12:54.863Z" }, - { url = "https://files.pythonhosted.org/packages/07/b3/1a5107bb66c261e23b9338070702597a2d374e5aa7004b7adfc754fbed02/matplotlib-3.10.6-cp311-cp311-win_arm64.whl", hash = "sha256:886f989ccfae63659183173bb3fced7fd65e9eb793c3cc21c273add368536951", size = 7992444, upload-time = "2025-08-30T00:12:57.067Z" }, - { url = "https://files.pythonhosted.org/packages/ea/1a/7042f7430055d567cc3257ac409fcf608599ab27459457f13772c2d9778b/matplotlib-3.10.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:31ca662df6a80bd426f871105fdd69db7543e28e73a9f2afe80de7e531eb2347", size = 8272404, upload-time = "2025-08-30T00:12:59.112Z" }, - { url = "https://files.pythonhosted.org/packages/a9/5d/1d5f33f5b43f4f9e69e6a5fe1fb9090936ae7bc8e2ff6158e7a76542633b/matplotlib-3.10.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1678bb61d897bb4ac4757b5ecfb02bfb3fddf7f808000fb81e09c510712fda75", size = 8128262, upload-time = "2025-08-30T00:13:01.141Z" }, - { url = "https://files.pythonhosted.org/packages/67/c3/135fdbbbf84e0979712df58e5e22b4f257b3f5e52a3c4aacf1b8abec0d09/matplotlib-3.10.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:56cd2d20842f58c03d2d6e6c1f1cf5548ad6f66b91e1e48f814e4fb5abd1cb95", size = 8697008, upload-time = "2025-08-30T00:13:03.24Z" }, - { url = "https://files.pythonhosted.org/packages/9c/be/c443ea428fb2488a3ea7608714b1bd85a82738c45da21b447dc49e2f8e5d/matplotlib-3.10.6-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:662df55604a2f9a45435566d6e2660e41efe83cd94f4288dfbf1e6d1eae4b0bb", size = 9530166, upload-time = "2025-08-30T00:13:05.951Z" }, - { url = "https://files.pythonhosted.org/packages/a9/35/48441422b044d74034aea2a3e0d1a49023f12150ebc58f16600132b9bbaf/matplotlib-3.10.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:08f141d55148cd1fc870c3387d70ca4df16dee10e909b3b038782bd4bda6ea07", size = 9593105, upload-time = "2025-08-30T00:13:08.356Z" }, - { url = "https://files.pythonhosted.org/packages/45/c3/994ef20eb4154ab84cc08d033834555319e4af970165e6c8894050af0b3c/matplotlib-3.10.6-cp312-cp312-win_amd64.whl", hash = "sha256:590f5925c2d650b5c9d813c5b3b5fc53f2929c3f8ef463e4ecfa7e052044fb2b", size = 8122784, upload-time = "2025-08-30T00:13:10.367Z" }, - { url = "https://files.pythonhosted.org/packages/57/b8/5c85d9ae0e40f04e71bedb053aada5d6bab1f9b5399a0937afb5d6b02d98/matplotlib-3.10.6-cp312-cp312-win_arm64.whl", hash = "sha256:f44c8d264a71609c79a78d50349e724f5d5fc3684ead7c2a473665ee63d868aa", size = 7992823, upload-time = "2025-08-30T00:13:12.24Z" }, - { url = "https://files.pythonhosted.org/packages/12/bb/02c35a51484aae5f49bd29f091286e7af5f3f677a9736c58a92b3c78baeb/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:f2d684c3204fa62421bbf770ddfebc6b50130f9cad65531eeba19236d73bb488", size = 8252296, upload-time = "2025-08-30T00:14:19.49Z" }, - { url = "https://files.pythonhosted.org/packages/7d/85/41701e3092005aee9a2445f5ee3904d9dbd4a7df7a45905ffef29b7ef098/matplotlib-3.10.6-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:6f4a69196e663a41d12a728fab8751177215357906436804217d6d9cf0d4d6cf", size = 8116749, upload-time = "2025-08-30T00:14:21.344Z" }, - { url = "https://files.pythonhosted.org/packages/16/53/8d8fa0ea32a8c8239e04d022f6c059ee5e1b77517769feccd50f1df43d6d/matplotlib-3.10.6-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d6ca6ef03dfd269f4ead566ec6f3fb9becf8dab146fb999022ed85ee9f6b3eb", size = 8693933, upload-time = "2025-08-30T00:14:22.942Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c7/1f2db90a1d43710478bb1e9b57b162852f79234d28e4f48a28cc415aa583/matplotlib-3.10.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:dcfc39c452c6a9f9028d3e44d2d721484f665304857188124b505b2c95e1eecf", size = 8239216, upload-time = "2025-07-31T18:07:51.947Z" }, + { url = "https://files.pythonhosted.org/packages/82/6d/ca6844c77a4f89b1c9e4d481c412e1d1dbabf2aae2cbc5aa2da4a1d6683e/matplotlib-3.10.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:903352681b59f3efbf4546985142a9686ea1d616bb054b09a537a06e4b892ccf", size = 8102130, upload-time = "2025-07-31T18:07:53.65Z" }, + { url = "https://files.pythonhosted.org/packages/1d/1e/5e187a30cc673a3e384f3723e5f3c416033c1d8d5da414f82e4e731128ea/matplotlib-3.10.5-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:080c3676a56b8ee1c762bcf8fca3fe709daa1ee23e6ef06ad9f3fc17332f2d2a", size = 8666471, upload-time = "2025-07-31T18:07:55.304Z" }, + { url = "https://files.pythonhosted.org/packages/03/c0/95540d584d7d645324db99a845ac194e915ef75011a0d5e19e1b5cee7e69/matplotlib-3.10.5-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b4984d5064a35b6f66d2c11d668565f4389b1119cc64db7a4c1725bc11adffc", size = 9500518, upload-time = "2025-07-31T18:07:57.199Z" }, + { url = "https://files.pythonhosted.org/packages/ba/2e/e019352099ea58b4169adb9c6e1a2ad0c568c6377c2b677ee1f06de2adc7/matplotlib-3.10.5-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3967424121d3a46705c9fa9bdb0931de3228f13f73d7bb03c999c88343a89d89", size = 9552372, upload-time = "2025-07-31T18:07:59.41Z" }, + { url = "https://files.pythonhosted.org/packages/b7/81/3200b792a5e8b354f31f4101ad7834743ad07b6d620259f2059317b25e4d/matplotlib-3.10.5-cp311-cp311-win_amd64.whl", hash = "sha256:33775bbeb75528555a15ac29396940128ef5613cf9a2d31fb1bfd18b3c0c0903", size = 8100634, upload-time = "2025-07-31T18:08:01.801Z" }, + { url = "https://files.pythonhosted.org/packages/52/46/a944f6f0c1f5476a0adfa501969d229ce5ae60cf9a663be0e70361381f89/matplotlib-3.10.5-cp311-cp311-win_arm64.whl", hash = "sha256:c61333a8e5e6240e73769d5826b9a31d8b22df76c0778f8480baf1b4b01c9420", size = 7978880, upload-time = "2025-07-31T18:08:03.407Z" }, + { url = "https://files.pythonhosted.org/packages/66/1e/c6f6bcd882d589410b475ca1fc22e34e34c82adff519caf18f3e6dd9d682/matplotlib-3.10.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:00b6feadc28a08bd3c65b2894f56cf3c94fc8f7adcbc6ab4516ae1e8ed8f62e2", size = 8253056, upload-time = "2025-07-31T18:08:05.385Z" }, + { url = "https://files.pythonhosted.org/packages/53/e6/d6f7d1b59413f233793dda14419776f5f443bcccb2dfc84b09f09fe05dbe/matplotlib-3.10.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ee98a5c5344dc7f48dc261b6ba5d9900c008fc12beb3fa6ebda81273602cc389", size = 8110131, upload-time = "2025-07-31T18:08:07.293Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/bed8a45e74957549197a2ac2e1259671cd80b55ed9e1fe2b5c94d88a9202/matplotlib-3.10.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a17e57e33de901d221a07af32c08870ed4528db0b6059dce7d7e65c1122d4bea", size = 8669603, upload-time = "2025-07-31T18:08:09.064Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a7/315e9435b10d057f5e52dfc603cd353167ae28bb1a4e033d41540c0067a4/matplotlib-3.10.5-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97b9d6443419085950ee4a5b1ee08c363e5c43d7176e55513479e53669e88468", size = 9508127, upload-time = "2025-07-31T18:08:10.845Z" }, + { url = "https://files.pythonhosted.org/packages/7f/d9/edcbb1f02ca99165365d2768d517898c22c6040187e2ae2ce7294437c413/matplotlib-3.10.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ceefe5d40807d29a66ae916c6a3915d60ef9f028ce1927b84e727be91d884369", size = 9566926, upload-time = "2025-07-31T18:08:13.186Z" }, + { url = "https://files.pythonhosted.org/packages/3b/d9/6dd924ad5616c97b7308e6320cf392c466237a82a2040381163b7500510a/matplotlib-3.10.5-cp312-cp312-win_amd64.whl", hash = "sha256:c04cba0f93d40e45b3c187c6c52c17f24535b27d545f757a2fffebc06c12b98b", size = 8107599, upload-time = "2025-07-31T18:08:15.116Z" }, + { url = "https://files.pythonhosted.org/packages/0e/f3/522dc319a50f7b0279fbe74f86f7a3506ce414bc23172098e8d2bdf21894/matplotlib-3.10.5-cp312-cp312-win_arm64.whl", hash = "sha256:a41bcb6e2c8e79dc99c5511ae6f7787d2fb52efd3d805fff06d5d4f667db16b2", size = 7978173, upload-time = "2025-07-31T18:08:21.518Z" }, + { url = "https://files.pythonhosted.org/packages/dc/d6/e921be4e1a5f7aca5194e1f016cb67ec294548e530013251f630713e456d/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:160e125da27a749481eaddc0627962990f6029811dbeae23881833a011a0907f", size = 8233224, upload-time = "2025-07-31T18:09:27.512Z" }, + { url = "https://files.pythonhosted.org/packages/ec/74/a2b9b04824b9c349c8f1b2d21d5af43fa7010039427f2b133a034cb09e59/matplotlib-3.10.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ac3d50760394d78a3c9be6b28318fe22b494c4fcf6407e8fd4794b538251899b", size = 8098539, upload-time = "2025-07-31T18:09:29.629Z" }, + { url = "https://files.pythonhosted.org/packages/fc/66/cd29ebc7f6c0d2a15d216fb572573e8fc38bd5d6dec3bd9d7d904c0949f7/matplotlib-3.10.5-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c49465bf689c4d59d174d0c7795fb42a21d4244d11d70e52b8011987367ac61", size = 8672192, upload-time = "2025-07-31T18:09:31.407Z" }, ] [[package]] @@ -981,27 +981,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, ] -[[package]] -name = "ml-dtypes" -version = "0.5.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "numpy" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/78/a7/aad060393123cfb383956dca68402aff3db1e1caffd5764887ed5153f41b/ml_dtypes-0.5.3.tar.gz", hash = "sha256:95ce33057ba4d05df50b1f3cfefab22e351868a843b3b15a46c65836283670c9", size = 692316, upload-time = "2025-07-29T18:39:19.454Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/af/f1/720cb1409b5d0c05cff9040c0e9fba73fa4c67897d33babf905d5d46a070/ml_dtypes-0.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:4a177b882667c69422402df6ed5c3428ce07ac2c1f844d8a1314944651439458", size = 667412, upload-time = "2025-07-29T18:38:25.275Z" }, - { url = "https://files.pythonhosted.org/packages/6a/d5/05861ede5d299f6599f86e6bc1291714e2116d96df003cfe23cc54bcc568/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9849ce7267444c0a717c80c6900997de4f36e2815ce34ac560a3edb2d9a64cd2", size = 4964606, upload-time = "2025-07-29T18:38:27.045Z" }, - { url = "https://files.pythonhosted.org/packages/db/dc/72992b68de367741bfab8df3b3fe7c29f982b7279d341aa5bf3e7ef737ea/ml_dtypes-0.5.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3f5ae0309d9f888fd825c2e9d0241102fadaca81d888f26f845bc8c13c1e4ee", size = 4938435, upload-time = "2025-07-29T18:38:29.193Z" }, - { url = "https://files.pythonhosted.org/packages/81/1c/d27a930bca31fb07d975a2d7eaf3404f9388114463b9f15032813c98f893/ml_dtypes-0.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:58e39349d820b5702bb6f94ea0cb2dc8ec62ee81c0267d9622067d8333596a46", size = 206334, upload-time = "2025-07-29T18:38:30.687Z" }, - { url = "https://files.pythonhosted.org/packages/1a/d8/6922499effa616012cb8dc445280f66d100a7ff39b35c864cfca019b3f89/ml_dtypes-0.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:66c2756ae6cfd7f5224e355c893cfd617fa2f747b8bbd8996152cbdebad9a184", size = 157584, upload-time = "2025-07-29T18:38:32.187Z" }, - { url = "https://files.pythonhosted.org/packages/0d/eb/bc07c88a6ab002b4635e44585d80fa0b350603f11a2097c9d1bfacc03357/ml_dtypes-0.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:156418abeeda48ea4797db6776db3c5bdab9ac7be197c1233771e0880c304057", size = 663864, upload-time = "2025-07-29T18:38:33.777Z" }, - { url = "https://files.pythonhosted.org/packages/cf/89/11af9b0f21b99e6386b6581ab40fb38d03225f9de5f55cf52097047e2826/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1db60c154989af253f6c4a34e8a540c2c9dce4d770784d426945e09908fbb177", size = 4951313, upload-time = "2025-07-29T18:38:36.45Z" }, - { url = "https://files.pythonhosted.org/packages/d8/a9/b98b86426c24900b0c754aad006dce2863df7ce0bb2bcc2c02f9cc7e8489/ml_dtypes-0.5.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1b255acada256d1fa8c35ed07b5f6d18bc21d1556f842fbc2d5718aea2cd9e55", size = 4928805, upload-time = "2025-07-29T18:38:38.29Z" }, - { url = "https://files.pythonhosted.org/packages/50/c1/85e6be4fc09c6175f36fb05a45917837f30af9a5146a5151cb3a3f0f9e09/ml_dtypes-0.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:da65e5fd3eea434ccb8984c3624bc234ddcc0d9f4c81864af611aaebcc08a50e", size = 208182, upload-time = "2025-07-29T18:38:39.72Z" }, - { url = "https://files.pythonhosted.org/packages/9e/17/cf5326d6867be057f232d0610de1458f70a8ce7b6290e4b4a277ea62b4cd/ml_dtypes-0.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:8bb9cd1ce63096567f5f42851f5843b5a0ea11511e50039a7649619abfb4ba6d", size = 161560, upload-time = "2025-07-29T18:38:41.072Z" }, -] - [[package]] name = "mouseinfo" version = "0.1.3" @@ -1176,28 +1155,27 @@ wheels = [ [[package]] name = "onnx" -version = "1.19.0" +version = "1.18.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "ml-dtypes" }, { name = "numpy" }, { name = "protobuf" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5b/bf/b0a63ee9f3759dcd177b28c6f2cb22f2aecc6d9b3efecaabc298883caa5f/onnx-1.19.0.tar.gz", hash = "sha256:aa3f70b60f54a29015e41639298ace06adf1dd6b023b9b30f1bca91bb0db9473", size = 11949859, upload-time = "2025-08-27T02:34:27.107Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3d/60/e56e8ec44ed34006e6d4a73c92a04d9eea6163cc12440e35045aec069175/onnx-1.18.0.tar.gz", hash = "sha256:3d8dbf9e996629131ba3aa1afd1d8239b660d1f830c6688dd7e03157cccd6b9c", size = 12563009, upload-time = "2025-05-12T22:03:09.626Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/db/5c/b959b17608cfb6ccf6359b39fe56a5b0b7d965b3d6e6a3c0add90812c36e/onnx-1.19.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:206f00c47b85b5c7af79671e3307147407991a17994c26974565aadc9e96e4e4", size = 18312580, upload-time = "2025-08-27T02:33:03.081Z" }, - { url = "https://files.pythonhosted.org/packages/2c/ee/ac052bbbc832abe0debb784c2c57f9582444fb5f51d63c2967fd04432444/onnx-1.19.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4d7bee94abaac28988b50da675ae99ef8dd3ce16210d591fbd0b214a5930beb3", size = 18029165, upload-time = "2025-08-27T02:33:05.771Z" }, - { url = "https://files.pythonhosted.org/packages/5c/c9/8687ba0948d46fd61b04e3952af9237883bbf8f16d716e7ed27e688d73b8/onnx-1.19.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7730b96b68c0c354bbc7857961bb4909b9aaa171360a8e3708d0a4c749aaadeb", size = 18202125, upload-time = "2025-08-27T02:33:09.325Z" }, - { url = "https://files.pythonhosted.org/packages/e2/16/6249c013e81bd689f46f96c7236d7677f1af5dd9ef22746716b48f10e506/onnx-1.19.0-cp311-cp311-win32.whl", hash = "sha256:7cb7a3ad8059d1a0dfdc5e0a98f71837d82002e441f112825403b137227c2c97", size = 16332738, upload-time = "2025-08-27T02:33:12.448Z" }, - { url = "https://files.pythonhosted.org/packages/6a/28/34a1e2166e418c6a78e5c82e66f409d9da9317832f11c647f7d4e23846a6/onnx-1.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:d75452a9be868bd30c3ef6aa5991df89bbfe53d0d90b2325c5e730fbd91fff85", size = 16452303, upload-time = "2025-08-27T02:33:15.176Z" }, - { url = "https://files.pythonhosted.org/packages/e6/b7/639664626e5ba8027860c4d2a639ee02b37e9c322215c921e9222513c3aa/onnx-1.19.0-cp311-cp311-win_arm64.whl", hash = "sha256:23c7959370d7b3236f821e609b0af7763cff7672a758e6c1fc877bac099e786b", size = 16425340, upload-time = "2025-08-27T02:33:17.78Z" }, - { url = "https://files.pythonhosted.org/packages/0d/94/f56f6ca5e2f921b28c0f0476705eab56486b279f04e1d568ed64c14e7764/onnx-1.19.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:61d94e6498ca636756f8f4ee2135708434601b2892b7c09536befb19bc8ca007", size = 18322331, upload-time = "2025-08-27T02:33:20.373Z" }, - { url = "https://files.pythonhosted.org/packages/c8/00/8cc3f3c40b54b28f96923380f57c9176872e475face726f7d7a78bd74098/onnx-1.19.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:224473354462f005bae985c72028aaa5c85ab11de1b71d55b06fdadd64a667dd", size = 18027513, upload-time = "2025-08-27T02:33:23.44Z" }, - { url = "https://files.pythonhosted.org/packages/61/90/17c4d2566fd0117a5e412688c9525f8950d467f477fbd574e6b32bc9cb8d/onnx-1.19.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ae475c85c89bc4d1f16571006fd21a3e7c0e258dd2c091f6e8aafb083d1ed9b", size = 18202278, upload-time = "2025-08-27T02:33:26.103Z" }, - { url = "https://files.pythonhosted.org/packages/bc/6e/a9383d9cf6db4ac761a129b081e9fa5d0cd89aad43cf1e3fc6285b915c7d/onnx-1.19.0-cp312-cp312-win32.whl", hash = "sha256:323f6a96383a9cdb3960396cffea0a922593d221f3929b17312781e9f9b7fb9f", size = 16333080, upload-time = "2025-08-27T02:33:28.559Z" }, - { url = "https://files.pythonhosted.org/packages/a7/2e/3ff480a8c1fa7939662bdc973e41914add2d4a1f2b8572a3c39c2e4982e5/onnx-1.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:50220f3499a499b1a15e19451a678a58e22ad21b34edf2c844c6ef1d9febddc2", size = 16453927, upload-time = "2025-08-27T02:33:31.177Z" }, - { url = "https://files.pythonhosted.org/packages/57/37/ad500945b1b5c154fe9d7b826b30816ebd629d10211ea82071b5bcc30aa4/onnx-1.19.0-cp312-cp312-win_arm64.whl", hash = "sha256:efb768299580b786e21abe504e1652ae6189f0beed02ab087cd841cb4bb37e43", size = 16426022, upload-time = "2025-08-27T02:33:33.515Z" }, + { url = "https://files.pythonhosted.org/packages/ed/3a/a336dac4db1eddba2bf577191e5b7d3e4c26fcee5ec518a5a5b11d13540d/onnx-1.18.0-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:735e06d8d0cf250dc498f54038831401063c655a8d6e5975b2527a4e7d24be3e", size = 18281831, upload-time = "2025-05-12T22:02:06.429Z" }, + { url = "https://files.pythonhosted.org/packages/02/3a/56475a111120d1e5d11939acbcbb17c92198c8e64a205cd68e00bdfd8a1f/onnx-1.18.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:73160799472e1a86083f786fecdf864cf43d55325492a9b5a1cfa64d8a523ecc", size = 17424359, upload-time = "2025-05-12T22:02:09.866Z" }, + { url = "https://files.pythonhosted.org/packages/cf/03/5eb5e9ef446ed9e78c4627faf3c1bc25e0f707116dd00e9811de232a8df5/onnx-1.18.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6acafb3823238bbe8f4340c7ac32fb218689442e074d797bee1c5c9a02fdae75", size = 17586006, upload-time = "2025-05-12T22:02:13.217Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4e/70943125729ce453271a6e46bb847b4a612496f64db6cbc6cb1f49f41ce1/onnx-1.18.0-cp311-cp311-win32.whl", hash = "sha256:4c8c4bbda760c654e65eaffddb1a7de71ec02e60092d33f9000521f897c99be9", size = 15734988, upload-time = "2025-05-12T22:02:16.561Z" }, + { url = "https://files.pythonhosted.org/packages/44/b0/435fd764011911e8f599e3361f0f33425b1004662c1ea33a0ad22e43db2d/onnx-1.18.0-cp311-cp311-win_amd64.whl", hash = "sha256:a5810194f0f6be2e58c8d6dedc6119510df7a14280dd07ed5f0f0a85bd74816a", size = 15849576, upload-time = "2025-05-12T22:02:19.569Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f0/9e31f4b4626d60f1c034f71b411810bc9fafe31f4e7dd3598effd1b50e05/onnx-1.18.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa1b7483fac6cdec26922174fc4433f8f5c2f239b1133c5625063bb3b35957d0", size = 15822961, upload-time = "2025-05-12T22:02:22.735Z" }, + { url = "https://files.pythonhosted.org/packages/a7/fe/16228aca685392a7114625b89aae98b2dc4058a47f0f467a376745efe8d0/onnx-1.18.0-cp312-cp312-macosx_12_0_universal2.whl", hash = "sha256:521bac578448667cbb37c50bf05b53c301243ede8233029555239930996a625b", size = 18285770, upload-time = "2025-05-12T22:02:26.116Z" }, + { url = "https://files.pythonhosted.org/packages/1e/77/ba50a903a9b5e6f9be0fa50f59eb2fca4a26ee653375408fbc72c3acbf9f/onnx-1.18.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e4da451bf1c5ae381f32d430004a89f0405bc57a8471b0bddb6325a5b334aa40", size = 17421291, upload-time = "2025-05-12T22:02:29.645Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/25ec2ba723ac62b99e8fed6d7b59094dadb15e38d4c007331cc9ae3dfa5f/onnx-1.18.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:99afac90b4cdb1471432203c3c1f74e16549c526df27056d39f41a9a47cfb4af", size = 17584084, upload-time = "2025-05-12T22:02:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/6a/4d/2c253a36070fb43f340ff1d2c450df6a9ef50b938adcd105693fee43c4ee/onnx-1.18.0-cp312-cp312-win32.whl", hash = "sha256:ee159b41a3ae58d9c7341cf432fc74b96aaf50bd7bb1160029f657b40dc69715", size = 15734892, upload-time = "2025-05-12T22:02:35.527Z" }, + { url = "https://files.pythonhosted.org/packages/e8/92/048ba8fafe6b2b9a268ec2fb80def7e66c0b32ab2cae74de886981f05a27/onnx-1.18.0-cp312-cp312-win_amd64.whl", hash = "sha256:102c04edc76b16e9dfeda5a64c1fccd7d3d2913b1544750c01d38f1ac3c04e05", size = 15850336, upload-time = "2025-05-12T22:02:38.545Z" }, + { url = "https://files.pythonhosted.org/packages/a1/66/bbc4ffedd44165dcc407a51ea4c592802a5391ce3dc94aa5045350f64635/onnx-1.18.0-cp312-cp312-win_arm64.whl", hash = "sha256:911b37d724a5d97396f3c2ef9ea25361c55cbc9aa18d75b12a52b620b67145af", size = 15823802, upload-time = "2025-05-12T22:02:42.037Z" }, ] [[package]] @@ -1489,11 +1467,11 @@ wheels = [ [[package]] name = "platformdirs" -version = "4.4.0" +version = "4.3.8" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/23/e8/21db9c9987b0e728855bd57bff6984f67952bea55d6f75e055c46b5383e8/platformdirs-4.4.0.tar.gz", hash = "sha256:ca753cf4d81dc309bc67b0ea38fd15dc97bc30ce419a7f58d13eb3bf14c4febf", size = 21634, upload-time = "2025-08-26T14:32:04.268Z" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/40/4b/2028861e724d3bd36227adfa20d3fd24c3fc6d52032f4a93c133be5d17ce/platformdirs-4.4.0-py3-none-any.whl", hash = "sha256:abd01743f24e5287cd7a5db3752faf1a2d65353f38ec26d98e25a6db65958c85", size = 18654, upload-time = "2025-08-26T14:32:02.735Z" }, + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, ] [[package]] @@ -4530,26 +4508,26 @@ wheels = [ [[package]] name = "raylib" -version = "5.5.0.3" +version = "5.5.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/86/77/be23455a3ad6588860daa7cea0c27e762858d7e3a6dc81b5b7fc2bb972a1/raylib-5.5.0.3.tar.gz", hash = "sha256:f7cfabe7400bf334fc953df6ab99c7435cd4b2251c495040a48d22d44544080a", size = 184322, upload-time = "2025-09-03T16:04:22.577Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/35/9bf3a2af73c55fd4310dcaec4f997c739888e0db9b4dfac71b7680810852/raylib-5.5.0.2.tar.gz", hash = "sha256:83c108ae3b4af40b53c93d1de2afbe309e986dd5efeb280ebe2e61c79956edb0", size = 181172, upload-time = "2024-11-26T11:12:02.791Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/17/f6/2d41282332286fcef2cd782580c5190513a5229c98e0e1d4569c00740d4f/raylib-5.5.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:07e43f2130017da557957dded9fd53abd81d7c0bc4d8f274f2f77027012026f7", size = 1640623, upload-time = "2025-09-03T16:02:36.43Z" }, - { url = "https://files.pythonhosted.org/packages/c0/3a/801c147ef5232ad87184357655330914e99d4a8b4ef3b0cc7c97dca9f935/raylib-5.5.0.3-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:299a0eb585255ff301c5625a139fceeb051bdd0610adcf573079e28c1b4573b1", size = 1256123, upload-time = "2025-09-03T16:08:15.313Z" }, - { url = "https://files.pythonhosted.org/packages/52/e5/975d330602020eb51f7f3d71e9138886a67bb0f49da490d52324efb5449a/raylib-5.5.0.3-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:02ed5e8aceea781a9b1ef7d2cbef5e972e471ac094da2d9d2ee0d659b6f8a3d3", size = 2187927, upload-time = "2025-09-03T16:08:18.035Z" }, - { url = "https://files.pythonhosted.org/packages/91/ca/f64a6e4f5a8231e80f6378fa495ed5f31cd60f9d09bc24adbc00553fd9b7/raylib-5.5.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ee9c6d5c1c10fd2a5969f3bbffa959ffa350d2ba4ff09e9aa46abc1bc0e610df", size = 2187545, upload-time = "2025-09-03T16:02:38.7Z" }, - { url = "https://files.pythonhosted.org/packages/36/46/d9957fcb5755aeb8f391db5147b126564f7f0242b1c780014d48cf7b71c8/raylib-5.5.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:995661b7987cac76e4b06f2561d465416c0232210aa3a13e721f018ad8be35df", size = 1705384, upload-time = "2025-09-03T16:02:41.196Z" }, - { url = "https://files.pythonhosted.org/packages/c3/12/8f4f08416de3a01c6c6c4ee9fd29a298a4f0c1de602f00e4add91a4b2bd3/raylib-5.5.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0efb1feac28512fc1097a360ef9b7f17625088bcbeeeb5275913f93358d41241", size = 1644996, upload-time = "2025-09-03T16:02:43.667Z" }, - { url = "https://files.pythonhosted.org/packages/8f/37/37f1a5e8f778f9d6715932a497136ff0bba06e3a304543c82503ad6be2dd/raylib-5.5.0.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:d620275ffaa279e4f6bb88be4fa1a50a5ebc6a94c74e411cf15c7474dfb06027", size = 1257758, upload-time = "2025-09-03T16:08:20.835Z" }, - { url = "https://files.pythonhosted.org/packages/38/9b/f79aaa3a3e043d2661cdea073cedd6ce9961aef59d9a7ad6a913d8b7bf30/raylib-5.5.0.3-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:7b1a7b2f5739407889bc38a68c9bcc91a30591f6d2f0837caad708e4113d9e0f", size = 2198841, upload-time = "2025-09-03T16:08:24.218Z" }, - { url = "https://files.pythonhosted.org/packages/bf/5c/ee254f409700dc2d0ffd417f01c4c0bc14a6c50f5fa8ffefa3259e06f5b0/raylib-5.5.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:791beba74c173ea856cbefaa9e7c5ad94948b81c532c68eaea32ad225285cff6", size = 2205544, upload-time = "2025-09-03T16:02:45.978Z" }, - { url = "https://files.pythonhosted.org/packages/d7/ed/5932409cb851039df540e3cd888a498b37b43fc7286c080f15af299de877/raylib-5.5.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf2f89735591c5f1a983e35ff0018b6a11c32b9a7e088d529e562e81ff52dd3c", size = 1708191, upload-time = "2025-09-03T16:02:48.452Z" }, - { url = "https://files.pythonhosted.org/packages/8d/47/a6661a5ef715966f740e05b52e5b9d6381de8077b64642315de9d0e4b80c/raylib-5.5.0.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:8778876dcc5895d5f360ad95c15231c8d0a0af43baf436c052f2a50cb4fb9788", size = 1218941, upload-time = "2025-09-03T16:03:10.234Z" }, - { url = "https://files.pythonhosted.org/packages/3e/74/8c28931789a4670da3389e270f92b27adef41a9b84504ae0a9d3940cee55/raylib-5.5.0.3-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d9eeedc8e6d7ad94f62ec33b89dd95e6873c4c0ed61173cb12e68938e66e68ae", size = 1433740, upload-time = "2025-09-03T16:03:12.288Z" }, - { url = "https://files.pythonhosted.org/packages/d7/35/dbc962cdb5cff97f111df58fa0a45f4c9af223409defb9e66034ec7aa007/raylib-5.5.0.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:415f5f0a0105ba32d3e433b7c1be8181bc4eec1a9af7ecead1af133bf9436af0", size = 1572295, upload-time = "2025-09-03T16:03:14.646Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c4/ce21721b474eb8f65379f7315b382ccfe1d5df728eea4dcf287b874e7461/raylib-5.5.0.2-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:37eb0ec97fc6b08f989489a50e09b5dde519e1bb8eb17e4033ac82227b0e5eda", size = 1703742, upload-time = "2024-11-26T11:09:31.115Z" }, + { url = "https://files.pythonhosted.org/packages/23/61/138e305c82549869bb8cd41abe75571559eafbeab6aed1ce7d8fbe3ffd58/raylib-5.5.0.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:bb9e506ecd3dbec6dba868eb036269837a8bde68220690842c3238239ee887ef", size = 1247449, upload-time = "2024-11-26T11:09:34.182Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/dc638c42d1a505f0992263d48e1434d82c21afdf376b06f549d2e281dfd4/raylib-5.5.0.2-cp311-cp311-manylinux2014_aarch64.whl", hash = "sha256:70aa8bed67875a8cf25191f35263ef92d646bdfcb1f507915c81562a321f4931", size = 2184315, upload-time = "2024-11-26T11:09:36.715Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1a/49db57283a28fdc1ff0e4604911b7fff085128c2ac8bdd9efa8c5c47439d/raylib-5.5.0.2-cp311-cp311-manylinux2014_x86_64.whl", hash = "sha256:0365e8c578f72f598795d9377fc70342f0d62aa193c2f304ca048b3e28866752", size = 2278139, upload-time = "2024-11-26T11:09:39.475Z" }, + { url = "https://files.pythonhosted.org/packages/f0/8a/e1a690ab6889d4cb67346a2d32bad8b8e8b0f85ec826b00f76b0ad7e6ad6/raylib-5.5.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:5219be70e7fca03e9c4fddebf7e60e885d77137125c7a13f3800a947f8562a13", size = 1693944, upload-time = "2024-11-26T11:09:41.596Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/49bfa6833ad74ddf318d54ecafe73d535f583531469ecbd5b009d79667d1/raylib-5.5.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5233c529d9a0cfd469d88239c2182e55c5215a7755d83cc3d611148d3b9c9e67", size = 1706157, upload-time = "2024-11-26T11:09:43.6Z" }, + { url = "https://files.pythonhosted.org/packages/58/9c/8a3f4de0c81ad1228bf26410cfe3ecdc73011c59f18e542685ffc92c0120/raylib-5.5.0.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:1f76204ffbc492722b571b12dbdc0dca89b10da76ddf48c12a3968d2db061dff", size = 1248027, upload-time = "2025-01-04T20:21:46.269Z" }, + { url = "https://files.pythonhosted.org/packages/7f/16/63baf1aae94832b9f5d15cafcee67bb6dd07a20cf64d40bac09903b79274/raylib-5.5.0.2-cp312-cp312-manylinux2014_aarch64.whl", hash = "sha256:f8cc2e39f1d6b29211a97ec0ac818a5b04c43a40e747e4b4622101d48c711f9e", size = 2195374, upload-time = "2024-11-26T11:09:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/70/bd/61a006b4e3ce4a6ca974cb0ceeb19f3816815ebabac650e9bf82767e65f6/raylib-5.5.0.2-cp312-cp312-manylinux2014_x86_64.whl", hash = "sha256:f12da578a28da7f48481f46323e5aab8dd25461982b0e80d045782d6e69649f5", size = 2299593, upload-time = "2024-11-26T11:09:48.963Z" }, + { url = "https://files.pythonhosted.org/packages/f4/4f/59d554cc495bea8235b17cebfc76ed57aaa602c613b870159e31282fd4c1/raylib-5.5.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:b40234bbad9523fd6a2049640c76a98b4d6f0b8f4bd19bd33eaee55faf5e050d", size = 1696780, upload-time = "2024-11-26T11:09:50.787Z" }, + { url = "https://files.pythonhosted.org/packages/4a/22/2e02e3738ad041f5ec2830aecdfab411fc2960bfc3400e03b477284bfaf7/raylib-5.5.0.2-pp311-pypy311_pp73-macosx_10_13_x86_64.whl", hash = "sha256:bc45fe1c0aac50aa319a9a66d44bb2bd0dcd038a44d95978191ae7bfeb4a06d8", size = 1216231, upload-time = "2025-02-12T04:21:59.38Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7d/b29afedc4a706b12143f74f322cb32ad5a6f43e56aaca2a9fb89b0d94eee/raylib-5.5.0.2-pp311-pypy311_pp73-manylinux2014_x86_64.whl", hash = "sha256:2242fd6079da5137e9863a447224f800adef6386ca8f59013a5d62cc5cadab2b", size = 1394928, upload-time = "2025-02-12T04:22:03.021Z" }, + { url = "https://files.pythonhosted.org/packages/b6/fa/2daf36d78078c6871b241168a36156169cfc8ea089faba5abe8edad304be/raylib-5.5.0.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:e475a40764c9f83f9e66406bd86d85587eb923329a61ade463c3c59e1e880b16", size = 1564224, upload-time = "2025-02-12T04:22:05.911Z" }, ] [[package]] @@ -4616,28 +4594,28 @@ wheels = [ [[package]] name = "ruff" -version = "0.12.11" +version = "0.12.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/de/55/16ab6a7d88d93001e1ae4c34cbdcfb376652d761799459ff27c1dc20f6fa/ruff-0.12.11.tar.gz", hash = "sha256:c6b09ae8426a65bbee5425b9d0b82796dbb07cb1af045743c79bfb163001165d", size = 5347103, upload-time = "2025-08-28T13:59:08.87Z" } +sdist = { url = "https://files.pythonhosted.org/packages/3b/eb/8c073deb376e46ae767f4961390d17545e8535921d2f65101720ed8bd434/ruff-0.12.10.tar.gz", hash = "sha256:189ab65149d11ea69a2d775343adf5f49bb2426fc4780f65ee33b423ad2e47f9", size = 5310076, upload-time = "2025-08-21T18:23:22.595Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/d6/a2/3b3573e474de39a7a475f3fbaf36a25600bfeb238e1a90392799163b64a0/ruff-0.12.11-py3-none-linux_armv6l.whl", hash = "sha256:93fce71e1cac3a8bf9200e63a38ac5c078f3b6baebffb74ba5274fb2ab276065", size = 11979885, upload-time = "2025-08-28T13:58:26.654Z" }, - { url = "https://files.pythonhosted.org/packages/76/e4/235ad6d1785a2012d3ded2350fd9bc5c5af8c6f56820e696b0118dfe7d24/ruff-0.12.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b8e33ac7b28c772440afa80cebb972ffd823621ded90404f29e5ab6d1e2d4b93", size = 12742364, upload-time = "2025-08-28T13:58:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/2c/0d/15b72c5fe6b1e402a543aa9d8960e0a7e19dfb079f5b0b424db48b7febab/ruff-0.12.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d69fb9d4937aa19adb2e9f058bc4fbfe986c2040acb1a4a9747734834eaa0bfd", size = 11920111, upload-time = "2025-08-28T13:58:33.677Z" }, - { url = "https://files.pythonhosted.org/packages/3e/c0/f66339d7893798ad3e17fa5a1e587d6fd9806f7c1c062b63f8b09dda6702/ruff-0.12.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:411954eca8464595077a93e580e2918d0a01a19317af0a72132283e28ae21bee", size = 12160060, upload-time = "2025-08-28T13:58:35.74Z" }, - { url = "https://files.pythonhosted.org/packages/03/69/9870368326db26f20c946205fb2d0008988aea552dbaec35fbacbb46efaa/ruff-0.12.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6a2c0a2e1a450f387bf2c6237c727dd22191ae8c00e448e0672d624b2bbd7fb0", size = 11799848, upload-time = "2025-08-28T13:58:38.051Z" }, - { url = "https://files.pythonhosted.org/packages/25/8c/dd2c7f990e9b3a8a55eee09d4e675027d31727ce33cdb29eab32d025bdc9/ruff-0.12.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:8ca4c3a7f937725fd2413c0e884b5248a19369ab9bdd850b5781348ba283f644", size = 13536288, upload-time = "2025-08-28T13:58:40.046Z" }, - { url = "https://files.pythonhosted.org/packages/7a/30/d5496fa09aba59b5e01ea76775a4c8897b13055884f56f1c35a4194c2297/ruff-0.12.11-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:4d1df0098124006f6a66ecf3581a7f7e754c4df7644b2e6704cd7ca80ff95211", size = 14490633, upload-time = "2025-08-28T13:58:42.285Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2f/81f998180ad53445d403c386549d6946d0748e536d58fce5b5e173511183/ruff-0.12.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5a8dd5f230efc99a24ace3b77e3555d3fbc0343aeed3fc84c8d89e75ab2ff793", size = 13888430, upload-time = "2025-08-28T13:58:44.641Z" }, - { url = "https://files.pythonhosted.org/packages/87/71/23a0d1d5892a377478c61dbbcffe82a3476b050f38b5162171942a029ef3/ruff-0.12.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4dc75533039d0ed04cd33fb8ca9ac9620b99672fe7ff1533b6402206901c34ee", size = 12913133, upload-time = "2025-08-28T13:58:47.039Z" }, - { url = "https://files.pythonhosted.org/packages/80/22/3c6cef96627f89b344c933781ed38329bfb87737aa438f15da95907cbfd5/ruff-0.12.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4fc58f9266d62c6eccc75261a665f26b4ef64840887fc6cbc552ce5b29f96cc8", size = 13169082, upload-time = "2025-08-28T13:58:49.157Z" }, - { url = "https://files.pythonhosted.org/packages/05/b5/68b3ff96160d8b49e8dd10785ff3186be18fd650d356036a3770386e6c7f/ruff-0.12.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:5a0113bd6eafd545146440225fe60b4e9489f59eb5f5f107acd715ba5f0b3d2f", size = 13139490, upload-time = "2025-08-28T13:58:51.593Z" }, - { url = "https://files.pythonhosted.org/packages/59/b9/050a3278ecd558f74f7ee016fbdf10591d50119df8d5f5da45a22c6afafc/ruff-0.12.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0d737b4059d66295c3ea5720e6efc152623bb83fde5444209b69cd33a53e2000", size = 11958928, upload-time = "2025-08-28T13:58:53.943Z" }, - { url = "https://files.pythonhosted.org/packages/f9/bc/93be37347db854806904a43b0493af8d6873472dfb4b4b8cbb27786eb651/ruff-0.12.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:916fc5defee32dbc1fc1650b576a8fed68f5e8256e2180d4d9855aea43d6aab2", size = 11764513, upload-time = "2025-08-28T13:58:55.976Z" }, - { url = "https://files.pythonhosted.org/packages/7a/a1/1471751e2015a81fd8e166cd311456c11df74c7e8769d4aabfbc7584c7ac/ruff-0.12.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:c984f07d7adb42d3ded5be894fb4007f30f82c87559438b4879fe7aa08c62b39", size = 12745154, upload-time = "2025-08-28T13:58:58.16Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/2542b14890d0f4872dd81b7b2a6aed3ac1786fae1ce9b17e11e6df9e31e3/ruff-0.12.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:e07fbb89f2e9249f219d88331c833860489b49cdf4b032b8e4432e9b13e8a4b9", size = 13227653, upload-time = "2025-08-28T13:59:00.276Z" }, - { url = "https://files.pythonhosted.org/packages/22/16/2fbfc61047dbfd009c58a28369a693a1484ad15441723be1cd7fe69bb679/ruff-0.12.11-py3-none-win32.whl", hash = "sha256:c792e8f597c9c756e9bcd4d87cf407a00b60af77078c96f7b6366ea2ce9ba9d3", size = 11944270, upload-time = "2025-08-28T13:59:02.347Z" }, - { url = "https://files.pythonhosted.org/packages/08/a5/34276984705bfe069cd383101c45077ee029c3fe3b28225bf67aa35f0647/ruff-0.12.11-py3-none-win_amd64.whl", hash = "sha256:a3283325960307915b6deb3576b96919ee89432ebd9c48771ca12ee8afe4a0fd", size = 13046600, upload-time = "2025-08-28T13:59:04.751Z" }, - { url = "https://files.pythonhosted.org/packages/84/a8/001d4a7c2b37623a3fd7463208267fb906df40ff31db496157549cfd6e72/ruff-0.12.11-py3-none-win_arm64.whl", hash = "sha256:bae4d6e6a2676f8fb0f98b74594a048bae1b944aab17e9f5d504062303c6dbea", size = 12135290, upload-time = "2025-08-28T13:59:06.933Z" }, + { url = "https://files.pythonhosted.org/packages/24/e7/560d049d15585d6c201f9eeacd2fd130def3741323e5ccf123786e0e3c95/ruff-0.12.10-py3-none-linux_armv6l.whl", hash = "sha256:8b593cb0fb55cc8692dac7b06deb29afda78c721c7ccfed22db941201b7b8f7b", size = 11935161, upload-time = "2025-08-21T18:22:26.965Z" }, + { url = "https://files.pythonhosted.org/packages/d1/b0/ad2464922a1113c365d12b8f80ed70fcfb39764288ac77c995156080488d/ruff-0.12.10-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ebb7333a45d56efc7c110a46a69a1b32365d5c5161e7244aaf3aa20ce62399c1", size = 12660884, upload-time = "2025-08-21T18:22:30.925Z" }, + { url = "https://files.pythonhosted.org/packages/d7/f1/97f509b4108d7bae16c48389f54f005b62ce86712120fd8b2d8e88a7cb49/ruff-0.12.10-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d59e58586829f8e4a9920788f6efba97a13d1fa320b047814e8afede381c6839", size = 11872754, upload-time = "2025-08-21T18:22:34.035Z" }, + { url = "https://files.pythonhosted.org/packages/12/ad/44f606d243f744a75adc432275217296095101f83f966842063d78eee2d3/ruff-0.12.10-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:822d9677b560f1fdeab69b89d1f444bf5459da4aa04e06e766cf0121771ab844", size = 12092276, upload-time = "2025-08-21T18:22:36.764Z" }, + { url = "https://files.pythonhosted.org/packages/06/1f/ed6c265e199568010197909b25c896d66e4ef2c5e1c3808caf461f6f3579/ruff-0.12.10-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:37b4a64f4062a50c75019c61c7017ff598cb444984b638511f48539d3a1c98db", size = 11734700, upload-time = "2025-08-21T18:22:39.822Z" }, + { url = "https://files.pythonhosted.org/packages/63/c5/b21cde720f54a1d1db71538c0bc9b73dee4b563a7dd7d2e404914904d7f5/ruff-0.12.10-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2c6f4064c69d2542029b2a61d39920c85240c39837599d7f2e32e80d36401d6e", size = 13468783, upload-time = "2025-08-21T18:22:42.559Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/39369e6ac7f2a1848f22fb0b00b690492f20811a1ac5c1fd1d2798329263/ruff-0.12.10-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:059e863ea3a9ade41407ad71c1de2badfbe01539117f38f763ba42a1206f7559", size = 14436642, upload-time = "2025-08-21T18:22:45.612Z" }, + { url = "https://files.pythonhosted.org/packages/e3/03/5da8cad4b0d5242a936eb203b58318016db44f5c5d351b07e3f5e211bb89/ruff-0.12.10-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1bef6161e297c68908b7218fa6e0e93e99a286e5ed9653d4be71e687dff101cf", size = 13859107, upload-time = "2025-08-21T18:22:48.886Z" }, + { url = "https://files.pythonhosted.org/packages/19/19/dd7273b69bf7f93a070c9cec9494a94048325ad18fdcf50114f07e6bf417/ruff-0.12.10-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:4f1345fbf8fb0531cd722285b5f15af49b2932742fc96b633e883da8d841896b", size = 12886521, upload-time = "2025-08-21T18:22:51.567Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1d/b4207ec35e7babaee62c462769e77457e26eb853fbdc877af29417033333/ruff-0.12.10-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1f68433c4fbc63efbfa3ba5db31727db229fa4e61000f452c540474b03de52a9", size = 13097528, upload-time = "2025-08-21T18:22:54.609Z" }, + { url = "https://files.pythonhosted.org/packages/ff/00/58f7b873b21114456e880b75176af3490d7a2836033779ca42f50de3b47a/ruff-0.12.10-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:141ce3d88803c625257b8a6debf4a0473eb6eed9643a6189b68838b43e78165a", size = 13080443, upload-time = "2025-08-21T18:22:57.413Z" }, + { url = "https://files.pythonhosted.org/packages/12/8c/9e6660007fb10189ccb78a02b41691288038e51e4788bf49b0a60f740604/ruff-0.12.10-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:f3fc21178cd44c98142ae7590f42ddcb587b8e09a3b849cbc84edb62ee95de60", size = 11896759, upload-time = "2025-08-21T18:23:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/67/4c/6d092bb99ea9ea6ebda817a0e7ad886f42a58b4501a7e27cd97371d0ba54/ruff-0.12.10-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:7d1a4e0bdfafcd2e3e235ecf50bf0176f74dd37902f241588ae1f6c827a36c56", size = 11701463, upload-time = "2025-08-21T18:23:03.211Z" }, + { url = "https://files.pythonhosted.org/packages/59/80/d982c55e91df981f3ab62559371380616c57ffd0172d96850280c2b04fa8/ruff-0.12.10-py3-none-musllinux_1_2_i686.whl", hash = "sha256:e67d96827854f50b9e3e8327b031647e7bcc090dbe7bb11101a81a3a2cbf1cc9", size = 12691603, upload-time = "2025-08-21T18:23:06.935Z" }, + { url = "https://files.pythonhosted.org/packages/ad/37/63a9c788bbe0b0850611669ec6b8589838faf2f4f959647f2d3e320383ae/ruff-0.12.10-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ae479e1a18b439c59138f066ae79cc0f3ee250712a873d00dbafadaad9481e5b", size = 13164356, upload-time = "2025-08-21T18:23:10.225Z" }, + { url = "https://files.pythonhosted.org/packages/47/d4/1aaa7fb201a74181989970ebccd12f88c0fc074777027e2a21de5a90657e/ruff-0.12.10-py3-none-win32.whl", hash = "sha256:9de785e95dc2f09846c5e6e1d3a3d32ecd0b283a979898ad427a9be7be22b266", size = 11896089, upload-time = "2025-08-21T18:23:14.232Z" }, + { url = "https://files.pythonhosted.org/packages/ad/14/2ad38fd4037daab9e023456a4a40ed0154e9971f8d6aed41bdea390aabd9/ruff-0.12.10-py3-none-win_amd64.whl", hash = "sha256:7837eca8787f076f67aba2ca559cefd9c5cbc3a9852fd66186f4201b87c1563e", size = 13004616, upload-time = "2025-08-21T18:23:17.422Z" }, + { url = "https://files.pythonhosted.org/packages/24/3c/21cf283d67af33a8e6ed242396863af195a8a6134ec581524fd22b9811b6/ruff-0.12.10-py3-none-win_arm64.whl", hash = "sha256:cc138cc06ed9d4bfa9d667a65af7172b47840e1a98b02ce7011c391e54635ffc", size = 12074225, upload-time = "2025-08-21T18:23:20.137Z" }, ] [[package]] @@ -4651,15 +4629,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.36.0" +version = "2.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/ac/52fcbba981793d3c90807b79cf6fa130cd25a54d152e653da3ed6d5defef/sentry_sdk-2.36.0.tar.gz", hash = "sha256:af9260e8155e41e8217615a453828e98aa40740865ac4b16b1ccb6a63b4b2e31", size = 343655, upload-time = "2025-09-04T07:56:37.688Z" } +sdist = { url = "https://files.pythonhosted.org/packages/31/83/055dc157b719651ef13db569bb8cf2103df11174478649735c1b2bf3f6bc/sentry_sdk-2.35.0.tar.gz", hash = "sha256:5ea58d352779ce45d17bc2fa71ec7185205295b83a9dbb5707273deb64720092", size = 343014, upload-time = "2025-08-14T17:11:20.223Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/cd/17/41ea723cb40f036d699cd954e2894fe7a044b0fd9a0e6bd881b1c9dda14e/sentry_sdk-2.36.0-py2.py3-none-any.whl", hash = "sha256:0f95586a141068d215376e5bf8ebd279e126f7f42805e9570190ef82a7e232b3", size = 364905, upload-time = "2025-09-04T07:56:36.159Z" }, + { url = "https://files.pythonhosted.org/packages/36/3d/742617a7c644deb0c1628dcf6bb2d2165ab7c6aab56fe5222758994007f8/sentry_sdk-2.35.0-py2.py3-none-any.whl", hash = "sha256:6e0c29b9a5d34de8575ffb04d289a987ff3053cf2c98ede445bea995e3830263", size = 363806, upload-time = "2025-08-14T17:11:18.29Z" }, ] [[package]] @@ -4732,22 +4710,22 @@ wheels = [ [[package]] name = "siphash24" -version = "1.8" +version = "1.7" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/67/a2/e049b6fccf7a94bd1b2f68b3059a7d6a7aea86a808cac80cb9ae71ab6254/siphash24-1.8.tar.gz", hash = "sha256:aa932f0af4a7335caef772fdaf73a433a32580405c41eb17ff24077944b0aa97", size = 19946, upload-time = "2025-09-02T20:42:04.856Z" } +sdist = { url = "https://files.pythonhosted.org/packages/0e/be/f0a0ffbb00c51c5633b41459b5ce9b017c025a9256b4403e648c18e70850/siphash24-1.7.tar.gz", hash = "sha256:6e90fee5f199ea25b4e7303646b31872a437174fe885a93dbd4cf7784eb48164", size = 19801, upload-time = "2024-10-15T13:41:51.924Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/82/23/f53f5bd8866c6ea3abe434c9f208e76ea027210d8b75cd0e0dc849661c7a/siphash24-1.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4662ac616bce4d3c9d6003a0d398e56f8be408fc53a166b79fad08d4f34268e", size = 76930, upload-time = "2025-09-02T20:41:00.869Z" }, - { url = "https://files.pythonhosted.org/packages/0b/25/aebf246904424a06e7ffb7a40cfa9ea9e590ea0fac82e182e0f5d1f1d7ef/siphash24-1.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:53d6bed0951a99c6d2891fa6f8acfd5ca80c3e96c60bcee99f6fa01a04773b1c", size = 74315, upload-time = "2025-09-02T20:41:02.38Z" }, - { url = "https://files.pythonhosted.org/packages/59/3f/7010407c3416ef052d46550d54afb2581fb247018fc6500af8c66669eff2/siphash24-1.8-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d114c03648630e9e07dac2fe95442404e4607adca91640d274ece1a4fa71123e", size = 99756, upload-time = "2025-09-02T20:41:03.902Z" }, - { url = "https://files.pythonhosted.org/packages/d4/9f/09c734833e69badd7e3faed806b4372bd6564ae0946bd250d5239885914f/siphash24-1.8-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:88c1a55ff82b127c5d3b96927a430d8859e6a98846a5b979833ac790682dd91b", size = 104044, upload-time = "2025-09-02T20:41:05.505Z" }, - { url = "https://files.pythonhosted.org/packages/24/30/56a26d9141a34433da221f732599e2b23d2d70a966c249a9f00feb9a2915/siphash24-1.8-cp311-cp311-win32.whl", hash = "sha256:9430255e6a1313470f52c07c4a4643c451a5b2853f6d4008e4dda05cafb6ce7c", size = 62196, upload-time = "2025-09-02T20:41:07.299Z" }, - { url = "https://files.pythonhosted.org/packages/47/b2/11b0ae63fd374652544e1b12f72ba2cc3fe6c93c1483bd8ff6935b0a8a4b/siphash24-1.8-cp311-cp311-win_amd64.whl", hash = "sha256:1e4b37e4ef0b4496169adce2a58b6c3f230b5852dfa5f7ad0b2d664596409e47", size = 77162, upload-time = "2025-09-02T20:41:08.878Z" }, - { url = "https://files.pythonhosted.org/packages/7f/82/ce3545ce8052ac7ca104b183415a27ec3335e5ed51978fdd7b433f3cfe5b/siphash24-1.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:df5ed437c6e6cc96196b38728e57cd30b0427df45223475a90e173f5015ef5ba", size = 78136, upload-time = "2025-09-02T20:41:10.083Z" }, - { url = "https://files.pythonhosted.org/packages/15/88/896c3b91bc9deb78c415448b1db67343917f35971a9e23a5967a9d323b8a/siphash24-1.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f4ef78abdf811325c7089a35504df339c48c0007d4af428a044431d329721e56", size = 74588, upload-time = "2025-09-02T20:41:11.251Z" }, - { url = "https://files.pythonhosted.org/packages/12/fd/8dad3f5601db485ba862e1c1f91a5d77fb563650856a6708e9acb40ee53c/siphash24-1.8-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:065eff55c4fefb3a29fd26afb2c072abf7f668ffd53b91d41f92a1c485fcbe5c", size = 98655, upload-time = "2025-09-02T20:41:12.45Z" }, - { url = "https://files.pythonhosted.org/packages/e3/cc/e0c352624c1f2faad270aeb5cce6e173977ef66b9b5e918aa6f32af896bf/siphash24-1.8-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ac6fa84ebfd47677262aa0bcb0f5a70f796f5fc5704b287ee1b65a3bd4fb7a5d", size = 103217, upload-time = "2025-09-02T20:41:13.746Z" }, - { url = "https://files.pythonhosted.org/packages/5b/f6/0b1675bea4d40affcae642d9c7337702a4138b93c544230280712403e968/siphash24-1.8-cp312-cp312-win32.whl", hash = "sha256:6582f73615552ca055e51e03cb02a28e570a641a7f500222c86c2d811b5037eb", size = 63114, upload-time = "2025-09-02T20:41:14.972Z" }, - { url = "https://files.pythonhosted.org/packages/3d/39/afefef85d72ed8b5cf1aa9283f712e3cd43c9682fabbc809dec54baa8452/siphash24-1.8-cp312-cp312-win_amd64.whl", hash = "sha256:44ea6d794a7cbe184e1e1da2df81c5ebb672ab3867935c3e87c08bb0c2fa4879", size = 76232, upload-time = "2025-09-02T20:41:16.112Z" }, + { url = "https://files.pythonhosted.org/packages/4e/67/4ffd23a848739966e1b314ef99f6410035bccee00be14261313787b8f506/siphash24-1.7-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:de75488e93f1cd12c8d5004efd1ebd958c0265205a9d73e8dd8b071900838841", size = 80493, upload-time = "2024-10-15T13:41:14.727Z" }, + { url = "https://files.pythonhosted.org/packages/56/bd/ec198a8c7aef65e967ae84f633bd9950d784c9e527d738c9a3e4bccc34a5/siphash24-1.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ffca9908450f9f346e97a223185fcd16217d67d84c6f246f3080c4224f41a514", size = 75350, upload-time = "2024-10-15T13:41:16.262Z" }, + { url = "https://files.pythonhosted.org/packages/50/5a/77838c916bd15addfc2e51286db4c442cb12e25eb4f8d296c394c2280240/siphash24-1.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b8ff44ce166452993fea267ea1b2fd089d8e7f103b13d360da441f12b0df121d", size = 100567, upload-time = "2024-10-15T13:41:17.435Z" }, + { url = "https://files.pythonhosted.org/packages/f0/aa/736a0a2efae9a6f69ac1ee4d28c2274fcad2150349fac752d6c525c4e06e/siphash24-1.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4062548dcb1eef13bbe0356d6f8675bfe4571ef38d7103445daa82ba167240d1", size = 105630, upload-time = "2024-10-15T13:41:18.578Z" }, + { url = "https://files.pythonhosted.org/packages/79/52/1afbd70142d3db093d49197e3abe15ca2f1a14678299327ba776944b4771/siphash24-1.7-cp311-cp311-win32.whl", hash = "sha256:7b4ea29376b688fbcc3d25707c15a9dfe7b4ebbc4322878d75bb77e199210a39", size = 67648, upload-time = "2024-10-15T13:41:19.606Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1d/bedcd04c2d1d199c9f6b3e61a6caae0e17257696c9f49594e49856b17a99/siphash24-1.7-cp311-cp311-win_amd64.whl", hash = "sha256:ec06104e6ef1e512ee30f1b8aeae2b83c0f55f12a94042f0df5a87d43a1f4c52", size = 80046, upload-time = "2024-10-15T13:41:20.654Z" }, + { url = "https://files.pythonhosted.org/packages/3e/62/93e552af9535a416f684327f870143ee42fc9e816091672467cdfd62cce6/siphash24-1.7-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:76a64ff0cdd192e4d0a391956d9b121c56ed56e773c5ab7eb7c3e035fd16e8cb", size = 82084, upload-time = "2024-10-15T13:41:21.776Z" }, + { url = "https://files.pythonhosted.org/packages/59/3e/b0791ab53aa9ac191b71a021eab2e75baa7c27d7feb7ec148d7961d148ba/siphash24-1.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:49ca649bc7437d614f758891deade3b187832792a853269219e77f10509f82fe", size = 76233, upload-time = "2024-10-15T13:41:22.787Z" }, + { url = "https://files.pythonhosted.org/packages/29/4c/4c1b809bf302e9b60f3ec09ba115b2a4ac1ff6755735ee8884924fcdb45e/siphash24-1.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc37dd0aed23f76bd257fbd2953fd5d954b329d7463c6ff57263a2699c52dde6", size = 98188, upload-time = "2024-10-15T13:41:24.327Z" }, + { url = "https://files.pythonhosted.org/packages/96/bf/e6b49f8ff88130bd224f291ea77d30fdde4df5f6572c519aca5d8fc8a27c/siphash24-1.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eea490a200891905856b6ad0f9c56d4ec787876220bcb34c49441b2566b97887", size = 102946, upload-time = "2024-10-15T13:41:25.633Z" }, + { url = "https://files.pythonhosted.org/packages/3d/75/45c831626013950fb2ea715c218c3397e5cf2328a67208bf5d8ff69aa9e6/siphash24-1.7-cp312-cp312-win32.whl", hash = "sha256:69eb8c2c112a738875bb283cd53ef5e86874bc5aed17f3020b38e9174208fb79", size = 68323, upload-time = "2024-10-15T13:41:27.349Z" }, + { url = "https://files.pythonhosted.org/packages/e0/d3/39190c40a68defd19b99c1082dd7455543a52283803bfa111b0e45fae968/siphash24-1.7-cp312-cp312-win_amd64.whl", hash = "sha256:7459569ea4669b6feeaf7d299fc5157cc5c69ca1231dc0decb7a7da2397c782e", size = 81000, upload-time = "2024-10-15T13:41:28.364Z" }, ] [[package]] From 220cfff04d28fceda9c2a258579240d679825918 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 4 Sep 2025 14:29:11 -0400 Subject: [PATCH 116/188] ci: skip uv lock upgrade on forks (#1213) --- .github/workflows/repo-maintenance.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 14b4d904e3..49dae93747 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -43,6 +43,7 @@ jobs: with: submodules: true - name: uv lock + if: github.repository == 'commaai/openpilot' run: | python3 -m ensurepip --upgrade pip3 install uv From fd7295c980b95c2120c4ccdc9651ee60e1532211 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 4 Sep 2025 14:31:01 -0400 Subject: [PATCH 117/188] [bot] Update Python packages (#1214) Update Python packages Co-authored-by: github-actions[bot] --- opendbc_repo | 2 +- panda | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index 004fa8df07..42bbd450b9 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 004fa8df07479ceb205691e0689b42180270c45b +Subproject commit 42bbd450b90b01c4f7f82fef324bea06341b1a54 diff --git a/panda b/panda index f10ddc6a89..7eab6fd61b 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit f10ddc6a89953440a15deec6352fff1d406a627a +Subproject commit 7eab6fd61bae085e0fd44cccb79dc6451163029e From ef870d5533917bf7db080029fb18e20ac02d0cc9 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 4 Sep 2025 18:11:44 -0700 Subject: [PATCH 118/188] bump opendbc (#36103) * bump * update refs --- opendbc_repo | 2 +- selfdrive/test/process_replay/ref_commit | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index ac6122e272..f85a5575ba 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit ac6122e272e0dc040d5abf3bde6fca4e034a7ef7 +Subproject commit f85a5575ba6b7e14329fdfa6a3f5d3427f00cab0 diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index e1133061f5..a833fadb94 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -4c677a3ebcbd3d4faa3de98e3fb9c0bb83b47926 \ No newline at end of file +afcab1abb62b9d5678342956cced4712f44e909e \ No newline at end of file From 2b7707ecf6f5a1759a88722460834a42a0e7bb17 Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Thu, 4 Sep 2025 18:20:43 -0700 Subject: [PATCH 119/188] Deduplicate car interface test (#36101) deduplicate test car interfaces --- opendbc_repo | 2 +- selfdrive/car/tests/test_car_interfaces.py | 45 ++-------------------- 2 files changed, 4 insertions(+), 43 deletions(-) diff --git a/opendbc_repo b/opendbc_repo index f85a5575ba..7afc25d8d4 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit f85a5575ba6b7e14329fdfa6a3f5d3427f00cab0 +Subproject commit 7afc25d8d4096bb31e25c0b7ae0b961ea05f5394 diff --git a/selfdrive/car/tests/test_car_interfaces.py b/selfdrive/car/tests/test_car_interfaces.py index 5c4729ee9a..c40443d7e7 100644 --- a/selfdrive/car/tests/test_car_interfaces.py +++ b/selfdrive/car/tests/test_car_interfaces.py @@ -1,15 +1,12 @@ import os -import math import hypothesis.strategies as st from hypothesis import Phase, given, settings from parameterized import parameterized from cereal import car from opendbc.car import DT_CTRL -from opendbc.car.car_helpers import interfaces from opendbc.car.structs import CarParams -from opendbc.car.tests.test_car_interfaces import get_fuzzy_car_interface_args -from opendbc.car.fw_versions import FW_VERSIONS, FW_QUERY_CONFIGS +from opendbc.car.tests.test_car_interfaces import get_fuzzy_car_interface from opendbc.car.mock.values import CAR as MOCK from opendbc.car.values import PLATFORMS from openpilot.selfdrive.controls.lib.latcontrol_angle import LatControlAngle @@ -18,11 +15,6 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.test.fuzzy_generation import FuzzyGenerator -ALL_ECUS = {ecu for ecus in FW_VERSIONS.values() for ecu in ecus.keys()} -ALL_ECUS |= {ecu for config in FW_QUERY_CONFIGS.values() for ecu in config.extra_ecus} - -ALL_REQUESTS = {tuple(r.request) for config in FW_QUERY_CONFIGS.values() for r in config.requests} - MAX_EXAMPLES = int(os.environ.get('MAX_EXAMPLES', '60')) @@ -34,39 +26,8 @@ class TestCarInterfaces: phases=(Phase.reuse, Phase.generate, Phase.shrink)) @given(data=st.data()) def test_car_interfaces(self, car_name, data): - CarInterface = interfaces[car_name] - - args = get_fuzzy_car_interface_args(data.draw) - - car_params = CarInterface.get_params(car_name, args['fingerprints'], args['car_fw'], - alpha_long=args['alpha_long'], is_release=False, docs=False) - car_params = car_params.as_reader() - car_interface = CarInterface(car_params) - assert car_params - assert car_interface - - assert car_params.mass > 1 - assert car_params.wheelbase > 0 - # centerToFront is center of gravity to front wheels, assert a reasonable range - assert car_params.wheelbase * 0.3 < car_params.centerToFront < car_params.wheelbase * 0.7 - assert car_params.maxLateralAccel > 0 - - # Longitudinal sanity checks - assert len(car_params.longitudinalTuning.kpV) == len(car_params.longitudinalTuning.kpBP) - assert len(car_params.longitudinalTuning.kiV) == len(car_params.longitudinalTuning.kiBP) - - # Lateral sanity checks - if car_params.steerControlType != CarParams.SteerControlType.angle: - tune = car_params.lateralTuning - if tune.which() == 'pid': - if car_name != MOCK.MOCK: - assert not math.isnan(tune.pid.kf) and tune.pid.kf > 0 - assert len(tune.pid.kpV) > 0 and len(tune.pid.kpV) == len(tune.pid.kpBP) - assert len(tune.pid.kiV) > 0 and len(tune.pid.kiV) == len(tune.pid.kiBP) - - elif tune.which() == 'torque': - assert not math.isnan(tune.torque.kf) and tune.torque.kf > 0 - assert not math.isnan(tune.torque.friction) and tune.torque.friction > 0 + car_interface = get_fuzzy_car_interface(car_name, data.draw) + car_params = car_interface.CP.as_reader() cc_msg = FuzzyGenerator.get_random_msg(data.draw, car.CarControl, real_floats=True) # Run car interface From f0f04d4b5b752be1e0b12bac33dbca830bcf6815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Harald=20Sch=C3=A4fer?= Date: Thu, 4 Sep 2025 18:51:29 -0700 Subject: [PATCH 120/188] Firehose model (#36087) 816ce390-c41a-42fa-a5df-f393cbe2dcc4/400 --- selfdrive/modeld/models/driving_policy.onnx | 2 +- selfdrive/modeld/models/driving_vision.onnx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index eb6bd7b8a4..7b87846748 100644 --- a/selfdrive/modeld/models/driving_policy.onnx +++ b/selfdrive/modeld/models/driving_policy.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:72e98a95541f200bd2faeae8d718997483696fd4801fc7d718c167b05854707d +oid sha256:ebb38a934d6472c061cc6010f46d9720ca132d631a47e585a893bdd41ade2419 size 12343535 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index ce0dc927e7..4b4fa05df8 100644 --- a/selfdrive/modeld/models/driving_vision.onnx +++ b/selfdrive/modeld/models/driving_vision.onnx @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:e66bb8d53eced3786ed71a59b55ffc6810944cb217f0518621cc76303260a1ef +oid sha256:befac016a247b7ad5dc5b55d339d127774ed7bd2b848f1583f72aa4caee37781 size 46271991 From daf5ea27839a2275795a93a0d1dd0c01fde2e18e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 4 Sep 2025 22:10:08 -0400 Subject: [PATCH 121/188] update: remove git cleanup in finalized stage (#1210) * updater: remove git cleanup in finalized stage for quicker updates * nah --- system/updated/updated.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/system/updated/updated.py b/system/updated/updated.py index 11928bc24c..dfeaf88cbd 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -7,7 +7,6 @@ import psutil import shutil import signal import fcntl -import time import threading from collections import defaultdict from pathlib import Path @@ -190,15 +189,6 @@ def finalize_update() -> None: run(["git", "reset", "--hard"], FINALIZED) run(["git", "submodule", "foreach", "--recursive", "git", "reset", "--hard"], FINALIZED) - cloudlog.info("Starting git cleanup in finalized update") - t = time.monotonic() - try: - run(["git", "gc"], FINALIZED) - run(["git", "lfs", "prune"], FINALIZED) - cloudlog.event("Done git cleanup", duration=time.monotonic() - t) - except subprocess.CalledProcessError: - cloudlog.exception(f"Failed git cleanup, took {time.monotonic() - t:.3f} s") - set_consistent_flag(True) cloudlog.info("done finalizing overlay") From 31918c067afac602c7b13ef7d2d2dfbfb1a75dda Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 4 Sep 2025 22:22:48 -0400 Subject: [PATCH 122/188] events: add `sunnypilot/openpilot` to remote origin check (#1216) events: add sunnypilot/openpilot to remote origin check --- system/version.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/system/version.py b/system/version.py index 5aa8d0115f..5e65a7c64b 100755 --- a/system/version.py +++ b/system/version.py @@ -85,7 +85,8 @@ class OpenpilotMetadata: @property def sunnypilot_remote(self) -> bool: - return self.git_normalized_origin == "github.com/sunnypilot/sunnypilot" + return self.git_normalized_origin in ("github.com/sunnypilot/sunnypilot", + "github.com/sunnypilot/openpilot") @property def git_normalized_origin(self) -> str: From 29fe152bd366f17c83ddc9b9286bd52a9039c7d2 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Thu, 4 Sep 2025 19:26:59 -0700 Subject: [PATCH 123/188] modeld_v2: desire rename and add many parts from thneed modeld (#1197) * Add model metadata lookup and update desire handling * Bump selector version to 10 * meh * Refactor shape mode parameters for desire handling in test buffer logic * loop more models * Refactor buffer handling for temporal inputs and streamline desire updates * Refactor lateral control input handling and remove unused code --- sunnypilot/modeld_v2/modeld.py | 48 ++++---- .../tests/test_buffer_logic_inspect.py | 109 ++++++++++-------- sunnypilot/models/helpers.py | 2 +- 3 files changed, 86 insertions(+), 73 deletions(-) diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index bf89bc98d6..0fd45940d8 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -27,7 +27,7 @@ from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase from openpilot.sunnypilot.models.helpers import get_active_bundle from openpilot.sunnypilot.models.runners.helpers import get_model_runner -PROCESS_NAME = "selfdrive.modeld.modeld" +PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad" class FrameMeta: @@ -77,42 +77,47 @@ class ModelState(ModelStateBase): self.numpy_inputs[key] = np.zeros(shape, dtype=np.float32) # Temporal input: shape is [batch, history, features] if len(shape) == 3 and shape[1] > 1: - buffer_history_len = max(100, (shape[1] * 4 if shape[1] < 100 else shape[1])) # Allow for higher history buffers in the future + buffer_history_len = shape[1] * 4 if shape[1] < 99 else shape[1] # Allow for higher history buffers in the future feature_len = shape[2] - self.temporal_buffers[key] = np.zeros((1, buffer_history_len, feature_len), dtype=np.float32) features_buffer_shape = self.model_runner.input_shapes.get('features_buffer') if shape[1] in (24, 25) and features_buffer_shape is not None and features_buffer_shape[1] == 24: # 20Hz + buffer_history_len = (features_buffer_shape[1] + 1) * 4 step = int(-buffer_history_len / shape[1]) self.temporal_idxs_map[key] = np.arange(step, step * (shape[1] + 1), step)[::-1] elif shape[1] == 25: # Split skip = buffer_history_len // shape[1] self.temporal_idxs_map[key] = np.arange(buffer_history_len)[-1 - (skip * (shape[1] - 1))::skip] - elif shape[1] == buffer_history_len: # non20hz - self.temporal_idxs_map[key] = np.arange(buffer_history_len) + elif shape[1] >= 99: # non20hz + self.temporal_idxs_map[key] = np.arange(shape[1]) + self.temporal_buffers[key] = np.zeros((1, buffer_history_len, feature_len), dtype=np.float32) @property def mlsim(self) -> bool: return bool(self.generation is not None and self.generation >= 11) + @property + def desire_key(self) -> str: + return next(key for key in self.numpy_inputs if key.startswith('desire')) + def run(self, bufs: dict[str, VisionBuf], transforms: dict[str, np.ndarray], inputs: dict[str, np.ndarray], prepare_only: bool) -> dict[str, np.ndarray] | None: # Model decides when action is completed, so desire input is just a pulse triggered on rising edge - inputs['desire'][0] = 0 - new_desire = np.where(inputs['desire'] - self.prev_desire > .99, inputs['desire'], 0) - self.prev_desire[:] = inputs['desire'] - self.temporal_buffers['desire'][0,:-1] = self.temporal_buffers['desire'][0,1:] - self.temporal_buffers['desire'][0,-1] = new_desire + inputs[self.desire_key][0] = 0 + new_desire = np.where(inputs[self.desire_key] - self.prev_desire > .99, inputs[self.desire_key], 0) + self.prev_desire[:] = inputs[self.desire_key] + self.temporal_buffers[self.desire_key][0,:-1] = self.temporal_buffers[self.desire_key][0,1:] + self.temporal_buffers[self.desire_key][0,-1] = new_desire # Roll buffer and assign based on desire.shape[1] value - if self.temporal_buffers['desire'].shape[1] > self.numpy_inputs['desire'].shape[1]: - skip = self.temporal_buffers['desire'].shape[1] // self.numpy_inputs['desire'].shape[1] - self.numpy_inputs['desire'][:] = ( - self.temporal_buffers['desire'][0].reshape(self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], skip, -1).max(axis=2)) + if self.temporal_buffers[self.desire_key].shape[1] > self.numpy_inputs[self.desire_key].shape[1]: + skip = self.temporal_buffers[self.desire_key].shape[1] // self.numpy_inputs[self.desire_key].shape[1] + self.numpy_inputs[self.desire_key][:] = (self.temporal_buffers[self.desire_key][0].reshape( + self.numpy_inputs[self.desire_key].shape[0], self.numpy_inputs[self.desire_key].shape[1], skip, -1).max(axis=2)) else: - self.numpy_inputs['desire'][:] = self.temporal_buffers['desire'][0, self.temporal_idxs_map['desire']] + self.numpy_inputs[self.desire_key][:] = self.temporal_buffers[self.desire_key][0, self.temporal_idxs_map[self.desire_key]] for key in self.numpy_inputs: - if key in inputs and key not in ['desire']: + if key in inputs and key not in [self.desire_key]: self.numpy_inputs[key][:] = inputs[key] imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.model_runner.vision_input_names} @@ -156,10 +161,11 @@ class ModelState(ModelStateBase): desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, self.LONG_SMOOTH_SECONDS) desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim) - if v_ego > self.MIN_LAT_CONTROL_SPEED: - desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) - else: - desired_curvature = prev_action.desiredCurvature + if self.generation is not None and self.generation >= 10: # smooth curvature for post FOF models + if v_ego > self.MIN_LAT_CONTROL_SPEED: + desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, self.LAT_SMOOTH_SECONDS) + else: + desired_curvature = prev_action.desiredCurvature return log.ModelDataV2.Action(desiredCurvature=float(desired_curvature),desiredAcceleration=float(desired_accel), shouldStop=bool(should_stop)) @@ -306,7 +312,7 @@ def main(demo=False): bufs = {name: buf_extra if 'big' in name else buf_main for name in model.model_runner.vision_input_names} transforms = {name: model_transform_extra if 'big' in name else model_transform_main for name in model.model_runner.vision_input_names} inputs:dict[str, np.ndarray] = { - 'desire': vec_desire, + model.desire_key: vec_desire, 'traffic_convention': traffic_convention, } diff --git a/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py b/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py index 8a0cfd97c8..f664db31b3 100644 --- a/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py +++ b/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py @@ -8,12 +8,16 @@ import openpilot.sunnypilot.modeld_v2.modeld as modeld_module ModelState = modeld_module.ModelState - # These are the shapes extracted/loaded from the model onnx SHAPE_MODE_PARAMS = [ - ({'desire': (1, 25, 8), 'features_buffer': (1, 25, 512), 'prev_desired_curv': (1, 25, 1)}, 'split'), - ({'desire': (1, 25, 8), 'features_buffer': (1, 24, 512), 'prev_desired_curv': (1, 25, 1)}, '20hz'), - ({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1)}, 'non20hz'), + ({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "nav_features": (1, 256), "nav_instructions": (1, 150)}, 'non20hz'), # Optimus Prime + ({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "lat_planner_state": (1, 4),}, 'non20hz'), # farmville + ({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), "lateral_control_params": (1, 2), "prev_desired_curv": (1, 100, 1)}, 'non20hz'), # wd40 + ({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1), "lateral_control_params": (1, 2),}, 'non20hz'), # NTS + ({'desire': (1, 25, 8), 'features_buffer': (1, 24, 512)}, '20hz'), # NPR + ({'desire': (1, 100, 8), 'features_buffer': (1, 99, 512), 'prev_desired_curv': (1, 100, 1), "lateral_control_params": (1, 2),}, 'non20hz'), # NTS + ({'desire': (1, 25, 8), 'features_buffer': (1, 25, 512)}, 'split'), # Steam Powered v2 + ({'desire_pulse': (1, 25, 8), 'features_buffer': (1, 25, 512)}, 'split'), # desire rename ] @@ -95,9 +99,7 @@ def get_expected_indices(shape, constants, mode, key=None): idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] return idxs elif mode == 'non20hz': - if key and shape[1] == constants.FULL_HISTORY_BUFFER_LEN: - return np.arange(constants.FULL_HISTORY_BUFFER_LEN) - return None + return np.arange(shape[1]) return None @@ -108,6 +110,8 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches): for key in shapes: buf = state.temporal_buffers.get(key, None) idxs = state.temporal_idxs_map.get(key, None) + if buf is None: + continue # not all shapes are 3D, and the non-3D ones are not buffered # Buffer shape logic if mode == 'split': expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2]) @@ -116,10 +120,7 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches): expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2]) expected_idxs = get_expected_indices(shapes[key], constants, '20hz', key) elif mode == 'non20hz': - if key == 'features_buffer': - expected_shape = (1, shapes[key][1]*4, shapes[key][2]) - else: - expected_shape = (1, shapes[key][1], shapes[key][2]) + expected_shape = (1, shapes[key][1], shapes[key][2]) expected_idxs = get_expected_indices(shapes[key], constants, 'non20hz', key) assert buf is not None, f"{key}: buffer not found" @@ -130,10 +131,10 @@ def test_buffer_shapes_and_indices(shapes, mode, apply_patches): assert idxs is None or idxs.size == 0, f"{key}: buffer idxs should be None or empty" -def legacy_buffer_update(buf, new_val, mode, key, constants, idxs): +def legacy_buffer_update(buf, new_val, mode, key, constants, idxs, input_shape, prev_desire=None): # This is what we compare the new dynamic logic to, to ensure it does the same thing if mode == 'split': - if key == 'desire': + if key == 'desire' or key.startswith('desire'): buf[0,:-1] = buf[0,1:] buf[0,-1] = new_val return buf.reshape((1, constants.INPUT_HISTORY_BUFFER_LEN, constants.TEMPORAL_SKIP, -1)).max(axis=2) @@ -173,15 +174,22 @@ def legacy_buffer_update(buf, new_val, mode, key, constants, idxs): return legacy_buf[idxs] elif mode == 'non20hz': if key == 'desire': - length = new_val.shape[0] - buf[0,:-1,:length] = buf[0,1:,:length] - buf[0,-1,:length] = new_val[:length] + desire_len = constants.DESIRE_LEN + if prev_desire is None: + prev_desire = np.zeros(desire_len, dtype=np.float32) + # Set first element to zero + new_val = new_val.copy() + new_val[0] = 0 + # Shift buffer by desire len + buf[0][:-desire_len] = buf[0][desire_len:] + # Only insert new desire if rising edge + buf[0][-desire_len:] = np.where(new_val - prev_desire > 0.99, new_val, 0) + prev_desire[:] = new_val return buf[0] elif key == 'features_buffer': - feature_len = new_val.shape[0] - buf[0,:-1,:feature_len] = buf[0,1:,:feature_len] - buf[0,-1,:feature_len] = new_val[:feature_len] - return buf[0] + buf[0, :-1] = buf[0, 1:] + buf[0, -1] = new_val + return buf[0, -input_shape[1]:] # (99, 512) elif key == 'prev_desired_curv': length = new_val.shape[0] buf[0,:-length,0] = buf[0,length:,0] @@ -191,32 +199,18 @@ def legacy_buffer_update(buf, new_val, mode, key, constants, idxs): def dynamic_buffer_update(state, key, new_val, mode): - if key == 'desire': - state.temporal_buffers['desire'][0,:-1] = state.temporal_buffers['desire'][0,1:] - state.temporal_buffers['desire'][0,-1] = new_val - if state.temporal_buffers['desire'].shape[1] > state.numpy_inputs['desire'].shape[1]: - skip = state.temporal_buffers['desire'].shape[1] // state.numpy_inputs['desire'].shape[1] - return state.temporal_buffers['desire'][0].reshape( - state.numpy_inputs['desire'].shape[0], state.numpy_inputs['desire'].shape[1], skip, -1 - ).max(axis=2) - else: - return state.temporal_buffers['desire'][0, state.temporal_idxs_map['desire']] - - inputs = {'desire': np.zeros((1, state.constants.DESIRE_LEN), dtype=np.float32)} - for k, tb in state.temporal_buffers.items(): - if k in state.temporal_idxs_map: - continue - buf_len = tb.shape[1] - if k in state.numpy_inputs: - out_len = state.numpy_inputs[k].shape[1] - if out_len <= buf_len: - state.temporal_idxs_map[k] = np.arange(buf_len)[-out_len:] - else: - state.temporal_idxs_map[k] = np.arange(buf_len) - else: - state.temporal_idxs_map[k] = np.arange(buf_len) + if key == 'desire' or key.startswith('desire'): + inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32) + for k, v in state.model_runner.input_shapes.items() if k != key} + inputs[key] = new_val.copy() + # ModelState.run expects desire as a pulse, so we zero the first element. + inputs[key][0] = 0 + state.run({}, {}, inputs, prepare_only=False) + return state.numpy_inputs[key] if key == 'features_buffer': + inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32) + for k, v in state.model_runner.input_shapes.items() if k != 'features_buffer'} def run_model_stub(): return { 'hidden_state': np.asarray(new_val, dtype=np.float32).reshape(1, -1), @@ -226,6 +220,8 @@ def dynamic_buffer_update(state, key, new_val, mode): return state.numpy_inputs['features_buffer'][0] if key == 'prev_desired_curv': + inputs = {k: np.zeros(v[2], dtype=np.float32) if len(v) == 3 else np.zeros(v[1], dtype=np.float32) + for k, v in state.model_runner.input_shapes.items() if k != 'prev_desired_curv'} def run_model_stub(): return { 'hidden_state': np.zeros((1, state.constants.FEATURE_LEN), dtype=np.float32), @@ -241,16 +237,27 @@ def dynamic_buffer_update(state, key, new_val, mode): @pytest.mark.parametrize("key", ["desire", "features_buffer", "prev_desired_curv"]) def test_buffer_update_equivalence(shapes, mode, key, apply_patches): state = ModelState(None) + if key == "desire": + desire_keys = [k for k in shapes.keys() if k.startswith('desire')] + if desire_keys: + actual_key = desire_keys[0] # Use the first (and likely only) desire key + else: + actual_key = key + + if actual_key not in state.numpy_inputs: + pytest.skip() + constants = DummyModelRunner(shapes).constants - buf = state.temporal_buffers.get(key, None) - idxs = state.temporal_idxs_map.get(key, None) - input_shape = shapes[key] + buf = state.temporal_buffers.get(actual_key, None) + idxs = state.temporal_idxs_map.get(actual_key, None) + input_shape = shapes[actual_key] + prev_desire = np.zeros(constants.DESIRE_LEN, dtype=np.float32) if key == 'desire' else None + for step in range(20): # multiple steps to ensure history is built up new_val = np.full((input_shape[2],), step, dtype=np.float32) - expected = legacy_buffer_update(buf, new_val, mode, key, constants, idxs) - actual = dynamic_buffer_update(state, key, new_val, mode) - # Model returns the reduced numpy_inputs history, compare the last n entries so the test is checking the same slices. + expected = legacy_buffer_update(buf, new_val, mode, actual_key, constants, idxs, input_shape, prev_desire) + actual = dynamic_buffer_update(state, actual_key, new_val, mode) if expected is not None and actual is not None and expected.shape != actual.shape: if expected.ndim == 2 and actual.ndim == 2 and expected.shape[1] == actual.shape[1]: expected = expected[-actual.shape[0]:] - assert np.allclose(actual, expected), f"{mode} {key}: dynamic buffer update does not match legacy logic" + assert np.allclose(actual, expected), f"{mode} {actual_key}: dynamic buffer update does not match legacy logic" diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index 79241cd831..ecf0a39b72 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -19,7 +19,7 @@ from openpilot.system.hardware.hw import Paths from pathlib import Path # see the README.md for more details on the model selector versioning -CURRENT_SELECTOR_VERSION = 9 +CURRENT_SELECTOR_VERSION = 10 REQUIRED_MIN_SELECTOR_VERSION = 9 USE_ONNX = os.getenv('USE_ONNX', PC) From 7d4df73ea5bb0557a0c50b681fdf60ea3e7b306e Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 5 Sep 2025 14:42:08 +0200 Subject: [PATCH 124/188] hotfix: model fetcher warning instead of exception when fetching fail --- sunnypilot/models/fetcher.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index e681d150b3..3be6e0b46c 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -148,7 +148,7 @@ class ModelFetcher: except SSLError as e: cloudlog.warning(f"SSL error while fetching models: {e}") except RequestException as e: - cloudlog.exception(f"Request transport error while fetching models: {e}") + cloudlog.warning(f"Request transport error while fetching models: {e}") except Exception as e: cloudlog.exception(f"Unexpected error fetching models: {e}") From 1f1efec4c9b80a0be60498b38651954b127357f3 Mon Sep 17 00:00:00 2001 From: pencilpusher <83676301+jakethesnake420@users.noreply.github.com> Date: Fri, 5 Sep 2025 18:05:06 -0500 Subject: [PATCH 125/188] replay: C3/C3X hardware decoder (#35821) * bump msgq * add third_party/linux/include/media/msm_vidc.h * add sde_rotator hw interface * add msm_vidc hw decoder interface * update SConscript to build qcom decoder and rotator * use qcom decoder in replay framereader * decode directly to NV12 with the correct stride without using the hw rotator * bump msgq back to master * don't compile rotator * cleanup * works now but much to simplify * rm signals * rm header --------- Co-authored-by: Test User Co-authored-by: Adeeb Shihadeh --- tools/replay/SConscript | 2 +- tools/replay/framereader.cc | 65 ++++++- tools/replay/framereader.h | 27 ++- tools/replay/qcom_decoder.cc | 346 +++++++++++++++++++++++++++++++++++ tools/replay/qcom_decoder.h | 88 +++++++++ 5 files changed, 515 insertions(+), 13 deletions(-) create mode 100644 tools/replay/qcom_decoder.cc create mode 100644 tools/replay/qcom_decoder.h diff --git a/tools/replay/SConscript b/tools/replay/SConscript index 18849407cf..99c8263a8c 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -12,7 +12,7 @@ else: base_libs.append('OpenCL') replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", - "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "api.cc"] + "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "api.cc", "qcom_decoder.cc"] replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index ed88626be3..f2b1faf2c4 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -8,6 +8,7 @@ #include "common/util.h" #include "third_party/libyuv/include/libyuv.h" #include "tools/replay/util.h" +#include "system/hardware/hw.h" #ifdef __APPLE__ #define HW_DEVICE_TYPE AV_HWDEVICE_TYPE_VIDEOTOOLBOX @@ -37,7 +38,13 @@ struct DecoderManager { return it->second.get(); } - auto decoder = std::make_unique(); + std::unique_ptr decoder; + if (Hardware::TICI() && hw_decoder) { + decoder = std::make_unique(); + } else { + decoder = std::make_unique(); + } + if (!decoder->open(codecpar, hw_decoder)) { decoder.reset(nullptr); } @@ -114,19 +121,19 @@ bool FrameReader::get(int idx, VisionBuf *buf) { // class VideoDecoder -VideoDecoder::VideoDecoder() { +FFmpegVideoDecoder::FFmpegVideoDecoder() { av_frame_ = av_frame_alloc(); hw_frame_ = av_frame_alloc(); } -VideoDecoder::~VideoDecoder() { +FFmpegVideoDecoder::~FFmpegVideoDecoder() { if (hw_device_ctx) av_buffer_unref(&hw_device_ctx); if (decoder_ctx) avcodec_free_context(&decoder_ctx); av_frame_free(&av_frame_); av_frame_free(&hw_frame_); } -bool VideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { +bool FFmpegVideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { const AVCodec *decoder = avcodec_find_decoder(codecpar->codec_id); if (!decoder) return false; @@ -149,7 +156,7 @@ bool VideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { return true; } -bool VideoDecoder::initHardwareDecoder(AVHWDeviceType hw_device_type) { +bool FFmpegVideoDecoder::initHardwareDecoder(AVHWDeviceType hw_device_type) { const AVCodecHWConfig *config = nullptr; for (int i = 0; (config = avcodec_get_hw_config(decoder_ctx->codec, i)) != nullptr; i++) { if (config->methods & AV_CODEC_HW_CONFIG_METHOD_HW_DEVICE_CTX && config->device_type == hw_device_type) { @@ -175,7 +182,7 @@ bool VideoDecoder::initHardwareDecoder(AVHWDeviceType hw_device_type) { return true; } -bool VideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { +bool FFmpegVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { int current_idx = idx; if (idx != reader->prev_idx + 1) { // seeking to the nearest key frame @@ -219,7 +226,7 @@ bool VideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { return false; } -AVFrame *VideoDecoder::decodeFrame(AVPacket *pkt) { +AVFrame *FFmpegVideoDecoder::decodeFrame(AVPacket *pkt) { int ret = avcodec_send_packet(decoder_ctx, pkt); if (ret < 0) { rError("Error sending a packet for decoding: %d", ret); @@ -239,7 +246,7 @@ AVFrame *VideoDecoder::decodeFrame(AVPacket *pkt) { return (av_frame_->format == hw_pix_fmt) ? hw_frame_ : av_frame_; } -bool VideoDecoder::copyBuffer(AVFrame *f, VisionBuf *buf) { +bool FFmpegVideoDecoder::copyBuffer(AVFrame *f, VisionBuf *buf) { if (hw_pix_fmt == HW_PIX_FMT) { for (int i = 0; i < height/2; i++) { memcpy(buf->y + (i*2 + 0)*buf->stride, f->data[0] + (i*2 + 0)*f->linesize[0], width); @@ -256,3 +263,45 @@ bool VideoDecoder::copyBuffer(AVFrame *f, VisionBuf *buf) { } return true; } + +bool QcomVideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { + if (codecpar->codec_id != AV_CODEC_ID_HEVC) { + rError("Hardware decoder only supports HEVC codec"); + return false; + } + width = codecpar->width; + height = codecpar->height; + msm_vidc.init(VIDEO_DEVICE, width, height, V4L2_PIX_FMT_HEVC); + return true; +} + +bool QcomVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { + int from_idx = idx; + if (idx != reader->prev_idx + 1) { + // seeking to the nearest key frame + for (int i = idx; i >= 0; --i) { + if (reader->packets_info[i].flags & AV_PKT_FLAG_KEY) { + from_idx = i; + break; + } + } + + auto pos = reader->packets_info[from_idx].pos; + int ret = avformat_seek_file(reader->input_ctx, 0, pos, pos, pos, AVSEEK_FLAG_BYTE); + if (ret < 0) { + rError("Failed to seek to byte position %lld: %d", pos, AVERROR(ret)); + return false; + } + } + reader->prev_idx = idx; + bool result = false; + AVPacket pkt; + msm_vidc.avctx = reader->input_ctx; + for (int i = from_idx; i <= idx; ++i) { + if (av_read_frame(reader->input_ctx, &pkt) == 0) { + result = msm_vidc.decodeFrame(&pkt, buf) && (i == idx); + av_packet_unref(&pkt); + } + } + return result; +} diff --git a/tools/replay/framereader.h b/tools/replay/framereader.h index a15847e311..1fb3cdfeb1 100644 --- a/tools/replay/framereader.h +++ b/tools/replay/framereader.h @@ -6,6 +6,7 @@ #include "msgq/visionipc/visionbuf.h" #include "tools/replay/filereader.h" #include "tools/replay/util.h" +#include "tools/replay/qcom_decoder.h" extern "C" { #include @@ -40,11 +41,18 @@ public: class VideoDecoder { public: - VideoDecoder(); - ~VideoDecoder(); - bool open(AVCodecParameters *codecpar, bool hw_decoder); - bool decode(FrameReader *reader, int idx, VisionBuf *buf); + virtual ~VideoDecoder() = default; + virtual bool open(AVCodecParameters *codecpar, bool hw_decoder) = 0; + virtual bool decode(FrameReader *reader, int idx, VisionBuf *buf) = 0; int width = 0, height = 0; +}; + +class FFmpegVideoDecoder : public VideoDecoder { +public: + FFmpegVideoDecoder(); + ~FFmpegVideoDecoder() override; + bool open(AVCodecParameters *codecpar, bool hw_decoder) override; + bool decode(FrameReader *reader, int idx, VisionBuf *buf) override; private: bool initHardwareDecoder(AVHWDeviceType hw_device_type); @@ -56,3 +64,14 @@ private: AVPixelFormat hw_pix_fmt = AV_PIX_FMT_NONE; AVBufferRef *hw_device_ctx = nullptr; }; + +class QcomVideoDecoder : public VideoDecoder { +public: + QcomVideoDecoder() {}; + ~QcomVideoDecoder() override {}; + bool open(AVCodecParameters *codecpar, bool hw_decoder) override; + bool decode(FrameReader *reader, int idx, VisionBuf *buf) override; + +private: + MsmVidc msm_vidc = MsmVidc(); +}; diff --git a/tools/replay/qcom_decoder.cc b/tools/replay/qcom_decoder.cc new file mode 100644 index 0000000000..eb5409daa3 --- /dev/null +++ b/tools/replay/qcom_decoder.cc @@ -0,0 +1,346 @@ +#include "qcom_decoder.h" + +#include +#include "third_party/linux/include/v4l2-controls.h" +#include + + +#include "common/swaglog.h" +#include "common/util.h" + +// echo "0xFFFF" > /sys/kernel/debug/msm_vidc/debug_level + +static void copyBuffer(VisionBuf *src_buf, VisionBuf *dst_buf) { + // Copy Y plane + memcpy(dst_buf->y, src_buf->y, src_buf->height * src_buf->stride); + // Copy UV plane + memcpy(dst_buf->uv, src_buf->uv, src_buf->height / 2 * src_buf->stride); +} + +static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) { + struct v4l2_requestbuffers reqbuf = { + .count = count, + .type = buf_type, + .memory = V4L2_MEMORY_USERPTR + }; + util::safe_ioctl(fd, VIDIOC_REQBUFS, &reqbuf, "VIDIOC_REQBUFS failed"); +} + +MsmVidc::~MsmVidc() { + if (fd > 0) { + close(fd); + } +} + +bool MsmVidc::init(const char* dev, size_t width, size_t height, uint64_t codec) { + LOG("Initializing msm_vidc device %s", dev); + this->w = width; + this->h = height; + this->fd = open(dev, O_RDWR, 0); + if (fd < 0) { + LOGE("failed to open video device %s", dev); + return false; + } + subscribeEvents(); + v4l2_buf_type out_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + setPlaneFormat(out_type, V4L2_PIX_FMT_HEVC); // Also allocates the output buffer + setFPS(FPS); + request_buffers(fd, out_type, OUTPUT_BUFFER_COUNT); + util::safe_ioctl(fd, VIDIOC_STREAMON, &out_type, "VIDIOC_STREAMON OUTPUT failed"); + restartCapture(); + setupPolling(); + + this->initialized = true; + return true; +} + +VisionBuf* MsmVidc::decodeFrame(AVPacket *pkt, VisionBuf *buf) { + assert(initialized && (pkt != nullptr) && (buf != nullptr)); + + this->frame_ready = false; + this->current_output_buf = buf; + bool sent_packet = false; + + while (!this->frame_ready) { + if (!sent_packet) { + int buf_index = getBufferUnlocked(); + if (buf_index >= 0) { + assert(buf_index < out_buf_cnt); + sendPacket(buf_index, pkt); + sent_packet = true; + } + } + + if (poll(pfd, nfds, -1) < 0) { + LOGE("poll() error: %d", errno); + return nullptr; + } + + if (VisionBuf* result = processEvents()) { + return result; + } + } + + return buf; +} + +VisionBuf* MsmVidc::processEvents() { + for (int idx = 0; idx < nfds; idx++) { + short revents = pfd[idx].revents; + if (!revents) continue; + + if (idx == ev[EV_VIDEO]) { + if (revents & (POLLIN | POLLRDNORM)) { + VisionBuf *result = handleCapture(); + if (result == this->current_output_buf) { + this->frame_ready = true; + } + } + if (revents & (POLLOUT | POLLWRNORM)) { + handleOutput(); + } + if (revents & POLLPRI) { + handleEvent(); + } + } else { + LOGE("Unexpected event on fd %d", pfd[idx].fd); + } + } + return nullptr; +} + +VisionBuf* MsmVidc::handleCapture() { + struct v4l2_buffer buf = {0}; + struct v4l2_plane planes[1] = {0}; + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + buf.memory = V4L2_MEMORY_USERPTR; + buf.m.planes = planes; + buf.length = 1; + util::safe_ioctl(this->fd, VIDIOC_DQBUF, &buf, "VIDIOC_DQBUF CAPTURE failed"); + + if (this->reconfigure_pending || buf.m.planes[0].bytesused == 0) { + return nullptr; + } + + copyBuffer(&cap_bufs[buf.index], this->current_output_buf); + queueCaptureBuffer(buf.index); + return this->current_output_buf; +} + +bool MsmVidc::subscribeEvents() { + for (uint32_t event : subscriptions) { + struct v4l2_event_subscription sub = { .type = event}; + util::safe_ioctl(fd, VIDIOC_SUBSCRIBE_EVENT, &sub, "VIDIOC_SUBSCRIBE_EVENT failed"); + } + return true; +} + +bool MsmVidc::setPlaneFormat(enum v4l2_buf_type type, uint32_t fourcc) { + struct v4l2_format fmt = {.type = type}; + struct v4l2_pix_format_mplane *pix = &fmt.fmt.pix_mp; + *pix = { + .width = (__u32)this->w, + .height = (__u32)this->h, + .pixelformat = fourcc + }; + util::safe_ioctl(fd, VIDIOC_S_FMT, &fmt, "VIDIOC_S_FMT failed"); + if (type == V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE) { + this->out_buf_size = pix->plane_fmt[0].sizeimage; + int ion_size = this->out_buf_size * OUTPUT_BUFFER_COUNT; // Output (input) buffers are ION buffer. + this->out_buf.allocate(ion_size); // mmap rw + for (int i = 0; i < OUTPUT_BUFFER_COUNT; i++) { + this->out_buf_off[i] = i * this->out_buf_size; + this->out_buf_addr[i] = (char *)this->out_buf.addr + this->out_buf_off[i]; + this->out_buf_flag[i] = false; + } + LOGD("Set output buffer size to %d, count %d, addr %p", this->out_buf_size, OUTPUT_BUFFER_COUNT, this->out_buf.addr); + } else if (type == V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE) { + request_buffers(this->fd, type, CAPTURE_BUFFER_COUNT); + util::safe_ioctl(fd, VIDIOC_G_FMT, &fmt, "VIDIOC_G_FMT failed"); + const __u32 y_size = pix->plane_fmt[0].sizeimage; + const __u32 y_stride = pix->plane_fmt[0].bytesperline; + for (int i = 0; i < CAPTURE_BUFFER_COUNT; i++) { + size_t uv_offset = (size_t)y_stride * pix->height; + size_t required = uv_offset + (y_stride * pix->height / 2); // enough for Y + UV. For linear NV12, UV plane starts at y_stride * height. + size_t alloc_size = std::max(y_size, required); + this->cap_bufs[i].allocate(alloc_size); + this->cap_bufs[i].init_yuv(pix->width, pix->height, y_stride, uv_offset); + } + LOGD("Set capture buffer size to %d, count %d, addr %p, extradata size %d", + pix->plane_fmt[0].sizeimage, CAPTURE_BUFFER_COUNT, this->cap_bufs[0].addr, pix->plane_fmt[1].sizeimage); + } + return true; +} + +bool MsmVidc::setFPS(uint32_t fps) { + struct v4l2_streamparm streamparam = { + .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, + .parm.output.timeperframe = {1, fps} + }; + util::safe_ioctl(fd, VIDIOC_S_PARM, &streamparam, "VIDIOC_S_PARM failed"); + return true; +} + +bool MsmVidc::restartCapture() { + // stop if already initialized + enum v4l2_buf_type type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + if (this->initialized) { + LOGD("Restarting capture, flushing buffers..."); + util::safe_ioctl(this->fd, VIDIOC_STREAMOFF, &type, "VIDIOC_STREAMOFF CAPTURE failed"); + struct v4l2_requestbuffers reqbuf = {.type = type, .memory = V4L2_MEMORY_USERPTR}; + util::safe_ioctl(this->fd, VIDIOC_REQBUFS, &reqbuf, "VIDIOC_REQBUFS failed"); + for (size_t i = 0; i < CAPTURE_BUFFER_COUNT; ++i) { + this->cap_bufs[i].free(); + this->cap_buf_flag[i] = false; // mark as not queued + cap_bufs[i].~VisionBuf(); + new (&cap_bufs[i]) VisionBuf(); + } + } + // setup, start and queue capture buffers + setDBP(); + setPlaneFormat(type, V4L2_PIX_FMT_NV12); + util::safe_ioctl(this->fd, VIDIOC_STREAMON, &type, "VIDIOC_STREAMON CAPTURE failed"); + for (size_t i = 0; i < CAPTURE_BUFFER_COUNT; ++i) { + queueCaptureBuffer(i); + } + + return true; +} + +bool MsmVidc::queueCaptureBuffer(int i) { + struct v4l2_buffer buf = {0}; + struct v4l2_plane planes[1] = {0}; + + buf.type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; + buf.memory = V4L2_MEMORY_USERPTR; + buf.index = i; + buf.m.planes = planes; + buf.length = 1; + // decoded frame plane + planes[0].m.userptr = (unsigned long)this->cap_bufs[i].addr; // no security + planes[0].length = this->cap_bufs[i].len; + planes[0].reserved[0] = this->cap_bufs[i].fd; // ION fd + planes[0].reserved[1] = 0; + planes[0].bytesused = this->cap_bufs[i].len; + planes[0].data_offset = 0; + util::safe_ioctl(this->fd, VIDIOC_QBUF, &buf, "VIDIOC_QBUF failed"); + this->cap_buf_flag[i] = true; // mark as queued + return true; +} + +bool MsmVidc::queueOutputBuffer(int i, size_t size) { + struct v4l2_buffer buf = {0}; + struct v4l2_plane planes[1] = {0}; + + buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + buf.memory = V4L2_MEMORY_USERPTR; + buf.index = i; + buf.m.planes = planes; + buf.length = 1; + // decoded frame plane + planes[0].m.userptr = (unsigned long)this->out_buf_off[i]; // check this + planes[0].length = this->out_buf_size; + planes[0].reserved[0] = this->out_buf.fd; // ION fd + planes[0].reserved[1] = 0; + planes[0].bytesused = size; + planes[0].data_offset = 0; + assert((this->out_buf_off[i] & 0xfff) == 0); // must be 4 KiB aligned + assert(this->out_buf_size % 4096 == 0); // ditto for size + + util::safe_ioctl(this->fd, VIDIOC_QBUF, &buf, "VIDIOC_QBUF failed"); + this->out_buf_flag[i] = true; // mark as queued + return true; +} + +bool MsmVidc::setDBP() { + struct v4l2_ext_control control[2] = {0}; + struct v4l2_ext_controls controls = {0}; + control[0].id = V4L2_CID_MPEG_VIDC_VIDEO_STREAM_OUTPUT_MODE; + control[0].value = 1; // V4L2_CID_MPEG_VIDC_VIDEO_STREAM_OUTPUT_SECONDARY + control[1].id = V4L2_CID_MPEG_VIDC_VIDEO_DPB_COLOR_FORMAT; + control[1].value = 0; // V4L2_MPEG_VIDC_VIDEO_DPB_COLOR_FMT_NONE + controls.count = 2; + controls.ctrl_class = V4L2_CTRL_CLASS_MPEG; + controls.controls = control; + util::safe_ioctl(fd, VIDIOC_S_EXT_CTRLS, &controls, "VIDIOC_S_EXT_CTRLS failed"); + return true; +} + +bool MsmVidc::setupPolling() { + // Initialize poll array + pfd[EV_VIDEO] = {fd, POLLIN | POLLOUT | POLLWRNORM | POLLRDNORM | POLLPRI, 0}; + ev[EV_VIDEO] = EV_VIDEO; + nfds = 1; + return true; +} + +bool MsmVidc::sendPacket(int buf_index, AVPacket *pkt) { + assert(buf_index >= 0 && buf_index < out_buf_cnt); + assert(pkt != nullptr && pkt->data != nullptr && pkt->size > 0); + // Prepare output buffer + memset(this->out_buf_addr[buf_index], 0, this->out_buf_size); + uint8_t * data = (uint8_t *)this->out_buf_addr[buf_index]; + memcpy(data, pkt->data, pkt->size); + queueOutputBuffer(buf_index, pkt->size); + return true; +} + +int MsmVidc::getBufferUnlocked() { + for (int i = 0; i < this->out_buf_cnt; i++) { + if (!out_buf_flag[i]) { + return i; + } + } + return -1; +} + + +bool MsmVidc::handleOutput() { + struct v4l2_buffer buf = {0}; + struct v4l2_plane planes[1]; + buf.type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; + buf.memory = V4L2_MEMORY_USERPTR; + buf.m.planes = planes; + buf.length = 1; + util::safe_ioctl(this->fd, VIDIOC_DQBUF, &buf, "VIDIOC_DQBUF OUTPUT failed"); + this->out_buf_flag[buf.index] = false; // mark as not queued + return true; +} + +bool MsmVidc::handleEvent() { + // dequeue event + struct v4l2_event event = {0}; + util::safe_ioctl(this->fd, VIDIOC_DQEVENT, &event, "VIDIOC_DQEVENT failed"); + switch (event.type) { + case V4L2_EVENT_MSM_VIDC_PORT_SETTINGS_CHANGED_INSUFFICIENT: { + unsigned int *ptr = (unsigned int *)event.u.data; + unsigned int height = ptr[0]; + unsigned int width = ptr[1]; + this->w = width; + this->h = height; + LOGD("Port Reconfig received insufficient, new size %ux%u, flushing capture bufs...", width, height); // This is normal + struct v4l2_decoder_cmd dec; + dec.flags = V4L2_QCOM_CMD_FLUSH_CAPTURE; + dec.cmd = V4L2_QCOM_CMD_FLUSH; + util::safe_ioctl(this->fd, VIDIOC_DECODER_CMD, &dec, "VIDIOC_DECODER_CMD FLUSH_CAPTURE failed"); + this->reconfigure_pending = true; + LOGD("Waiting for flush done event to reconfigure capture queue"); + break; + } + + case V4L2_EVENT_MSM_VIDC_FLUSH_DONE: { + unsigned int *ptr = (unsigned int *)event.u.data; + unsigned int flags = ptr[0]; + if (flags & V4L2_QCOM_CMD_FLUSH_CAPTURE) { + if (this->reconfigure_pending) { + this->restartCapture(); + this->reconfigure_pending = false; + } + } + break; + } + default: + break; + } + return true; +} \ No newline at end of file diff --git a/tools/replay/qcom_decoder.h b/tools/replay/qcom_decoder.h new file mode 100644 index 0000000000..1d7522cc70 --- /dev/null +++ b/tools/replay/qcom_decoder.h @@ -0,0 +1,88 @@ +#pragma once + +#include +#include + +#include "msgq/visionipc/visionbuf.h" + +extern "C" { + #include + #include +} + +#define V4L2_EVENT_MSM_VIDC_START (V4L2_EVENT_PRIVATE_START + 0x00001000) +#define V4L2_EVENT_MSM_VIDC_FLUSH_DONE (V4L2_EVENT_MSM_VIDC_START + 1) +#define V4L2_EVENT_MSM_VIDC_PORT_SETTINGS_CHANGED_INSUFFICIENT (V4L2_EVENT_MSM_VIDC_START + 3) +#define V4L2_CID_MPEG_MSM_VIDC_BASE 0x00992000 +#define V4L2_CID_MPEG_VIDC_VIDEO_DPB_COLOR_FORMAT (V4L2_CID_MPEG_MSM_VIDC_BASE + 44) +#define V4L2_CID_MPEG_VIDC_VIDEO_STREAM_OUTPUT_MODE (V4L2_CID_MPEG_MSM_VIDC_BASE + 22) +#define V4L2_QCOM_CMD_FLUSH_CAPTURE (1 << 1) +#define V4L2_QCOM_CMD_FLUSH (4) + +#define VIDEO_DEVICE "/dev/video32" +#define OUTPUT_BUFFER_COUNT 8 +#define CAPTURE_BUFFER_COUNT 8 +#define FPS 20 + + +class MsmVidc { +public: + MsmVidc() = default; + ~MsmVidc(); + + bool init(const char* dev, size_t width, size_t height, uint64_t codec); + VisionBuf* decodeFrame(AVPacket* pkt, VisionBuf* buf); + + AVFormatContext* avctx = nullptr; + int fd = 0; + +private: + bool initialized = false; + bool reconfigure_pending = false; + bool frame_ready = false; + + VisionBuf* current_output_buf = nullptr; + VisionBuf out_buf; // Single input buffer + VisionBuf cap_bufs[CAPTURE_BUFFER_COUNT]; // Capture (output) buffers + + size_t w = 1928, h = 1208; + size_t cap_height = 0, cap_width = 0; + + int cap_buf_size = 0; + int out_buf_size = 0; + + size_t cap_plane_off[CAPTURE_BUFFER_COUNT] = {0}; + size_t cap_plane_stride[CAPTURE_BUFFER_COUNT] = {0}; + bool cap_buf_flag[CAPTURE_BUFFER_COUNT] = {false}; + + size_t out_buf_off[OUTPUT_BUFFER_COUNT] = {0}; + void* out_buf_addr[OUTPUT_BUFFER_COUNT] = {0}; + bool out_buf_flag[OUTPUT_BUFFER_COUNT] = {false}; + const int out_buf_cnt = OUTPUT_BUFFER_COUNT; + + const int subscriptions[2] = { + V4L2_EVENT_MSM_VIDC_FLUSH_DONE, + V4L2_EVENT_MSM_VIDC_PORT_SETTINGS_CHANGED_INSUFFICIENT + }; + + enum { EV_VIDEO, EV_COUNT }; + struct pollfd pfd[EV_COUNT] = {0}; + int ev[EV_COUNT] = {-1}; + int nfds = 0; + + VisionBuf* processEvents(); + bool setupOutput(); + bool subscribeEvents(); + bool setPlaneFormat(v4l2_buf_type type, uint32_t fourcc); + bool setFPS(uint32_t fps); + bool restartCapture(); + bool queueCaptureBuffer(int i); + bool queueOutputBuffer(int i, size_t size); + bool setDBP(); + bool setupPolling(); + bool sendPacket(int buf_index, AVPacket* pkt); + int getBufferUnlocked(); + VisionBuf* handleCapture(); + bool handleOutput(); + bool handleEvent(); +}; From 7057c5741908df51079fefe23b7305109f4df8f7 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 5 Sep 2025 20:51:58 -0400 Subject: [PATCH 126/188] ui: cleanup cereal event params parsing (#1219) * Revert "bugfix: streamline LiveDelay parameter loading with safe handling (#1204)" This reverts commit 288a5e14daf5bdb3948cf4de8aab96c62f1b9cca. * ui: use AlignedBuffer for cereal data processing for Models panel * align * separate * split * event it * no more backup * Revert "no more backup" This reverts commit fa66ce5e774db6832a0cf3a32ff6fb6803cf0df1. --- .../qt/offroad/settings/models_panel.cc | 58 ++++++------------- .../qt/offroad/settings/models_panel.h | 1 + selfdrive/ui/sunnypilot/qt/util.cc | 13 +++++ selfdrive/ui/sunnypilot/qt/util.h | 3 + 4 files changed, 34 insertions(+), 41 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index 02a01a4b63..c3f795e18c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -48,25 +48,6 @@ static const QString progressStyleError = progressStyleActive + " background-color: transparent;" "}"; -std::optional safeParamEventLoad(Params& params, const std::string& paramName) { - std::string raw = params.get(paramName); - if (raw.empty()) { - return std::nullopt; - } - - try { - AlignedBuffer alignedBuf; - auto buf = alignedBuf.align(raw.data(), raw.size()); - - capnp::FlatArrayMessageReader msg(kj::ArrayPtr(buf.begin(), buf.size())); - return msg.getRoot(); - } - catch (const kj::Exception& e) { - qInfo() << "Invalid param" << QString::fromStdString(paramName) << ":" << e.getDescription().cStr(); - return std::nullopt; - } -} - ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { QVBoxLayout *main_layout = new QVBoxLayout(this); main_layout->setContentsMargins(50, 20, 50, 20); @@ -152,16 +133,10 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { list->addItem(lagd_toggle_control); // Software delay control - int liveDelayMaxInt = 30; - if (const auto event = safeParamEventLoad(params, "LiveDelay"); event && event->hasLiveDelay()) { - auto liveDelay = event->getLiveDelay(); - float lateralDelay = liveDelay.getLateralDelay(); - liveDelayMaxInt = static_cast(lateralDelay * 100.0f) + 20; - } delay_control = new OptionControlSP("LagdToggleDelay", tr("Adjust Software Delay"), - tr("Adjust the software delay when Live Learning Steer Delay is toggled off." - "\nThe default software delay value is 0.2"), - "", {5, liveDelayMaxInt}, 1, false, nullptr, true, true); + tr("Adjust the software delay when Live Learning Steer Delay is toggled off." + "\nThe default software delay value is 0.2"), + "", {5, 50}, 1, false, nullptr, true, true); connect(delay_control, &OptionControlSP::updateLabels, [=]() { float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); @@ -449,27 +424,28 @@ void ModelsPanel::updateLabels() { "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."); bool lagdEnabled = params.getBool("LagdToggle"); if (lagdEnabled) { - if (const auto event = safeParamEventLoad(params, "LiveDelay"); event && event->hasLiveDelay()) { - auto liveDelay = event->getLiveDelay(); - float lateralDelay = liveDelay.getLateralDelay(); + auto liveDelayBytes = params.get("LiveDelay"); + if (!liveDelayBytes.empty()) { + auto LD = loadCerealEvent(params, "LiveDelay"); + float lateralDelay = LD->getLiveDelay().getLateralDelay(); desc += QString("

%1 %2 s") - .arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3)); + .arg(tr("Live Steer Delay:")).arg(QString::number(lateralDelay, 'f', 3)); } } else { - std::string carParamsBytes = params.get("CarParamsPersistent"); + auto carParamsBytes = params.get("CarParamsPersistent"); if (!carParamsBytes.empty()) { - capnp::FlatArrayMessageReader msg(kj::ArrayPtr( - reinterpret_cast(carParamsBytes.data()), - carParamsBytes.size() / sizeof(capnp::word))); - auto carParams = msg.getRoot(); - float steerDelay = carParams.getSteerActuatorDelay(); + AlignedBuffer aligned_buf_cp; + capnp::FlatArrayMessageReader cmsg(aligned_buf_cp.align(carParamsBytes.data(), carParamsBytes.size())); + cereal::CarParams::Reader CP = cmsg.getRoot(); + + float steerDelay = CP.getSteerActuatorDelay(); float softwareDelay = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); float totalLag = steerDelay + softwareDelay; desc += QString("

" "%1 %2 s + %3 %4 s = %5 %6 s") - .arg(tr("Actuator Delay:"), QString::number(steerDelay, 'f', 2), - tr("Software Delay:"), QString::number(softwareDelay, 'f', 2), - tr("Total Delay:"), QString::number(totalLag, 'f', 2)); + .arg(tr("Actuator Delay:"), QString::number(steerDelay, 'f', 2), + tr("Software Delay:"), QString::number(softwareDelay, 'f', 2), + tr("Total Delay:"), QString::number(totalLag, 'f', 2)); } } lagd_toggle_control->setDescription(desc); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h index 1906ebd2a0..1a39800dde 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h @@ -9,6 +9,7 @@ #include +#include "selfdrive/ui/sunnypilot/qt/util.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" class ModelsPanel : public QWidget { diff --git a/selfdrive/ui/sunnypilot/qt/util.cc b/selfdrive/ui/sunnypilot/qt/util.cc index ca85935d0b..2e066e88b5 100644 --- a/selfdrive/ui/sunnypilot/qt/util.cc +++ b/selfdrive/ui/sunnypilot/qt/util.cc @@ -110,3 +110,16 @@ QStringList searchFromList(const QString &query, const QStringList &list) { } return search_results; } + +std::optional loadCerealEvent(Params& params, const std::string& _param) { + std::string bytes = params.get(_param); + + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(bytes.data(), bytes.size())); + return cmsg.getRoot(); + } catch (kj::Exception& e) { + qInfo() << "invalid " << QString::fromStdString(_param) << ":" << e.getDescription().cStr(); + return std::nullopt; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/util.h b/selfdrive/ui/sunnypilot/qt/util.h index 089b5370cc..4b9d615ce5 100644 --- a/selfdrive/ui/sunnypilot/qt/util.h +++ b/selfdrive/ui/sunnypilot/qt/util.h @@ -15,8 +15,11 @@ #include #include +#include "selfdrive/ui/sunnypilot/ui.h" + QString getUserAgent(bool sunnylink = false); std::optional getSunnylinkDongleId(); std::optional getParamIgnoringDefault(const std::string ¶m_name, const std::string &default_value); QMap loadPlatformList(); QStringList searchFromList(const QString &query, const QStringList &list); +std::optional loadCerealEvent(Params& params, const std::string& _param); From 1033d3d80e5e804f11e0e379849111249c28828f Mon Sep 17 00:00:00 2001 From: Shane Smiskol Date: Fri, 5 Sep 2025 22:34:04 -0700 Subject: [PATCH 127/188] Desire helper: set lane change direction on entering preLaneChange state (#36074) * set immediately to avoid flash on right lane changes * one function * name * comment --- selfdrive/controls/lib/desire_helper.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 730aaeb8f1..ee4567f1e9 100644 --- a/selfdrive/controls/lib/desire_helper.py +++ b/selfdrive/controls/lib/desire_helper.py @@ -40,6 +40,10 @@ class DesireHelper: self.prev_one_blinker = False self.desire = log.Desire.none + @staticmethod + def get_lane_change_direction(CS): + return LaneChangeDirection.left if CS.leftBlinker else LaneChangeDirection.right + def update(self, carstate, lateral_active, lane_change_prob): v_ego = carstate.vEgo one_blinker = carstate.leftBlinker != carstate.rightBlinker @@ -53,12 +57,13 @@ class DesireHelper: if self.lane_change_state == LaneChangeState.off and one_blinker and not self.prev_one_blinker and not below_lane_change_speed: self.lane_change_state = LaneChangeState.preLaneChange self.lane_change_ll_prob = 1.0 + # Initialize lane change direction to prevent UI alert flicker + self.lane_change_direction = self.get_lane_change_direction(carstate) # LaneChangeState.preLaneChange elif self.lane_change_state == LaneChangeState.preLaneChange: - # Set lane change direction - self.lane_change_direction = LaneChangeDirection.left if \ - carstate.leftBlinker else LaneChangeDirection.right + # Update lane change direction + self.lane_change_direction = self.get_lane_change_direction(carstate) torque_applied = carstate.steeringPressed and \ ((carstate.steeringTorque > 0 and self.lane_change_direction == LaneChangeDirection.left) or From 03e9777c3ff29c9bdaca33facb9eb2bb58df25c6 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sat, 6 Sep 2025 21:05:15 +0200 Subject: [PATCH 128/188] Improve debugging for safety (#36055) * feat: add debugging configurations for replay drive and LLDB attachment * Add readme with video demo * clean * docs: update debugging safety documentation with demo link * no need for mp4 then added on PR * Update SConstruct * bump opendbc * updating readme * updating readme * updating readme * is this better / worth it? * final cleanups * hacky. but does it work? * Yep that worked! --------- Co-authored-by: Adeeb Shihadeh --- .vscode/launch.json | 41 +++++++++++++++++++++++++++++++++++++++- docs/DEBUGGING_SAFETY.md | 30 +++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 1 deletion(-) create mode 100644 docs/DEBUGGING_SAFETY.md diff --git a/.vscode/launch.json b/.vscode/launch.json index a6b341d9ea..f090061c42 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -23,6 +23,11 @@ "id": "args", "description": "Arguments to pass to the process", "type": "promptString" + }, + { + "id": "replayArg", + "type": "promptString", + "description": "Enter route or segment to replay." } ], "configurations": [ @@ -40,7 +45,41 @@ "type": "cppdbg", "request": "launch", "program": "${workspaceFolder}/${input:cpp_process}", - "cwd": "${workspaceFolder}", + "cwd": "${workspaceFolder}" + }, + { + "name": "Attach LLDB to Replay drive", + "type": "lldb", + "request": "attach", + "pid": "${command:pickMyProcess}", + "initCommands": [ + "script import time; time.sleep(3)" + ] + }, + { + "name": "Replay drive", + "type": "debugpy", + "request": "launch", + "program": "${workspaceFolder}/opendbc/safety/tests/safety_replay/replay_drive.py", + "args": [ + "${input:replayArg}" + ], + "console": "integratedTerminal", + "justMyCode": false, + "env": { + "PYTHONPATH": "${workspaceFolder}" + }, + "subProcess": true, + "stopOnEntry": false + } + ], + "compounds": [ + { + "name": "Replay drive + Safety LLDB", + "configurations": [ + "Replay drive", + "Attach LLDB to Replay drive" + ] } ] } \ No newline at end of file diff --git a/docs/DEBUGGING_SAFETY.md b/docs/DEBUGGING_SAFETY.md new file mode 100644 index 0000000000..cd0a46b446 --- /dev/null +++ b/docs/DEBUGGING_SAFETY.md @@ -0,0 +1,30 @@ +# Debugging Panda Safety with Replay Drive + LLDB + +## 1. Start the debugger in VS Code + +* Select **Replay drive + Safety LLDB**. +* Enter the route or segment when prompted. +[](https://github.com/user-attachments/assets/b0cc320a-083e-46a7-a9f8-ca775bbe5604) + +## 2. Attach LLDB + +* When prompted, pick the running **`replay_drive` process**. +* âš ï¸ Attach quickly, or `replay_drive` will start consuming messages. + +> [!TIP] +> Add a Python breakpoint at the start of `replay_drive.py` to pause execution and give yourself time to attach LLDB. + +## 3. Set breakpoints in VS Code +Breakpoints can be set directly in `modes/xxx.h` (or any C file). +No extra LLDB commands are required — just place breakpoints in the editor. + +## 4. Resume execution +Once attached, you can step through both Python (on the replay) and C safety code as CAN logs are replayed. + +> [!NOTE] +> * Use short routes for quicker iteration. +> * Pause `replay_drive` early to avoid wasting log messages. + +## Video + +View a demo of this workflow on the PR that added it: https://github.com/commaai/openpilot/pull/36055#issue-3352911578 \ No newline at end of file From b161764b1e9a4152c9641b3a4168c7e88960e59e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 6 Sep 2025 15:26:32 -0400 Subject: [PATCH 129/188] update: sunnypilot branch migrations for tici (#1212) * update: sunnypilot branch migrations for tici * block onroad and notify * type * check channel type * update * ui init * no search and locked list for tici * whenever available --- common/params_keys.h | 1 + selfdrive/selfdrived/alerts_offroad.json | 5 ++++ .../qt/offroad/settings/software_panel.cc | 29 ++++++++++++++++++- system/hardware/hardwared.py | 12 +++++++- system/updated/updated.py | 6 ++-- system/version.py | 11 ++++++- 6 files changed, 57 insertions(+), 7 deletions(-) diff --git a/common/params_keys.h b/common/params_keys.h index e0af53b299..afb6b348eb 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -154,6 +154,7 @@ inline static std::unordered_map keys = { {"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}}, {"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}}, {"OffroadMode", {CLEAR_ON_MANAGER_START, BOOL}}, + {"Offroad_TiciSupport", {CLEAR_ON_MANAGER_START, JSON}}, {"QuickBootToggle", {PERSISTENT | BACKUP, BOOL, "0"}}, {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 183c1f8547..87a007211f 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -49,5 +49,10 @@ "text": "openpilot detected excessive %1 actuation on your last drive. Please contact support at https://comma.ai/support and share your device's Dongle ID for troubleshooting.", "severity": 1, "_comment": "Set extra field to lateral or longitudinal." + }, + "Offroad_TiciSupport": { + "text": "Unsupported branch detected - The current version of %1 branch is no longer supported on the comma three. Please go to [Device > Software] and install a supported branch with -tici in the branch name for the comma three.", + "severity": 1, + "_comment": "Set extra field to the current branch name." } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index a1961cb1ea..8bdb7703f2 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -11,12 +11,39 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { // branch selector QObject::disconnect(targetBranchBtn, nullptr, nullptr, nullptr); connect(targetBranchBtn, &ButtonControlSP::clicked, [=]() { - InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false); + if (Hardware::get_device_type() == cereal::InitData::DeviceType::TICI) { + auto current = params.get("GitBranch"); + QStringList allBranches = QString::fromStdString(params.get("UpdaterAvailableBranches")).split(","); + QStringList branches; + for (const QString &b : allBranches) { + if (b.endsWith("-tici")) { + branches.append(b); + } + } + + for (QString b : {current.c_str(), "master-tici", "staging-tici", "release-tici"}) { + auto i = branches.indexOf(b); + if (i >= 0) { + branches.removeAt(i); + branches.insert(0, b); + } + } + + QString cur = QString::fromStdString(params.get("UpdaterTargetBranch")); + QString selection = MultiOptionDialog::getSelection(tr("Select a branch"), branches, cur, this); + if (!selection.isEmpty()) { + params.put("UpdaterTargetBranch", selection.toStdString()); + targetBranchBtn->setValue(QString::fromStdString(params.get("UpdaterTargetBranch"))); + checkForUpdates(); + } + } else { + InputDialog d(tr("Search Branch"), this, tr("Enter search keywords, or leave blank to list all branches."), false); d.setMinLength(0); const int ret = d.exec(); if (ret) { searchBranches(d.text()); } + } }); // Disable Updates toggle diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index d702334fa8..af01154144 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -24,7 +24,7 @@ from openpilot.system.statsd import statlog from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.power_monitoring import PowerMonitoring from openpilot.system.hardware.fan_controller import TiciFanController -from openpilot.system.version import terms_version, training_version +from openpilot.system.version import terms_version, training_version, get_build_metadata ThermalStatus = log.DeviceState.ThermalStatus NetworkType = log.DeviceState.NetworkType @@ -326,6 +326,16 @@ def hardware_thread(end_event, hw_queue) -> None: startup_conditions["not_always_offroad"] = not offroad_mode onroad_conditions["not_always_offroad"] = not offroad_mode + # if an unsupported device and branch is detected, going onroad is blocked + # only allow going onroad when: + # - TIZI, or + # - TICI and channel_type is "tici" + build_metadata = get_build_metadata() + is_unsupported_combo = TICI and build_metadata.channel_type != "tici" + startup_conditions["not_tici"] = not is_unsupported_combo + onroad_conditions["not_tici"] = not is_unsupported_combo + set_offroad_alert("Offroad_TiciSupport", is_unsupported_combo, extra_text=build_metadata.channel) + # if the temperature enters the danger zone, go offroad to cool down onroad_conditions["device_temp_good"] = thermal_status < ThermalStatus.danger extra_text = f"{offroad_comp_temp:.1f}C" diff --git a/system/updated/updated.py b/system/updated/updated.py index dfeaf88cbd..7ab9e070dc 100755 --- a/system/updated/updated.py +++ b/system/updated/updated.py @@ -18,7 +18,7 @@ from openpilot.common.markdown import parse_markdown from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.system.hardware import AGNOS, HARDWARE -from openpilot.system.version import get_build_metadata +from openpilot.system.version import get_build_metadata, SP_BRANCH_MIGRATIONS LOCK_FILE = os.getenv("UPDATER_LOCK_FILE", "/tmp/safe_staging_overlay.lock") STAGING_ROOT = os.getenv("UPDATER_STAGING_ROOT", "/data/safe_staging") @@ -232,9 +232,7 @@ class Updater: b: str | None = self.params.get("UpdaterTargetBranch") if b is None: b = self.get_branch(BASEDIR) - b = { - ("tici", "release3"): "release-tici" - }.get((HARDWARE.get_device_type(), b), b) + b = SP_BRANCH_MIGRATIONS.get((HARDWARE.get_device_type(), b), b) return b @property diff --git a/system/version.py b/system/version.py index 5e65a7c64b..87044b84a8 100755 --- a/system/version.py +++ b/system/version.py @@ -16,6 +16,13 @@ MASTER_SP_BRANCHES = ['master'] RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES +SP_BRANCH_MIGRATIONS = { + ("tici", "staging-c3-new"): "staging-tici", + ("tici", "dev-c3-new"): "staging-tici", + ("tici", "master"): "master-tici", + ("tici", "master-dev-c3-new"): "master-tici", +} + BUILD_METADATA_FILENAME = "build.json" training_version: str = "0.2.0" @@ -128,7 +135,9 @@ class BuildMetadata: @property def channel_type(self) -> str: - if self.development_channel: + if self.channel.endswith("-tici"): + return "tici" + elif self.development_channel: return "development" elif self.channel.startswith("staging-"): return "staging" From 3a91ae08a9ebc4d051a7c226e6c869c1a81c0d37 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 6 Sep 2025 18:03:16 -0400 Subject: [PATCH 130/188] update: actually detect device type as TICI (#1221) --- system/hardware/hardwared.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index af01154144..df8ae23ab5 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -331,7 +331,7 @@ def hardware_thread(end_event, hw_queue) -> None: # - TIZI, or # - TICI and channel_type is "tici" build_metadata = get_build_metadata() - is_unsupported_combo = TICI and build_metadata.channel_type != "tici" + is_unsupported_combo = TICI and HARDWARE.get_device_type() == "tici" and build_metadata.channel_type != "tici" startup_conditions["not_tici"] = not is_unsupported_combo onroad_conditions["not_tici"] = not is_unsupported_combo set_offroad_alert("Offroad_TiciSupport", is_unsupported_combo, extra_text=build_metadata.channel) From ff4d1923f05ca3978148ba3c8033a3bec9914972 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sat, 6 Sep 2025 18:46:10 -0400 Subject: [PATCH 131/188] tici: fix staging root updates (#1223) --- sunnypilot/system/hardware/c3/launch_env.sh | 2 ++ 1 file changed, 2 insertions(+) diff --git a/sunnypilot/system/hardware/c3/launch_env.sh b/sunnypilot/system/hardware/c3/launch_env.sh index 593aacc078..4c011c6ac0 100755 --- a/sunnypilot/system/hardware/c3/launch_env.sh +++ b/sunnypilot/system/hardware/c3/launch_env.sh @@ -9,3 +9,5 @@ export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then export AGNOS_VERSION="12.8" fi + +export STAGING_ROOT="/data/safe_staging" From ff34b8af76e697e56648e93ec716b865679fa925 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Sun, 7 Sep 2025 11:59:48 -0400 Subject: [PATCH 132/188] selfdrived: disable HUD VisualAlert for belowSteerSpeed events (#36109) --- selfdrive/selfdrived/events.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 9da7125dd8..d4a00115d4 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -243,7 +243,7 @@ def below_steer_speed_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.S f"Steer Unavailable Below {get_display_speed(CP.minSteerSpeed, metric)}", "", AlertStatus.userPrompt, AlertSize.small, - Priority.LOW, VisualAlert.steerRequired, AudibleAlert.prompt, 0.4) + Priority.LOW, VisualAlert.none, AudibleAlert.prompt, 0.4) def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: From 275abc1eb5a8bf4f694280c5408bc62bd3994b9a Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 7 Sep 2025 11:13:39 -0700 Subject: [PATCH 133/188] Rewrite proclogd in Python (#36110) * Rewrite proclogd in Python * lil more * lil more * Update system/proclogd.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update system/proclogd.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * Update system/proclogd.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .gitignore | 1 - SConstruct | 1 - pyproject.toml | 1 - selfdrive/test/test_onroad.py | 4 +- system/manager/process_config.py | 2 +- system/proclogd.py | 227 ++++++++++++++++++++++++ system/proclogd/SConscript | 6 - system/proclogd/main.cc | 25 --- system/proclogd/proclog.cc | 239 -------------------------- system/proclogd/proclog.h | 40 ----- system/proclogd/tests/.gitignore | 1 - system/proclogd/tests/test_proclog.cc | 142 --------------- 12 files changed, 230 insertions(+), 459 deletions(-) create mode 100755 system/proclogd.py delete mode 100644 system/proclogd/SConscript delete mode 100644 system/proclogd/main.cc delete mode 100644 system/proclogd/proclog.cc delete mode 100644 system/proclogd/proclog.h delete mode 100644 system/proclogd/tests/.gitignore delete mode 100644 system/proclogd/tests/test_proclog.cc diff --git a/.gitignore b/.gitignore index 8dc8f41de5..c594fb53d3 100644 --- a/.gitignore +++ b/.gitignore @@ -50,7 +50,6 @@ cereal/services.h cereal/gen cereal/messaging/bridge selfdrive/mapd/default_speeds_by_region.json -system/proclogd/proclogd selfdrive/ui/translations/tmp selfdrive/test/longitudinal_maneuvers/out selfdrive/car/tests/cars_dump diff --git a/SConstruct b/SConstruct index 56788e5842..21657ad556 100644 --- a/SConstruct +++ b/SConstruct @@ -341,7 +341,6 @@ SConscript([ if arch != "Darwin": SConscript([ 'system/logcatd/SConscript', - 'system/proclogd/SConscript', ]) if arch == "larch64": diff --git a/pyproject.toml b/pyproject.toml index 7d6516c0fb..3e362556a3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -158,7 +158,6 @@ testpaths = [ "system/camerad", "system/hardware", "system/loggerd", - "system/proclogd", "system/tests", "system/ubloxd", "system/webrtc", diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 3cb1af10df..26225ef400 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -32,7 +32,7 @@ CPU usage budget TEST_DURATION = 25 LOG_OFFSET = 8 -MAX_TOTAL_CPU = 287. # total for all 8 cores +MAX_TOTAL_CPU = 300. # total for all 8 cores PROCS = { # Baseline CPU usage by process "selfdrive.controls.controlsd": 16.0, @@ -56,7 +56,7 @@ PROCS = { "selfdrive.ui.soundd": 3.0, "selfdrive.ui.feedback.feedbackd": 1.0, "selfdrive.monitoring.dmonitoringd": 4.0, - "./proclogd": 2.0, + "system.proclogd": 3.0, "system.logmessaged": 1.0, "system.tombstoned": 0, "./logcatd": 1.0, diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 5758512f2b..e25c0e985f 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -72,7 +72,7 @@ procs = [ NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM), PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), NativeProcess("logcatd", "system/logcatd", ["./logcatd"], only_onroad, platform.system() != "Darwin"), - NativeProcess("proclogd", "system/proclogd", ["./proclogd"], only_onroad, platform.system() != "Darwin"), + PythonProcess("proclogd", "system.proclogd", only_onroad, enabled=platform.system() != "Darwin"), PythonProcess("micd", "system.micd", iscar), PythonProcess("timed", "system.timed", always_run, enabled=not PC), diff --git a/system/proclogd.py b/system/proclogd.py new file mode 100755 index 0000000000..3279425b7b --- /dev/null +++ b/system/proclogd.py @@ -0,0 +1,227 @@ +#!/usr/bin/env python3 +import os +from typing import NoReturn, TypedDict + +from cereal import messaging +from openpilot.common.realtime import Ratekeeper +from openpilot.common.swaglog import cloudlog + +JIFFY = os.sysconf(os.sysconf_names['SC_CLK_TCK']) +PAGE_SIZE = os.sysconf(os.sysconf_names['SC_PAGE_SIZE']) + + +def _cpu_times() -> list[dict[str, float]]: + cpu_times: list[dict[str, float]] = [] + try: + with open('/proc/stat') as f: + lines = f.readlines()[1:] + for line in lines: + if not line.startswith('cpu') or len(line) < 4 or not line[3].isdigit(): + break + parts = line.split() + cpu_times.append({ + 'cpuNum': int(parts[0][3:]), + 'user': float(parts[1]) / JIFFY, + 'nice': float(parts[2]) / JIFFY, + 'system': float(parts[3]) / JIFFY, + 'idle': float(parts[4]) / JIFFY, + 'iowait': float(parts[5]) / JIFFY, + 'irq': float(parts[6]) / JIFFY, + 'softirq': float(parts[7]) / JIFFY, + }) + except Exception: + cloudlog.exception("failed to read /proc/stat") + return cpu_times + + +def _mem_info() -> dict[str, int]: + keys = ["MemTotal:", "MemFree:", "MemAvailable:", "Buffers:", "Cached:", "Active:", "Inactive:", "Shmem:"] + info: dict[str, int] = dict.fromkeys(keys, 0) + try: + with open('/proc/meminfo') as f: + for line in f: + parts = line.split() + if parts and parts[0] in info: + info[parts[0]] = int(parts[1]) * 1024 + except Exception: + cloudlog.exception("failed to read /proc/meminfo") + return info + + +_STAT_POS = { + 'pid': 1, + 'state': 3, + 'ppid': 4, + 'utime': 14, + 'stime': 15, + 'cutime': 16, + 'cstime': 17, + 'priority': 18, + 'nice': 19, + 'num_threads': 20, + 'starttime': 22, + 'vsize': 23, + 'rss': 24, + 'processor': 39, +} + +class ProcStat(TypedDict): + name: str + pid: int + state: str + ppid: int + utime: int + stime: int + cutime: int + cstime: int + priority: int + nice: int + num_threads: int + starttime: int + vms: int + rss: int + processor: int + + +def _parse_proc_stat(stat: str) -> ProcStat | None: + open_paren = stat.find('(') + close_paren = stat.rfind(')') + if open_paren == -1 or close_paren == -1 or open_paren > close_paren: + return None + name = stat[open_paren + 1:close_paren] + stat = stat[:open_paren] + stat[open_paren:close_paren].replace(' ', '_') + stat[close_paren:] + parts = stat.split() + if len(parts) < 52: + return None + try: + return { + 'name': name, + 'pid': int(parts[_STAT_POS['pid'] - 1]), + 'state': parts[_STAT_POS['state'] - 1][0], + 'ppid': int(parts[_STAT_POS['ppid'] - 1]), + 'utime': int(parts[_STAT_POS['utime'] - 1]), + 'stime': int(parts[_STAT_POS['stime'] - 1]), + 'cutime': int(parts[_STAT_POS['cutime'] - 1]), + 'cstime': int(parts[_STAT_POS['cstime'] - 1]), + 'priority': int(parts[_STAT_POS['priority'] - 1]), + 'nice': int(parts[_STAT_POS['nice'] - 1]), + 'num_threads': int(parts[_STAT_POS['num_threads'] - 1]), + 'starttime': int(parts[_STAT_POS['starttime'] - 1]), + 'vms': int(parts[_STAT_POS['vsize'] - 1]), + 'rss': int(parts[_STAT_POS['rss'] - 1]), + 'processor': int(parts[_STAT_POS['processor'] - 1]), + } + except Exception: + cloudlog.exception("failed to parse /proc//stat") + return None + +class ProcExtra(TypedDict): + pid: int + name: str + exe: str + cmdline: list[str] + + +_proc_cache: dict[int, ProcExtra] = {} + + +def _get_proc_extra(pid: int, name: str) -> ProcExtra: + cache: ProcExtra | None = _proc_cache.get(pid) + if cache is None or cache.get('name') != name: + exe = '' + cmdline: list[str] = [] + try: + exe = os.readlink(f'/proc/{pid}/exe') + except OSError: + pass + try: + with open(f'/proc/{pid}/cmdline', 'rb') as f: + cmdline = [c.decode('utf-8', errors='replace') for c in f.read().split(b'\0') if c] + except OSError: + pass + cache = {'pid': pid, 'name': name, 'exe': exe, 'cmdline': cmdline} + _proc_cache[pid] = cache + return cache + + +def _procs() -> list[ProcStat]: + stats: list[ProcStat] = [] + for pid_str in os.listdir('/proc'): + if not pid_str.isdigit(): + continue + try: + with open(f'/proc/{pid_str}/stat') as f: + stat = f.read() + parsed = _parse_proc_stat(stat) + if parsed is not None: + stats.append(parsed) + except OSError: + continue + return stats + + +def build_proc_log_message(msg) -> None: + pl = msg.procLog + + procs = _procs() + l = pl.init('procs', len(procs)) + for i, r in enumerate(procs): + proc = l[i] + proc.pid = r['pid'] + proc.state = ord(r['state'][0]) + proc.ppid = r['ppid'] + proc.cpuUser = r['utime'] / JIFFY + proc.cpuSystem = r['stime'] / JIFFY + proc.cpuChildrenUser = r['cutime'] / JIFFY + proc.cpuChildrenSystem = r['cstime'] / JIFFY + proc.priority = r['priority'] + proc.nice = r['nice'] + proc.numThreads = r['num_threads'] + proc.startTime = r['starttime'] / JIFFY + proc.memVms = r['vms'] + proc.memRss = r['rss'] * PAGE_SIZE + proc.processor = r['processor'] + proc.name = r['name'] + + extra = _get_proc_extra(r['pid'], r['name']) + proc.exe = extra['exe'] + cmdline = proc.init('cmdline', len(extra['cmdline'])) + for j, arg in enumerate(extra['cmdline']): + cmdline[j] = arg + + cpu_times = _cpu_times() + cpu_list = pl.init('cpuTimes', len(cpu_times)) + for i, ct in enumerate(cpu_times): + cpu = cpu_list[i] + cpu.cpuNum = ct['cpuNum'] + cpu.user = ct['user'] + cpu.nice = ct['nice'] + cpu.system = ct['system'] + cpu.idle = ct['idle'] + cpu.iowait = ct['iowait'] + cpu.irq = ct['irq'] + cpu.softirq = ct['softirq'] + + mem_info = _mem_info() + pl.mem.total = mem_info["MemTotal:"] + pl.mem.free = mem_info["MemFree:"] + pl.mem.available = mem_info["MemAvailable:"] + pl.mem.buffers = mem_info["Buffers:"] + pl.mem.cached = mem_info["Cached:"] + pl.mem.active = mem_info["Active:"] + pl.mem.inactive = mem_info["Inactive:"] + pl.mem.shared = mem_info["Shmem:"] + + +def main() -> NoReturn: + pm = messaging.PubMaster(['procLog']) + rk = Ratekeeper(0.5) + while True: + msg = messaging.new_message('procLog', valid=True) + build_proc_log_message(msg) + pm.send('procLog', msg) + rk.keep_time() + + +if __name__ == '__main__': + main() diff --git a/system/proclogd/SConscript b/system/proclogd/SConscript deleted file mode 100644 index 08814d5ccb..0000000000 --- a/system/proclogd/SConscript +++ /dev/null @@ -1,6 +0,0 @@ -Import('env', 'messaging', 'common') -libs = [messaging, 'pthread', common] -env.Program('proclogd', ['main.cc', 'proclog.cc'], LIBS=libs) - -if GetOption('extras'): - env.Program('tests/test_proclog', ['tests/test_proclog.cc', 'proclog.cc'], LIBS=libs) diff --git a/system/proclogd/main.cc b/system/proclogd/main.cc deleted file mode 100644 index 3f8a889eea..0000000000 --- a/system/proclogd/main.cc +++ /dev/null @@ -1,25 +0,0 @@ - -#include - -#include "common/ratekeeper.h" -#include "common/util.h" -#include "system/proclogd/proclog.h" - -ExitHandler do_exit; - -int main(int argc, char **argv) { - setpriority(PRIO_PROCESS, 0, -15); - - RateKeeper rk("proclogd", 0.5); - PubMaster publisher({"procLog"}); - - while (!do_exit) { - MessageBuilder msg; - buildProcLogMessage(msg); - publisher.send("procLog", msg); - - rk.keepTime(); - } - - return 0; -} diff --git a/system/proclogd/proclog.cc b/system/proclogd/proclog.cc deleted file mode 100644 index 09ab4f559e..0000000000 --- a/system/proclogd/proclog.cc +++ /dev/null @@ -1,239 +0,0 @@ -#include "system/proclogd/proclog.h" - -#include - -#include -#include -#include -#include - -#include "common/swaglog.h" -#include "common/util.h" - -namespace Parser { - -// parse /proc/stat -std::vector cpuTimes(std::istream &stream) { - std::vector cpu_times; - std::string line; - // skip the first line for cpu total - std::getline(stream, line); - while (std::getline(stream, line)) { - if (line.compare(0, 3, "cpu") != 0) break; - - CPUTime t = {}; - std::istringstream iss(line); - if (iss.ignore(3) >> t.id >> t.utime >> t.ntime >> t.stime >> t.itime >> t.iowtime >> t.irqtime >> t.sirqtime) - cpu_times.push_back(t); - } - return cpu_times; -} - -// parse /proc/meminfo -std::unordered_map memInfo(std::istream &stream) { - std::unordered_map mem_info; - std::string line, key; - while (std::getline(stream, line)) { - uint64_t val = 0; - std::istringstream iss(line); - if (iss >> key >> val) { - mem_info[key] = val * 1024; - } - } - return mem_info; -} - -// field position (https://man7.org/linux/man-pages/man5/proc.5.html) -enum StatPos { - pid = 1, - state = 3, - ppid = 4, - utime = 14, - stime = 15, - cutime = 16, - cstime = 17, - priority = 18, - nice = 19, - num_threads = 20, - starttime = 22, - vsize = 23, - rss = 24, - processor = 39, - MAX_FIELD = 52, -}; - -// parse /proc/pid/stat -std::optional procStat(std::string stat) { - // To avoid being fooled by names containing a closing paren, scan backwards. - auto open_paren = stat.find('('); - auto close_paren = stat.rfind(')'); - if (open_paren == std::string::npos || close_paren == std::string::npos || open_paren > close_paren) { - return std::nullopt; - } - - std::string name = stat.substr(open_paren + 1, close_paren - open_paren - 1); - // replace space in name with _ - std::replace(&stat[open_paren], &stat[close_paren], ' ', '_'); - std::istringstream iss(stat); - std::vector v{std::istream_iterator(iss), - std::istream_iterator()}; - try { - if (v.size() != StatPos::MAX_FIELD) { - throw std::invalid_argument("stat"); - } - ProcStat p = { - .name = name, - .pid = stoi(v[StatPos::pid - 1]), - .state = v[StatPos::state - 1][0], - .ppid = stoi(v[StatPos::ppid - 1]), - .utime = stoul(v[StatPos::utime - 1]), - .stime = stoul(v[StatPos::stime - 1]), - .cutime = stol(v[StatPos::cutime - 1]), - .cstime = stol(v[StatPos::cstime - 1]), - .priority = stol(v[StatPos::priority - 1]), - .nice = stol(v[StatPos::nice - 1]), - .num_threads = stol(v[StatPos::num_threads - 1]), - .starttime = stoull(v[StatPos::starttime - 1]), - .vms = stoul(v[StatPos::vsize - 1]), - .rss = stol(v[StatPos::rss - 1]), - .processor = stoi(v[StatPos::processor - 1]), - }; - return p; - } catch (const std::invalid_argument &e) { - LOGE("failed to parse procStat (%s) :%s", e.what(), stat.c_str()); - } catch (const std::out_of_range &e) { - LOGE("failed to parse procStat (%s) :%s", e.what(), stat.c_str()); - } - return std::nullopt; -} - -// return list of PIDs from /proc -std::vector pids() { - std::vector ids; - DIR *d = opendir("/proc"); - assert(d); - char *p_end; - struct dirent *de = NULL; - while ((de = readdir(d))) { - if (de->d_type == DT_DIR) { - int pid = strtol(de->d_name, &p_end, 10); - if (p_end == (de->d_name + strlen(de->d_name))) { - ids.push_back(pid); - } - } - } - closedir(d); - return ids; -} - -// null-delimited cmdline arguments to vector -std::vector cmdline(std::istream &stream) { - std::vector ret; - std::string line; - while (std::getline(stream, line, '\0')) { - if (!line.empty()) { - ret.push_back(line); - } - } - return ret; -} - -const ProcCache &getProcExtraInfo(int pid, const std::string &name) { - static std::unordered_map proc_cache; - ProcCache &cache = proc_cache[pid]; - if (cache.pid != pid || cache.name != name) { - cache.pid = pid; - cache.name = name; - std::string proc_path = "/proc/" + std::to_string(pid); - cache.exe = util::readlink(proc_path + "/exe"); - std::ifstream stream(proc_path + "/cmdline"); - cache.cmdline = cmdline(stream); - } - return cache; -} - -} // namespace Parser - -const double jiffy = sysconf(_SC_CLK_TCK); -const size_t page_size = sysconf(_SC_PAGE_SIZE); - -void buildCPUTimes(cereal::ProcLog::Builder &builder) { - std::ifstream stream("/proc/stat"); - std::vector stats = Parser::cpuTimes(stream); - - auto log_cpu_times = builder.initCpuTimes(stats.size()); - for (int i = 0; i < stats.size(); ++i) { - auto l = log_cpu_times[i]; - const CPUTime &r = stats[i]; - l.setCpuNum(r.id); - l.setUser(r.utime / jiffy); - l.setNice(r.ntime / jiffy); - l.setSystem(r.stime / jiffy); - l.setIdle(r.itime / jiffy); - l.setIowait(r.iowtime / jiffy); - l.setIrq(r.irqtime / jiffy); - l.setSoftirq(r.sirqtime / jiffy); - } -} - -void buildMemInfo(cereal::ProcLog::Builder &builder) { - std::ifstream stream("/proc/meminfo"); - auto mem_info = Parser::memInfo(stream); - - auto mem = builder.initMem(); - mem.setTotal(mem_info["MemTotal:"]); - mem.setFree(mem_info["MemFree:"]); - mem.setAvailable(mem_info["MemAvailable:"]); - mem.setBuffers(mem_info["Buffers:"]); - mem.setCached(mem_info["Cached:"]); - mem.setActive(mem_info["Active:"]); - mem.setInactive(mem_info["Inactive:"]); - mem.setShared(mem_info["Shmem:"]); -} - -void buildProcs(cereal::ProcLog::Builder &builder) { - auto pids = Parser::pids(); - std::vector proc_stats; - proc_stats.reserve(pids.size()); - for (int pid : pids) { - std::string path = "/proc/" + std::to_string(pid) + "/stat"; - if (auto stat = Parser::procStat(util::read_file(path))) { - proc_stats.push_back(*stat); - } - } - - auto procs = builder.initProcs(proc_stats.size()); - for (size_t i = 0; i < proc_stats.size(); i++) { - auto l = procs[i]; - const ProcStat &r = proc_stats[i]; - l.setPid(r.pid); - l.setState(r.state); - l.setPpid(r.ppid); - l.setCpuUser(r.utime / jiffy); - l.setCpuSystem(r.stime / jiffy); - l.setCpuChildrenUser(r.cutime / jiffy); - l.setCpuChildrenSystem(r.cstime / jiffy); - l.setPriority(r.priority); - l.setNice(r.nice); - l.setNumThreads(r.num_threads); - l.setStartTime(r.starttime / jiffy); - l.setMemVms(r.vms); - l.setMemRss((uint64_t)r.rss * page_size); - l.setProcessor(r.processor); - l.setName(r.name); - - const ProcCache &extra_info = Parser::getProcExtraInfo(r.pid, r.name); - l.setExe(extra_info.exe); - auto lcmdline = l.initCmdline(extra_info.cmdline.size()); - for (size_t j = 0; j < lcmdline.size(); j++) { - lcmdline.set(j, extra_info.cmdline[j]); - } - } -} - -void buildProcLogMessage(MessageBuilder &msg) { - auto procLog = msg.initEvent().initProcLog(); - buildProcs(procLog); - buildCPUTimes(procLog); - buildMemInfo(procLog); -} diff --git a/system/proclogd/proclog.h b/system/proclogd/proclog.h deleted file mode 100644 index 49f97cdd36..0000000000 --- a/system/proclogd/proclog.h +++ /dev/null @@ -1,40 +0,0 @@ -#include -#include -#include -#include - -#include "cereal/messaging/messaging.h" - -struct CPUTime { - int id; - unsigned long utime, ntime, stime, itime; - unsigned long iowtime, irqtime, sirqtime; -}; - -struct ProcCache { - int pid; - std::string name, exe; - std::vector cmdline; -}; - -struct ProcStat { - int pid, ppid, processor; - char state; - long cutime, cstime, priority, nice, num_threads, rss; - unsigned long utime, stime, vms; - unsigned long long starttime; - std::string name; -}; - -namespace Parser { - -std::vector pids(); -std::optional procStat(std::string stat); -std::vector cmdline(std::istream &stream); -std::vector cpuTimes(std::istream &stream); -std::unordered_map memInfo(std::istream &stream); -const ProcCache &getProcExtraInfo(int pid, const std::string &name); - -}; // namespace Parser - -void buildProcLogMessage(MessageBuilder &msg); diff --git a/system/proclogd/tests/.gitignore b/system/proclogd/tests/.gitignore deleted file mode 100644 index 5230b1598d..0000000000 --- a/system/proclogd/tests/.gitignore +++ /dev/null @@ -1 +0,0 @@ -test_proclog diff --git a/system/proclogd/tests/test_proclog.cc b/system/proclogd/tests/test_proclog.cc deleted file mode 100644 index b86229a499..0000000000 --- a/system/proclogd/tests/test_proclog.cc +++ /dev/null @@ -1,142 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" -#include "common/util.h" -#include "system/proclogd/proclog.h" - -const std::string allowed_states = "RSDTZtWXxKWPI"; - -TEST_CASE("Parser::procStat") { - SECTION("from string") { - const std::string stat_str = - "33012 (code )) S 32978 6620 6620 0 -1 4194368 2042377 0 144 0 24510 11627 0 " - "0 20 0 39 0 53077 830029824 62214 18446744073709551615 94257242783744 94257366235808 " - "140735738643248 0 0 0 0 4098 1073808632 0 0 0 17 2 0 0 2 0 0 94257370858656 94257371248232 " - "94257404952576 140735738648768 140735738648823 140735738648823 140735738650595 0"; - auto stat = Parser::procStat(stat_str); - REQUIRE(stat); - REQUIRE(stat->pid == 33012); - REQUIRE(stat->name == "code )"); - REQUIRE(stat->state == 'S'); - REQUIRE(stat->ppid == 32978); - REQUIRE(stat->utime == 24510); - REQUIRE(stat->stime == 11627); - REQUIRE(stat->cutime == 0); - REQUIRE(stat->cstime == 0); - REQUIRE(stat->priority == 20); - REQUIRE(stat->nice == 0); - REQUIRE(stat->num_threads == 39); - REQUIRE(stat->starttime == 53077); - REQUIRE(stat->vms == 830029824); - REQUIRE(stat->rss == 62214); - REQUIRE(stat->processor == 2); - } - SECTION("all processes") { - std::vector pids = Parser::pids(); - REQUIRE(pids.size() > 1); - for (int pid : pids) { - std::string stat_path = "/proc/" + std::to_string(pid) + "/stat"; - INFO(stat_path); - if (auto stat = Parser::procStat(util::read_file(stat_path))) { - REQUIRE(stat->pid == pid); - REQUIRE(allowed_states.find(stat->state) != std::string::npos); - } else { - REQUIRE(util::file_exists(stat_path) == false); - } - } - } -} - -TEST_CASE("Parser::cpuTimes") { - SECTION("from string") { - std::string stat = - "cpu 0 0 0 0 0 0 0 0 0 0\n" - "cpu0 1 2 3 4 5 6 7 8 9 10\n" - "cpu1 1 2 3 4 5 6 7 8 9 10\n"; - std::istringstream stream(stat); - auto stats = Parser::cpuTimes(stream); - REQUIRE(stats.size() == 2); - for (int i = 0; i < stats.size(); ++i) { - REQUIRE(stats[i].id == i); - REQUIRE(stats[i].utime == 1); - REQUIRE(stats[i].ntime ==2); - REQUIRE(stats[i].stime == 3); - REQUIRE(stats[i].itime == 4); - REQUIRE(stats[i].iowtime == 5); - REQUIRE(stats[i].irqtime == 6); - REQUIRE(stats[i].sirqtime == 7); - } - } - SECTION("all cpus") { - std::istringstream stream(util::read_file("/proc/stat")); - auto stats = Parser::cpuTimes(stream); - REQUIRE(stats.size() == sysconf(_SC_NPROCESSORS_ONLN)); - for (int i = 0; i < stats.size(); ++i) { - REQUIRE(stats[i].id == i); - } - } -} - -TEST_CASE("Parser::memInfo") { - SECTION("from string") { - std::istringstream stream("MemTotal: 1024 kb\nMemFree: 2048 kb\n"); - auto meminfo = Parser::memInfo(stream); - REQUIRE(meminfo["MemTotal:"] == 1024 * 1024); - REQUIRE(meminfo["MemFree:"] == 2048 * 1024); - } - SECTION("from /proc/meminfo") { - std::string require_keys[] = {"MemTotal:", "MemFree:", "MemAvailable:", "Buffers:", "Cached:", "Active:", "Inactive:", "Shmem:"}; - std::istringstream stream(util::read_file("/proc/meminfo")); - auto meminfo = Parser::memInfo(stream); - for (auto &key : require_keys) { - REQUIRE(meminfo.find(key) != meminfo.end()); - REQUIRE(meminfo[key] > 0); - } - } -} - -void test_cmdline(std::string cmdline, const std::vector requires) { - std::stringstream ss; - ss.write(&cmdline[0], cmdline.size()); - auto cmds = Parser::cmdline(ss); - REQUIRE(cmds.size() == requires.size()); - for (int i = 0; i < requires.size(); ++i) { - REQUIRE(cmds[i] == requires[i]); - } -} -TEST_CASE("Parser::cmdline") { - test_cmdline(std::string("a\0b\0c\0", 7), {"a", "b", "c"}); - test_cmdline(std::string("a\0\0c\0", 6), {"a", "c"}); - test_cmdline(std::string("a\0b\0c\0\0\0", 9), {"a", "b", "c"}); -} - -TEST_CASE("buildProcLoggerMessage") { - MessageBuilder msg; - buildProcLogMessage(msg); - - kj::Array buf = capnp::messageToFlatArray(msg); - capnp::FlatArrayMessageReader reader(buf); - auto log = reader.getRoot().getProcLog(); - REQUIRE(log.totalSize().wordCount > 0); - - // test cereal::ProcLog::CPUTimes - auto cpu_times = log.getCpuTimes(); - REQUIRE(cpu_times.size() == sysconf(_SC_NPROCESSORS_ONLN)); - REQUIRE(cpu_times[cpu_times.size() - 1].getCpuNum() == cpu_times.size() - 1); - - // test cereal::ProcLog::Mem - auto mem = log.getMem(); - REQUIRE(mem.getTotal() > 0); - REQUIRE(mem.getShared() > 0); - - // test cereal::ProcLog::Process - auto procs = log.getProcs(); - for (auto p : procs) { - REQUIRE(allowed_states.find(p.getState()) != std::string::npos); - if (p.getPid() == ::getpid()) { - REQUIRE(p.getName() == "test_proclog"); - REQUIRE(p.getState() == 'R'); - REQUIRE_THAT(p.getExe().cStr(), Catch::Matchers::Contains("test_proclog")); - REQUIRE_THAT(p.getCmdline()[0], Catch::Matchers::Contains("test_proclog")); - } - } -} From 608c16007e6580fe746f9a7b18a4e790eae62b12 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 7 Sep 2025 11:32:44 -0700 Subject: [PATCH 134/188] Rewrite logcatd in Python (#36111) * Add Python logcatd implementation * lil more --- SConstruct | 4 -- selfdrive/test/test_onroad.py | 2 +- system/journald.py | 43 ++++++++++++++++++ system/logcatd/.gitignore | 1 - system/logcatd/SConscript | 3 -- system/logcatd/logcatd_systemd.cc | 75 ------------------------------- system/manager/process_config.py | 2 +- 7 files changed, 45 insertions(+), 85 deletions(-) create mode 100755 system/journald.py delete mode 100644 system/logcatd/.gitignore delete mode 100644 system/logcatd/SConscript delete mode 100644 system/logcatd/logcatd_systemd.cc diff --git a/SConstruct b/SConstruct index 21657ad556..5b13bd635a 100644 --- a/SConstruct +++ b/SConstruct @@ -338,10 +338,6 @@ SConscript([ 'system/ubloxd/SConscript', 'system/loggerd/SConscript', ]) -if arch != "Darwin": - SConscript([ - 'system/logcatd/SConscript', - ]) if arch == "larch64": SConscript(['system/camerad/SConscript']) diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index 26225ef400..b4b9b9dbbe 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -59,7 +59,7 @@ PROCS = { "system.proclogd": 3.0, "system.logmessaged": 1.0, "system.tombstoned": 0, - "./logcatd": 1.0, + "system.journald": 1.0, "system.micd": 5.0, "system.timed": 0, "selfdrive.pandad.pandad": 0, diff --git a/system/journald.py b/system/journald.py new file mode 100755 index 0000000000..37158b9251 --- /dev/null +++ b/system/journald.py @@ -0,0 +1,43 @@ +#!/usr/bin/env python3 +import json +import subprocess + +import cereal.messaging as messaging +from openpilot.common.swaglog import cloudlog + + +def main(): + pm = messaging.PubMaster(['androidLog']) + cmd = ['journalctl', '-f', '-o', 'json'] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, text=True) + assert proc.stdout is not None + try: + for line in proc.stdout: + line = line.strip() + if not line: + continue + try: + kv = json.loads(line) + except json.JSONDecodeError: + cloudlog.exception("failed to parse journalctl output") + continue + + msg = messaging.new_message('androidLog') + entry = msg.androidLog + entry.ts = int(kv.get('__REALTIME_TIMESTAMP', 0)) + entry.message = json.dumps(kv) + if '_PID' in kv: + entry.pid = int(kv['_PID']) + if 'PRIORITY' in kv: + entry.priority = int(kv['PRIORITY']) + if 'SYSLOG_IDENTIFIER' in kv: + entry.tag = kv['SYSLOG_IDENTIFIER'] + + pm.send('androidLog', msg) + finally: + proc.terminate() + proc.wait() + + +if __name__ == '__main__': + main() diff --git a/system/logcatd/.gitignore b/system/logcatd/.gitignore deleted file mode 100644 index c66f7622d9..0000000000 --- a/system/logcatd/.gitignore +++ /dev/null @@ -1 +0,0 @@ -logcatd diff --git a/system/logcatd/SConscript b/system/logcatd/SConscript deleted file mode 100644 index 39c45d1093..0000000000 --- a/system/logcatd/SConscript +++ /dev/null @@ -1,3 +0,0 @@ -Import('env', 'messaging', 'common') - -env.Program('logcatd', 'logcatd_systemd.cc', LIBS=[messaging, common, 'systemd']) diff --git a/system/logcatd/logcatd_systemd.cc b/system/logcatd/logcatd_systemd.cc deleted file mode 100644 index 54b3782132..0000000000 --- a/system/logcatd/logcatd_systemd.cc +++ /dev/null @@ -1,75 +0,0 @@ -#include - -#include -#include -#include -#include - -#include "third_party/json11/json11.hpp" - -#include "cereal/messaging/messaging.h" -#include "common/timing.h" -#include "common/util.h" - -ExitHandler do_exit; -int main(int argc, char *argv[]) { - - PubMaster pm({"androidLog"}); - - sd_journal *journal; - int err = sd_journal_open(&journal, 0); - assert(err >= 0); - err = sd_journal_get_fd(journal); // needed so sd_journal_wait() works properly if files rotate - assert(err >= 0); - err = sd_journal_seek_tail(journal); - assert(err >= 0); - - // workaround for bug https://github.com/systemd/systemd/issues/9934 - // call sd_journal_previous_skip after sd_journal_seek_tail (like journalctl -f does) to makes things work. - sd_journal_previous_skip(journal, 1); - - while (!do_exit) { - err = sd_journal_next(journal); - assert(err >= 0); - - // Wait for new message if we didn't receive anything - if (err == 0) { - err = sd_journal_wait(journal, 1000 * 1000); - assert(err >= 0); - continue; // Try again - } - - uint64_t timestamp = 0; - err = sd_journal_get_realtime_usec(journal, ×tamp); - assert(err >= 0); - - const void *data; - size_t length; - std::map kv; - - SD_JOURNAL_FOREACH_DATA(journal, data, length) { - std::string str((char*)data, length); - - // Split "KEY=VALUE"" on "=" and put in map - std::size_t found = str.find("="); - if (found != std::string::npos) { - kv[str.substr(0, found)] = str.substr(found + 1, std::string::npos); - } - } - - MessageBuilder msg; - - // Build message - auto androidEntry = msg.initEvent().initAndroidLog(); - androidEntry.setTs(timestamp); - androidEntry.setMessage(json11::Json(kv).dump()); - if (kv.count("_PID")) androidEntry.setPid(std::atoi(kv["_PID"].c_str())); - if (kv.count("PRIORITY")) androidEntry.setPriority(std::atoi(kv["PRIORITY"].c_str())); - if (kv.count("SYSLOG_IDENTIFIER")) androidEntry.setTag(kv["SYSLOG_IDENTIFIER"]); - - pm.send("androidLog", msg); - } - - sd_journal_close(journal); - return 0; -} diff --git a/system/manager/process_config.py b/system/manager/process_config.py index e25c0e985f..9ed99d9560 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -71,8 +71,8 @@ procs = [ NativeProcess("camerad", "system/camerad", ["./camerad"], driverview, enabled=not WEBCAM), PythonProcess("webcamerad", "tools.webcam.camerad", driverview, enabled=WEBCAM), - NativeProcess("logcatd", "system/logcatd", ["./logcatd"], only_onroad, platform.system() != "Darwin"), PythonProcess("proclogd", "system.proclogd", only_onroad, enabled=platform.system() != "Darwin"), + PythonProcess("journald", "system.journald", only_onroad, platform.system() != "Darwin"), PythonProcess("micd", "system.micd", iscar), PythonProcess("timed", "system.timed", always_run, enabled=not PC), From bd73664f4c9dae102daa16ffe1efb6d051f46bd1 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 7 Sep 2025 12:21:13 -0700 Subject: [PATCH 135/188] add kaitai python package --- pyproject.toml | 4 +++ uv.lock | 77 ++++++++++++++++++++++++++++---------------------- 2 files changed, 48 insertions(+), 33 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 3e362556a3..9ed1d8a4cb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,9 @@ dependencies = [ "pyopenssl < 24.3.0", "pyaudio", + # ubloxd (TODO: just use struct) + "kaitaistruct", + # panda "libusb1", "spidev; platform_system == 'Linux'", @@ -246,6 +249,7 @@ exclude = [ "teleoprtc_repo", "third_party", "*.ipynb", + "generated", ] lint.flake8-implicit-str-concat.allow-multiline = false diff --git a/uv.lock b/uv.lock index e1678029cf..9977ff1bf7 100644 --- a/uv.lock +++ b/uv.lock @@ -622,10 +622,10 @@ name = "gymnasium" version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "cloudpickle" }, - { name = "farama-notifications" }, - { name = "numpy" }, - { name = "typing-extensions" }, + { name = "cloudpickle", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "farama-notifications", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "numpy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fd/17/c2a0e15c2cd5a8e788389b280996db927b923410de676ec5c7b2695e9261/gymnasium-1.2.0.tar.gz", hash = "sha256:344e87561012558f603880baf264ebc97f8a5c997a957b0c9f910281145534b0", size = 821142, upload-time = "2025-06-27T08:21:20.262Z" } wheels = [ @@ -720,6 +720,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/9e/820c4b086ad01ba7d77369fb8b11470a01fac9b4977f02e18659cf378b6b/json_rpc-1.15.0-py2.py3-none-any.whl", hash = "sha256:4a4668bbbe7116feb4abbd0f54e64a4adcf4b8f648f19ffa0848ad0f6606a9bf", size = 39450, upload-time = "2023-06-11T09:45:47.136Z" }, ] +[[package]] +name = "kaitaistruct" +version = "0.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/04/dd60b9cb65d580ef6cb6eaee975ad1bdd22d46a3f51b07a1e0606710ea88/kaitaistruct-0.10.tar.gz", hash = "sha256:a044dee29173d6afbacf27bcac39daf89b654dd418cfa009ab82d9178a9ae52a", size = 7061, upload-time = "2022-07-09T00:34:06.729Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/bf/88ad23efc08708bda9a2647169828e3553bb2093a473801db61f75356395/kaitaistruct-0.10-py2.py3-none-any.whl", hash = "sha256:a97350919adbf37fda881f75e9365e2fb88d04832b7a4e57106ec70119efb235", size = 7013, upload-time = "2022-07-09T00:34:03.905Z" }, +] + [[package]] name = "kiwisolver" version = "1.4.9" @@ -903,22 +912,22 @@ name = "metadrive-simulator" version = "0.4.2.4" source = { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" } dependencies = [ - { name = "filelock" }, - { name = "gymnasium" }, - { name = "lxml" }, - { name = "matplotlib" }, - { name = "numpy" }, - { name = "opencv-python-headless" }, - { name = "panda3d" }, - { name = "panda3d-gltf" }, - { name = "pillow" }, - { name = "progressbar" }, - { name = "psutil" }, - { name = "pygments" }, - { name = "requests" }, - { name = "shapely" }, - { name = "tqdm" }, - { name = "yapf" }, + { name = "filelock", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "gymnasium", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "lxml", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "matplotlib", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "numpy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "opencv-python-headless", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "panda3d", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "panda3d-gltf", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "pillow", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "progressbar", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "psutil", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "pygments", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "requests", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "shapely", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "tqdm", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "yapf", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] wheels = [ { url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl", hash = "sha256:fbf0ea9be67e65cd45d38ff930e3d49f705dd76c9ddbd1e1482e3f87b61efcef" }, @@ -1240,6 +1249,7 @@ dependencies = [ { name = "future-fstrings" }, { name = "inputs" }, { name = "json-rpc" }, + { name = "kaitaistruct" }, { name = "libusb1" }, { name = "numpy" }, { name = "onnx" }, @@ -1334,6 +1344,7 @@ requires-dist = [ { name = "jeepney", marker = "extra == 'dev'" }, { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, + { name = "kaitaistruct" }, { name = "libusb1" }, { name = "matplotlib", marker = "extra == 'dev'" }, { name = "metadrive-simulator", marker = "platform_machine != 'aarch64' and extra == 'tools'", url = "https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl" }, @@ -1422,8 +1433,8 @@ name = "panda3d-gltf" version = "0.13" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "panda3d" }, - { name = "panda3d-simplepbr" }, + { name = "panda3d", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "panda3d-simplepbr", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/7f/9f18fc3fa843a080acb891af6bcc12262e7bdf1d194a530f7042bebfc81f/panda3d-gltf-0.13.tar.gz", hash = "sha256:d06d373bdd91cf530909b669f43080e599463bbf6d3ef00c3558bad6c6b19675", size = 25573, upload-time = "2021-05-21T05:46:32.738Z" } wheels = [ @@ -1435,8 +1446,8 @@ name = "panda3d-simplepbr" version = "0.13.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "panda3d" }, - { name = "typing-extensions" }, + { name = "panda3d", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/be/c4d1ded04c22b357277cf6e6a44c1ab4abb285a700bd1991460460e05b99/panda3d_simplepbr-0.13.1.tar.gz", hash = "sha256:c83766d7c8f47499f365a07fe1dff078fc8b3054c2689bdc8dceabddfe7f1a35", size = 6216055, upload-time = "2025-03-30T16:57:41.087Z" } wheels = [ @@ -4173,9 +4184,9 @@ name = "pyopencl" version = "2025.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, - { name = "platformdirs" }, - { name = "pytools" }, + { name = "numpy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "platformdirs", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "pytools", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/88/0ac460d3e2def08b2ad6345db6a13613815f616bbbd60c6f4bdf774f4c41/pyopencl-2025.1.tar.gz", hash = "sha256:0116736d7f7920f87b8db4b66a03f27b1d930d2e37ddd14518407cc22dd24779", size = 422510, upload-time = "2025-01-22T00:16:58.421Z" } wheels = [ @@ -4351,7 +4362,7 @@ wheels = [ [[package]] name = "pytest-xdist" -version = "3.7.1.dev24+g2b4372bd6" +version = "3.7.1.dev24+g2b4372b" source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" } dependencies = [ { name = "execnet" }, @@ -4393,9 +4404,9 @@ name = "pytools" version = "2024.1.10" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs" }, - { name = "siphash24" }, - { name = "typing-extensions" }, + { name = "platformdirs", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "siphash24", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, + { name = "typing-extensions", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/0f/56e109c0307f831b5d598ad73976aaaa84b4d0e98da29a642e797eaa940c/pytools-2024.1.10.tar.gz", hash = "sha256:9af6f4b045212c49be32bb31fe19606c478ee4b09631886d05a32459f4ce0a12", size = 81741, upload-time = "2024-07-17T18:47:38.287Z" } wheels = [ @@ -4719,7 +4730,7 @@ name = "shapely" version = "2.1.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "numpy" }, + { name = "numpy", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ca/3c/2da625233f4e605155926566c0e7ea8dda361877f48e8b1655e53456f252/shapely-2.1.1.tar.gz", hash = "sha256:500621967f2ffe9642454808009044c21e5b35db89ce69f8a2042c2ffd0e2772", size = 315422, upload-time = "2025-05-19T11:04:41.265Z" } wheels = [ @@ -4948,7 +4959,7 @@ name = "yapf" version = "0.43.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "platformdirs" }, + { name = "platformdirs", marker = "platform_machine != 'aarch64' or sys_platform != 'linux'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907, upload-time = "2024-11-14T00:11:41.584Z" } wheels = [ From a885111c0ccb466ea75e2d808ac84786287035bf Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 7 Sep 2025 14:21:14 -0700 Subject: [PATCH 136/188] agnos 13.1 (#36113) --- launch_env.sh | 2 +- system/hardware/tici/agnos.json | 26 ++++++------ system/hardware/tici/all-partitions.json | 50 ++++++++++++------------ 3 files changed, 39 insertions(+), 39 deletions(-) diff --git a/launch_env.sh b/launch_env.sh index 7124b360f8..67dd5ee795 100755 --- a/launch_env.sh +++ b/launch_env.sh @@ -7,7 +7,7 @@ export OPENBLAS_NUM_THREADS=1 export VECLIB_MAXIMUM_THREADS=1 if [ -z "$AGNOS_VERSION" ]; then - export AGNOS_VERSION="13" + export AGNOS_VERSION="13.1" fi export STAGING_ROOT="/data/safe_staging" diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index b252a8110e..d93963cf2c 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -56,29 +56,29 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a.img.xz", - "hash": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", - "hash_raw": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", - "size": 18515968, + "url": "https://commadist.azureedge.net/agnosupdate/boot-b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb.img.xz", + "hash": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", + "hash_raw": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", + "size": 17442816, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "41d693d7e752c04210b4f8d68015d2367ee83e1cd54cc7b0aca3b79b4855e6b1" + "ondevice_hash": "8ed6c2796be5c5b29d64e6413b8e878d5bd1a3981d15216d2b5e84140cc4ea2a" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img.xz", - "hash": "3596cd5d8a51dabcdd75c29f9317ca3dad9036b1083630ad719eaf584fdb1ce9", - "hash_raw": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", - "size": 5368709120, + "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img.xz", + "hash": "325414e5c9f7516b2bf0fedb6abe6682f717897a6d84ab70d5afe91a59f244e9", + "hash_raw": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", + "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "32cdbc0ce176e0ea92944e53be875c12374512fa09b6041e42e683519d36591e", + "ondevice_hash": "79f4f6d0b5b4a416f0f31261b430943a78e37c26d0e226e0ef412fe0eae3c727", "alt": { - "hash": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", - "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img", - "size": 5368709120 + "hash": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", + "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img", + "size": 4718592000 } } ] \ No newline at end of file diff --git a/system/hardware/tici/all-partitions.json b/system/hardware/tici/all-partitions.json index 8bc6680b98..ebffc01dfd 100644 --- a/system/hardware/tici/all-partitions.json +++ b/system/hardware/tici/all-partitions.json @@ -339,62 +339,62 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a.img.xz", - "hash": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", - "hash_raw": "3e26a0d330a1be1614a5c25cae238ca5d01c1be90802ad296c805c3bcbad0e7a", - "size": 18515968, + "url": "https://commadist.azureedge.net/agnosupdate/boot-b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb.img.xz", + "hash": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", + "hash_raw": "b96882012ab6cddda04f440009c798a6cff65977f984b12072e89afa592d86cb", + "size": 17442816, "sparse": false, "full_check": true, "has_ab": true, - "ondevice_hash": "41d693d7e752c04210b4f8d68015d2367ee83e1cd54cc7b0aca3b79b4855e6b1" + "ondevice_hash": "8ed6c2796be5c5b29d64e6413b8e878d5bd1a3981d15216d2b5e84140cc4ea2a" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img.xz", - "hash": "3596cd5d8a51dabcdd75c29f9317ca3dad9036b1083630ad719eaf584fdb1ce9", - "hash_raw": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", - "size": 5368709120, + "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img.xz", + "hash": "325414e5c9f7516b2bf0fedb6abe6682f717897a6d84ab70d5afe91a59f244e9", + "hash_raw": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", + "size": 4718592000, "sparse": true, "full_check": false, "has_ab": true, - "ondevice_hash": "32cdbc0ce176e0ea92944e53be875c12374512fa09b6041e42e683519d36591e", + "ondevice_hash": "79f4f6d0b5b4a416f0f31261b430943a78e37c26d0e226e0ef412fe0eae3c727", "alt": { - "hash": "4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3", - "url": "https://commadist.azureedge.net/agnosupdate/system-4d0c7005e242fc757adbefd43b44ab2e78be53ca5145fceb160cc32ecf8d6cc3.img", - "size": 5368709120 + "hash": "2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc", + "url": "https://commadist.azureedge.net/agnosupdate/system-2b1bb223bf2100376ad5d543bfa4a483f33327b3478ec20ab36048388472c4bc.img", + "size": 4718592000 } }, { "name": "userdata_90", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-a3695e6b4bade3dd9c2711cd92e93e9ac7744207c2af03b78f0b9a17e89d357f.img.xz", - "hash": "eeb50afb13973d7e54013fdb3ce0f4f396b8608c8325442966cad6b67e39a8d9", - "hash_raw": "a3695e6b4bade3dd9c2711cd92e93e9ac7744207c2af03b78f0b9a17e89d357f", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-b3112984d2a8534a83d2ce43d35efdd10c7d163d9699f611f0f72ad9e9cb5af9.img.xz", + "hash": "bea163e6fb6ac6224c7f32619affb5afb834cd859971b0cab6d8297dd0098f0a", + "hash_raw": "b3112984d2a8534a83d2ce43d35efdd10c7d163d9699f611f0f72ad9e9cb5af9", "size": 96636764160, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "537088b516805b32b1b4ad176e7f3fc6bc828e296398ce65cbf5f6150fb1a26f" + "ondevice_hash": "f4841c6ae3207197886e5efbd50f44cc24822680d7b785fa2d2743c657f23287" }, { "name": "userdata_89", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-cbe9979b42b265c9e25a50e876faf5b592fe175aeb5936f2a97b345a6d4e53f5.img.xz", - "hash": "9456a8b117736e6f8eb35cc97fc62ddc8255f38a1be5959a6911498d6aaee08d", - "hash_raw": "cbe9979b42b265c9e25a50e876faf5b592fe175aeb5936f2a97b345a6d4e53f5", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-3e63f670e4270474cec96f4da9250ee4e87e3106b0b043b7e82371e1c761e167.img.xz", + "hash": "b5458a29dd7d4a4c9b7ad77b8baa5f804142ac78d97c6668839bf2a650e32518", + "hash_raw": "3e63f670e4270474cec96f4da9250ee4e87e3106b0b043b7e82371e1c761e167", "size": 95563022336, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "9e7293cf9a377cb2f3477698e7143e6085a42f7355d7eace5bf9e590992941a8" + "ondevice_hash": "1dc10c542d3b019258fc08dc7dfdb49d9abad065e46d030b89bc1a2e0197f526" }, { "name": "userdata_30", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-0b25bb660f1c0c4475fc22a32a51cd32bb980f55b95069e6ab56dd8e47f00c31.img.xz", - "hash": "6c5c98c0fec64355ead5dfc9c1902653b4ea9a071e7b968d1ccd36565082f6b7", - "hash_raw": "0b25bb660f1c0c4475fc22a32a51cd32bb980f55b95069e6ab56dd8e47f00c31", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-1d3885d4370974e55f0c6f567fd0344fc5ee10db067aa5810fbaf402eadb032c.img.xz", + "hash": "687d178cfc91be5d7e8aa1333405b610fdce01775b8333bd0985b81642b94eea", + "hash_raw": "1d3885d4370974e55f0c6f567fd0344fc5ee10db067aa5810fbaf402eadb032c", "size": 32212254720, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "42b5c09a36866d9a52e78b038901669d5bebb02176c498ce11618f200bdfe6b5" + "ondevice_hash": "9ddbd1dae6ee7dc919f018364cf2f29dad138c9203c5a49aea0cbb9bf2e137e5" } ] \ No newline at end of file From 8dca43881a9e253158d483dd02a5522563386283 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Sun, 7 Sep 2025 15:40:48 -0700 Subject: [PATCH 137/188] Rewrite ubloxd in Python (#36112) * Rewrite ubloxd in Python * lil more * rm from third_party/ * cleanup * ubx replay * try this * back to kaitai * Revert "ubx replay" This reverts commit 570bd3d25fbabc590379ce0a9f98d30de55cf1b3. --- system/manager/process_config.py | 2 +- system/ubloxd/.gitignore | 2 - system/ubloxd/SConscript | 25 +- system/ubloxd/generated/glonass.cpp | 353 ----------- system/ubloxd/generated/glonass.h | 375 ----------- system/ubloxd/generated/glonass.py | 247 ++++++++ system/ubloxd/generated/gps.cpp | 325 ---------- system/ubloxd/generated/gps.h | 359 ----------- system/ubloxd/generated/gps.py | 193 ++++++ system/ubloxd/generated/ubx.cpp | 424 ------------- system/ubloxd/generated/ubx.h | 484 --------------- system/ubloxd/generated/ubx.py | 273 ++++++++ system/ubloxd/tests/print_gps_stats.py | 20 - system/ubloxd/tests/test_glonass_kaitai.cc | 360 ----------- system/ubloxd/tests/test_glonass_runner.cc | 2 - system/ubloxd/ublox_msg.cc | 530 ---------------- system/ubloxd/ublox_msg.h | 131 ---- system/ubloxd/ubloxd.cc | 62 -- system/ubloxd/ubloxd.py | 519 ++++++++++++++++ third_party/SConscript | 1 - third_party/kaitai/custom_decoder.h | 16 - third_party/kaitai/exceptions.h | 189 ------ third_party/kaitai/kaitaistream.cpp | 689 --------------------- third_party/kaitai/kaitaistream.h | 268 -------- third_party/kaitai/kaitaistruct.h | 20 - 25 files changed, 1241 insertions(+), 4628 deletions(-) delete mode 100644 system/ubloxd/.gitignore delete mode 100644 system/ubloxd/generated/glonass.cpp delete mode 100644 system/ubloxd/generated/glonass.h create mode 100644 system/ubloxd/generated/glonass.py delete mode 100644 system/ubloxd/generated/gps.cpp delete mode 100644 system/ubloxd/generated/gps.h create mode 100644 system/ubloxd/generated/gps.py delete mode 100644 system/ubloxd/generated/ubx.cpp delete mode 100644 system/ubloxd/generated/ubx.h create mode 100644 system/ubloxd/generated/ubx.py delete mode 100755 system/ubloxd/tests/print_gps_stats.py delete mode 100644 system/ubloxd/tests/test_glonass_kaitai.cc delete mode 100644 system/ubloxd/tests/test_glonass_runner.cc delete mode 100644 system/ubloxd/ublox_msg.cc delete mode 100644 system/ubloxd/ublox_msg.h delete mode 100644 system/ubloxd/ubloxd.cc create mode 100755 system/ubloxd/ubloxd.py delete mode 100644 third_party/kaitai/custom_decoder.h delete mode 100644 third_party/kaitai/exceptions.h delete mode 100644 third_party/kaitai/kaitaistream.cpp delete mode 100644 third_party/kaitai/kaitaistream.h delete mode 100644 third_party/kaitai/kaitaistruct.h diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 9ed99d9560..22f159e891 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -97,7 +97,7 @@ procs = [ PythonProcess("pandad", "selfdrive.pandad.pandad", always_run), PythonProcess("paramsd", "selfdrive.locationd.paramsd", only_onroad), PythonProcess("lagd", "selfdrive.locationd.lagd", only_onroad), - NativeProcess("ubloxd", "system/ubloxd", ["./ubloxd"], ublox, enabled=TICI), + PythonProcess("ubloxd", "system.ubloxd.ubloxd", ublox, enabled=TICI), PythonProcess("pigeond", "system.ubloxd.pigeond", ublox, enabled=TICI), PythonProcess("plannerd", "selfdrive.controls.plannerd", not_long_maneuver), PythonProcess("maneuversd", "tools.longitudinal_maneuvers.maneuversd", long_maneuver), diff --git a/system/ubloxd/.gitignore b/system/ubloxd/.gitignore deleted file mode 100644 index 05263ff67c..0000000000 --- a/system/ubloxd/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -ubloxd -tests/test_glonass_runner diff --git a/system/ubloxd/SConscript b/system/ubloxd/SConscript index ce09e235e6..9eb50760ba 100644 --- a/system/ubloxd/SConscript +++ b/system/ubloxd/SConscript @@ -1,20 +1,11 @@ -Import('env', 'common', 'messaging') - -loc_libs = [messaging, common, 'kaitai', 'pthread'] +Import('env') if GetOption('kaitai'): - generated = Dir('generated').srcnode().abspath - cmd = f"kaitai-struct-compiler --target cpp_stl --outdir {generated} $SOURCES" - env.Command(['generated/ubx.cpp', 'generated/ubx.h'], 'ubx.ksy', cmd) - env.Command(['generated/gps.cpp', 'generated/gps.h'], 'gps.ksy', cmd) - glonass = env.Command(['generated/glonass.cpp', 'generated/glonass.h'], 'glonass.ksy', cmd) - + current_dir = Dir('./generated/').srcnode().abspath + python_cmd = f"kaitai-struct-compiler --target python --outdir {current_dir} $SOURCES" + env.Command(File('./generated/ubx.py'), 'ubx.ksy', python_cmd) + env.Command(File('./generated/gps.py'), 'gps.ksy', python_cmd) + env.Command(File('./generated/glonass.py'), 'glonass.ksy', python_cmd) # kaitai issue: https://github.com/kaitai-io/kaitai_struct/issues/910 - patch = env.Command(None, 'glonass_fix.patch', 'git apply $SOURCES') - env.Depends(patch, glonass) - -glonass_obj = env.Object('generated/glonass.cpp') -env.Program("ubloxd", ["ubloxd.cc", "ublox_msg.cc", "generated/ubx.cpp", "generated/gps.cpp", glonass_obj], LIBS=loc_libs) - -if GetOption('extras'): - env.Program("tests/test_glonass_runner", ['tests/test_glonass_runner.cc', 'tests/test_glonass_kaitai.cc', glonass_obj], LIBS=[loc_libs]) \ No newline at end of file + py_glonass_fix = env.Command(None, File('./generated/glonass.py'), "sed -i 's/self._io.align_to_byte()/# self._io.align_to_byte()/' $SOURCES") + env.Depends(py_glonass_fix, File('./generated/glonass.py')) diff --git a/system/ubloxd/generated/glonass.cpp b/system/ubloxd/generated/glonass.cpp deleted file mode 100644 index cd0f96ab68..0000000000 --- a/system/ubloxd/generated/glonass.cpp +++ /dev/null @@ -1,353 +0,0 @@ -// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -#include "glonass.h" - -glonass_t::glonass_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent, glonass_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = this; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void glonass_t::_read() { - m_idle_chip = m__io->read_bits_int_be(1); - m_string_number = m__io->read_bits_int_be(4); - //m__io->align_to_byte(); - switch (string_number()) { - case 4: { - m_data = new string_4_t(m__io, this, m__root); - break; - } - case 1: { - m_data = new string_1_t(m__io, this, m__root); - break; - } - case 3: { - m_data = new string_3_t(m__io, this, m__root); - break; - } - case 5: { - m_data = new string_5_t(m__io, this, m__root); - break; - } - case 2: { - m_data = new string_2_t(m__io, this, m__root); - break; - } - default: { - m_data = new string_non_immediate_t(m__io, this, m__root); - break; - } - } - m_hamming_code = m__io->read_bits_int_be(8); - m_pad_1 = m__io->read_bits_int_be(11); - m_superframe_number = m__io->read_bits_int_be(16); - m_pad_2 = m__io->read_bits_int_be(8); - m_frame_number = m__io->read_bits_int_be(8); -} - -glonass_t::~glonass_t() { - _clean_up(); -} - -void glonass_t::_clean_up() { - if (m_data) { - delete m_data; m_data = 0; - } -} - -glonass_t::string_4_t::string_4_t(kaitai::kstream* p__io, glonass_t* p__parent, glonass_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - f_tau_n = false; - f_delta_tau_n = false; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void glonass_t::string_4_t::_read() { - m_tau_n_sign = m__io->read_bits_int_be(1); - m_tau_n_value = m__io->read_bits_int_be(21); - m_delta_tau_n_sign = m__io->read_bits_int_be(1); - m_delta_tau_n_value = m__io->read_bits_int_be(4); - m_e_n = m__io->read_bits_int_be(5); - m_not_used_1 = m__io->read_bits_int_be(14); - m_p4 = m__io->read_bits_int_be(1); - m_f_t = m__io->read_bits_int_be(4); - m_not_used_2 = m__io->read_bits_int_be(3); - m_n_t = m__io->read_bits_int_be(11); - m_n = m__io->read_bits_int_be(5); - m_m = m__io->read_bits_int_be(2); -} - -glonass_t::string_4_t::~string_4_t() { - _clean_up(); -} - -void glonass_t::string_4_t::_clean_up() { -} - -int32_t glonass_t::string_4_t::tau_n() { - if (f_tau_n) - return m_tau_n; - m_tau_n = ((tau_n_sign()) ? ((tau_n_value() * -1)) : (tau_n_value())); - f_tau_n = true; - return m_tau_n; -} - -int32_t glonass_t::string_4_t::delta_tau_n() { - if (f_delta_tau_n) - return m_delta_tau_n; - m_delta_tau_n = ((delta_tau_n_sign()) ? ((delta_tau_n_value() * -1)) : (delta_tau_n_value())); - f_delta_tau_n = true; - return m_delta_tau_n; -} - -glonass_t::string_non_immediate_t::string_non_immediate_t(kaitai::kstream* p__io, glonass_t* p__parent, glonass_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void glonass_t::string_non_immediate_t::_read() { - m_data_1 = m__io->read_bits_int_be(64); - m_data_2 = m__io->read_bits_int_be(8); -} - -glonass_t::string_non_immediate_t::~string_non_immediate_t() { - _clean_up(); -} - -void glonass_t::string_non_immediate_t::_clean_up() { -} - -glonass_t::string_5_t::string_5_t(kaitai::kstream* p__io, glonass_t* p__parent, glonass_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void glonass_t::string_5_t::_read() { - m_n_a = m__io->read_bits_int_be(11); - m_tau_c = m__io->read_bits_int_be(32); - m_not_used = m__io->read_bits_int_be(1); - m_n_4 = m__io->read_bits_int_be(5); - m_tau_gps = m__io->read_bits_int_be(22); - m_l_n = m__io->read_bits_int_be(1); -} - -glonass_t::string_5_t::~string_5_t() { - _clean_up(); -} - -void glonass_t::string_5_t::_clean_up() { -} - -glonass_t::string_1_t::string_1_t(kaitai::kstream* p__io, glonass_t* p__parent, glonass_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - f_x_vel = false; - f_x_accel = false; - f_x = false; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void glonass_t::string_1_t::_read() { - m_not_used = m__io->read_bits_int_be(2); - m_p1 = m__io->read_bits_int_be(2); - m_t_k = m__io->read_bits_int_be(12); - m_x_vel_sign = m__io->read_bits_int_be(1); - m_x_vel_value = m__io->read_bits_int_be(23); - m_x_accel_sign = m__io->read_bits_int_be(1); - m_x_accel_value = m__io->read_bits_int_be(4); - m_x_sign = m__io->read_bits_int_be(1); - m_x_value = m__io->read_bits_int_be(26); -} - -glonass_t::string_1_t::~string_1_t() { - _clean_up(); -} - -void glonass_t::string_1_t::_clean_up() { -} - -int32_t glonass_t::string_1_t::x_vel() { - if (f_x_vel) - return m_x_vel; - m_x_vel = ((x_vel_sign()) ? ((x_vel_value() * -1)) : (x_vel_value())); - f_x_vel = true; - return m_x_vel; -} - -int32_t glonass_t::string_1_t::x_accel() { - if (f_x_accel) - return m_x_accel; - m_x_accel = ((x_accel_sign()) ? ((x_accel_value() * -1)) : (x_accel_value())); - f_x_accel = true; - return m_x_accel; -} - -int32_t glonass_t::string_1_t::x() { - if (f_x) - return m_x; - m_x = ((x_sign()) ? ((x_value() * -1)) : (x_value())); - f_x = true; - return m_x; -} - -glonass_t::string_2_t::string_2_t(kaitai::kstream* p__io, glonass_t* p__parent, glonass_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - f_y_vel = false; - f_y_accel = false; - f_y = false; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void glonass_t::string_2_t::_read() { - m_b_n = m__io->read_bits_int_be(3); - m_p2 = m__io->read_bits_int_be(1); - m_t_b = m__io->read_bits_int_be(7); - m_not_used = m__io->read_bits_int_be(5); - m_y_vel_sign = m__io->read_bits_int_be(1); - m_y_vel_value = m__io->read_bits_int_be(23); - m_y_accel_sign = m__io->read_bits_int_be(1); - m_y_accel_value = m__io->read_bits_int_be(4); - m_y_sign = m__io->read_bits_int_be(1); - m_y_value = m__io->read_bits_int_be(26); -} - -glonass_t::string_2_t::~string_2_t() { - _clean_up(); -} - -void glonass_t::string_2_t::_clean_up() { -} - -int32_t glonass_t::string_2_t::y_vel() { - if (f_y_vel) - return m_y_vel; - m_y_vel = ((y_vel_sign()) ? ((y_vel_value() * -1)) : (y_vel_value())); - f_y_vel = true; - return m_y_vel; -} - -int32_t glonass_t::string_2_t::y_accel() { - if (f_y_accel) - return m_y_accel; - m_y_accel = ((y_accel_sign()) ? ((y_accel_value() * -1)) : (y_accel_value())); - f_y_accel = true; - return m_y_accel; -} - -int32_t glonass_t::string_2_t::y() { - if (f_y) - return m_y; - m_y = ((y_sign()) ? ((y_value() * -1)) : (y_value())); - f_y = true; - return m_y; -} - -glonass_t::string_3_t::string_3_t(kaitai::kstream* p__io, glonass_t* p__parent, glonass_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - f_gamma_n = false; - f_z_vel = false; - f_z_accel = false; - f_z = false; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void glonass_t::string_3_t::_read() { - m_p3 = m__io->read_bits_int_be(1); - m_gamma_n_sign = m__io->read_bits_int_be(1); - m_gamma_n_value = m__io->read_bits_int_be(10); - m_not_used = m__io->read_bits_int_be(1); - m_p = m__io->read_bits_int_be(2); - m_l_n = m__io->read_bits_int_be(1); - m_z_vel_sign = m__io->read_bits_int_be(1); - m_z_vel_value = m__io->read_bits_int_be(23); - m_z_accel_sign = m__io->read_bits_int_be(1); - m_z_accel_value = m__io->read_bits_int_be(4); - m_z_sign = m__io->read_bits_int_be(1); - m_z_value = m__io->read_bits_int_be(26); -} - -glonass_t::string_3_t::~string_3_t() { - _clean_up(); -} - -void glonass_t::string_3_t::_clean_up() { -} - -int32_t glonass_t::string_3_t::gamma_n() { - if (f_gamma_n) - return m_gamma_n; - m_gamma_n = ((gamma_n_sign()) ? ((gamma_n_value() * -1)) : (gamma_n_value())); - f_gamma_n = true; - return m_gamma_n; -} - -int32_t glonass_t::string_3_t::z_vel() { - if (f_z_vel) - return m_z_vel; - m_z_vel = ((z_vel_sign()) ? ((z_vel_value() * -1)) : (z_vel_value())); - f_z_vel = true; - return m_z_vel; -} - -int32_t glonass_t::string_3_t::z_accel() { - if (f_z_accel) - return m_z_accel; - m_z_accel = ((z_accel_sign()) ? ((z_accel_value() * -1)) : (z_accel_value())); - f_z_accel = true; - return m_z_accel; -} - -int32_t glonass_t::string_3_t::z() { - if (f_z) - return m_z; - m_z = ((z_sign()) ? ((z_value() * -1)) : (z_value())); - f_z = true; - return m_z; -} diff --git a/system/ubloxd/generated/glonass.h b/system/ubloxd/generated/glonass.h deleted file mode 100644 index 19867ba22b..0000000000 --- a/system/ubloxd/generated/glonass.h +++ /dev/null @@ -1,375 +0,0 @@ -#ifndef GLONASS_H_ -#define GLONASS_H_ - -// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -#include "kaitai/kaitaistruct.h" -#include - -#if KAITAI_STRUCT_VERSION < 9000L -#error "Incompatible Kaitai Struct C++/STL API: version 0.9 or later is required" -#endif - -class glonass_t : public kaitai::kstruct { - -public: - class string_4_t; - class string_non_immediate_t; - class string_5_t; - class string_1_t; - class string_2_t; - class string_3_t; - - glonass_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent = 0, glonass_t* p__root = 0); - -private: - void _read(); - void _clean_up(); - -public: - ~glonass_t(); - - class string_4_t : public kaitai::kstruct { - - public: - - string_4_t(kaitai::kstream* p__io, glonass_t* p__parent = 0, glonass_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~string_4_t(); - - private: - bool f_tau_n; - int32_t m_tau_n; - - public: - int32_t tau_n(); - - private: - bool f_delta_tau_n; - int32_t m_delta_tau_n; - - public: - int32_t delta_tau_n(); - - private: - bool m_tau_n_sign; - uint64_t m_tau_n_value; - bool m_delta_tau_n_sign; - uint64_t m_delta_tau_n_value; - uint64_t m_e_n; - uint64_t m_not_used_1; - bool m_p4; - uint64_t m_f_t; - uint64_t m_not_used_2; - uint64_t m_n_t; - uint64_t m_n; - uint64_t m_m; - glonass_t* m__root; - glonass_t* m__parent; - - public: - bool tau_n_sign() const { return m_tau_n_sign; } - uint64_t tau_n_value() const { return m_tau_n_value; } - bool delta_tau_n_sign() const { return m_delta_tau_n_sign; } - uint64_t delta_tau_n_value() const { return m_delta_tau_n_value; } - uint64_t e_n() const { return m_e_n; } - uint64_t not_used_1() const { return m_not_used_1; } - bool p4() const { return m_p4; } - uint64_t f_t() const { return m_f_t; } - uint64_t not_used_2() const { return m_not_used_2; } - uint64_t n_t() const { return m_n_t; } - uint64_t n() const { return m_n; } - uint64_t m() const { return m_m; } - glonass_t* _root() const { return m__root; } - glonass_t* _parent() const { return m__parent; } - }; - - class string_non_immediate_t : public kaitai::kstruct { - - public: - - string_non_immediate_t(kaitai::kstream* p__io, glonass_t* p__parent = 0, glonass_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~string_non_immediate_t(); - - private: - uint64_t m_data_1; - uint64_t m_data_2; - glonass_t* m__root; - glonass_t* m__parent; - - public: - uint64_t data_1() const { return m_data_1; } - uint64_t data_2() const { return m_data_2; } - glonass_t* _root() const { return m__root; } - glonass_t* _parent() const { return m__parent; } - }; - - class string_5_t : public kaitai::kstruct { - - public: - - string_5_t(kaitai::kstream* p__io, glonass_t* p__parent = 0, glonass_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~string_5_t(); - - private: - uint64_t m_n_a; - uint64_t m_tau_c; - bool m_not_used; - uint64_t m_n_4; - uint64_t m_tau_gps; - bool m_l_n; - glonass_t* m__root; - glonass_t* m__parent; - - public: - uint64_t n_a() const { return m_n_a; } - uint64_t tau_c() const { return m_tau_c; } - bool not_used() const { return m_not_used; } - uint64_t n_4() const { return m_n_4; } - uint64_t tau_gps() const { return m_tau_gps; } - bool l_n() const { return m_l_n; } - glonass_t* _root() const { return m__root; } - glonass_t* _parent() const { return m__parent; } - }; - - class string_1_t : public kaitai::kstruct { - - public: - - string_1_t(kaitai::kstream* p__io, glonass_t* p__parent = 0, glonass_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~string_1_t(); - - private: - bool f_x_vel; - int32_t m_x_vel; - - public: - int32_t x_vel(); - - private: - bool f_x_accel; - int32_t m_x_accel; - - public: - int32_t x_accel(); - - private: - bool f_x; - int32_t m_x; - - public: - int32_t x(); - - private: - uint64_t m_not_used; - uint64_t m_p1; - uint64_t m_t_k; - bool m_x_vel_sign; - uint64_t m_x_vel_value; - bool m_x_accel_sign; - uint64_t m_x_accel_value; - bool m_x_sign; - uint64_t m_x_value; - glonass_t* m__root; - glonass_t* m__parent; - - public: - uint64_t not_used() const { return m_not_used; } - uint64_t p1() const { return m_p1; } - uint64_t t_k() const { return m_t_k; } - bool x_vel_sign() const { return m_x_vel_sign; } - uint64_t x_vel_value() const { return m_x_vel_value; } - bool x_accel_sign() const { return m_x_accel_sign; } - uint64_t x_accel_value() const { return m_x_accel_value; } - bool x_sign() const { return m_x_sign; } - uint64_t x_value() const { return m_x_value; } - glonass_t* _root() const { return m__root; } - glonass_t* _parent() const { return m__parent; } - }; - - class string_2_t : public kaitai::kstruct { - - public: - - string_2_t(kaitai::kstream* p__io, glonass_t* p__parent = 0, glonass_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~string_2_t(); - - private: - bool f_y_vel; - int32_t m_y_vel; - - public: - int32_t y_vel(); - - private: - bool f_y_accel; - int32_t m_y_accel; - - public: - int32_t y_accel(); - - private: - bool f_y; - int32_t m_y; - - public: - int32_t y(); - - private: - uint64_t m_b_n; - bool m_p2; - uint64_t m_t_b; - uint64_t m_not_used; - bool m_y_vel_sign; - uint64_t m_y_vel_value; - bool m_y_accel_sign; - uint64_t m_y_accel_value; - bool m_y_sign; - uint64_t m_y_value; - glonass_t* m__root; - glonass_t* m__parent; - - public: - uint64_t b_n() const { return m_b_n; } - bool p2() const { return m_p2; } - uint64_t t_b() const { return m_t_b; } - uint64_t not_used() const { return m_not_used; } - bool y_vel_sign() const { return m_y_vel_sign; } - uint64_t y_vel_value() const { return m_y_vel_value; } - bool y_accel_sign() const { return m_y_accel_sign; } - uint64_t y_accel_value() const { return m_y_accel_value; } - bool y_sign() const { return m_y_sign; } - uint64_t y_value() const { return m_y_value; } - glonass_t* _root() const { return m__root; } - glonass_t* _parent() const { return m__parent; } - }; - - class string_3_t : public kaitai::kstruct { - - public: - - string_3_t(kaitai::kstream* p__io, glonass_t* p__parent = 0, glonass_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~string_3_t(); - - private: - bool f_gamma_n; - int32_t m_gamma_n; - - public: - int32_t gamma_n(); - - private: - bool f_z_vel; - int32_t m_z_vel; - - public: - int32_t z_vel(); - - private: - bool f_z_accel; - int32_t m_z_accel; - - public: - int32_t z_accel(); - - private: - bool f_z; - int32_t m_z; - - public: - int32_t z(); - - private: - bool m_p3; - bool m_gamma_n_sign; - uint64_t m_gamma_n_value; - bool m_not_used; - uint64_t m_p; - bool m_l_n; - bool m_z_vel_sign; - uint64_t m_z_vel_value; - bool m_z_accel_sign; - uint64_t m_z_accel_value; - bool m_z_sign; - uint64_t m_z_value; - glonass_t* m__root; - glonass_t* m__parent; - - public: - bool p3() const { return m_p3; } - bool gamma_n_sign() const { return m_gamma_n_sign; } - uint64_t gamma_n_value() const { return m_gamma_n_value; } - bool not_used() const { return m_not_used; } - uint64_t p() const { return m_p; } - bool l_n() const { return m_l_n; } - bool z_vel_sign() const { return m_z_vel_sign; } - uint64_t z_vel_value() const { return m_z_vel_value; } - bool z_accel_sign() const { return m_z_accel_sign; } - uint64_t z_accel_value() const { return m_z_accel_value; } - bool z_sign() const { return m_z_sign; } - uint64_t z_value() const { return m_z_value; } - glonass_t* _root() const { return m__root; } - glonass_t* _parent() const { return m__parent; } - }; - -private: - bool m_idle_chip; - uint64_t m_string_number; - kaitai::kstruct* m_data; - uint64_t m_hamming_code; - uint64_t m_pad_1; - uint64_t m_superframe_number; - uint64_t m_pad_2; - uint64_t m_frame_number; - glonass_t* m__root; - kaitai::kstruct* m__parent; - -public: - bool idle_chip() const { return m_idle_chip; } - uint64_t string_number() const { return m_string_number; } - kaitai::kstruct* data() const { return m_data; } - uint64_t hamming_code() const { return m_hamming_code; } - uint64_t pad_1() const { return m_pad_1; } - uint64_t superframe_number() const { return m_superframe_number; } - uint64_t pad_2() const { return m_pad_2; } - uint64_t frame_number() const { return m_frame_number; } - glonass_t* _root() const { return m__root; } - kaitai::kstruct* _parent() const { return m__parent; } -}; - -#endif // GLONASS_H_ diff --git a/system/ubloxd/generated/glonass.py b/system/ubloxd/generated/glonass.py new file mode 100644 index 0000000000..40aa16bb6f --- /dev/null +++ b/system/ubloxd/generated/glonass.py @@ -0,0 +1,247 @@ +# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild + +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO + + +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) + +class Glonass(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.idle_chip = self._io.read_bits_int_be(1) != 0 + self.string_number = self._io.read_bits_int_be(4) + # workaround for kaitai bit alignment issue (see glonass_fix.patch for C++) + # self._io.align_to_byte() + _on = self.string_number + if _on == 4: + self.data = Glonass.String4(self._io, self, self._root) + elif _on == 1: + self.data = Glonass.String1(self._io, self, self._root) + elif _on == 3: + self.data = Glonass.String3(self._io, self, self._root) + elif _on == 5: + self.data = Glonass.String5(self._io, self, self._root) + elif _on == 2: + self.data = Glonass.String2(self._io, self, self._root) + else: + self.data = Glonass.StringNonImmediate(self._io, self, self._root) + self.hamming_code = self._io.read_bits_int_be(8) + self.pad_1 = self._io.read_bits_int_be(11) + self.superframe_number = self._io.read_bits_int_be(16) + self.pad_2 = self._io.read_bits_int_be(8) + self.frame_number = self._io.read_bits_int_be(8) + + class String4(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.tau_n_sign = self._io.read_bits_int_be(1) != 0 + self.tau_n_value = self._io.read_bits_int_be(21) + self.delta_tau_n_sign = self._io.read_bits_int_be(1) != 0 + self.delta_tau_n_value = self._io.read_bits_int_be(4) + self.e_n = self._io.read_bits_int_be(5) + self.not_used_1 = self._io.read_bits_int_be(14) + self.p4 = self._io.read_bits_int_be(1) != 0 + self.f_t = self._io.read_bits_int_be(4) + self.not_used_2 = self._io.read_bits_int_be(3) + self.n_t = self._io.read_bits_int_be(11) + self.n = self._io.read_bits_int_be(5) + self.m = self._io.read_bits_int_be(2) + + @property + def tau_n(self): + if hasattr(self, '_m_tau_n'): + return self._m_tau_n + + self._m_tau_n = ((self.tau_n_value * -1) if self.tau_n_sign else self.tau_n_value) + return getattr(self, '_m_tau_n', None) + + @property + def delta_tau_n(self): + if hasattr(self, '_m_delta_tau_n'): + return self._m_delta_tau_n + + self._m_delta_tau_n = ((self.delta_tau_n_value * -1) if self.delta_tau_n_sign else self.delta_tau_n_value) + return getattr(self, '_m_delta_tau_n', None) + + + class StringNonImmediate(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.data_1 = self._io.read_bits_int_be(64) + self.data_2 = self._io.read_bits_int_be(8) + + + class String5(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.n_a = self._io.read_bits_int_be(11) + self.tau_c = self._io.read_bits_int_be(32) + self.not_used = self._io.read_bits_int_be(1) != 0 + self.n_4 = self._io.read_bits_int_be(5) + self.tau_gps = self._io.read_bits_int_be(22) + self.l_n = self._io.read_bits_int_be(1) != 0 + + + class String1(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.not_used = self._io.read_bits_int_be(2) + self.p1 = self._io.read_bits_int_be(2) + self.t_k = self._io.read_bits_int_be(12) + self.x_vel_sign = self._io.read_bits_int_be(1) != 0 + self.x_vel_value = self._io.read_bits_int_be(23) + self.x_accel_sign = self._io.read_bits_int_be(1) != 0 + self.x_accel_value = self._io.read_bits_int_be(4) + self.x_sign = self._io.read_bits_int_be(1) != 0 + self.x_value = self._io.read_bits_int_be(26) + + @property + def x_vel(self): + if hasattr(self, '_m_x_vel'): + return self._m_x_vel + + self._m_x_vel = ((self.x_vel_value * -1) if self.x_vel_sign else self.x_vel_value) + return getattr(self, '_m_x_vel', None) + + @property + def x_accel(self): + if hasattr(self, '_m_x_accel'): + return self._m_x_accel + + self._m_x_accel = ((self.x_accel_value * -1) if self.x_accel_sign else self.x_accel_value) + return getattr(self, '_m_x_accel', None) + + @property + def x(self): + if hasattr(self, '_m_x'): + return self._m_x + + self._m_x = ((self.x_value * -1) if self.x_sign else self.x_value) + return getattr(self, '_m_x', None) + + + class String2(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.b_n = self._io.read_bits_int_be(3) + self.p2 = self._io.read_bits_int_be(1) != 0 + self.t_b = self._io.read_bits_int_be(7) + self.not_used = self._io.read_bits_int_be(5) + self.y_vel_sign = self._io.read_bits_int_be(1) != 0 + self.y_vel_value = self._io.read_bits_int_be(23) + self.y_accel_sign = self._io.read_bits_int_be(1) != 0 + self.y_accel_value = self._io.read_bits_int_be(4) + self.y_sign = self._io.read_bits_int_be(1) != 0 + self.y_value = self._io.read_bits_int_be(26) + + @property + def y_vel(self): + if hasattr(self, '_m_y_vel'): + return self._m_y_vel + + self._m_y_vel = ((self.y_vel_value * -1) if self.y_vel_sign else self.y_vel_value) + return getattr(self, '_m_y_vel', None) + + @property + def y_accel(self): + if hasattr(self, '_m_y_accel'): + return self._m_y_accel + + self._m_y_accel = ((self.y_accel_value * -1) if self.y_accel_sign else self.y_accel_value) + return getattr(self, '_m_y_accel', None) + + @property + def y(self): + if hasattr(self, '_m_y'): + return self._m_y + + self._m_y = ((self.y_value * -1) if self.y_sign else self.y_value) + return getattr(self, '_m_y', None) + + + class String3(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.p3 = self._io.read_bits_int_be(1) != 0 + self.gamma_n_sign = self._io.read_bits_int_be(1) != 0 + self.gamma_n_value = self._io.read_bits_int_be(10) + self.not_used = self._io.read_bits_int_be(1) != 0 + self.p = self._io.read_bits_int_be(2) + self.l_n = self._io.read_bits_int_be(1) != 0 + self.z_vel_sign = self._io.read_bits_int_be(1) != 0 + self.z_vel_value = self._io.read_bits_int_be(23) + self.z_accel_sign = self._io.read_bits_int_be(1) != 0 + self.z_accel_value = self._io.read_bits_int_be(4) + self.z_sign = self._io.read_bits_int_be(1) != 0 + self.z_value = self._io.read_bits_int_be(26) + + @property + def gamma_n(self): + if hasattr(self, '_m_gamma_n'): + return self._m_gamma_n + + self._m_gamma_n = ((self.gamma_n_value * -1) if self.gamma_n_sign else self.gamma_n_value) + return getattr(self, '_m_gamma_n', None) + + @property + def z_vel(self): + if hasattr(self, '_m_z_vel'): + return self._m_z_vel + + self._m_z_vel = ((self.z_vel_value * -1) if self.z_vel_sign else self.z_vel_value) + return getattr(self, '_m_z_vel', None) + + @property + def z_accel(self): + if hasattr(self, '_m_z_accel'): + return self._m_z_accel + + self._m_z_accel = ((self.z_accel_value * -1) if self.z_accel_sign else self.z_accel_value) + return getattr(self, '_m_z_accel', None) + + @property + def z(self): + if hasattr(self, '_m_z'): + return self._m_z + + self._m_z = ((self.z_value * -1) if self.z_sign else self.z_value) + return getattr(self, '_m_z', None) + + diff --git a/system/ubloxd/generated/gps.cpp b/system/ubloxd/generated/gps.cpp deleted file mode 100644 index 8e1cb85b95..0000000000 --- a/system/ubloxd/generated/gps.cpp +++ /dev/null @@ -1,325 +0,0 @@ -// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -#include "gps.h" -#include "kaitai/exceptions.h" - -gps_t::gps_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = this; - m_tlm = 0; - m_how = 0; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::_read() { - m_tlm = new tlm_t(m__io, this, m__root); - m_how = new how_t(m__io, this, m__root); - n_body = true; - switch (how()->subframe_id()) { - case 1: { - n_body = false; - m_body = new subframe_1_t(m__io, this, m__root); - break; - } - case 2: { - n_body = false; - m_body = new subframe_2_t(m__io, this, m__root); - break; - } - case 3: { - n_body = false; - m_body = new subframe_3_t(m__io, this, m__root); - break; - } - case 4: { - n_body = false; - m_body = new subframe_4_t(m__io, this, m__root); - break; - } - } -} - -gps_t::~gps_t() { - _clean_up(); -} - -void gps_t::_clean_up() { - if (m_tlm) { - delete m_tlm; m_tlm = 0; - } - if (m_how) { - delete m_how; m_how = 0; - } - if (!n_body) { - if (m_body) { - delete m_body; m_body = 0; - } - } -} - -gps_t::subframe_1_t::subframe_1_t(kaitai::kstream* p__io, gps_t* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - f_af_0 = false; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::subframe_1_t::_read() { - m_week_no = m__io->read_bits_int_be(10); - m_code = m__io->read_bits_int_be(2); - m_sv_accuracy = m__io->read_bits_int_be(4); - m_sv_health = m__io->read_bits_int_be(6); - m_iodc_msb = m__io->read_bits_int_be(2); - m_l2_p_data_flag = m__io->read_bits_int_be(1); - m_reserved1 = m__io->read_bits_int_be(23); - m_reserved2 = m__io->read_bits_int_be(24); - m_reserved3 = m__io->read_bits_int_be(24); - m_reserved4 = m__io->read_bits_int_be(16); - m__io->align_to_byte(); - m_t_gd = m__io->read_s1(); - m_iodc_lsb = m__io->read_u1(); - m_t_oc = m__io->read_u2be(); - m_af_2 = m__io->read_s1(); - m_af_1 = m__io->read_s2be(); - m_af_0_sign = m__io->read_bits_int_be(1); - m_af_0_value = m__io->read_bits_int_be(21); - m_reserved5 = m__io->read_bits_int_be(2); -} - -gps_t::subframe_1_t::~subframe_1_t() { - _clean_up(); -} - -void gps_t::subframe_1_t::_clean_up() { -} - -int32_t gps_t::subframe_1_t::af_0() { - if (f_af_0) - return m_af_0; - m_af_0 = ((af_0_sign()) ? ((af_0_value() - (1 << 21))) : (af_0_value())); - f_af_0 = true; - return m_af_0; -} - -gps_t::subframe_3_t::subframe_3_t(kaitai::kstream* p__io, gps_t* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - f_omega_dot = false; - f_idot = false; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::subframe_3_t::_read() { - m_c_ic = m__io->read_s2be(); - m_omega_0 = m__io->read_s4be(); - m_c_is = m__io->read_s2be(); - m_i_0 = m__io->read_s4be(); - m_c_rc = m__io->read_s2be(); - m_omega = m__io->read_s4be(); - m_omega_dot_sign = m__io->read_bits_int_be(1); - m_omega_dot_value = m__io->read_bits_int_be(23); - m__io->align_to_byte(); - m_iode = m__io->read_u1(); - m_idot_sign = m__io->read_bits_int_be(1); - m_idot_value = m__io->read_bits_int_be(13); - m_reserved = m__io->read_bits_int_be(2); -} - -gps_t::subframe_3_t::~subframe_3_t() { - _clean_up(); -} - -void gps_t::subframe_3_t::_clean_up() { -} - -int32_t gps_t::subframe_3_t::omega_dot() { - if (f_omega_dot) - return m_omega_dot; - m_omega_dot = ((omega_dot_sign()) ? ((omega_dot_value() - (1 << 23))) : (omega_dot_value())); - f_omega_dot = true; - return m_omega_dot; -} - -int32_t gps_t::subframe_3_t::idot() { - if (f_idot) - return m_idot; - m_idot = ((idot_sign()) ? ((idot_value() - (1 << 13))) : (idot_value())); - f_idot = true; - return m_idot; -} - -gps_t::subframe_4_t::subframe_4_t(kaitai::kstream* p__io, gps_t* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::subframe_4_t::_read() { - m_data_id = m__io->read_bits_int_be(2); - m_page_id = m__io->read_bits_int_be(6); - m__io->align_to_byte(); - n_body = true; - switch (page_id()) { - case 56: { - n_body = false; - m_body = new ionosphere_data_t(m__io, this, m__root); - break; - } - } -} - -gps_t::subframe_4_t::~subframe_4_t() { - _clean_up(); -} - -void gps_t::subframe_4_t::_clean_up() { - if (!n_body) { - if (m_body) { - delete m_body; m_body = 0; - } - } -} - -gps_t::subframe_4_t::ionosphere_data_t::ionosphere_data_t(kaitai::kstream* p__io, gps_t::subframe_4_t* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::subframe_4_t::ionosphere_data_t::_read() { - m_a0 = m__io->read_s1(); - m_a1 = m__io->read_s1(); - m_a2 = m__io->read_s1(); - m_a3 = m__io->read_s1(); - m_b0 = m__io->read_s1(); - m_b1 = m__io->read_s1(); - m_b2 = m__io->read_s1(); - m_b3 = m__io->read_s1(); -} - -gps_t::subframe_4_t::ionosphere_data_t::~ionosphere_data_t() { - _clean_up(); -} - -void gps_t::subframe_4_t::ionosphere_data_t::_clean_up() { -} - -gps_t::how_t::how_t(kaitai::kstream* p__io, gps_t* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::how_t::_read() { - m_tow_count = m__io->read_bits_int_be(17); - m_alert = m__io->read_bits_int_be(1); - m_anti_spoof = m__io->read_bits_int_be(1); - m_subframe_id = m__io->read_bits_int_be(3); - m_reserved = m__io->read_bits_int_be(2); -} - -gps_t::how_t::~how_t() { - _clean_up(); -} - -void gps_t::how_t::_clean_up() { -} - -gps_t::tlm_t::tlm_t(kaitai::kstream* p__io, gps_t* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::tlm_t::_read() { - m_preamble = m__io->read_bytes(1); - if (!(preamble() == std::string("\x8B", 1))) { - throw kaitai::validation_not_equal_error(std::string("\x8B", 1), preamble(), _io(), std::string("/types/tlm/seq/0")); - } - m_tlm = m__io->read_bits_int_be(14); - m_integrity_status = m__io->read_bits_int_be(1); - m_reserved = m__io->read_bits_int_be(1); -} - -gps_t::tlm_t::~tlm_t() { - _clean_up(); -} - -void gps_t::tlm_t::_clean_up() { -} - -gps_t::subframe_2_t::subframe_2_t(kaitai::kstream* p__io, gps_t* p__parent, gps_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void gps_t::subframe_2_t::_read() { - m_iode = m__io->read_u1(); - m_c_rs = m__io->read_s2be(); - m_delta_n = m__io->read_s2be(); - m_m_0 = m__io->read_s4be(); - m_c_uc = m__io->read_s2be(); - m_e = m__io->read_s4be(); - m_c_us = m__io->read_s2be(); - m_sqrt_a = m__io->read_u4be(); - m_t_oe = m__io->read_u2be(); - m_fit_interval_flag = m__io->read_bits_int_be(1); - m_aoda = m__io->read_bits_int_be(5); - m_reserved = m__io->read_bits_int_be(2); -} - -gps_t::subframe_2_t::~subframe_2_t() { - _clean_up(); -} - -void gps_t::subframe_2_t::_clean_up() { -} diff --git a/system/ubloxd/generated/gps.h b/system/ubloxd/generated/gps.h deleted file mode 100644 index 9dfc5031f5..0000000000 --- a/system/ubloxd/generated/gps.h +++ /dev/null @@ -1,359 +0,0 @@ -#ifndef GPS_H_ -#define GPS_H_ - -// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -#include "kaitai/kaitaistruct.h" -#include - -#if KAITAI_STRUCT_VERSION < 9000L -#error "Incompatible Kaitai Struct C++/STL API: version 0.9 or later is required" -#endif - -class gps_t : public kaitai::kstruct { - -public: - class subframe_1_t; - class subframe_3_t; - class subframe_4_t; - class how_t; - class tlm_t; - class subframe_2_t; - - gps_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent = 0, gps_t* p__root = 0); - -private: - void _read(); - void _clean_up(); - -public: - ~gps_t(); - - class subframe_1_t : public kaitai::kstruct { - - public: - - subframe_1_t(kaitai::kstream* p__io, gps_t* p__parent = 0, gps_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~subframe_1_t(); - - private: - bool f_af_0; - int32_t m_af_0; - - public: - int32_t af_0(); - - private: - uint64_t m_week_no; - uint64_t m_code; - uint64_t m_sv_accuracy; - uint64_t m_sv_health; - uint64_t m_iodc_msb; - bool m_l2_p_data_flag; - uint64_t m_reserved1; - uint64_t m_reserved2; - uint64_t m_reserved3; - uint64_t m_reserved4; - int8_t m_t_gd; - uint8_t m_iodc_lsb; - uint16_t m_t_oc; - int8_t m_af_2; - int16_t m_af_1; - bool m_af_0_sign; - uint64_t m_af_0_value; - uint64_t m_reserved5; - gps_t* m__root; - gps_t* m__parent; - - public: - uint64_t week_no() const { return m_week_no; } - uint64_t code() const { return m_code; } - uint64_t sv_accuracy() const { return m_sv_accuracy; } - uint64_t sv_health() const { return m_sv_health; } - uint64_t iodc_msb() const { return m_iodc_msb; } - bool l2_p_data_flag() const { return m_l2_p_data_flag; } - uint64_t reserved1() const { return m_reserved1; } - uint64_t reserved2() const { return m_reserved2; } - uint64_t reserved3() const { return m_reserved3; } - uint64_t reserved4() const { return m_reserved4; } - int8_t t_gd() const { return m_t_gd; } - uint8_t iodc_lsb() const { return m_iodc_lsb; } - uint16_t t_oc() const { return m_t_oc; } - int8_t af_2() const { return m_af_2; } - int16_t af_1() const { return m_af_1; } - bool af_0_sign() const { return m_af_0_sign; } - uint64_t af_0_value() const { return m_af_0_value; } - uint64_t reserved5() const { return m_reserved5; } - gps_t* _root() const { return m__root; } - gps_t* _parent() const { return m__parent; } - }; - - class subframe_3_t : public kaitai::kstruct { - - public: - - subframe_3_t(kaitai::kstream* p__io, gps_t* p__parent = 0, gps_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~subframe_3_t(); - - private: - bool f_omega_dot; - int32_t m_omega_dot; - - public: - int32_t omega_dot(); - - private: - bool f_idot; - int32_t m_idot; - - public: - int32_t idot(); - - private: - int16_t m_c_ic; - int32_t m_omega_0; - int16_t m_c_is; - int32_t m_i_0; - int16_t m_c_rc; - int32_t m_omega; - bool m_omega_dot_sign; - uint64_t m_omega_dot_value; - uint8_t m_iode; - bool m_idot_sign; - uint64_t m_idot_value; - uint64_t m_reserved; - gps_t* m__root; - gps_t* m__parent; - - public: - int16_t c_ic() const { return m_c_ic; } - int32_t omega_0() const { return m_omega_0; } - int16_t c_is() const { return m_c_is; } - int32_t i_0() const { return m_i_0; } - int16_t c_rc() const { return m_c_rc; } - int32_t omega() const { return m_omega; } - bool omega_dot_sign() const { return m_omega_dot_sign; } - uint64_t omega_dot_value() const { return m_omega_dot_value; } - uint8_t iode() const { return m_iode; } - bool idot_sign() const { return m_idot_sign; } - uint64_t idot_value() const { return m_idot_value; } - uint64_t reserved() const { return m_reserved; } - gps_t* _root() const { return m__root; } - gps_t* _parent() const { return m__parent; } - }; - - class subframe_4_t : public kaitai::kstruct { - - public: - class ionosphere_data_t; - - subframe_4_t(kaitai::kstream* p__io, gps_t* p__parent = 0, gps_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~subframe_4_t(); - - class ionosphere_data_t : public kaitai::kstruct { - - public: - - ionosphere_data_t(kaitai::kstream* p__io, gps_t::subframe_4_t* p__parent = 0, gps_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~ionosphere_data_t(); - - private: - int8_t m_a0; - int8_t m_a1; - int8_t m_a2; - int8_t m_a3; - int8_t m_b0; - int8_t m_b1; - int8_t m_b2; - int8_t m_b3; - gps_t* m__root; - gps_t::subframe_4_t* m__parent; - - public: - int8_t a0() const { return m_a0; } - int8_t a1() const { return m_a1; } - int8_t a2() const { return m_a2; } - int8_t a3() const { return m_a3; } - int8_t b0() const { return m_b0; } - int8_t b1() const { return m_b1; } - int8_t b2() const { return m_b2; } - int8_t b3() const { return m_b3; } - gps_t* _root() const { return m__root; } - gps_t::subframe_4_t* _parent() const { return m__parent; } - }; - - private: - uint64_t m_data_id; - uint64_t m_page_id; - ionosphere_data_t* m_body; - bool n_body; - - public: - bool _is_null_body() { body(); return n_body; }; - - private: - gps_t* m__root; - gps_t* m__parent; - - public: - uint64_t data_id() const { return m_data_id; } - uint64_t page_id() const { return m_page_id; } - ionosphere_data_t* body() const { return m_body; } - gps_t* _root() const { return m__root; } - gps_t* _parent() const { return m__parent; } - }; - - class how_t : public kaitai::kstruct { - - public: - - how_t(kaitai::kstream* p__io, gps_t* p__parent = 0, gps_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~how_t(); - - private: - uint64_t m_tow_count; - bool m_alert; - bool m_anti_spoof; - uint64_t m_subframe_id; - uint64_t m_reserved; - gps_t* m__root; - gps_t* m__parent; - - public: - uint64_t tow_count() const { return m_tow_count; } - bool alert() const { return m_alert; } - bool anti_spoof() const { return m_anti_spoof; } - uint64_t subframe_id() const { return m_subframe_id; } - uint64_t reserved() const { return m_reserved; } - gps_t* _root() const { return m__root; } - gps_t* _parent() const { return m__parent; } - }; - - class tlm_t : public kaitai::kstruct { - - public: - - tlm_t(kaitai::kstream* p__io, gps_t* p__parent = 0, gps_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~tlm_t(); - - private: - std::string m_preamble; - uint64_t m_tlm; - bool m_integrity_status; - bool m_reserved; - gps_t* m__root; - gps_t* m__parent; - - public: - std::string preamble() const { return m_preamble; } - uint64_t tlm() const { return m_tlm; } - bool integrity_status() const { return m_integrity_status; } - bool reserved() const { return m_reserved; } - gps_t* _root() const { return m__root; } - gps_t* _parent() const { return m__parent; } - }; - - class subframe_2_t : public kaitai::kstruct { - - public: - - subframe_2_t(kaitai::kstream* p__io, gps_t* p__parent = 0, gps_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~subframe_2_t(); - - private: - uint8_t m_iode; - int16_t m_c_rs; - int16_t m_delta_n; - int32_t m_m_0; - int16_t m_c_uc; - int32_t m_e; - int16_t m_c_us; - uint32_t m_sqrt_a; - uint16_t m_t_oe; - bool m_fit_interval_flag; - uint64_t m_aoda; - uint64_t m_reserved; - gps_t* m__root; - gps_t* m__parent; - - public: - uint8_t iode() const { return m_iode; } - int16_t c_rs() const { return m_c_rs; } - int16_t delta_n() const { return m_delta_n; } - int32_t m_0() const { return m_m_0; } - int16_t c_uc() const { return m_c_uc; } - int32_t e() const { return m_e; } - int16_t c_us() const { return m_c_us; } - uint32_t sqrt_a() const { return m_sqrt_a; } - uint16_t t_oe() const { return m_t_oe; } - bool fit_interval_flag() const { return m_fit_interval_flag; } - uint64_t aoda() const { return m_aoda; } - uint64_t reserved() const { return m_reserved; } - gps_t* _root() const { return m__root; } - gps_t* _parent() const { return m__parent; } - }; - -private: - tlm_t* m_tlm; - how_t* m_how; - kaitai::kstruct* m_body; - bool n_body; - -public: - bool _is_null_body() { body(); return n_body; }; - -private: - gps_t* m__root; - kaitai::kstruct* m__parent; - -public: - tlm_t* tlm() const { return m_tlm; } - how_t* how() const { return m_how; } - kaitai::kstruct* body() const { return m_body; } - gps_t* _root() const { return m__root; } - kaitai::kstruct* _parent() const { return m__parent; } -}; - -#endif // GPS_H_ diff --git a/system/ubloxd/generated/gps.py b/system/ubloxd/generated/gps.py new file mode 100644 index 0000000000..a999016f3e --- /dev/null +++ b/system/ubloxd/generated/gps.py @@ -0,0 +1,193 @@ +# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild + +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO + + +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) + +class Gps(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.tlm = Gps.Tlm(self._io, self, self._root) + self.how = Gps.How(self._io, self, self._root) + _on = self.how.subframe_id + if _on == 1: + self.body = Gps.Subframe1(self._io, self, self._root) + elif _on == 2: + self.body = Gps.Subframe2(self._io, self, self._root) + elif _on == 3: + self.body = Gps.Subframe3(self._io, self, self._root) + elif _on == 4: + self.body = Gps.Subframe4(self._io, self, self._root) + + class Subframe1(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.week_no = self._io.read_bits_int_be(10) + self.code = self._io.read_bits_int_be(2) + self.sv_accuracy = self._io.read_bits_int_be(4) + self.sv_health = self._io.read_bits_int_be(6) + self.iodc_msb = self._io.read_bits_int_be(2) + self.l2_p_data_flag = self._io.read_bits_int_be(1) != 0 + self.reserved1 = self._io.read_bits_int_be(23) + self.reserved2 = self._io.read_bits_int_be(24) + self.reserved3 = self._io.read_bits_int_be(24) + self.reserved4 = self._io.read_bits_int_be(16) + self._io.align_to_byte() + self.t_gd = self._io.read_s1() + self.iodc_lsb = self._io.read_u1() + self.t_oc = self._io.read_u2be() + self.af_2 = self._io.read_s1() + self.af_1 = self._io.read_s2be() + self.af_0_sign = self._io.read_bits_int_be(1) != 0 + self.af_0_value = self._io.read_bits_int_be(21) + self.reserved5 = self._io.read_bits_int_be(2) + + @property + def af_0(self): + if hasattr(self, '_m_af_0'): + return self._m_af_0 + + self._m_af_0 = ((self.af_0_value - (1 << 21)) if self.af_0_sign else self.af_0_value) + return getattr(self, '_m_af_0', None) + + + class Subframe3(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.c_ic = self._io.read_s2be() + self.omega_0 = self._io.read_s4be() + self.c_is = self._io.read_s2be() + self.i_0 = self._io.read_s4be() + self.c_rc = self._io.read_s2be() + self.omega = self._io.read_s4be() + self.omega_dot_sign = self._io.read_bits_int_be(1) != 0 + self.omega_dot_value = self._io.read_bits_int_be(23) + self._io.align_to_byte() + self.iode = self._io.read_u1() + self.idot_sign = self._io.read_bits_int_be(1) != 0 + self.idot_value = self._io.read_bits_int_be(13) + self.reserved = self._io.read_bits_int_be(2) + + @property + def omega_dot(self): + if hasattr(self, '_m_omega_dot'): + return self._m_omega_dot + + self._m_omega_dot = ((self.omega_dot_value - (1 << 23)) if self.omega_dot_sign else self.omega_dot_value) + return getattr(self, '_m_omega_dot', None) + + @property + def idot(self): + if hasattr(self, '_m_idot'): + return self._m_idot + + self._m_idot = ((self.idot_value - (1 << 13)) if self.idot_sign else self.idot_value) + return getattr(self, '_m_idot', None) + + + class Subframe4(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.data_id = self._io.read_bits_int_be(2) + self.page_id = self._io.read_bits_int_be(6) + self._io.align_to_byte() + _on = self.page_id + if _on == 56: + self.body = Gps.Subframe4.IonosphereData(self._io, self, self._root) + + class IonosphereData(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.a0 = self._io.read_s1() + self.a1 = self._io.read_s1() + self.a2 = self._io.read_s1() + self.a3 = self._io.read_s1() + self.b0 = self._io.read_s1() + self.b1 = self._io.read_s1() + self.b2 = self._io.read_s1() + self.b3 = self._io.read_s1() + + + + class How(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.tow_count = self._io.read_bits_int_be(17) + self.alert = self._io.read_bits_int_be(1) != 0 + self.anti_spoof = self._io.read_bits_int_be(1) != 0 + self.subframe_id = self._io.read_bits_int_be(3) + self.reserved = self._io.read_bits_int_be(2) + + + class Tlm(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.preamble = self._io.read_bytes(1) + if not self.preamble == b"\x8B": + raise kaitaistruct.ValidationNotEqualError(b"\x8B", self.preamble, self._io, u"/types/tlm/seq/0") + self.tlm = self._io.read_bits_int_be(14) + self.integrity_status = self._io.read_bits_int_be(1) != 0 + self.reserved = self._io.read_bits_int_be(1) != 0 + + + class Subframe2(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.iode = self._io.read_u1() + self.c_rs = self._io.read_s2be() + self.delta_n = self._io.read_s2be() + self.m_0 = self._io.read_s4be() + self.c_uc = self._io.read_s2be() + self.e = self._io.read_s4be() + self.c_us = self._io.read_s2be() + self.sqrt_a = self._io.read_u4be() + self.t_oe = self._io.read_u2be() + self.fit_interval_flag = self._io.read_bits_int_be(1) != 0 + self.aoda = self._io.read_bits_int_be(5) + self.reserved = self._io.read_bits_int_be(2) + + + diff --git a/system/ubloxd/generated/ubx.cpp b/system/ubloxd/generated/ubx.cpp deleted file mode 100644 index 81b82ccafc..0000000000 --- a/system/ubloxd/generated/ubx.cpp +++ /dev/null @@ -1,424 +0,0 @@ -// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -#include "ubx.h" -#include "kaitai/exceptions.h" - -ubx_t::ubx_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = this; - f_checksum = false; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::_read() { - m_magic = m__io->read_bytes(2); - if (!(magic() == std::string("\xB5\x62", 2))) { - throw kaitai::validation_not_equal_error(std::string("\xB5\x62", 2), magic(), _io(), std::string("/seq/0")); - } - m_msg_type = m__io->read_u2be(); - m_length = m__io->read_u2le(); - n_body = true; - switch (msg_type()) { - case 2569: { - n_body = false; - m_body = new mon_hw_t(m__io, this, m__root); - break; - } - case 533: { - n_body = false; - m_body = new rxm_rawx_t(m__io, this, m__root); - break; - } - case 531: { - n_body = false; - m_body = new rxm_sfrbx_t(m__io, this, m__root); - break; - } - case 309: { - n_body = false; - m_body = new nav_sat_t(m__io, this, m__root); - break; - } - case 2571: { - n_body = false; - m_body = new mon_hw2_t(m__io, this, m__root); - break; - } - case 263: { - n_body = false; - m_body = new nav_pvt_t(m__io, this, m__root); - break; - } - } -} - -ubx_t::~ubx_t() { - _clean_up(); -} - -void ubx_t::_clean_up() { - if (!n_body) { - if (m_body) { - delete m_body; m_body = 0; - } - } - if (f_checksum) { - } -} - -ubx_t::rxm_rawx_t::rxm_rawx_t(kaitai::kstream* p__io, ubx_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - m_meas = 0; - m__raw_meas = 0; - m__io__raw_meas = 0; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::rxm_rawx_t::_read() { - m_rcv_tow = m__io->read_f8le(); - m_week = m__io->read_u2le(); - m_leap_s = m__io->read_s1(); - m_num_meas = m__io->read_u1(); - m_rec_stat = m__io->read_u1(); - m_reserved1 = m__io->read_bytes(3); - m__raw_meas = new std::vector(); - m__io__raw_meas = new std::vector(); - m_meas = new std::vector(); - const int l_meas = num_meas(); - for (int i = 0; i < l_meas; i++) { - m__raw_meas->push_back(m__io->read_bytes(32)); - kaitai::kstream* io__raw_meas = new kaitai::kstream(m__raw_meas->at(m__raw_meas->size() - 1)); - m__io__raw_meas->push_back(io__raw_meas); - m_meas->push_back(new measurement_t(io__raw_meas, this, m__root)); - } -} - -ubx_t::rxm_rawx_t::~rxm_rawx_t() { - _clean_up(); -} - -void ubx_t::rxm_rawx_t::_clean_up() { - if (m__raw_meas) { - delete m__raw_meas; m__raw_meas = 0; - } - if (m__io__raw_meas) { - for (std::vector::iterator it = m__io__raw_meas->begin(); it != m__io__raw_meas->end(); ++it) { - delete *it; - } - delete m__io__raw_meas; m__io__raw_meas = 0; - } - if (m_meas) { - for (std::vector::iterator it = m_meas->begin(); it != m_meas->end(); ++it) { - delete *it; - } - delete m_meas; m_meas = 0; - } -} - -ubx_t::rxm_rawx_t::measurement_t::measurement_t(kaitai::kstream* p__io, ubx_t::rxm_rawx_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::rxm_rawx_t::measurement_t::_read() { - m_pr_mes = m__io->read_f8le(); - m_cp_mes = m__io->read_f8le(); - m_do_mes = m__io->read_f4le(); - m_gnss_id = static_cast(m__io->read_u1()); - m_sv_id = m__io->read_u1(); - m_reserved2 = m__io->read_bytes(1); - m_freq_id = m__io->read_u1(); - m_lock_time = m__io->read_u2le(); - m_cno = m__io->read_u1(); - m_pr_stdev = m__io->read_u1(); - m_cp_stdev = m__io->read_u1(); - m_do_stdev = m__io->read_u1(); - m_trk_stat = m__io->read_u1(); - m_reserved3 = m__io->read_bytes(1); -} - -ubx_t::rxm_rawx_t::measurement_t::~measurement_t() { - _clean_up(); -} - -void ubx_t::rxm_rawx_t::measurement_t::_clean_up() { -} - -ubx_t::rxm_sfrbx_t::rxm_sfrbx_t(kaitai::kstream* p__io, ubx_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - m_body = 0; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::rxm_sfrbx_t::_read() { - m_gnss_id = static_cast(m__io->read_u1()); - m_sv_id = m__io->read_u1(); - m_reserved1 = m__io->read_bytes(1); - m_freq_id = m__io->read_u1(); - m_num_words = m__io->read_u1(); - m_reserved2 = m__io->read_bytes(1); - m_version = m__io->read_u1(); - m_reserved3 = m__io->read_bytes(1); - m_body = new std::vector(); - const int l_body = num_words(); - for (int i = 0; i < l_body; i++) { - m_body->push_back(m__io->read_u4le()); - } -} - -ubx_t::rxm_sfrbx_t::~rxm_sfrbx_t() { - _clean_up(); -} - -void ubx_t::rxm_sfrbx_t::_clean_up() { - if (m_body) { - delete m_body; m_body = 0; - } -} - -ubx_t::nav_sat_t::nav_sat_t(kaitai::kstream* p__io, ubx_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - m_svs = 0; - m__raw_svs = 0; - m__io__raw_svs = 0; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::nav_sat_t::_read() { - m_itow = m__io->read_u4le(); - m_version = m__io->read_u1(); - m_num_svs = m__io->read_u1(); - m_reserved = m__io->read_bytes(2); - m__raw_svs = new std::vector(); - m__io__raw_svs = new std::vector(); - m_svs = new std::vector(); - const int l_svs = num_svs(); - for (int i = 0; i < l_svs; i++) { - m__raw_svs->push_back(m__io->read_bytes(12)); - kaitai::kstream* io__raw_svs = new kaitai::kstream(m__raw_svs->at(m__raw_svs->size() - 1)); - m__io__raw_svs->push_back(io__raw_svs); - m_svs->push_back(new nav_t(io__raw_svs, this, m__root)); - } -} - -ubx_t::nav_sat_t::~nav_sat_t() { - _clean_up(); -} - -void ubx_t::nav_sat_t::_clean_up() { - if (m__raw_svs) { - delete m__raw_svs; m__raw_svs = 0; - } - if (m__io__raw_svs) { - for (std::vector::iterator it = m__io__raw_svs->begin(); it != m__io__raw_svs->end(); ++it) { - delete *it; - } - delete m__io__raw_svs; m__io__raw_svs = 0; - } - if (m_svs) { - for (std::vector::iterator it = m_svs->begin(); it != m_svs->end(); ++it) { - delete *it; - } - delete m_svs; m_svs = 0; - } -} - -ubx_t::nav_sat_t::nav_t::nav_t(kaitai::kstream* p__io, ubx_t::nav_sat_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::nav_sat_t::nav_t::_read() { - m_gnss_id = static_cast(m__io->read_u1()); - m_sv_id = m__io->read_u1(); - m_cno = m__io->read_u1(); - m_elev = m__io->read_s1(); - m_azim = m__io->read_s2le(); - m_pr_res = m__io->read_s2le(); - m_flags = m__io->read_u4le(); -} - -ubx_t::nav_sat_t::nav_t::~nav_t() { - _clean_up(); -} - -void ubx_t::nav_sat_t::nav_t::_clean_up() { -} - -ubx_t::nav_pvt_t::nav_pvt_t(kaitai::kstream* p__io, ubx_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::nav_pvt_t::_read() { - m_i_tow = m__io->read_u4le(); - m_year = m__io->read_u2le(); - m_month = m__io->read_u1(); - m_day = m__io->read_u1(); - m_hour = m__io->read_u1(); - m_min = m__io->read_u1(); - m_sec = m__io->read_u1(); - m_valid = m__io->read_u1(); - m_t_acc = m__io->read_u4le(); - m_nano = m__io->read_s4le(); - m_fix_type = m__io->read_u1(); - m_flags = m__io->read_u1(); - m_flags2 = m__io->read_u1(); - m_num_sv = m__io->read_u1(); - m_lon = m__io->read_s4le(); - m_lat = m__io->read_s4le(); - m_height = m__io->read_s4le(); - m_h_msl = m__io->read_s4le(); - m_h_acc = m__io->read_u4le(); - m_v_acc = m__io->read_u4le(); - m_vel_n = m__io->read_s4le(); - m_vel_e = m__io->read_s4le(); - m_vel_d = m__io->read_s4le(); - m_g_speed = m__io->read_s4le(); - m_head_mot = m__io->read_s4le(); - m_s_acc = m__io->read_s4le(); - m_head_acc = m__io->read_u4le(); - m_p_dop = m__io->read_u2le(); - m_flags3 = m__io->read_u1(); - m_reserved1 = m__io->read_bytes(5); - m_head_veh = m__io->read_s4le(); - m_mag_dec = m__io->read_s2le(); - m_mag_acc = m__io->read_u2le(); -} - -ubx_t::nav_pvt_t::~nav_pvt_t() { - _clean_up(); -} - -void ubx_t::nav_pvt_t::_clean_up() { -} - -ubx_t::mon_hw2_t::mon_hw2_t(kaitai::kstream* p__io, ubx_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::mon_hw2_t::_read() { - m_ofs_i = m__io->read_s1(); - m_mag_i = m__io->read_u1(); - m_ofs_q = m__io->read_s1(); - m_mag_q = m__io->read_u1(); - m_cfg_source = static_cast(m__io->read_u1()); - m_reserved1 = m__io->read_bytes(3); - m_low_lev_cfg = m__io->read_u4le(); - m_reserved2 = m__io->read_bytes(8); - m_post_status = m__io->read_u4le(); - m_reserved3 = m__io->read_bytes(4); -} - -ubx_t::mon_hw2_t::~mon_hw2_t() { - _clean_up(); -} - -void ubx_t::mon_hw2_t::_clean_up() { -} - -ubx_t::mon_hw_t::mon_hw_t(kaitai::kstream* p__io, ubx_t* p__parent, ubx_t* p__root) : kaitai::kstruct(p__io) { - m__parent = p__parent; - m__root = p__root; - - try { - _read(); - } catch(...) { - _clean_up(); - throw; - } -} - -void ubx_t::mon_hw_t::_read() { - m_pin_sel = m__io->read_u4le(); - m_pin_bank = m__io->read_u4le(); - m_pin_dir = m__io->read_u4le(); - m_pin_val = m__io->read_u4le(); - m_noise_per_ms = m__io->read_u2le(); - m_agc_cnt = m__io->read_u2le(); - m_a_status = static_cast(m__io->read_u1()); - m_a_power = static_cast(m__io->read_u1()); - m_flags = m__io->read_u1(); - m_reserved1 = m__io->read_bytes(1); - m_used_mask = m__io->read_u4le(); - m_vp = m__io->read_bytes(17); - m_jam_ind = m__io->read_u1(); - m_reserved2 = m__io->read_bytes(2); - m_pin_irq = m__io->read_u4le(); - m_pull_h = m__io->read_u4le(); - m_pull_l = m__io->read_u4le(); -} - -ubx_t::mon_hw_t::~mon_hw_t() { - _clean_up(); -} - -void ubx_t::mon_hw_t::_clean_up() { -} - -uint16_t ubx_t::checksum() { - if (f_checksum) - return m_checksum; - std::streampos _pos = m__io->pos(); - m__io->seek((length() + 6)); - m_checksum = m__io->read_u2le(); - m__io->seek(_pos); - f_checksum = true; - return m_checksum; -} diff --git a/system/ubloxd/generated/ubx.h b/system/ubloxd/generated/ubx.h deleted file mode 100644 index 022108489f..0000000000 --- a/system/ubloxd/generated/ubx.h +++ /dev/null @@ -1,484 +0,0 @@ -#ifndef UBX_H_ -#define UBX_H_ - -// This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild - -#include "kaitai/kaitaistruct.h" -#include -#include - -#if KAITAI_STRUCT_VERSION < 9000L -#error "Incompatible Kaitai Struct C++/STL API: version 0.9 or later is required" -#endif - -class ubx_t : public kaitai::kstruct { - -public: - class rxm_rawx_t; - class rxm_sfrbx_t; - class nav_sat_t; - class nav_pvt_t; - class mon_hw2_t; - class mon_hw_t; - - enum gnss_type_t { - GNSS_TYPE_GPS = 0, - GNSS_TYPE_SBAS = 1, - GNSS_TYPE_GALILEO = 2, - GNSS_TYPE_BEIDOU = 3, - GNSS_TYPE_IMES = 4, - GNSS_TYPE_QZSS = 5, - GNSS_TYPE_GLONASS = 6 - }; - - ubx_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent = 0, ubx_t* p__root = 0); - -private: - void _read(); - void _clean_up(); - -public: - ~ubx_t(); - - class rxm_rawx_t : public kaitai::kstruct { - - public: - class measurement_t; - - rxm_rawx_t(kaitai::kstream* p__io, ubx_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~rxm_rawx_t(); - - class measurement_t : public kaitai::kstruct { - - public: - - measurement_t(kaitai::kstream* p__io, ubx_t::rxm_rawx_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~measurement_t(); - - private: - double m_pr_mes; - double m_cp_mes; - float m_do_mes; - gnss_type_t m_gnss_id; - uint8_t m_sv_id; - std::string m_reserved2; - uint8_t m_freq_id; - uint16_t m_lock_time; - uint8_t m_cno; - uint8_t m_pr_stdev; - uint8_t m_cp_stdev; - uint8_t m_do_stdev; - uint8_t m_trk_stat; - std::string m_reserved3; - ubx_t* m__root; - ubx_t::rxm_rawx_t* m__parent; - - public: - double pr_mes() const { return m_pr_mes; } - double cp_mes() const { return m_cp_mes; } - float do_mes() const { return m_do_mes; } - gnss_type_t gnss_id() const { return m_gnss_id; } - uint8_t sv_id() const { return m_sv_id; } - std::string reserved2() const { return m_reserved2; } - uint8_t freq_id() const { return m_freq_id; } - uint16_t lock_time() const { return m_lock_time; } - uint8_t cno() const { return m_cno; } - uint8_t pr_stdev() const { return m_pr_stdev; } - uint8_t cp_stdev() const { return m_cp_stdev; } - uint8_t do_stdev() const { return m_do_stdev; } - uint8_t trk_stat() const { return m_trk_stat; } - std::string reserved3() const { return m_reserved3; } - ubx_t* _root() const { return m__root; } - ubx_t::rxm_rawx_t* _parent() const { return m__parent; } - }; - - private: - double m_rcv_tow; - uint16_t m_week; - int8_t m_leap_s; - uint8_t m_num_meas; - uint8_t m_rec_stat; - std::string m_reserved1; - std::vector* m_meas; - ubx_t* m__root; - ubx_t* m__parent; - std::vector* m__raw_meas; - std::vector* m__io__raw_meas; - - public: - double rcv_tow() const { return m_rcv_tow; } - uint16_t week() const { return m_week; } - int8_t leap_s() const { return m_leap_s; } - uint8_t num_meas() const { return m_num_meas; } - uint8_t rec_stat() const { return m_rec_stat; } - std::string reserved1() const { return m_reserved1; } - std::vector* meas() const { return m_meas; } - ubx_t* _root() const { return m__root; } - ubx_t* _parent() const { return m__parent; } - std::vector* _raw_meas() const { return m__raw_meas; } - std::vector* _io__raw_meas() const { return m__io__raw_meas; } - }; - - class rxm_sfrbx_t : public kaitai::kstruct { - - public: - - rxm_sfrbx_t(kaitai::kstream* p__io, ubx_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~rxm_sfrbx_t(); - - private: - gnss_type_t m_gnss_id; - uint8_t m_sv_id; - std::string m_reserved1; - uint8_t m_freq_id; - uint8_t m_num_words; - std::string m_reserved2; - uint8_t m_version; - std::string m_reserved3; - std::vector* m_body; - ubx_t* m__root; - ubx_t* m__parent; - - public: - gnss_type_t gnss_id() const { return m_gnss_id; } - uint8_t sv_id() const { return m_sv_id; } - std::string reserved1() const { return m_reserved1; } - uint8_t freq_id() const { return m_freq_id; } - uint8_t num_words() const { return m_num_words; } - std::string reserved2() const { return m_reserved2; } - uint8_t version() const { return m_version; } - std::string reserved3() const { return m_reserved3; } - std::vector* body() const { return m_body; } - ubx_t* _root() const { return m__root; } - ubx_t* _parent() const { return m__parent; } - }; - - class nav_sat_t : public kaitai::kstruct { - - public: - class nav_t; - - nav_sat_t(kaitai::kstream* p__io, ubx_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~nav_sat_t(); - - class nav_t : public kaitai::kstruct { - - public: - - nav_t(kaitai::kstream* p__io, ubx_t::nav_sat_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~nav_t(); - - private: - gnss_type_t m_gnss_id; - uint8_t m_sv_id; - uint8_t m_cno; - int8_t m_elev; - int16_t m_azim; - int16_t m_pr_res; - uint32_t m_flags; - ubx_t* m__root; - ubx_t::nav_sat_t* m__parent; - - public: - gnss_type_t gnss_id() const { return m_gnss_id; } - uint8_t sv_id() const { return m_sv_id; } - uint8_t cno() const { return m_cno; } - int8_t elev() const { return m_elev; } - int16_t azim() const { return m_azim; } - int16_t pr_res() const { return m_pr_res; } - uint32_t flags() const { return m_flags; } - ubx_t* _root() const { return m__root; } - ubx_t::nav_sat_t* _parent() const { return m__parent; } - }; - - private: - uint32_t m_itow; - uint8_t m_version; - uint8_t m_num_svs; - std::string m_reserved; - std::vector* m_svs; - ubx_t* m__root; - ubx_t* m__parent; - std::vector* m__raw_svs; - std::vector* m__io__raw_svs; - - public: - uint32_t itow() const { return m_itow; } - uint8_t version() const { return m_version; } - uint8_t num_svs() const { return m_num_svs; } - std::string reserved() const { return m_reserved; } - std::vector* svs() const { return m_svs; } - ubx_t* _root() const { return m__root; } - ubx_t* _parent() const { return m__parent; } - std::vector* _raw_svs() const { return m__raw_svs; } - std::vector* _io__raw_svs() const { return m__io__raw_svs; } - }; - - class nav_pvt_t : public kaitai::kstruct { - - public: - - nav_pvt_t(kaitai::kstream* p__io, ubx_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~nav_pvt_t(); - - private: - uint32_t m_i_tow; - uint16_t m_year; - uint8_t m_month; - uint8_t m_day; - uint8_t m_hour; - uint8_t m_min; - uint8_t m_sec; - uint8_t m_valid; - uint32_t m_t_acc; - int32_t m_nano; - uint8_t m_fix_type; - uint8_t m_flags; - uint8_t m_flags2; - uint8_t m_num_sv; - int32_t m_lon; - int32_t m_lat; - int32_t m_height; - int32_t m_h_msl; - uint32_t m_h_acc; - uint32_t m_v_acc; - int32_t m_vel_n; - int32_t m_vel_e; - int32_t m_vel_d; - int32_t m_g_speed; - int32_t m_head_mot; - int32_t m_s_acc; - uint32_t m_head_acc; - uint16_t m_p_dop; - uint8_t m_flags3; - std::string m_reserved1; - int32_t m_head_veh; - int16_t m_mag_dec; - uint16_t m_mag_acc; - ubx_t* m__root; - ubx_t* m__parent; - - public: - uint32_t i_tow() const { return m_i_tow; } - uint16_t year() const { return m_year; } - uint8_t month() const { return m_month; } - uint8_t day() const { return m_day; } - uint8_t hour() const { return m_hour; } - uint8_t min() const { return m_min; } - uint8_t sec() const { return m_sec; } - uint8_t valid() const { return m_valid; } - uint32_t t_acc() const { return m_t_acc; } - int32_t nano() const { return m_nano; } - uint8_t fix_type() const { return m_fix_type; } - uint8_t flags() const { return m_flags; } - uint8_t flags2() const { return m_flags2; } - uint8_t num_sv() const { return m_num_sv; } - int32_t lon() const { return m_lon; } - int32_t lat() const { return m_lat; } - int32_t height() const { return m_height; } - int32_t h_msl() const { return m_h_msl; } - uint32_t h_acc() const { return m_h_acc; } - uint32_t v_acc() const { return m_v_acc; } - int32_t vel_n() const { return m_vel_n; } - int32_t vel_e() const { return m_vel_e; } - int32_t vel_d() const { return m_vel_d; } - int32_t g_speed() const { return m_g_speed; } - int32_t head_mot() const { return m_head_mot; } - int32_t s_acc() const { return m_s_acc; } - uint32_t head_acc() const { return m_head_acc; } - uint16_t p_dop() const { return m_p_dop; } - uint8_t flags3() const { return m_flags3; } - std::string reserved1() const { return m_reserved1; } - int32_t head_veh() const { return m_head_veh; } - int16_t mag_dec() const { return m_mag_dec; } - uint16_t mag_acc() const { return m_mag_acc; } - ubx_t* _root() const { return m__root; } - ubx_t* _parent() const { return m__parent; } - }; - - class mon_hw2_t : public kaitai::kstruct { - - public: - - enum config_source_t { - CONFIG_SOURCE_FLASH = 102, - CONFIG_SOURCE_OTP = 111, - CONFIG_SOURCE_CONFIG_PINS = 112, - CONFIG_SOURCE_ROM = 113 - }; - - mon_hw2_t(kaitai::kstream* p__io, ubx_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~mon_hw2_t(); - - private: - int8_t m_ofs_i; - uint8_t m_mag_i; - int8_t m_ofs_q; - uint8_t m_mag_q; - config_source_t m_cfg_source; - std::string m_reserved1; - uint32_t m_low_lev_cfg; - std::string m_reserved2; - uint32_t m_post_status; - std::string m_reserved3; - ubx_t* m__root; - ubx_t* m__parent; - - public: - int8_t ofs_i() const { return m_ofs_i; } - uint8_t mag_i() const { return m_mag_i; } - int8_t ofs_q() const { return m_ofs_q; } - uint8_t mag_q() const { return m_mag_q; } - config_source_t cfg_source() const { return m_cfg_source; } - std::string reserved1() const { return m_reserved1; } - uint32_t low_lev_cfg() const { return m_low_lev_cfg; } - std::string reserved2() const { return m_reserved2; } - uint32_t post_status() const { return m_post_status; } - std::string reserved3() const { return m_reserved3; } - ubx_t* _root() const { return m__root; } - ubx_t* _parent() const { return m__parent; } - }; - - class mon_hw_t : public kaitai::kstruct { - - public: - - enum antenna_status_t { - ANTENNA_STATUS_INIT = 0, - ANTENNA_STATUS_DONTKNOW = 1, - ANTENNA_STATUS_OK = 2, - ANTENNA_STATUS_SHORT = 3, - ANTENNA_STATUS_OPEN = 4 - }; - - enum antenna_power_t { - ANTENNA_POWER_FALSE = 0, - ANTENNA_POWER_TRUE = 1, - ANTENNA_POWER_DONTKNOW = 2 - }; - - mon_hw_t(kaitai::kstream* p__io, ubx_t* p__parent = 0, ubx_t* p__root = 0); - - private: - void _read(); - void _clean_up(); - - public: - ~mon_hw_t(); - - private: - uint32_t m_pin_sel; - uint32_t m_pin_bank; - uint32_t m_pin_dir; - uint32_t m_pin_val; - uint16_t m_noise_per_ms; - uint16_t m_agc_cnt; - antenna_status_t m_a_status; - antenna_power_t m_a_power; - uint8_t m_flags; - std::string m_reserved1; - uint32_t m_used_mask; - std::string m_vp; - uint8_t m_jam_ind; - std::string m_reserved2; - uint32_t m_pin_irq; - uint32_t m_pull_h; - uint32_t m_pull_l; - ubx_t* m__root; - ubx_t* m__parent; - - public: - uint32_t pin_sel() const { return m_pin_sel; } - uint32_t pin_bank() const { return m_pin_bank; } - uint32_t pin_dir() const { return m_pin_dir; } - uint32_t pin_val() const { return m_pin_val; } - uint16_t noise_per_ms() const { return m_noise_per_ms; } - uint16_t agc_cnt() const { return m_agc_cnt; } - antenna_status_t a_status() const { return m_a_status; } - antenna_power_t a_power() const { return m_a_power; } - uint8_t flags() const { return m_flags; } - std::string reserved1() const { return m_reserved1; } - uint32_t used_mask() const { return m_used_mask; } - std::string vp() const { return m_vp; } - uint8_t jam_ind() const { return m_jam_ind; } - std::string reserved2() const { return m_reserved2; } - uint32_t pin_irq() const { return m_pin_irq; } - uint32_t pull_h() const { return m_pull_h; } - uint32_t pull_l() const { return m_pull_l; } - ubx_t* _root() const { return m__root; } - ubx_t* _parent() const { return m__parent; } - }; - -private: - bool f_checksum; - uint16_t m_checksum; - -public: - uint16_t checksum(); - -private: - std::string m_magic; - uint16_t m_msg_type; - uint16_t m_length; - kaitai::kstruct* m_body; - bool n_body; - -public: - bool _is_null_body() { body(); return n_body; }; - -private: - ubx_t* m__root; - kaitai::kstruct* m__parent; - -public: - std::string magic() const { return m_magic; } - uint16_t msg_type() const { return m_msg_type; } - uint16_t length() const { return m_length; } - kaitai::kstruct* body() const { return m_body; } - ubx_t* _root() const { return m__root; } - kaitai::kstruct* _parent() const { return m__parent; } -}; - -#endif // UBX_H_ diff --git a/system/ubloxd/generated/ubx.py b/system/ubloxd/generated/ubx.py new file mode 100644 index 0000000000..9946584388 --- /dev/null +++ b/system/ubloxd/generated/ubx.py @@ -0,0 +1,273 @@ +# This is a generated file! Please edit source .ksy file and use kaitai-struct-compiler to rebuild + +import kaitaistruct +from kaitaistruct import KaitaiStruct, KaitaiStream, BytesIO +from enum import Enum + + +if getattr(kaitaistruct, 'API_VERSION', (0, 9)) < (0, 9): + raise Exception("Incompatible Kaitai Struct Python API: 0.9 or later is required, but you have %s" % (kaitaistruct.__version__)) + +class Ubx(KaitaiStruct): + + class GnssType(Enum): + gps = 0 + sbas = 1 + galileo = 2 + beidou = 3 + imes = 4 + qzss = 5 + glonass = 6 + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.magic = self._io.read_bytes(2) + if not self.magic == b"\xB5\x62": + raise kaitaistruct.ValidationNotEqualError(b"\xB5\x62", self.magic, self._io, u"/seq/0") + self.msg_type = self._io.read_u2be() + self.length = self._io.read_u2le() + _on = self.msg_type + if _on == 2569: + self.body = Ubx.MonHw(self._io, self, self._root) + elif _on == 533: + self.body = Ubx.RxmRawx(self._io, self, self._root) + elif _on == 531: + self.body = Ubx.RxmSfrbx(self._io, self, self._root) + elif _on == 309: + self.body = Ubx.NavSat(self._io, self, self._root) + elif _on == 2571: + self.body = Ubx.MonHw2(self._io, self, self._root) + elif _on == 263: + self.body = Ubx.NavPvt(self._io, self, self._root) + + class RxmRawx(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.rcv_tow = self._io.read_f8le() + self.week = self._io.read_u2le() + self.leap_s = self._io.read_s1() + self.num_meas = self._io.read_u1() + self.rec_stat = self._io.read_u1() + self.reserved1 = self._io.read_bytes(3) + self._raw_meas = [] + self.meas = [] + for i in range(self.num_meas): + self._raw_meas.append(self._io.read_bytes(32)) + _io__raw_meas = KaitaiStream(BytesIO(self._raw_meas[i])) + self.meas.append(Ubx.RxmRawx.Measurement(_io__raw_meas, self, self._root)) + + + class Measurement(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.pr_mes = self._io.read_f8le() + self.cp_mes = self._io.read_f8le() + self.do_mes = self._io.read_f4le() + self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) + self.sv_id = self._io.read_u1() + self.reserved2 = self._io.read_bytes(1) + self.freq_id = self._io.read_u1() + self.lock_time = self._io.read_u2le() + self.cno = self._io.read_u1() + self.pr_stdev = self._io.read_u1() + self.cp_stdev = self._io.read_u1() + self.do_stdev = self._io.read_u1() + self.trk_stat = self._io.read_u1() + self.reserved3 = self._io.read_bytes(1) + + + + class RxmSfrbx(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) + self.sv_id = self._io.read_u1() + self.reserved1 = self._io.read_bytes(1) + self.freq_id = self._io.read_u1() + self.num_words = self._io.read_u1() + self.reserved2 = self._io.read_bytes(1) + self.version = self._io.read_u1() + self.reserved3 = self._io.read_bytes(1) + self.body = [] + for i in range(self.num_words): + self.body.append(self._io.read_u4le()) + + + + class NavSat(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.itow = self._io.read_u4le() + self.version = self._io.read_u1() + self.num_svs = self._io.read_u1() + self.reserved = self._io.read_bytes(2) + self._raw_svs = [] + self.svs = [] + for i in range(self.num_svs): + self._raw_svs.append(self._io.read_bytes(12)) + _io__raw_svs = KaitaiStream(BytesIO(self._raw_svs[i])) + self.svs.append(Ubx.NavSat.Nav(_io__raw_svs, self, self._root)) + + + class Nav(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.gnss_id = KaitaiStream.resolve_enum(Ubx.GnssType, self._io.read_u1()) + self.sv_id = self._io.read_u1() + self.cno = self._io.read_u1() + self.elev = self._io.read_s1() + self.azim = self._io.read_s2le() + self.pr_res = self._io.read_s2le() + self.flags = self._io.read_u4le() + + + + class NavPvt(KaitaiStruct): + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.i_tow = self._io.read_u4le() + self.year = self._io.read_u2le() + self.month = self._io.read_u1() + self.day = self._io.read_u1() + self.hour = self._io.read_u1() + self.min = self._io.read_u1() + self.sec = self._io.read_u1() + self.valid = self._io.read_u1() + self.t_acc = self._io.read_u4le() + self.nano = self._io.read_s4le() + self.fix_type = self._io.read_u1() + self.flags = self._io.read_u1() + self.flags2 = self._io.read_u1() + self.num_sv = self._io.read_u1() + self.lon = self._io.read_s4le() + self.lat = self._io.read_s4le() + self.height = self._io.read_s4le() + self.h_msl = self._io.read_s4le() + self.h_acc = self._io.read_u4le() + self.v_acc = self._io.read_u4le() + self.vel_n = self._io.read_s4le() + self.vel_e = self._io.read_s4le() + self.vel_d = self._io.read_s4le() + self.g_speed = self._io.read_s4le() + self.head_mot = self._io.read_s4le() + self.s_acc = self._io.read_s4le() + self.head_acc = self._io.read_u4le() + self.p_dop = self._io.read_u2le() + self.flags3 = self._io.read_u1() + self.reserved1 = self._io.read_bytes(5) + self.head_veh = self._io.read_s4le() + self.mag_dec = self._io.read_s2le() + self.mag_acc = self._io.read_u2le() + + + class MonHw2(KaitaiStruct): + + class ConfigSource(Enum): + flash = 102 + otp = 111 + config_pins = 112 + rom = 113 + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.ofs_i = self._io.read_s1() + self.mag_i = self._io.read_u1() + self.ofs_q = self._io.read_s1() + self.mag_q = self._io.read_u1() + self.cfg_source = KaitaiStream.resolve_enum(Ubx.MonHw2.ConfigSource, self._io.read_u1()) + self.reserved1 = self._io.read_bytes(3) + self.low_lev_cfg = self._io.read_u4le() + self.reserved2 = self._io.read_bytes(8) + self.post_status = self._io.read_u4le() + self.reserved3 = self._io.read_bytes(4) + + + class MonHw(KaitaiStruct): + + class AntennaStatus(Enum): + init = 0 + dontknow = 1 + ok = 2 + short = 3 + open = 4 + + class AntennaPower(Enum): + false = 0 + true = 1 + dontknow = 2 + def __init__(self, _io, _parent=None, _root=None): + self._io = _io + self._parent = _parent + self._root = _root if _root else self + self._read() + + def _read(self): + self.pin_sel = self._io.read_u4le() + self.pin_bank = self._io.read_u4le() + self.pin_dir = self._io.read_u4le() + self.pin_val = self._io.read_u4le() + self.noise_per_ms = self._io.read_u2le() + self.agc_cnt = self._io.read_u2le() + self.a_status = KaitaiStream.resolve_enum(Ubx.MonHw.AntennaStatus, self._io.read_u1()) + self.a_power = KaitaiStream.resolve_enum(Ubx.MonHw.AntennaPower, self._io.read_u1()) + self.flags = self._io.read_u1() + self.reserved1 = self._io.read_bytes(1) + self.used_mask = self._io.read_u4le() + self.vp = self._io.read_bytes(17) + self.jam_ind = self._io.read_u1() + self.reserved2 = self._io.read_bytes(2) + self.pin_irq = self._io.read_u4le() + self.pull_h = self._io.read_u4le() + self.pull_l = self._io.read_u4le() + + + @property + def checksum(self): + if hasattr(self, '_m_checksum'): + return self._m_checksum + + _pos = self._io.pos() + self._io.seek((self.length + 6)) + self._m_checksum = self._io.read_u2le() + self._io.seek(_pos) + return getattr(self, '_m_checksum', None) + + diff --git a/system/ubloxd/tests/print_gps_stats.py b/system/ubloxd/tests/print_gps_stats.py deleted file mode 100755 index 8d190f9ec1..0000000000 --- a/system/ubloxd/tests/print_gps_stats.py +++ /dev/null @@ -1,20 +0,0 @@ -#!/usr/bin/env python3 -import time -import cereal.messaging as messaging - -if __name__ == "__main__": - sm = messaging.SubMaster(['ubloxGnss', 'gpsLocationExternal']) - - while 1: - ug = sm['ubloxGnss'] - gle = sm['gpsLocationExternal'] - - try: - cnos = [] - for m in ug.measurementReport.measurements: - cnos.append(m.cno) - print(f"Sats: {ug.measurementReport.numMeas} Accuracy: {gle.horizontalAccuracy:.2f} m cnos", sorted(cnos)) - except Exception: - pass - sm.update() - time.sleep(0.1) diff --git a/system/ubloxd/tests/test_glonass_kaitai.cc b/system/ubloxd/tests/test_glonass_kaitai.cc deleted file mode 100644 index 96f43742b4..0000000000 --- a/system/ubloxd/tests/test_glonass_kaitai.cc +++ /dev/null @@ -1,360 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "catch2/catch.hpp" -#include "system/ubloxd/generated/glonass.h" - -typedef std::vector> string_data; - -#define IDLE_CHIP_IDX 0 -#define STRING_NUMBER_IDX 1 -// string data 1-5 -#define HC_IDX 0 -#define PAD1_IDX 1 -#define SUPERFRAME_IDX 2 -#define PAD2_IDX 3 -#define FRAME_IDX 4 - -// Indexes for string number 1 -#define ST1_NU_IDX 2 -#define ST1_P1_IDX 3 -#define ST1_T_K_IDX 4 -#define ST1_X_VEL_S_IDX 5 -#define ST1_X_VEL_V_IDX 6 -#define ST1_X_ACCEL_S_IDX 7 -#define ST1_X_ACCEL_V_IDX 8 -#define ST1_X_S_IDX 9 -#define ST1_X_V_IDX 10 -#define ST1_HC_OFF 11 - -// Indexes for string number 2 -#define ST2_BN_IDX 2 -#define ST2_P2_IDX 3 -#define ST2_TB_IDX 4 -#define ST2_NU_IDX 5 -#define ST2_Y_VEL_S_IDX 6 -#define ST2_Y_VEL_V_IDX 7 -#define ST2_Y_ACCEL_S_IDX 8 -#define ST2_Y_ACCEL_V_IDX 9 -#define ST2_Y_S_IDX 10 -#define ST2_Y_V_IDX 11 -#define ST2_HC_OFF 12 - -// Indexes for string number 3 -#define ST3_P3_IDX 2 -#define ST3_GAMMA_N_S_IDX 3 -#define ST3_GAMMA_N_V_IDX 4 -#define ST3_NU_1_IDX 5 -#define ST3_P_IDX 6 -#define ST3_L_N_IDX 7 -#define ST3_Z_VEL_S_IDX 8 -#define ST3_Z_VEL_V_IDX 9 -#define ST3_Z_ACCEL_S_IDX 10 -#define ST3_Z_ACCEL_V_IDX 11 -#define ST3_Z_S_IDX 12 -#define ST3_Z_V_IDX 13 -#define ST3_HC_OFF 14 - -// Indexes for string number 4 -#define ST4_TAU_N_S_IDX 2 -#define ST4_TAU_N_V_IDX 3 -#define ST4_DELTA_TAU_N_S_IDX 4 -#define ST4_DELTA_TAU_N_V_IDX 5 -#define ST4_E_N_IDX 6 -#define ST4_NU_1_IDX 7 -#define ST4_P4_IDX 8 -#define ST4_F_T_IDX 9 -#define ST4_NU_2_IDX 10 -#define ST4_N_T_IDX 11 -#define ST4_N_IDX 12 -#define ST4_M_IDX 13 -#define ST4_HC_OFF 14 - -// Indexes for string number 5 -#define ST5_N_A_IDX 2 -#define ST5_TAU_C_IDX 3 -#define ST5_NU_IDX 4 -#define ST5_N_4_IDX 5 -#define ST5_TAU_GPS_IDX 6 -#define ST5_L_N_IDX 7 -#define ST5_HC_OFF 8 - -// Indexes for non immediate -#define ST6_DATA_1_IDX 2 -#define ST6_DATA_2_IDX 3 -#define ST6_HC_OFF 4 - - -std::string generate_inp_data(string_data& data) { - std::string inp_data = ""; - for (auto& [b, v] : data) { - std::string tmp = std::bitset<64>(v).to_string(); - inp_data += tmp.substr(64-b, b); - } - assert(inp_data.size() == 128); - - std::string string_data; - string_data.reserve(16); - for (int i = 0; i < 128; i+=8) { - std::string substr = inp_data.substr(i, 8); - string_data.push_back((uint8_t)std::stoi(substr.c_str(), 0, 2)); - } - - return string_data; -} - -string_data generate_string_data(uint8_t string_number) { - - srand((unsigned)time(0)); - string_data data; // - data.push_back({1, 0}); // idle chip - data.push_back({4, string_number}); // string number - - if (string_number == 1) { - data.push_back({2, 3}); // not_used - data.push_back({2, 1}); // p1 - data.push_back({12, 113}); // t_k - data.push_back({1, rand() & 1}); // x_vel_sign - data.push_back({23, 7122}); // x_vel_value - data.push_back({1, rand() & 1}); // x_accel_sign - data.push_back({4, 3}); // x_accel_value - data.push_back({1, rand() & 1}); // x_sign - data.push_back({26, 33554431}); // x_value - } else if (string_number == 2) { - data.push_back({3, 3}); // b_n - data.push_back({1, 1}); // p2 - data.push_back({7, 123}); // t_b - data.push_back({5, 31}); // not_used - data.push_back({1, rand() & 1}); // y_vel_sign - data.push_back({23, 7422}); // y_vel_value - data.push_back({1, rand() & 1}); // y_accel_sign - data.push_back({4, 3}); // y_accel_value - data.push_back({1, rand() & 1}); // y_sign - data.push_back({26, 67108863}); // y_value - } else if (string_number == 3) { - data.push_back({1, 0}); // p3 - data.push_back({1, 1}); // gamma_n_sign - data.push_back({10, 123}); // gamma_n_value - data.push_back({1, 0}); // not_used - data.push_back({2, 2}); // p - data.push_back({1, 1}); // l_n - data.push_back({1, rand() & 1}); // z_vel_sign - data.push_back({23, 1337}); // z_vel_value - data.push_back({1, rand() & 1}); // z_accel_sign - data.push_back({4, 9}); // z_accel_value - data.push_back({1, rand() & 1}); // z_sign - data.push_back({26, 100023}); // z_value - } else if (string_number == 4) { - data.push_back({1, rand() & 1}); // tau_n_sign - data.push_back({21, 197152}); // tau_n_value - data.push_back({1, rand() & 1}); // delta_tau_n_sign - data.push_back({4, 4}); // delta_tau_n_value - data.push_back({5, 0}); // e_n - data.push_back({14, 2}); // not_used_1 - data.push_back({1, 1}); // p4 - data.push_back({4, 9}); // f_t - data.push_back({3, 3}); // not_used_2 - data.push_back({11, 2047}); // n_t - data.push_back({5, 2}); // n - data.push_back({2, 1}); // m - } else if (string_number == 5) { - data.push_back({11, 2047}); // n_a - data.push_back({32, 4294767295}); // tau_c - data.push_back({1, 0}); // not_used_1 - data.push_back({5, 2}); // n_4 - data.push_back({22, 4114304}); // tau_gps - data.push_back({1, 0}); // l_n - } else { // non-immediate data is not parsed - data.push_back({64, rand()}); // data_1 - data.push_back({8, 6}); // data_2 - } - - data.push_back({8, rand() & 0xFF}); // hamming code - data.push_back({11, rand() & 0x7FF}); // pad - data.push_back({16, rand() & 0xFFFF}); // superframe - data.push_back({8, rand() & 0xFF}); // pad - data.push_back({8, rand() & 0xFF}); // frame - return data; -} - -TEST_CASE("parse_string_number_1"){ - string_data data = generate_string_data(1); - std::string inp_data = generate_inp_data(data); - - kaitai::kstream stream(inp_data); - glonass_t gl_string(&stream); - - REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second); - REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second); - REQUIRE(gl_string.hamming_code() == data[ST1_HC_OFF + HC_IDX].second); - REQUIRE(gl_string.pad_1() == data[ST1_HC_OFF + PAD1_IDX].second); - REQUIRE(gl_string.superframe_number() == data[ST1_HC_OFF + SUPERFRAME_IDX].second); - REQUIRE(gl_string.pad_2() == data[ST1_HC_OFF + PAD2_IDX].second); - REQUIRE(gl_string.frame_number() == data[ST1_HC_OFF + FRAME_IDX].second); - - kaitai::kstream str1(inp_data); - glonass_t str1_data(&str1); - glonass_t::string_1_t* s1 = static_cast(str1_data.data()); - - REQUIRE(s1->not_used() == data[ST1_NU_IDX].second); - REQUIRE(s1->p1() == data[ST1_P1_IDX].second); - REQUIRE(s1->t_k() == data[ST1_T_K_IDX].second); - - int mul = s1->x_vel_sign() ? (-1) : 1; - REQUIRE(s1->x_vel() == (data[ST1_X_VEL_V_IDX].second * mul)); - mul = s1->x_accel_sign() ? (-1) : 1; - REQUIRE(s1->x_accel() == (data[ST1_X_ACCEL_V_IDX].second * mul)); - mul = s1->x_sign() ? (-1) : 1; - REQUIRE(s1->x() == (data[ST1_X_V_IDX].second * mul)); -} - -TEST_CASE("parse_string_number_2"){ - string_data data = generate_string_data(2); - std::string inp_data = generate_inp_data(data); - - kaitai::kstream stream(inp_data); - glonass_t gl_string(&stream); - - REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second); - REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second); - REQUIRE(gl_string.hamming_code() == data[ST2_HC_OFF + HC_IDX].second); - REQUIRE(gl_string.pad_1() == data[ST2_HC_OFF + PAD1_IDX].second); - REQUIRE(gl_string.superframe_number() == data[ST2_HC_OFF + SUPERFRAME_IDX].second); - REQUIRE(gl_string.pad_2() == data[ST2_HC_OFF + PAD2_IDX].second); - REQUIRE(gl_string.frame_number() == data[ST2_HC_OFF + FRAME_IDX].second); - - kaitai::kstream str2(inp_data); - glonass_t str2_data(&str2); - glonass_t::string_2_t* s2 = static_cast(str2_data.data()); - - REQUIRE(s2->b_n() == data[ST2_BN_IDX].second); - REQUIRE(s2->not_used() == data[ST2_NU_IDX].second); - REQUIRE(s2->p2() == data[ST2_P2_IDX].second); - REQUIRE(s2->t_b() == data[ST2_TB_IDX].second); - int mul = s2->y_vel_sign() ? (-1) : 1; - REQUIRE(s2->y_vel() == (data[ST2_Y_VEL_V_IDX].second * mul)); - mul = s2->y_accel_sign() ? (-1) : 1; - REQUIRE(s2->y_accel() == (data[ST2_Y_ACCEL_V_IDX].second * mul)); - mul = s2->y_sign() ? (-1) : 1; - REQUIRE(s2->y() == (data[ST2_Y_V_IDX].second * mul)); -} - -TEST_CASE("parse_string_number_3"){ - string_data data = generate_string_data(3); - std::string inp_data = generate_inp_data(data); - - kaitai::kstream stream(inp_data); - glonass_t gl_string(&stream); - - REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second); - REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second); - REQUIRE(gl_string.hamming_code() == data[ST3_HC_OFF + HC_IDX].second); - REQUIRE(gl_string.pad_1() == data[ST3_HC_OFF + PAD1_IDX].second); - REQUIRE(gl_string.superframe_number() == data[ST3_HC_OFF + SUPERFRAME_IDX].second); - REQUIRE(gl_string.pad_2() == data[ST3_HC_OFF + PAD2_IDX].second); - REQUIRE(gl_string.frame_number() == data[ST3_HC_OFF + FRAME_IDX].second); - - kaitai::kstream str3(inp_data); - glonass_t str3_data(&str3); - glonass_t::string_3_t* s3 = static_cast(str3_data.data()); - - REQUIRE(s3->p3() == data[ST3_P3_IDX].second); - int mul = s3->gamma_n_sign() ? (-1) : 1; - REQUIRE(s3->gamma_n() == (data[ST3_GAMMA_N_V_IDX].second * mul)); - REQUIRE(s3->not_used() == data[ST3_NU_1_IDX].second); - REQUIRE(s3->p() == data[ST3_P_IDX].second); - REQUIRE(s3->l_n() == data[ST3_L_N_IDX].second); - mul = s3->z_vel_sign() ? (-1) : 1; - REQUIRE(s3->z_vel() == (data[ST3_Z_VEL_V_IDX].second * mul)); - mul = s3->z_accel_sign() ? (-1) : 1; - REQUIRE(s3->z_accel() == (data[ST3_Z_ACCEL_V_IDX].second * mul)); - mul = s3->z_sign() ? (-1) : 1; - REQUIRE(s3->z() == (data[ST3_Z_V_IDX].second * mul)); -} - -TEST_CASE("parse_string_number_4"){ - string_data data = generate_string_data(4); - std::string inp_data = generate_inp_data(data); - - kaitai::kstream stream(inp_data); - glonass_t gl_string(&stream); - - REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second); - REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second); - REQUIRE(gl_string.hamming_code() == data[ST4_HC_OFF + HC_IDX].second); - REQUIRE(gl_string.pad_1() == data[ST4_HC_OFF + PAD1_IDX].second); - REQUIRE(gl_string.superframe_number() == data[ST4_HC_OFF + SUPERFRAME_IDX].second); - REQUIRE(gl_string.pad_2() == data[ST4_HC_OFF + PAD2_IDX].second); - REQUIRE(gl_string.frame_number() == data[ST4_HC_OFF + FRAME_IDX].second); - - kaitai::kstream str4(inp_data); - glonass_t str4_data(&str4); - glonass_t::string_4_t* s4 = static_cast(str4_data.data()); - - int mul = s4->tau_n_sign() ? (-1) : 1; - REQUIRE(s4->tau_n() == (data[ST4_TAU_N_V_IDX].second * mul)); - mul = s4->delta_tau_n_sign() ? (-1) : 1; - REQUIRE(s4->delta_tau_n() == (data[ST4_DELTA_TAU_N_V_IDX].second * mul)); - REQUIRE(s4->e_n() == data[ST4_E_N_IDX].second); - REQUIRE(s4->not_used_1() == data[ST4_NU_1_IDX].second); - REQUIRE(s4->p4() == data[ST4_P4_IDX].second); - REQUIRE(s4->f_t() == data[ST4_F_T_IDX].second); - REQUIRE(s4->not_used_2() == data[ST4_NU_2_IDX].second); - REQUIRE(s4->n_t() == data[ST4_N_T_IDX].second); - REQUIRE(s4->n() == data[ST4_N_IDX].second); - REQUIRE(s4->m() == data[ST4_M_IDX].second); -} - -TEST_CASE("parse_string_number_5"){ - string_data data = generate_string_data(5); - std::string inp_data = generate_inp_data(data); - - kaitai::kstream stream(inp_data); - glonass_t gl_string(&stream); - - REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second); - REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second); - REQUIRE(gl_string.hamming_code() == data[ST5_HC_OFF + HC_IDX].second); - REQUIRE(gl_string.pad_1() == data[ST5_HC_OFF + PAD1_IDX].second); - REQUIRE(gl_string.superframe_number() == data[ST5_HC_OFF + SUPERFRAME_IDX].second); - REQUIRE(gl_string.pad_2() == data[ST5_HC_OFF + PAD2_IDX].second); - REQUIRE(gl_string.frame_number() == data[ST5_HC_OFF + FRAME_IDX].second); - - kaitai::kstream str5(inp_data); - glonass_t str5_data(&str5); - glonass_t::string_5_t* s5 = static_cast(str5_data.data()); - - REQUIRE(s5->n_a() == data[ST5_N_A_IDX].second); - REQUIRE(s5->tau_c() == data[ST5_TAU_C_IDX].second); - REQUIRE(s5->not_used() == data[ST5_NU_IDX].second); - REQUIRE(s5->n_4() == data[ST5_N_4_IDX].second); - REQUIRE(s5->tau_gps() == data[ST5_TAU_GPS_IDX].second); - REQUIRE(s5->l_n() == data[ST5_L_N_IDX].second); -} - -TEST_CASE("parse_string_number_NI"){ - string_data data = generate_string_data((rand() % 10) + 6); - std::string inp_data = generate_inp_data(data); - - kaitai::kstream stream(inp_data); - glonass_t gl_string(&stream); - - REQUIRE(gl_string.idle_chip() == data[IDLE_CHIP_IDX].second); - REQUIRE(gl_string.string_number() == data[STRING_NUMBER_IDX].second); - REQUIRE(gl_string.hamming_code() == data[ST6_HC_OFF + HC_IDX].second); - REQUIRE(gl_string.pad_1() == data[ST6_HC_OFF + PAD1_IDX].second); - REQUIRE(gl_string.superframe_number() == data[ST6_HC_OFF + SUPERFRAME_IDX].second); - REQUIRE(gl_string.pad_2() == data[ST6_HC_OFF + PAD2_IDX].second); - REQUIRE(gl_string.frame_number() == data[ST6_HC_OFF + FRAME_IDX].second); - - kaitai::kstream strni(inp_data); - glonass_t strni_data(&strni); - glonass_t::string_non_immediate_t* sni = static_cast(strni_data.data()); - - REQUIRE(sni->data_1() == data[ST6_DATA_1_IDX].second); - REQUIRE(sni->data_2() == data[ST6_DATA_2_IDX].second); -} diff --git a/system/ubloxd/tests/test_glonass_runner.cc b/system/ubloxd/tests/test_glonass_runner.cc deleted file mode 100644 index 62bf7476a1..0000000000 --- a/system/ubloxd/tests/test_glonass_runner.cc +++ /dev/null @@ -1,2 +0,0 @@ -#define CATCH_CONFIG_MAIN -#include "catch2/catch.hpp" diff --git a/system/ubloxd/ublox_msg.cc b/system/ubloxd/ublox_msg.cc deleted file mode 100644 index 728f3b15fa..0000000000 --- a/system/ubloxd/ublox_msg.cc +++ /dev/null @@ -1,530 +0,0 @@ -#include "system/ubloxd/ublox_msg.h" - -#include - -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include "common/swaglog.h" - -const double gpsPi = 3.1415926535898; -#define UBLOX_MSG_SIZE(hdr) (*(uint16_t *)&hdr[4]) - -inline static bool bit_to_bool(uint8_t val, int shifts) { - return (bool)(val & (1 << shifts)); -} - -inline int UbloxMsgParser::needed_bytes() { - // Msg header incomplete? - if (bytes_in_parse_buf < ublox::UBLOX_HEADER_SIZE) - return ublox::UBLOX_HEADER_SIZE + ublox::UBLOX_CHECKSUM_SIZE - bytes_in_parse_buf; - uint16_t needed = UBLOX_MSG_SIZE(msg_parse_buf) + ublox::UBLOX_HEADER_SIZE + ublox::UBLOX_CHECKSUM_SIZE; - // too much data - if (needed < (uint16_t)bytes_in_parse_buf) - return -1; - return needed - (uint16_t)bytes_in_parse_buf; -} - -inline bool UbloxMsgParser::valid_cheksum() { - uint8_t ck_a = 0, ck_b = 0; - for (int i = 2; i < bytes_in_parse_buf - ublox::UBLOX_CHECKSUM_SIZE; i++) { - ck_a = (ck_a + msg_parse_buf[i]) & 0xFF; - ck_b = (ck_b + ck_a) & 0xFF; - } - if (ck_a != msg_parse_buf[bytes_in_parse_buf - 2]) { - LOGD("Checksum a mismatch: %02X, %02X", ck_a, msg_parse_buf[6]); - return false; - } - if (ck_b != msg_parse_buf[bytes_in_parse_buf - 1]) { - LOGD("Checksum b mismatch: %02X, %02X", ck_b, msg_parse_buf[7]); - return false; - } - return true; -} - -inline bool UbloxMsgParser::valid() { - return bytes_in_parse_buf >= ublox::UBLOX_HEADER_SIZE + ublox::UBLOX_CHECKSUM_SIZE && - needed_bytes() == 0 && valid_cheksum(); -} - -inline bool UbloxMsgParser::valid_so_far() { - if (bytes_in_parse_buf > 0 && msg_parse_buf[0] != ublox::PREAMBLE1) { - return false; - } - if (bytes_in_parse_buf > 1 && msg_parse_buf[1] != ublox::PREAMBLE2) { - return false; - } - if (needed_bytes() == 0 && !valid()) { - return false; - } - return true; -} - -bool UbloxMsgParser::add_data(float log_time, const uint8_t *incoming_data, uint32_t incoming_data_len, size_t &bytes_consumed) { - last_log_time = log_time; - int needed = needed_bytes(); - if (needed > 0) { - bytes_consumed = std::min((uint32_t)needed, incoming_data_len); - // Add data to buffer - memcpy(msg_parse_buf + bytes_in_parse_buf, incoming_data, bytes_consumed); - bytes_in_parse_buf += bytes_consumed; - } else { - bytes_consumed = incoming_data_len; - } - - // Validate msg format, detect invalid header and invalid checksum. - while (!valid_so_far() && bytes_in_parse_buf != 0) { - // Corrupted msg, drop a byte. - bytes_in_parse_buf -= 1; - if (bytes_in_parse_buf > 0) - memmove(&msg_parse_buf[0], &msg_parse_buf[1], bytes_in_parse_buf); - } - - // There is redundant data at the end of buffer, reset the buffer. - if (needed_bytes() == -1) { - bytes_in_parse_buf = 0; - } - return valid(); -} - - -std::pair> UbloxMsgParser::gen_msg() { - std::string dat = data(); - kaitai::kstream stream(dat); - - ubx_t ubx_message(&stream); - auto body = ubx_message.body(); - - switch (ubx_message.msg_type()) { - case 0x0107: - return {"gpsLocationExternal", gen_nav_pvt(static_cast(body))}; - case 0x0213: // UBX-RXM-SFRB (Broadcast Navigation Data Subframe) - return {"ubloxGnss", gen_rxm_sfrbx(static_cast(body))}; - case 0x0215: // UBX-RXM-RAW (Multi-GNSS Raw Measurement Data) - return {"ubloxGnss", gen_rxm_rawx(static_cast(body))}; - case 0x0a09: - return {"ubloxGnss", gen_mon_hw(static_cast(body))}; - case 0x0a0b: - return {"ubloxGnss", gen_mon_hw2(static_cast(body))}; - case 0x0135: - return {"ubloxGnss", gen_nav_sat(static_cast(body))}; - default: - LOGE("Unknown message type %x", ubx_message.msg_type()); - return {"ubloxGnss", kj::Array()}; - } -} - - -kj::Array UbloxMsgParser::gen_nav_pvt(ubx_t::nav_pvt_t *msg) { - MessageBuilder msg_builder; - auto gpsLoc = msg_builder.initEvent().initGpsLocationExternal(); - gpsLoc.setSource(cereal::GpsLocationData::SensorSource::UBLOX); - gpsLoc.setFlags(msg->flags()); - gpsLoc.setHasFix((msg->flags() % 2) == 1); - gpsLoc.setLatitude(msg->lat() * 1e-07); - gpsLoc.setLongitude(msg->lon() * 1e-07); - gpsLoc.setAltitude(msg->height() * 1e-03); - gpsLoc.setSpeed(msg->g_speed() * 1e-03); - gpsLoc.setBearingDeg(msg->head_mot() * 1e-5); - gpsLoc.setHorizontalAccuracy(msg->h_acc() * 1e-03); - gpsLoc.setSatelliteCount(msg->num_sv()); - std::tm timeinfo = std::tm(); - timeinfo.tm_year = msg->year() - 1900; - timeinfo.tm_mon = msg->month() - 1; - timeinfo.tm_mday = msg->day(); - timeinfo.tm_hour = msg->hour(); - timeinfo.tm_min = msg->min(); - timeinfo.tm_sec = msg->sec(); - - std::time_t utc_tt = timegm(&timeinfo); - gpsLoc.setUnixTimestampMillis(utc_tt * 1e+03 + msg->nano() * 1e-06); - float f[] = { msg->vel_n() * 1e-03f, msg->vel_e() * 1e-03f, msg->vel_d() * 1e-03f }; - gpsLoc.setVNED(f); - gpsLoc.setVerticalAccuracy(msg->v_acc() * 1e-03); - gpsLoc.setSpeedAccuracy(msg->s_acc() * 1e-03); - gpsLoc.setBearingAccuracyDeg(msg->head_acc() * 1e-05); - return capnp::messageToFlatArray(msg_builder); -} - -kj::Array UbloxMsgParser::parse_gps_ephemeris(ubx_t::rxm_sfrbx_t *msg) { - // GPS subframes are packed into 10x 4 bytes, each containing 3 actual bytes - // We will first need to separate the data from the padding and parity - auto body = *msg->body(); - assert(body.size() == 10); - - std::string subframe_data; - subframe_data.reserve(30); - for (uint32_t word : body) { - word = word >> 6; // TODO: Verify parity - subframe_data.push_back(word >> 16); - subframe_data.push_back(word >> 8); - subframe_data.push_back(word >> 0); - } - - // Collect subframes in map and parse when we have all the parts - { - kaitai::kstream stream(subframe_data); - gps_t subframe(&stream); - - int subframe_id = subframe.how()->subframe_id(); - if (subframe_id > 3 || subframe_id < 1) { - // don't parse almanac subframes - return kj::Array(); - } - gps_subframes[msg->sv_id()][subframe_id] = subframe_data; - } - - // publish if subframes 1-3 have been collected - if (gps_subframes[msg->sv_id()].size() == 3) { - MessageBuilder msg_builder; - auto eph = msg_builder.initEvent().initUbloxGnss().initEphemeris(); - eph.setSvId(msg->sv_id()); - - int iode_s2 = 0; - int iode_s3 = 0; - int iodc_lsb = 0; - int week; - - // Subframe 1 - { - kaitai::kstream stream(gps_subframes[msg->sv_id()][1]); - gps_t subframe(&stream); - gps_t::subframe_1_t* subframe_1 = static_cast(subframe.body()); - - // Each message is incremented to be greater or equal than week 1877 (2015-12-27). - // To skip this use the current_time argument - week = subframe_1->week_no(); - week += 1024; - if (week < 1877) { - week += 1024; - } - //eph.setGpsWeek(subframe_1->week_no()); - eph.setTgd(subframe_1->t_gd() * pow(2, -31)); - eph.setToc(subframe_1->t_oc() * pow(2, 4)); - eph.setAf2(subframe_1->af_2() * pow(2, -55)); - eph.setAf1(subframe_1->af_1() * pow(2, -43)); - eph.setAf0(subframe_1->af_0() * pow(2, -31)); - eph.setSvHealth(subframe_1->sv_health()); - eph.setTowCount(subframe.how()->tow_count()); - iodc_lsb = subframe_1->iodc_lsb(); - } - - // Subframe 2 - { - kaitai::kstream stream(gps_subframes[msg->sv_id()][2]); - gps_t subframe(&stream); - gps_t::subframe_2_t* subframe_2 = static_cast(subframe.body()); - - // GPS week refers to current week, the ephemeris can be valid for the next - // if toe equals 0, this can be verified by the TOW count if it is within the - // last 2 hours of the week (gps ephemeris valid for 4hours) - if (subframe_2->t_oe() == 0 and subframe.how()->tow_count()*6 >= (SECS_IN_WEEK - 2*SECS_IN_HR)){ - week += 1; - } - eph.setCrs(subframe_2->c_rs() * pow(2, -5)); - eph.setDeltaN(subframe_2->delta_n() * pow(2, -43) * gpsPi); - eph.setM0(subframe_2->m_0() * pow(2, -31) * gpsPi); - eph.setCuc(subframe_2->c_uc() * pow(2, -29)); - eph.setEcc(subframe_2->e() * pow(2, -33)); - eph.setCus(subframe_2->c_us() * pow(2, -29)); - eph.setA(pow(subframe_2->sqrt_a() * pow(2, -19), 2.0)); - eph.setToe(subframe_2->t_oe() * pow(2, 4)); - iode_s2 = subframe_2->iode(); - } - - // Subframe 3 - { - kaitai::kstream stream(gps_subframes[msg->sv_id()][3]); - gps_t subframe(&stream); - gps_t::subframe_3_t* subframe_3 = static_cast(subframe.body()); - - eph.setCic(subframe_3->c_ic() * pow(2, -29)); - eph.setOmega0(subframe_3->omega_0() * pow(2, -31) * gpsPi); - eph.setCis(subframe_3->c_is() * pow(2, -29)); - eph.setI0(subframe_3->i_0() * pow(2, -31) * gpsPi); - eph.setCrc(subframe_3->c_rc() * pow(2, -5)); - eph.setOmega(subframe_3->omega() * pow(2, -31) * gpsPi); - eph.setOmegaDot(subframe_3->omega_dot() * pow(2, -43) * gpsPi); - eph.setIode(subframe_3->iode()); - eph.setIDot(subframe_3->idot() * pow(2, -43) * gpsPi); - iode_s3 = subframe_3->iode(); - } - - eph.setToeWeek(week); - eph.setTocWeek(week); - - gps_subframes[msg->sv_id()].clear(); - if (iodc_lsb != iode_s2 || iodc_lsb != iode_s3) { - // data set cutover, reject ephemeris - return kj::Array(); - } - return capnp::messageToFlatArray(msg_builder); - } - return kj::Array(); -} - -kj::Array UbloxMsgParser::parse_glonass_ephemeris(ubx_t::rxm_sfrbx_t *msg) { - // This parser assumes that no 2 satellites of the same frequency - // can be in view at the same time - auto body = *msg->body(); - assert(body.size() == 4); - { - std::string string_data; - string_data.reserve(16); - for (uint32_t word : body) { - for (int i = 3; i >= 0; i--) - string_data.push_back(word >> 8*i); - } - - kaitai::kstream stream(string_data); - glonass_t gl_string(&stream); - int string_number = gl_string.string_number(); - if (string_number < 1 || string_number > 5 || gl_string.idle_chip()) { - // don't parse non immediate data, idle_chip == 0 - return kj::Array(); - } - - // Check if new string either has same superframe_id or log transmission times make sense - bool superframe_unknown = false; - bool needs_clear = false; - for (int i = 1; i <= 5; i++) { - if (glonass_strings[msg->freq_id()].find(i) == glonass_strings[msg->freq_id()].end()) - continue; - if (glonass_string_superframes[msg->freq_id()][i] == 0 || gl_string.superframe_number() == 0) { - superframe_unknown = true; - } else if (glonass_string_superframes[msg->freq_id()][i] != gl_string.superframe_number()) { - needs_clear = true; - } - // Check if string times add up to being from the same frame - // If superframe is known this is redundant - // Strings are sent 2s apart and frames are 30s apart - if (superframe_unknown && - std::abs((glonass_string_times[msg->freq_id()][i] - 2.0 * i) - (last_log_time - 2.0 * string_number)) > 10) - needs_clear = true; - } - if (needs_clear) { - glonass_strings[msg->freq_id()].clear(); - glonass_string_superframes[msg->freq_id()].clear(); - glonass_string_times[msg->freq_id()].clear(); - } - glonass_strings[msg->freq_id()][string_number] = string_data; - glonass_string_superframes[msg->freq_id()][string_number] = gl_string.superframe_number(); - glonass_string_times[msg->freq_id()][string_number] = last_log_time; - } - if (msg->sv_id() == 255) { - // data can be decoded before identifying the SV number, in this case 255 - // is returned, which means "unknown" (ublox p32) - return kj::Array(); - } - - // publish if strings 1-5 have been collected - if (glonass_strings[msg->freq_id()].size() != 5) { - return kj::Array(); - } - - MessageBuilder msg_builder; - auto eph = msg_builder.initEvent().initUbloxGnss().initGlonassEphemeris(); - eph.setSvId(msg->sv_id()); - eph.setFreqNum(msg->freq_id() - 7); - - uint16_t current_day = 0; - uint16_t tk = 0; - - // string number 1 - { - kaitai::kstream stream(glonass_strings[msg->freq_id()][1]); - glonass_t gl_stream(&stream); - glonass_t::string_1_t* data = static_cast(gl_stream.data()); - - eph.setP1(data->p1()); - tk = data->t_k(); - eph.setTkDEPRECATED(tk); - eph.setXVel(data->x_vel() * pow(2, -20)); - eph.setXAccel(data->x_accel() * pow(2, -30)); - eph.setX(data->x() * pow(2, -11)); - } - - // string number 2 - { - kaitai::kstream stream(glonass_strings[msg->freq_id()][2]); - glonass_t gl_stream(&stream); - glonass_t::string_2_t* data = static_cast(gl_stream.data()); - - eph.setSvHealth(data->b_n()>>2); // MSB indicates health - eph.setP2(data->p2()); - eph.setTb(data->t_b()); - eph.setYVel(data->y_vel() * pow(2, -20)); - eph.setYAccel(data->y_accel() * pow(2, -30)); - eph.setY(data->y() * pow(2, -11)); - } - - // string number 3 - { - kaitai::kstream stream(glonass_strings[msg->freq_id()][3]); - glonass_t gl_stream(&stream); - glonass_t::string_3_t* data = static_cast(gl_stream.data()); - - eph.setP3(data->p3()); - eph.setGammaN(data->gamma_n() * pow(2, -40)); - eph.setSvHealth(eph.getSvHealth() | data->l_n()); - eph.setZVel(data->z_vel() * pow(2, -20)); - eph.setZAccel(data->z_accel() * pow(2, -30)); - eph.setZ(data->z() * pow(2, -11)); - } - - // string number 4 - { - kaitai::kstream stream(glonass_strings[msg->freq_id()][4]); - glonass_t gl_stream(&stream); - glonass_t::string_4_t* data = static_cast(gl_stream.data()); - - current_day = data->n_t(); - eph.setNt(current_day); - eph.setTauN(data->tau_n() * pow(2, -30)); - eph.setDeltaTauN(data->delta_tau_n() * pow(2, -30)); - eph.setAge(data->e_n()); - eph.setP4(data->p4()); - eph.setSvURA(glonass_URA_lookup.at(data->f_t())); - if (msg->sv_id() != data->n()) { - LOGE("SV_ID != SLOT_NUMBER: %d %" PRIu64, msg->sv_id(), data->n()); - } - eph.setSvType(data->m()); - } - - // string number 5 - { - kaitai::kstream stream(glonass_strings[msg->freq_id()][5]); - glonass_t gl_stream(&stream); - glonass_t::string_5_t* data = static_cast(gl_stream.data()); - - // string5 parsing is only needed to get the year, this can be removed and - // the year can be fetched later in laika (note rollovers and leap year) - eph.setN4(data->n_4()); - int tk_seconds = SECS_IN_HR * ((tk>>7) & 0x1F) + SECS_IN_MIN * ((tk>>1) & 0x3F) + (tk & 0x1) * 30; - eph.setTkSeconds(tk_seconds); - } - - glonass_strings[msg->freq_id()].clear(); - return capnp::messageToFlatArray(msg_builder); -} - - -kj::Array UbloxMsgParser::gen_rxm_sfrbx(ubx_t::rxm_sfrbx_t *msg) { - switch (msg->gnss_id()) { - case ubx_t::gnss_type_t::GNSS_TYPE_GPS: - return parse_gps_ephemeris(msg); - case ubx_t::gnss_type_t::GNSS_TYPE_GLONASS: - return parse_glonass_ephemeris(msg); - default: - return kj::Array(); - } -} - -kj::Array UbloxMsgParser::gen_rxm_rawx(ubx_t::rxm_rawx_t *msg) { - MessageBuilder msg_builder; - auto mr = msg_builder.initEvent().initUbloxGnss().initMeasurementReport(); - mr.setRcvTow(msg->rcv_tow()); - mr.setGpsWeek(msg->week()); - mr.setLeapSeconds(msg->leap_s()); - mr.setGpsWeek(msg->week()); - - auto mb = mr.initMeasurements(msg->num_meas()); - auto measurements = *msg->meas(); - for (int8_t i = 0; i < msg->num_meas(); i++) { - mb[i].setSvId(measurements[i]->sv_id()); - mb[i].setPseudorange(measurements[i]->pr_mes()); - mb[i].setCarrierCycles(measurements[i]->cp_mes()); - mb[i].setDoppler(measurements[i]->do_mes()); - mb[i].setGnssId(measurements[i]->gnss_id()); - mb[i].setGlonassFrequencyIndex(measurements[i]->freq_id()); - mb[i].setLocktime(measurements[i]->lock_time()); - mb[i].setCno(measurements[i]->cno()); - mb[i].setPseudorangeStdev(0.01 * (pow(2, (measurements[i]->pr_stdev() & 15)))); // weird scaling, might be wrong - mb[i].setCarrierPhaseStdev(0.004 * (measurements[i]->cp_stdev() & 15)); - mb[i].setDopplerStdev(0.002 * (pow(2, (measurements[i]->do_stdev() & 15)))); // weird scaling, might be wrong - - auto ts = mb[i].initTrackingStatus(); - auto trk_stat = measurements[i]->trk_stat(); - ts.setPseudorangeValid(bit_to_bool(trk_stat, 0)); - ts.setCarrierPhaseValid(bit_to_bool(trk_stat, 1)); - ts.setHalfCycleValid(bit_to_bool(trk_stat, 2)); - ts.setHalfCycleSubtracted(bit_to_bool(trk_stat, 3)); - } - - mr.setNumMeas(msg->num_meas()); - auto rs = mr.initReceiverStatus(); - rs.setLeapSecValid(bit_to_bool(msg->rec_stat(), 0)); - rs.setClkReset(bit_to_bool(msg->rec_stat(), 2)); - return capnp::messageToFlatArray(msg_builder); -} - -kj::Array UbloxMsgParser::gen_nav_sat(ubx_t::nav_sat_t *msg) { - MessageBuilder msg_builder; - auto sr = msg_builder.initEvent().initUbloxGnss().initSatReport(); - sr.setITow(msg->itow()); - - auto svs = sr.initSvs(msg->num_svs()); - auto svs_data = *msg->svs(); - for (int8_t i = 0; i < msg->num_svs(); i++) { - svs[i].setSvId(svs_data[i]->sv_id()); - svs[i].setGnssId(svs_data[i]->gnss_id()); - svs[i].setFlagsBitfield(svs_data[i]->flags()); - svs[i].setCno(svs_data[i]->cno()); - svs[i].setElevationDeg(svs_data[i]->elev()); - svs[i].setAzimuthDeg(svs_data[i]->azim()); - svs[i].setPseudorangeResidual(svs_data[i]->pr_res() * 0.1); - } - - return capnp::messageToFlatArray(msg_builder); -} - -kj::Array UbloxMsgParser::gen_mon_hw(ubx_t::mon_hw_t *msg) { - MessageBuilder msg_builder; - auto hwStatus = msg_builder.initEvent().initUbloxGnss().initHwStatus(); - hwStatus.setNoisePerMS(msg->noise_per_ms()); - hwStatus.setFlags(msg->flags()); - hwStatus.setAgcCnt(msg->agc_cnt()); - hwStatus.setAStatus((cereal::UbloxGnss::HwStatus::AntennaSupervisorState) msg->a_status()); - hwStatus.setAPower((cereal::UbloxGnss::HwStatus::AntennaPowerStatus) msg->a_power()); - hwStatus.setJamInd(msg->jam_ind()); - return capnp::messageToFlatArray(msg_builder); -} - -kj::Array UbloxMsgParser::gen_mon_hw2(ubx_t::mon_hw2_t *msg) { - MessageBuilder msg_builder; - auto hwStatus = msg_builder.initEvent().initUbloxGnss().initHwStatus2(); - hwStatus.setOfsI(msg->ofs_i()); - hwStatus.setMagI(msg->mag_i()); - hwStatus.setOfsQ(msg->ofs_q()); - hwStatus.setMagQ(msg->mag_q()); - - switch (msg->cfg_source()) { - case ubx_t::mon_hw2_t::config_source_t::CONFIG_SOURCE_ROM: - hwStatus.setCfgSource(cereal::UbloxGnss::HwStatus2::ConfigSource::ROM); - break; - case ubx_t::mon_hw2_t::config_source_t::CONFIG_SOURCE_OTP: - hwStatus.setCfgSource(cereal::UbloxGnss::HwStatus2::ConfigSource::OTP); - break; - case ubx_t::mon_hw2_t::config_source_t::CONFIG_SOURCE_CONFIG_PINS: - hwStatus.setCfgSource(cereal::UbloxGnss::HwStatus2::ConfigSource::CONFIGPINS); - break; - case ubx_t::mon_hw2_t::config_source_t::CONFIG_SOURCE_FLASH: - hwStatus.setCfgSource(cereal::UbloxGnss::HwStatus2::ConfigSource::FLASH); - break; - default: - hwStatus.setCfgSource(cereal::UbloxGnss::HwStatus2::ConfigSource::UNDEFINED); - break; - } - - hwStatus.setLowLevCfg(msg->low_lev_cfg()); - hwStatus.setPostStatus(msg->post_status()); - - return capnp::messageToFlatArray(msg_builder); -} diff --git a/system/ubloxd/ublox_msg.h b/system/ubloxd/ublox_msg.h deleted file mode 100644 index d21760edc2..0000000000 --- a/system/ubloxd/ublox_msg.h +++ /dev/null @@ -1,131 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include -#include - -#include "cereal/messaging/messaging.h" -#include "common/util.h" -#include "system/ubloxd/generated/gps.h" -#include "system/ubloxd/generated/glonass.h" -#include "system/ubloxd/generated/ubx.h" - -using namespace std::string_literals; - -const int SECS_IN_MIN = 60; -const int SECS_IN_HR = 60 * SECS_IN_MIN; -const int SECS_IN_DAY = 24 * SECS_IN_HR; -const int SECS_IN_WEEK = 7 * SECS_IN_DAY; - -// protocol constants -namespace ublox { - const uint8_t PREAMBLE1 = 0xb5; - const uint8_t PREAMBLE2 = 0x62; - - const int UBLOX_HEADER_SIZE = 6; - const int UBLOX_CHECKSUM_SIZE = 2; - const int UBLOX_MAX_MSG_SIZE = 65536; - - struct ubx_mga_ini_time_utc_t { - uint8_t type; - uint8_t version; - uint8_t ref; - int8_t leapSecs; - uint16_t year; - uint8_t month; - uint8_t day; - uint8_t hour; - uint8_t minute; - uint8_t second; - uint8_t reserved1; - uint32_t ns; - uint16_t tAccS; - uint16_t reserved2; - uint32_t tAccNs; - } __attribute__((packed)); - - inline std::string ubx_add_checksum(const std::string &msg) { - assert(msg.size() > 2); - - uint8_t ck_a = 0, ck_b = 0; - for (int i = 2; i < msg.size(); i++) { - ck_a = (ck_a + msg[i]) & 0xFF; - ck_b = (ck_b + ck_a) & 0xFF; - } - - std::string r = msg; - r.push_back(ck_a); - r.push_back(ck_b); - return r; - } - - inline std::string build_ubx_mga_ini_time_utc(struct tm time) { - ublox::ubx_mga_ini_time_utc_t payload = { - .type = 0x10, - .version = 0x0, - .ref = 0x0, - .leapSecs = -128, // Unknown - .year = (uint16_t)(1900 + time.tm_year), - .month = (uint8_t)(1 + time.tm_mon), - .day = (uint8_t)time.tm_mday, - .hour = (uint8_t)time.tm_hour, - .minute = (uint8_t)time.tm_min, - .second = (uint8_t)time.tm_sec, - .reserved1 = 0x0, - .ns = 0, - .tAccS = 30, - .reserved2 = 0x0, - .tAccNs = 0, - }; - assert(sizeof(payload) == 24); - - std::string msg = "\xb5\x62\x13\x40\x18\x00"s; - msg += std::string((char*)&payload, sizeof(payload)); - - return ubx_add_checksum(msg); - } -} - -class UbloxMsgParser { - public: - bool add_data(float log_time, const uint8_t *incoming_data, uint32_t incoming_data_len, size_t &bytes_consumed); - inline void reset() {bytes_in_parse_buf = 0;} - inline int needed_bytes(); - inline std::string data() {return std::string((const char*)msg_parse_buf, bytes_in_parse_buf);} - - std::pair> gen_msg(); - kj::Array gen_nav_pvt(ubx_t::nav_pvt_t *msg); - kj::Array gen_rxm_sfrbx(ubx_t::rxm_sfrbx_t *msg); - kj::Array gen_rxm_rawx(ubx_t::rxm_rawx_t *msg); - kj::Array gen_mon_hw(ubx_t::mon_hw_t *msg); - kj::Array gen_mon_hw2(ubx_t::mon_hw2_t *msg); - kj::Array gen_nav_sat(ubx_t::nav_sat_t *msg); - - private: - inline bool valid_cheksum(); - inline bool valid(); - inline bool valid_so_far(); - - kj::Array parse_gps_ephemeris(ubx_t::rxm_sfrbx_t *msg); - kj::Array parse_glonass_ephemeris(ubx_t::rxm_sfrbx_t *msg); - - std::unordered_map> gps_subframes; - - float last_log_time = 0.0; - size_t bytes_in_parse_buf = 0; - uint8_t msg_parse_buf[ublox::UBLOX_HEADER_SIZE + ublox::UBLOX_MAX_MSG_SIZE]; - - // user range accuracy in meters - const std::unordered_map glonass_URA_lookup = - {{ 0, 1}, { 1, 2}, { 2, 2.5}, { 3, 4}, { 4, 5}, {5, 7}, - { 6, 10}, { 7, 12}, { 8, 14}, { 9, 16}, {10, 32}, - {11, 64}, {12, 128}, {13, 256}, {14, 512}, {15, 1024}}; - - std::unordered_map> glonass_strings; - std::unordered_map> glonass_string_times; - std::unordered_map> glonass_string_superframes; -}; diff --git a/system/ubloxd/ubloxd.cc b/system/ubloxd/ubloxd.cc deleted file mode 100644 index 4e7e91f830..0000000000 --- a/system/ubloxd/ubloxd.cc +++ /dev/null @@ -1,62 +0,0 @@ -#include - -#include - -#include "cereal/messaging/messaging.h" -#include "common/swaglog.h" -#include "common/util.h" -#include "system/ubloxd/ublox_msg.h" - -ExitHandler do_exit; -using namespace ublox; - -int main() { - LOGW("starting ubloxd"); - AlignedBuffer aligned_buf; - UbloxMsgParser parser; - - PubMaster pm({"ubloxGnss", "gpsLocationExternal"}); - - std::unique_ptr context(Context::create()); - std::unique_ptr subscriber(SubSocket::create(context.get(), "ubloxRaw")); - assert(subscriber != NULL); - subscriber->setTimeout(100); - - - while (!do_exit) { - std::unique_ptr msg(subscriber->receive()); - if (!msg) { - continue; - } - - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(msg.get())); - cereal::Event::Reader event = cmsg.getRoot(); - auto ubloxRaw = event.getUbloxRaw(); - float log_time = 1e-9 * event.getLogMonoTime(); - - const uint8_t *data = ubloxRaw.begin(); - size_t len = ubloxRaw.size(); - size_t bytes_consumed = 0; - - while (bytes_consumed < len && !do_exit) { - size_t bytes_consumed_this_time = 0U; - if (parser.add_data(log_time, data + bytes_consumed, (uint32_t)(len - bytes_consumed), bytes_consumed_this_time)) { - - try { - auto ublox_msg = parser.gen_msg(); - if (ublox_msg.second.size() > 0) { - auto bytes = ublox_msg.second.asBytes(); - pm.send(ublox_msg.first.c_str(), bytes.begin(), bytes.size()); - } - } catch (const std::exception& e) { - LOGE("Error parsing ublox message %s", e.what()); - } - - parser.reset(); - } - bytes_consumed += bytes_consumed_this_time; - } - } - - return 0; -} diff --git a/system/ubloxd/ubloxd.py b/system/ubloxd/ubloxd.py new file mode 100755 index 0000000000..84a926dd78 --- /dev/null +++ b/system/ubloxd/ubloxd.py @@ -0,0 +1,519 @@ +#!/usr/bin/env python3 +import math +import capnp +import calendar +import numpy as np +from collections import defaultdict +from dataclasses import dataclass + +from cereal import log +from cereal import messaging +from openpilot.system.ubloxd.generated.ubx import Ubx +from openpilot.system.ubloxd.generated.gps import Gps +from openpilot.system.ubloxd.generated.glonass import Glonass + + +SECS_IN_MIN = 60 +SECS_IN_HR = 60 * SECS_IN_MIN +SECS_IN_DAY = 24 * SECS_IN_HR +SECS_IN_WEEK = 7 * SECS_IN_DAY + + +class UbxFramer: + PREAMBLE1 = 0xB5 + PREAMBLE2 = 0x62 + HEADER_SIZE = 6 + CHECKSUM_SIZE = 2 + + def __init__(self) -> None: + self.buf = bytearray() + self.last_log_time = 0.0 + + def reset(self) -> None: + self.buf.clear() + + @staticmethod + def _checksum_ok(frame: bytes) -> bool: + ck_a = 0 + ck_b = 0 + for b in frame[2:-2]: + ck_a = (ck_a + b) & 0xFF + ck_b = (ck_b + ck_a) & 0xFF + return ck_a == frame[-2] and ck_b == frame[-1] + + def add_data(self, log_time: float, incoming: bytes) -> list[bytes]: + self.last_log_time = log_time + out: list[bytes] = [] + if not incoming: + return out + self.buf += incoming + + while True: + # find preamble + if len(self.buf) < 2: + break + start = self.buf.find(b"\xB5\x62") + if start < 0: + # no preamble in buffer + self.buf.clear() + break + if start > 0: + # drop garbage before preamble + self.buf = self.buf[start:] + + if len(self.buf) < self.HEADER_SIZE: + break + + length_le = int.from_bytes(self.buf[4:6], 'little', signed=False) + total_len = self.HEADER_SIZE + length_le + self.CHECKSUM_SIZE + if len(self.buf) < total_len: + break + + candidate = bytes(self.buf[:total_len]) + if self._checksum_ok(candidate): + out.append(candidate) + # consume this frame + self.buf = self.buf[total_len:] + else: + # drop first byte and retry + self.buf = self.buf[1:] + + return out + + +def _bit(b: int, shift: int) -> bool: + return (b & (1 << shift)) != 0 + + +@dataclass +class EphemerisCaches: + gps_subframes: defaultdict[int, dict[int, bytes]] + glonass_strings: defaultdict[int, dict[int, bytes]] + glonass_string_times: defaultdict[int, dict[int, float]] + glonass_string_superframes: defaultdict[int, dict[int, int]] + + +class UbloxMsgParser: + gpsPi = 3.1415926535898 + + # user range accuracy in meters + glonass_URA_lookup: dict[int, float] = { + 0: 1, 1: 2, 2: 2.5, 3: 4, 4: 5, 5: 7, + 6: 10, 7: 12, 8: 14, 9: 16, 10: 32, + 11: 64, 12: 128, 13: 256, 14: 512, 15: 1024, + } + + def __init__(self) -> None: + self.framer = UbxFramer() + self.caches = EphemerisCaches( + gps_subframes=defaultdict(dict), + glonass_strings=defaultdict(dict), + glonass_string_times=defaultdict(dict), + glonass_string_superframes=defaultdict(dict), + ) + + # Message generation entry point + def parse_frame(self, frame: bytes) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None: + # Quick header parse + msg_type = int.from_bytes(frame[2:4], 'big') + payload = frame[6:-2] + if msg_type == 0x0107: + body = Ubx.NavPvt.from_bytes(payload) + return self._gen_nav_pvt(body) + if msg_type == 0x0213: + # Manually parse RXM-SFRBX to avoid Kaitai EOF on some frames + if len(payload) < 8: + return None + gnss_id = payload[0] + sv_id = payload[1] + freq_id = payload[3] + num_words = payload[4] + exp = 8 + 4 * num_words + if exp != len(payload): + return None + words: list[int] = [] + off = 8 + for _ in range(num_words): + words.append(int.from_bytes(payload[off:off+4], 'little')) + off += 4 + + class _SfrbxView: + def __init__(self, gid: int, sid: int, fid: int, body: list[int]): + self.gnss_id = Ubx.GnssType(gid) + self.sv_id = sid + self.freq_id = fid + self.body = body + view = _SfrbxView(gnss_id, sv_id, freq_id, words) + return self._gen_rxm_sfrbx(view) + if msg_type == 0x0215: + body = Ubx.RxmRawx.from_bytes(payload) + return self._gen_rxm_rawx(body) + if msg_type == 0x0A09: + body = Ubx.MonHw.from_bytes(payload) + return self._gen_mon_hw(body) + if msg_type == 0x0A0B: + body = Ubx.MonHw2.from_bytes(payload) + return self._gen_mon_hw2(body) + if msg_type == 0x0135: + body = Ubx.NavSat.from_bytes(payload) + return self._gen_nav_sat(body) + return None + + # NAV-PVT -> gpsLocationExternal + def _gen_nav_pvt(self, msg: Ubx.NavPvt) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]: + dat = messaging.new_message('gpsLocationExternal', valid=True) + gps = dat.gpsLocationExternal + gps.source = log.GpsLocationData.SensorSource.ublox + gps.flags = msg.flags + gps.hasFix = (msg.flags % 2) == 1 + gps.latitude = msg.lat * 1e-07 + gps.longitude = msg.lon * 1e-07 + gps.altitude = msg.height * 1e-03 + gps.speed = msg.g_speed * 1e-03 + gps.bearingDeg = msg.head_mot * 1e-5 + gps.horizontalAccuracy = msg.h_acc * 1e-03 + gps.satelliteCount = msg.num_sv + + # build UTC timestamp millis (NAV-PVT is in UTC) + # tolerate invalid or unset date values like C++ timegm + try: + utc_tt = calendar.timegm((msg.year, msg.month, msg.day, msg.hour, msg.min, msg.sec, 0, 0, 0)) + except Exception: + utc_tt = 0 + gps.unixTimestampMillis = int(utc_tt * 1e3 + (msg.nano * 1e-6)) + + # match C++ float32 rounding semantics exactly + gps.vNED = [ + float(np.float32(msg.vel_n) * np.float32(1e-03)), + float(np.float32(msg.vel_e) * np.float32(1e-03)), + float(np.float32(msg.vel_d) * np.float32(1e-03)), + ] + gps.verticalAccuracy = msg.v_acc * 1e-03 + gps.speedAccuracy = msg.s_acc * 1e-03 + gps.bearingAccuracyDeg = msg.head_acc * 1e-05 + return ('gpsLocationExternal', dat) + + # RXM-SFRBX dispatch to GPS or GLONASS ephemeris + def _gen_rxm_sfrbx(self, msg) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None: + if msg.gnss_id == Ubx.GnssType.gps: + return self._parse_gps_ephemeris(msg) + if msg.gnss_id == Ubx.GnssType.glonass: + return self._parse_glonass_ephemeris(msg) + return None + + def _parse_gps_ephemeris(self, msg: Ubx.RxmSfrbx) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None: + # body is list of 10 words; convert to 30-byte subframe (strip parity/padding) + body = msg.body + if len(body) != 10: + return None + subframe_data = bytearray() + for word in body: + word >>= 6 + subframe_data.append((word >> 16) & 0xFF) + subframe_data.append((word >> 8) & 0xFF) + subframe_data.append(word & 0xFF) + + sf = Gps.from_bytes(bytes(subframe_data)) + subframe_id = sf.how.subframe_id + if subframe_id < 1 or subframe_id > 3: + return None + self.caches.gps_subframes[msg.sv_id][subframe_id] = bytes(subframe_data) + + if len(self.caches.gps_subframes[msg.sv_id]) != 3: + return None + + dat = messaging.new_message('ubloxGnss', valid=True) + eph = dat.ubloxGnss.init('ephemeris') + eph.svId = msg.sv_id + + iode_s2 = 0 + iode_s3 = 0 + iodc_lsb = 0 + week = 0 + + # Subframe 1 + sf1 = Gps.from_bytes(self.caches.gps_subframes[msg.sv_id][1]) + s1 = sf1.body + assert isinstance(s1, Gps.Subframe1) + week = s1.week_no + week += 1024 + if week < 1877: + week += 1024 + eph.tgd = s1.t_gd * math.pow(2, -31) + eph.toc = s1.t_oc * math.pow(2, 4) + eph.af2 = s1.af_2 * math.pow(2, -55) + eph.af1 = s1.af_1 * math.pow(2, -43) + eph.af0 = s1.af_0 * math.pow(2, -31) + eph.svHealth = s1.sv_health + eph.towCount = sf1.how.tow_count + iodc_lsb = s1.iodc_lsb + + # Subframe 2 + sf2 = Gps.from_bytes(self.caches.gps_subframes[msg.sv_id][2]) + s2 = sf2.body + assert isinstance(s2, Gps.Subframe2) + if s2.t_oe == 0 and sf2.how.tow_count * 6 >= (SECS_IN_WEEK - 2 * SECS_IN_HR): + week += 1 + eph.crs = s2.c_rs * math.pow(2, -5) + eph.deltaN = s2.delta_n * math.pow(2, -43) * self.gpsPi + eph.m0 = s2.m_0 * math.pow(2, -31) * self.gpsPi + eph.cuc = s2.c_uc * math.pow(2, -29) + eph.ecc = s2.e * math.pow(2, -33) + eph.cus = s2.c_us * math.pow(2, -29) + eph.a = math.pow(s2.sqrt_a * math.pow(2, -19), 2.0) + eph.toe = s2.t_oe * math.pow(2, 4) + iode_s2 = s2.iode + + # Subframe 3 + sf3 = Gps.from_bytes(self.caches.gps_subframes[msg.sv_id][3]) + s3 = sf3.body + assert isinstance(s3, Gps.Subframe3) + eph.cic = s3.c_ic * math.pow(2, -29) + eph.omega0 = s3.omega_0 * math.pow(2, -31) * self.gpsPi + eph.cis = s3.c_is * math.pow(2, -29) + eph.i0 = s3.i_0 * math.pow(2, -31) * self.gpsPi + eph.crc = s3.c_rc * math.pow(2, -5) + eph.omega = s3.omega * math.pow(2, -31) * self.gpsPi + eph.omegaDot = s3.omega_dot * math.pow(2, -43) * self.gpsPi + eph.iode = s3.iode + eph.iDot = s3.idot * math.pow(2, -43) * self.gpsPi + iode_s3 = s3.iode + + eph.toeWeek = week + eph.tocWeek = week + + # clear cache for this SV + self.caches.gps_subframes[msg.sv_id].clear() + if not (iodc_lsb == iode_s2 == iode_s3): + return None + return ('ubloxGnss', dat) + + def _parse_glonass_ephemeris(self, msg: Ubx.RxmSfrbx) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder] | None: + # words are 4 bytes each; Glonass parser expects 16 bytes (string) + body = msg.body + if len(body) != 4: + return None + string_bytes = bytearray() + for word in body: + for i in (3, 2, 1, 0): + string_bytes.append((word >> (8 * i)) & 0xFF) + + gl = Glonass.from_bytes(bytes(string_bytes)) + string_number = gl.string_number + if string_number < 1 or string_number > 5 or gl.idle_chip: + return None + + # correlate by superframe and timing, similar to C++ logic + freq_id = msg.freq_id + superframe_unknown = False + needs_clear = False + for i in range(1, 6): + if i not in self.caches.glonass_strings[freq_id]: + continue + sf_prev = self.caches.glonass_string_superframes[freq_id].get(i, 0) + if sf_prev == 0 or gl.superframe_number == 0: + superframe_unknown = True + elif sf_prev != gl.superframe_number: + needs_clear = True + if superframe_unknown: + prev_time = self.caches.glonass_string_times[freq_id].get(i, 0.0) + if abs((prev_time - 2.0 * i) - (self.framer.last_log_time - 2.0 * string_number)) > 10: + needs_clear = True + + if needs_clear: + self.caches.glonass_strings[freq_id].clear() + self.caches.glonass_string_superframes[freq_id].clear() + self.caches.glonass_string_times[freq_id].clear() + + self.caches.glonass_strings[freq_id][string_number] = bytes(string_bytes) + self.caches.glonass_string_superframes[freq_id][string_number] = gl.superframe_number + self.caches.glonass_string_times[freq_id][string_number] = self.framer.last_log_time + + if msg.sv_id == 255: + # unknown SV id + return None + if len(self.caches.glonass_strings[freq_id]) != 5: + return None + + dat = messaging.new_message('ubloxGnss', valid=True) + eph = dat.ubloxGnss.init('glonassEphemeris') + eph.svId = msg.sv_id + eph.freqNum = msg.freq_id - 7 + + current_day = 0 + tk = 0 + + # string 1 + try: + s1 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][1]).data + except Exception: + return None + assert isinstance(s1, Glonass.String1) + eph.p1 = int(s1.p1) + tk = int(s1.t_k) + eph.tkDEPRECATED = tk + eph.xVel = float(s1.x_vel) * math.pow(2, -20) + eph.xAccel = float(s1.x_accel) * math.pow(2, -30) + eph.x = float(s1.x) * math.pow(2, -11) + + # string 2 + try: + s2 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][2]).data + except Exception: + return None + assert isinstance(s2, Glonass.String2) + eph.svHealth = int(s2.b_n >> 2) + eph.p2 = int(s2.p2) + eph.tb = int(s2.t_b) + eph.yVel = float(s2.y_vel) * math.pow(2, -20) + eph.yAccel = float(s2.y_accel) * math.pow(2, -30) + eph.y = float(s2.y) * math.pow(2, -11) + + # string 3 + try: + s3 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][3]).data + except Exception: + return None + assert isinstance(s3, Glonass.String3) + eph.p3 = int(s3.p3) + eph.gammaN = float(s3.gamma_n) * math.pow(2, -40) + eph.svHealth = int(eph.svHealth | (1 if s3.l_n else 0)) + eph.zVel = float(s3.z_vel) * math.pow(2, -20) + eph.zAccel = float(s3.z_accel) * math.pow(2, -30) + eph.z = float(s3.z) * math.pow(2, -11) + + # string 4 + try: + s4 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][4]).data + except Exception: + return None + assert isinstance(s4, Glonass.String4) + current_day = int(s4.n_t) + eph.nt = current_day + eph.tauN = float(s4.tau_n) * math.pow(2, -30) + eph.deltaTauN = float(s4.delta_tau_n) * math.pow(2, -30) + eph.age = int(s4.e_n) + eph.p4 = int(s4.p4) + eph.svURA = float(self.glonass_URA_lookup.get(int(s4.f_t), 0.0)) + # consistency check: SV slot number + # if it doesn't match, keep going but note mismatch (no logging here) + eph.svType = int(s4.m) + + # string 5 + try: + s5 = Glonass.from_bytes(self.caches.glonass_strings[freq_id][5]).data + except Exception: + return None + assert isinstance(s5, Glonass.String5) + eph.n4 = int(s5.n_4) + tk_seconds = int(SECS_IN_HR * ((tk >> 7) & 0x1F) + SECS_IN_MIN * ((tk >> 1) & 0x3F) + (tk & 0x1) * 30) + eph.tkSeconds = tk_seconds + + self.caches.glonass_strings[freq_id].clear() + return ('ubloxGnss', dat) + + def _gen_rxm_rawx(self, msg: Ubx.RxmRawx) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]: + dat = messaging.new_message('ubloxGnss', valid=True) + mr = dat.ubloxGnss.init('measurementReport') + mr.rcvTow = msg.rcv_tow + mr.gpsWeek = msg.week + mr.leapSeconds = msg.leap_s + + mb = mr.init('measurements', msg.num_meas) + for i, m in enumerate(msg.meas): + mb[i].svId = m.sv_id + mb[i].pseudorange = m.pr_mes + mb[i].carrierCycles = m.cp_mes + mb[i].doppler = m.do_mes + mb[i].gnssId = int(m.gnss_id.value) + mb[i].glonassFrequencyIndex = m.freq_id + mb[i].locktime = m.lock_time + mb[i].cno = m.cno + mb[i].pseudorangeStdev = 0.01 * (math.pow(2, (m.pr_stdev & 15))) + mb[i].carrierPhaseStdev = 0.004 * (m.cp_stdev & 15) + mb[i].dopplerStdev = 0.002 * (math.pow(2, (m.do_stdev & 15))) + + ts = mb[i].init('trackingStatus') + trk = m.trk_stat + ts.pseudorangeValid = _bit(trk, 0) + ts.carrierPhaseValid = _bit(trk, 1) + ts.halfCycleValid = _bit(trk, 2) + ts.halfCycleSubtracted = _bit(trk, 3) + + mr.numMeas = msg.num_meas + rs = mr.init('receiverStatus') + rs.leapSecValid = _bit(msg.rec_stat, 0) + rs.clkReset = _bit(msg.rec_stat, 2) + return ('ubloxGnss', dat) + + def _gen_nav_sat(self, msg: Ubx.NavSat) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]: + dat = messaging.new_message('ubloxGnss', valid=True) + sr = dat.ubloxGnss.init('satReport') + sr.iTow = msg.itow + svs = sr.init('svs', msg.num_svs) + for i, s in enumerate(msg.svs): + svs[i].svId = s.sv_id + svs[i].gnssId = int(s.gnss_id.value) + svs[i].flagsBitfield = s.flags + svs[i].cno = s.cno + svs[i].elevationDeg = s.elev + svs[i].azimuthDeg = s.azim + svs[i].pseudorangeResidual = s.pr_res * 0.1 + return ('ubloxGnss', dat) + + def _gen_mon_hw(self, msg: Ubx.MonHw) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]: + dat = messaging.new_message('ubloxGnss', valid=True) + hw = dat.ubloxGnss.init('hwStatus') + hw.noisePerMS = msg.noise_per_ms + hw.flags = msg.flags + hw.agcCnt = msg.agc_cnt + hw.aStatus = int(msg.a_status.value) + hw.aPower = int(msg.a_power.value) + hw.jamInd = msg.jam_ind + return ('ubloxGnss', dat) + + def _gen_mon_hw2(self, msg: Ubx.MonHw2) -> tuple[str, capnp.lib.capnp._DynamicStructBuilder]: + dat = messaging.new_message('ubloxGnss', valid=True) + hw = dat.ubloxGnss.init('hwStatus2') + hw.ofsI = msg.ofs_i + hw.magI = msg.mag_i + hw.ofsQ = msg.ofs_q + hw.magQ = msg.mag_q + # Map Ubx enum to cereal enum {undefined=0, rom=1, otp=2, configpins=3, flash=4} + cfg_map = { + Ubx.MonHw2.ConfigSource.rom: 1, + Ubx.MonHw2.ConfigSource.otp: 2, + Ubx.MonHw2.ConfigSource.config_pins: 3, + Ubx.MonHw2.ConfigSource.flash: 4, + } + hw.cfgSource = cfg_map.get(msg.cfg_source, 0) + hw.lowLevCfg = msg.low_lev_cfg + hw.postStatus = msg.post_status + return ('ubloxGnss', dat) + + +def main(): + parser = UbloxMsgParser() + pm = messaging.PubMaster(['ubloxGnss', 'gpsLocationExternal']) + sock = messaging.sub_sock('ubloxRaw', timeout=100, conflate=False) + + while True: + msg = messaging.recv_one_or_none(sock) + if msg is None: + continue + + data = bytes(msg.ubloxRaw) + log_time = msg.logMonoTime * 1e-9 + frames = parser.framer.add_data(log_time, data) + for frame in frames: + try: + res = parser.parse_frame(frame) + except Exception: + continue + if not res: + continue + service, dat = res + pm.send(service, dat) + +if __name__ == '__main__': + main() diff --git a/third_party/SConscript b/third_party/SConscript index 507c17c4a5..3a7497d162 100644 --- a/third_party/SConscript +++ b/third_party/SConscript @@ -1,4 +1,3 @@ Import('env') env.Library('json11', ['json11/json11.cpp'], CCFLAGS=env['CCFLAGS'] + ['-Wno-unqualified-std-cast-call']) -env.Library('kaitai', ['kaitai/kaitaistream.cpp'], CPPDEFINES=['KS_STR_ENCODING_NONE']) diff --git a/third_party/kaitai/custom_decoder.h b/third_party/kaitai/custom_decoder.h deleted file mode 100644 index 6da7f5fd23..0000000000 --- a/third_party/kaitai/custom_decoder.h +++ /dev/null @@ -1,16 +0,0 @@ -#ifndef KAITAI_CUSTOM_DECODER_H -#define KAITAI_CUSTOM_DECODER_H - -#include - -namespace kaitai { - -class custom_decoder { -public: - virtual ~custom_decoder() {}; - virtual std::string decode(std::string src) = 0; -}; - -} - -#endif diff --git a/third_party/kaitai/exceptions.h b/third_party/kaitai/exceptions.h deleted file mode 100644 index 5c09c4672b..0000000000 --- a/third_party/kaitai/exceptions.h +++ /dev/null @@ -1,189 +0,0 @@ -#ifndef KAITAI_EXCEPTIONS_H -#define KAITAI_EXCEPTIONS_H - -#include - -#include -#include - -// We need to use "noexcept" in virtual destructor of our exceptions -// subclasses. Different compilers have different ideas on how to -// achieve that: C++98 compilers prefer `throw()`, C++11 and later -// use `noexcept`. We define KS_NOEXCEPT macro for that. - -#if __cplusplus >= 201103L || (defined(_MSC_VER) && _MSC_VER >= 1900) -#define KS_NOEXCEPT noexcept -#else -#define KS_NOEXCEPT throw() -#endif - -namespace kaitai { - -/** - * Common ancestor for all error originating from Kaitai Struct usage. - * Stores KSY source path, pointing to an element supposedly guilty of - * an error. - */ -class kstruct_error: public std::runtime_error { -public: - kstruct_error(const std::string what, const std::string src_path): - std::runtime_error(src_path + ": " + what), - m_src_path(src_path) - { - } - - virtual ~kstruct_error() KS_NOEXCEPT {}; - -protected: - const std::string m_src_path; -}; - -/** - * Error that occurs when default endianness should be decided with - * a switch, but nothing matches (although using endianness expression - * implies that there should be some positive result). - */ -class undecided_endianness_error: public kstruct_error { -public: - undecided_endianness_error(const std::string src_path): - kstruct_error("unable to decide on endianness for a type", src_path) - { - } - - virtual ~undecided_endianness_error() KS_NOEXCEPT {}; -}; - -/** - * Common ancestor for all validation failures. Stores pointer to - * KaitaiStream IO object which was involved in an error. - */ -class validation_failed_error: public kstruct_error { -public: - validation_failed_error(const std::string what, kstream* io, const std::string src_path): - kstruct_error("at pos " + kstream::to_string(static_cast(io->pos())) + ": validation failed: " + what, src_path), - m_io(io) - { - } - -// "at pos #{io.pos}: validation failed: #{msg}" - - virtual ~validation_failed_error() KS_NOEXCEPT {}; - -protected: - kstream* m_io; -}; - -/** - * Signals validation failure: we required "actual" value to be equal to - * "expected", but it turned out that it's not. - */ -template -class validation_not_equal_error: public validation_failed_error { -public: - validation_not_equal_error(const T& expected, const T& actual, kstream* io, const std::string src_path): - validation_failed_error("not equal", io, src_path), - m_expected(expected), - m_actual(actual) - { - } - - // "not equal, expected #{expected.inspect}, but got #{actual.inspect}" - - virtual ~validation_not_equal_error() KS_NOEXCEPT {}; - -protected: - const T& m_expected; - const T& m_actual; -}; - -/** - * Signals validation failure: we required "actual" value to be greater - * than or equal to "min", but it turned out that it's not. - */ -template -class validation_less_than_error: public validation_failed_error { -public: - validation_less_than_error(const T& min, const T& actual, kstream* io, const std::string src_path): - validation_failed_error("not in range", io, src_path), - m_min(min), - m_actual(actual) - { - } - - // "not in range, min #{min.inspect}, but got #{actual.inspect}" - - virtual ~validation_less_than_error() KS_NOEXCEPT {}; - -protected: - const T& m_min; - const T& m_actual; -}; - -/** - * Signals validation failure: we required "actual" value to be less - * than or equal to "max", but it turned out that it's not. - */ -template -class validation_greater_than_error: public validation_failed_error { -public: - validation_greater_than_error(const T& max, const T& actual, kstream* io, const std::string src_path): - validation_failed_error("not in range", io, src_path), - m_max(max), - m_actual(actual) - { - } - - // "not in range, max #{max.inspect}, but got #{actual.inspect}" - - virtual ~validation_greater_than_error() KS_NOEXCEPT {}; - -protected: - const T& m_max; - const T& m_actual; -}; - -/** - * Signals validation failure: we required "actual" value to be from - * the list, but it turned out that it's not. - */ -template -class validation_not_any_of_error: public validation_failed_error { -public: - validation_not_any_of_error(const T& actual, kstream* io, const std::string src_path): - validation_failed_error("not any of the list", io, src_path), - m_actual(actual) - { - } - - // "not any of the list, got #{actual.inspect}" - - virtual ~validation_not_any_of_error() KS_NOEXCEPT {}; - -protected: - const T& m_actual; -}; - -/** - * Signals validation failure: we required "actual" value to match - * the expression, but it turned out that it doesn't. - */ -template -class validation_expr_error: public validation_failed_error { -public: - validation_expr_error(const T& actual, kstream* io, const std::string src_path): - validation_failed_error("not matching the expression", io, src_path), - m_actual(actual) - { - } - - // "not matching the expression, got #{actual.inspect}" - - virtual ~validation_expr_error() KS_NOEXCEPT {}; - -protected: - const T& m_actual; -}; - -} - -#endif diff --git a/third_party/kaitai/kaitaistream.cpp b/third_party/kaitai/kaitaistream.cpp deleted file mode 100644 index d82ddb7e82..0000000000 --- a/third_party/kaitai/kaitaistream.cpp +++ /dev/null @@ -1,689 +0,0 @@ -#include - -#if defined(__APPLE__) -#include -#include -#define bswap_16(x) OSSwapInt16(x) -#define bswap_32(x) OSSwapInt32(x) -#define bswap_64(x) OSSwapInt64(x) -#define __BYTE_ORDER BYTE_ORDER -#define __BIG_ENDIAN BIG_ENDIAN -#define __LITTLE_ENDIAN LITTLE_ENDIAN -#elif defined(_MSC_VER) // !__APPLE__ -#include -#define __LITTLE_ENDIAN 1234 -#define __BIG_ENDIAN 4321 -#define __BYTE_ORDER __LITTLE_ENDIAN -#define bswap_16(x) _byteswap_ushort(x) -#define bswap_32(x) _byteswap_ulong(x) -#define bswap_64(x) _byteswap_uint64(x) -#else // !__APPLE__ or !_MSC_VER -#include -#include -#endif - -#include -#include -#include - -kaitai::kstream::kstream(std::istream* io) { - m_io = io; - init(); -} - -kaitai::kstream::kstream(std::string& data): m_io_str(data) { - m_io = &m_io_str; - init(); -} - -void kaitai::kstream::init() { - exceptions_enable(); - align_to_byte(); -} - -void kaitai::kstream::close() { - // m_io->close(); -} - -void kaitai::kstream::exceptions_enable() const { - m_io->exceptions( - std::istream::eofbit | - std::istream::failbit | - std::istream::badbit - ); -} - -// ======================================================================== -// Stream positioning -// ======================================================================== - -bool kaitai::kstream::is_eof() const { - if (m_bits_left > 0) { - return false; - } - char t; - m_io->exceptions( - std::istream::badbit - ); - m_io->get(t); - if (m_io->eof()) { - m_io->clear(); - exceptions_enable(); - return true; - } else { - m_io->unget(); - exceptions_enable(); - return false; - } -} - -void kaitai::kstream::seek(uint64_t pos) { - m_io->seekg(pos); -} - -uint64_t kaitai::kstream::pos() { - return m_io->tellg(); -} - -uint64_t kaitai::kstream::size() { - std::iostream::pos_type cur_pos = m_io->tellg(); - m_io->seekg(0, std::ios::end); - std::iostream::pos_type len = m_io->tellg(); - m_io->seekg(cur_pos); - return len; -} - -// ======================================================================== -// Integer numbers -// ======================================================================== - -// ------------------------------------------------------------------------ -// Signed -// ------------------------------------------------------------------------ - -int8_t kaitai::kstream::read_s1() { - char t; - m_io->get(t); - return t; -} - -// ........................................................................ -// Big-endian -// ........................................................................ - -int16_t kaitai::kstream::read_s2be() { - int16_t t; - m_io->read(reinterpret_cast(&t), 2); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_16(t); -#endif - return t; -} - -int32_t kaitai::kstream::read_s4be() { - int32_t t; - m_io->read(reinterpret_cast(&t), 4); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_32(t); -#endif - return t; -} - -int64_t kaitai::kstream::read_s8be() { - int64_t t; - m_io->read(reinterpret_cast(&t), 8); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_64(t); -#endif - return t; -} - -// ........................................................................ -// Little-endian -// ........................................................................ - -int16_t kaitai::kstream::read_s2le() { - int16_t t; - m_io->read(reinterpret_cast(&t), 2); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_16(t); -#endif - return t; -} - -int32_t kaitai::kstream::read_s4le() { - int32_t t; - m_io->read(reinterpret_cast(&t), 4); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_32(t); -#endif - return t; -} - -int64_t kaitai::kstream::read_s8le() { - int64_t t; - m_io->read(reinterpret_cast(&t), 8); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_64(t); -#endif - return t; -} - -// ------------------------------------------------------------------------ -// Unsigned -// ------------------------------------------------------------------------ - -uint8_t kaitai::kstream::read_u1() { - char t; - m_io->get(t); - return t; -} - -// ........................................................................ -// Big-endian -// ........................................................................ - -uint16_t kaitai::kstream::read_u2be() { - uint16_t t; - m_io->read(reinterpret_cast(&t), 2); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_16(t); -#endif - return t; -} - -uint32_t kaitai::kstream::read_u4be() { - uint32_t t; - m_io->read(reinterpret_cast(&t), 4); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_32(t); -#endif - return t; -} - -uint64_t kaitai::kstream::read_u8be() { - uint64_t t; - m_io->read(reinterpret_cast(&t), 8); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_64(t); -#endif - return t; -} - -// ........................................................................ -// Little-endian -// ........................................................................ - -uint16_t kaitai::kstream::read_u2le() { - uint16_t t; - m_io->read(reinterpret_cast(&t), 2); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_16(t); -#endif - return t; -} - -uint32_t kaitai::kstream::read_u4le() { - uint32_t t; - m_io->read(reinterpret_cast(&t), 4); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_32(t); -#endif - return t; -} - -uint64_t kaitai::kstream::read_u8le() { - uint64_t t; - m_io->read(reinterpret_cast(&t), 8); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_64(t); -#endif - return t; -} - -// ======================================================================== -// Floating point numbers -// ======================================================================== - -// ........................................................................ -// Big-endian -// ........................................................................ - -float kaitai::kstream::read_f4be() { - uint32_t t; - m_io->read(reinterpret_cast(&t), 4); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_32(t); -#endif - return reinterpret_cast(t); -} - -double kaitai::kstream::read_f8be() { - uint64_t t; - m_io->read(reinterpret_cast(&t), 8); -#if __BYTE_ORDER == __LITTLE_ENDIAN - t = bswap_64(t); -#endif - return reinterpret_cast(t); -} - -// ........................................................................ -// Little-endian -// ........................................................................ - -float kaitai::kstream::read_f4le() { - uint32_t t; - m_io->read(reinterpret_cast(&t), 4); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_32(t); -#endif - return reinterpret_cast(t); -} - -double kaitai::kstream::read_f8le() { - uint64_t t; - m_io->read(reinterpret_cast(&t), 8); -#if __BYTE_ORDER == __BIG_ENDIAN - t = bswap_64(t); -#endif - return reinterpret_cast(t); -} - -// ======================================================================== -// Unaligned bit values -// ======================================================================== - -void kaitai::kstream::align_to_byte() { - m_bits_left = 0; - m_bits = 0; -} - -uint64_t kaitai::kstream::read_bits_int_be(int n) { - int bits_needed = n - m_bits_left; - if (bits_needed > 0) { - // 1 bit => 1 byte - // 8 bits => 1 byte - // 9 bits => 2 bytes - int bytes_needed = ((bits_needed - 1) / 8) + 1; - if (bytes_needed > 8) - throw std::runtime_error("read_bits_int: more than 8 bytes requested"); - char buf[8]; - m_io->read(buf, bytes_needed); - for (int i = 0; i < bytes_needed; i++) { - uint8_t b = buf[i]; - m_bits <<= 8; - m_bits |= b; - m_bits_left += 8; - } - } - - // raw mask with required number of 1s, starting from lowest bit - uint64_t mask = get_mask_ones(n); - // shift mask to align with highest bits available in @bits - int shift_bits = m_bits_left - n; - mask <<= shift_bits; - // derive reading result - uint64_t res = (m_bits & mask) >> shift_bits; - // clear top bits that we've just read => AND with 1s - m_bits_left -= n; - mask = get_mask_ones(m_bits_left); - m_bits &= mask; - - return res; -} - -// Deprecated, use read_bits_int_be() instead. -uint64_t kaitai::kstream::read_bits_int(int n) { - return read_bits_int_be(n); -} - -uint64_t kaitai::kstream::read_bits_int_le(int n) { - int bits_needed = n - m_bits_left; - if (bits_needed > 0) { - // 1 bit => 1 byte - // 8 bits => 1 byte - // 9 bits => 2 bytes - int bytes_needed = ((bits_needed - 1) / 8) + 1; - if (bytes_needed > 8) - throw std::runtime_error("read_bits_int_le: more than 8 bytes requested"); - char buf[8]; - m_io->read(buf, bytes_needed); - for (int i = 0; i < bytes_needed; i++) { - uint8_t b = buf[i]; - m_bits |= (static_cast(b) << m_bits_left); - m_bits_left += 8; - } - } - - // raw mask with required number of 1s, starting from lowest bit - uint64_t mask = get_mask_ones(n); - // derive reading result - uint64_t res = m_bits & mask; - // remove bottom bits that we've just read by shifting - m_bits >>= n; - m_bits_left -= n; - - return res; -} - -uint64_t kaitai::kstream::get_mask_ones(int n) { - if (n == 64) { - return 0xFFFFFFFFFFFFFFFF; - } else { - return ((uint64_t) 1 << n) - 1; - } -} - -// ======================================================================== -// Byte arrays -// ======================================================================== - -std::string kaitai::kstream::read_bytes(std::streamsize len) { - std::vector result(len); - - // NOTE: streamsize type is signed, negative values are only *supposed* to not be used. - // http://en.cppreference.com/w/cpp/io/streamsize - if (len < 0) { - throw std::runtime_error("read_bytes: requested a negative amount"); - } - - if (len > 0) { - m_io->read(&result[0], len); - } - - return std::string(result.begin(), result.end()); -} - -std::string kaitai::kstream::read_bytes_full() { - std::iostream::pos_type p1 = m_io->tellg(); - m_io->seekg(0, std::ios::end); - std::iostream::pos_type p2 = m_io->tellg(); - size_t len = p2 - p1; - - // Note: this requires a std::string to be backed with a - // contiguous buffer. Officially, it's a only requirement since - // C++11 (C++98 and C++03 didn't have this requirement), but all - // major implementations had contiguous buffers anyway. - std::string result(len, ' '); - m_io->seekg(p1); - m_io->read(&result[0], len); - - return result; -} - -std::string kaitai::kstream::read_bytes_term(char term, bool include, bool consume, bool eos_error) { - std::string result; - std::getline(*m_io, result, term); - if (m_io->eof()) { - // encountered EOF - if (eos_error) { - throw std::runtime_error("read_bytes_term: encountered EOF"); - } - } else { - // encountered terminator - if (include) - result.push_back(term); - if (!consume) - m_io->unget(); - } - return result; -} - -std::string kaitai::kstream::ensure_fixed_contents(std::string expected) { - std::string actual = read_bytes(expected.length()); - - if (actual != expected) { - // NOTE: I think printing it outright is not best idea, it could contain non-ascii charactes like backspace and beeps and whatnot. It would be better to print hexlified version, and also to redirect it to stderr. - throw std::runtime_error("ensure_fixed_contents: actual data does not match expected data"); - } - - return actual; -} - -std::string kaitai::kstream::bytes_strip_right(std::string src, char pad_byte) { - std::size_t new_len = src.length(); - - while (new_len > 0 && src[new_len - 1] == pad_byte) - new_len--; - - return src.substr(0, new_len); -} - -std::string kaitai::kstream::bytes_terminate(std::string src, char term, bool include) { - std::size_t new_len = 0; - std::size_t max_len = src.length(); - - while (new_len < max_len && src[new_len] != term) - new_len++; - - if (include && new_len < max_len) - new_len++; - - return src.substr(0, new_len); -} - -// ======================================================================== -// Byte array processing -// ======================================================================== - -std::string kaitai::kstream::process_xor_one(std::string data, uint8_t key) { - size_t len = data.length(); - std::string result(len, ' '); - - for (size_t i = 0; i < len; i++) - result[i] = data[i] ^ key; - - return result; -} - -std::string kaitai::kstream::process_xor_many(std::string data, std::string key) { - size_t len = data.length(); - size_t kl = key.length(); - std::string result(len, ' '); - - size_t ki = 0; - for (size_t i = 0; i < len; i++) { - result[i] = data[i] ^ key[ki]; - ki++; - if (ki >= kl) - ki = 0; - } - - return result; -} - -std::string kaitai::kstream::process_rotate_left(std::string data, int amount) { - size_t len = data.length(); - std::string result(len, ' '); - - for (size_t i = 0; i < len; i++) { - uint8_t bits = data[i]; - result[i] = (bits << amount) | (bits >> (8 - amount)); - } - - return result; -} - -#ifdef KS_ZLIB -#include - -std::string kaitai::kstream::process_zlib(std::string data) { - int ret; - - unsigned char *src_ptr = reinterpret_cast(&data[0]); - std::stringstream dst_strm; - - z_stream strm; - strm.zalloc = Z_NULL; - strm.zfree = Z_NULL; - strm.opaque = Z_NULL; - - ret = inflateInit(&strm); - if (ret != Z_OK) - throw std::runtime_error("process_zlib: inflateInit error"); - - strm.next_in = src_ptr; - strm.avail_in = data.length(); - - unsigned char outbuffer[ZLIB_BUF_SIZE]; - std::string outstring; - - // get the decompressed bytes blockwise using repeated calls to inflate - do { - strm.next_out = reinterpret_cast(outbuffer); - strm.avail_out = sizeof(outbuffer); - - ret = inflate(&strm, 0); - - if (outstring.size() < strm.total_out) - outstring.append(reinterpret_cast(outbuffer), strm.total_out - outstring.size()); - } while (ret == Z_OK); - - if (ret != Z_STREAM_END) { // an error occurred that was not EOF - std::ostringstream exc_msg; - exc_msg << "process_zlib: error #" << ret << "): " << strm.msg; - throw std::runtime_error(exc_msg.str()); - } - - if (inflateEnd(&strm) != Z_OK) - throw std::runtime_error("process_zlib: inflateEnd error"); - - return outstring; -} -#endif - -// ======================================================================== -// Misc utility methods -// ======================================================================== - -int kaitai::kstream::mod(int a, int b) { - if (b <= 0) - throw std::invalid_argument("mod: divisor b <= 0"); - int r = a % b; - if (r < 0) - r += b; - return r; -} - -#include -std::string kaitai::kstream::to_string(int val) { - // if int is 32 bits, "-2147483648" is the longest string representation - // => 11 chars + zero => 12 chars - // if int is 64 bits, "-9223372036854775808" is the longest - // => 20 chars + zero => 21 chars - char buf[25]; - int got_len = snprintf(buf, sizeof(buf), "%d", val); - - // should never happen, but check nonetheless - if (got_len > sizeof(buf)) - throw std::invalid_argument("to_string: integer is longer than string buffer"); - - return std::string(buf); -} - -#include -std::string kaitai::kstream::reverse(std::string val) { - std::reverse(val.begin(), val.end()); - - return val; -} - -uint8_t kaitai::kstream::byte_array_min(const std::string val) { - uint8_t min = 0xff; // UINT8_MAX - std::string::const_iterator end = val.end(); - for (std::string::const_iterator it = val.begin(); it != end; ++it) { - uint8_t cur = static_cast(*it); - if (cur < min) { - min = cur; - } - } - return min; -} - -uint8_t kaitai::kstream::byte_array_max(const std::string val) { - uint8_t max = 0; // UINT8_MIN - std::string::const_iterator end = val.end(); - for (std::string::const_iterator it = val.begin(); it != end; ++it) { - uint8_t cur = static_cast(*it); - if (cur > max) { - max = cur; - } - } - return max; -} - -// ======================================================================== -// Other internal methods -// ======================================================================== - -#ifndef KS_STR_DEFAULT_ENCODING -#define KS_STR_DEFAULT_ENCODING "UTF-8" -#endif - -#ifdef KS_STR_ENCODING_ICONV - -#include -#include -#include - -std::string kaitai::kstream::bytes_to_str(std::string src, std::string src_enc) { - iconv_t cd = iconv_open(KS_STR_DEFAULT_ENCODING, src_enc.c_str()); - - if (cd == (iconv_t) -1) { - if (errno == EINVAL) { - throw std::runtime_error("bytes_to_str: invalid encoding pair conversion requested"); - } else { - throw std::runtime_error("bytes_to_str: error opening iconv"); - } - } - - size_t src_len = src.length(); - size_t src_left = src_len; - - // Start with a buffer length of double the source length. - size_t dst_len = src_len * 2; - std::string dst(dst_len, ' '); - size_t dst_left = dst_len; - - char *src_ptr = &src[0]; - char *dst_ptr = &dst[0]; - - while (true) { - size_t res = iconv(cd, &src_ptr, &src_left, &dst_ptr, &dst_left); - - if (res == (size_t) -1) { - if (errno == E2BIG) { - // dst buffer is not enough to accomodate whole string - // enlarge the buffer and try again - size_t dst_used = dst_len - dst_left; - dst_left += dst_len; - dst_len += dst_len; - dst.resize(dst_len); - - // dst.resize might have allocated destination buffer in another area - // of memory, thus our previous pointer "dst" will be invalid; re-point - // it using "dst_used". - dst_ptr = &dst[dst_used]; - } else { - throw std::runtime_error("bytes_to_str: iconv error"); - } - } else { - // conversion successful - dst.resize(dst_len - dst_left); - break; - } - } - - if (iconv_close(cd) != 0) { - throw std::runtime_error("bytes_to_str: iconv close error"); - } - - return dst; -} -#elif defined(KS_STR_ENCODING_NONE) -std::string kaitai::kstream::bytes_to_str(std::string src, std::string src_enc) { - return src; -} -#else -#error Need to decide how to handle strings: please define one of: KS_STR_ENCODING_ICONV, KS_STR_ENCODING_NONE -#endif diff --git a/third_party/kaitai/kaitaistream.h b/third_party/kaitai/kaitaistream.h deleted file mode 100644 index e7f4c6ce34..0000000000 --- a/third_party/kaitai/kaitaistream.h +++ /dev/null @@ -1,268 +0,0 @@ -#ifndef KAITAI_STREAM_H -#define KAITAI_STREAM_H - -// Kaitai Struct runtime API version: x.y.z = 'xxxyyyzzz' decimal -#define KAITAI_STRUCT_VERSION 9000L - -#include -#include -#include -#include - -namespace kaitai { - -/** - * Kaitai Stream class (kaitai::kstream) is an implementation of - * Kaitai Struct stream API - * for C++/STL. It's implemented as a wrapper over generic STL std::istream. - * - * It provides a wide variety of simple methods to read (parse) binary - * representations of primitive types, such as integer and floating - * point numbers, byte arrays and strings, and also provides stream - * positioning / navigation methods with unified cross-language and - * cross-toolkit semantics. - * - * Typically, end users won't access Kaitai Stream class manually, but would - * describe a binary structure format using .ksy language and then would use - * Kaitai Struct compiler to generate source code in desired target language. - * That code, in turn, would use this class and API to do the actual parsing - * job. - */ -class kstream { -public: - /** - * Constructs new Kaitai Stream object, wrapping a given std::istream. - * \param io istream object to use for this Kaitai Stream - */ - kstream(std::istream* io); - - /** - * Constructs new Kaitai Stream object, wrapping a given in-memory data - * buffer. - * \param data data buffer to use for this Kaitai Stream - */ - kstream(std::string& data); - - void close(); - - /** @name Stream positioning */ - //@{ - /** - * Check if stream pointer is at the end of stream. Note that the semantics - * are different from traditional STL semantics: one does *not* need to do a - * read (which will fail) after the actual end of the stream to trigger EOF - * flag, which can be accessed after that read. It is sufficient to just be - * at the end of the stream for this method to return true. - * \return "true" if we are located at the end of the stream. - */ - bool is_eof() const; - - /** - * Set stream pointer to designated position. - * \param pos new position (offset in bytes from the beginning of the stream) - */ - void seek(uint64_t pos); - - /** - * Get current position of a stream pointer. - * \return pointer position, number of bytes from the beginning of the stream - */ - uint64_t pos(); - - /** - * Get total size of the stream in bytes. - * \return size of the stream in bytes - */ - uint64_t size(); - //@} - - /** @name Integer numbers */ - //@{ - - // ------------------------------------------------------------------------ - // Signed - // ------------------------------------------------------------------------ - - int8_t read_s1(); - - // ........................................................................ - // Big-endian - // ........................................................................ - - int16_t read_s2be(); - int32_t read_s4be(); - int64_t read_s8be(); - - // ........................................................................ - // Little-endian - // ........................................................................ - - int16_t read_s2le(); - int32_t read_s4le(); - int64_t read_s8le(); - - // ------------------------------------------------------------------------ - // Unsigned - // ------------------------------------------------------------------------ - - uint8_t read_u1(); - - // ........................................................................ - // Big-endian - // ........................................................................ - - uint16_t read_u2be(); - uint32_t read_u4be(); - uint64_t read_u8be(); - - // ........................................................................ - // Little-endian - // ........................................................................ - - uint16_t read_u2le(); - uint32_t read_u4le(); - uint64_t read_u8le(); - - //@} - - /** @name Floating point numbers */ - //@{ - - // ........................................................................ - // Big-endian - // ........................................................................ - - float read_f4be(); - double read_f8be(); - - // ........................................................................ - // Little-endian - // ........................................................................ - - float read_f4le(); - double read_f8le(); - - //@} - - /** @name Unaligned bit values */ - //@{ - - void align_to_byte(); - uint64_t read_bits_int_be(int n); - uint64_t read_bits_int(int n); - uint64_t read_bits_int_le(int n); - - //@} - - /** @name Byte arrays */ - //@{ - - std::string read_bytes(std::streamsize len); - std::string read_bytes_full(); - std::string read_bytes_term(char term, bool include, bool consume, bool eos_error); - std::string ensure_fixed_contents(std::string expected); - - static std::string bytes_strip_right(std::string src, char pad_byte); - static std::string bytes_terminate(std::string src, char term, bool include); - static std::string bytes_to_str(std::string src, std::string src_enc); - - //@} - - /** @name Byte array processing */ - //@{ - - /** - * Performs a XOR processing with given data, XORing every byte of input with a single - * given value. - * @param data data to process - * @param key value to XOR with - * @return processed data - */ - static std::string process_xor_one(std::string data, uint8_t key); - - /** - * Performs a XOR processing with given data, XORing every byte of input with a key - * array, repeating key array many times, if necessary (i.e. if data array is longer - * than key array). - * @param data data to process - * @param key array of bytes to XOR with - * @return processed data - */ - static std::string process_xor_many(std::string data, std::string key); - - /** - * Performs a circular left rotation shift for a given buffer by a given amount of bits, - * using groups of 1 bytes each time. Right circular rotation should be performed - * using this procedure with corrected amount. - * @param data source data to process - * @param amount number of bits to shift by - * @return copy of source array with requested shift applied - */ - static std::string process_rotate_left(std::string data, int amount); - -#ifdef KS_ZLIB - /** - * Performs an unpacking ("inflation") of zlib-compressed data with usual zlib headers. - * @param data data to unpack - * @return unpacked data - * @throws IOException - */ - static std::string process_zlib(std::string data); -#endif - - //@} - - /** - * Performs modulo operation between two integers: dividend `a` - * and divisor `b`. Divisor `b` is expected to be positive. The - * result is always 0 <= x <= b - 1. - */ - static int mod(int a, int b); - - /** - * Converts given integer `val` to a decimal string representation. - * Should be used in place of std::to_string() (which is available only - * since C++11) in older C++ implementations. - */ - static std::string to_string(int val); - - /** - * Reverses given string `val`, so that the first character becomes the - * last and the last one becomes the first. This should be used to avoid - * the need of local variables at the caller. - */ - static std::string reverse(std::string val); - - /** - * Finds the minimal byte in a byte array, treating bytes as - * unsigned values. - * @param val byte array to scan - * @return minimal byte in byte array as integer - */ - static uint8_t byte_array_min(const std::string val); - - /** - * Finds the maximal byte in a byte array, treating bytes as - * unsigned values. - * @param val byte array to scan - * @return maximal byte in byte array as integer - */ - static uint8_t byte_array_max(const std::string val); - -private: - std::istream* m_io; - std::istringstream m_io_str; - int m_bits_left; - uint64_t m_bits; - - void init(); - void exceptions_enable() const; - - static uint64_t get_mask_ones(int n); - - static const int ZLIB_BUF_SIZE = 128 * 1024; -}; - -} - -#endif diff --git a/third_party/kaitai/kaitaistruct.h b/third_party/kaitai/kaitaistruct.h deleted file mode 100644 index 8172ede6c9..0000000000 --- a/third_party/kaitai/kaitaistruct.h +++ /dev/null @@ -1,20 +0,0 @@ -#ifndef KAITAI_STRUCT_H -#define KAITAI_STRUCT_H - -#include - -namespace kaitai { - -class kstruct { -public: - kstruct(kstream *_io) { m__io = _io; } - virtual ~kstruct() {} -protected: - kstream *m__io; -public: - kstream *_io() { return m__io; } -}; - -} - -#endif From 698e0ca00f53f8ed211aee698d48ace29302d57c Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 7 Sep 2025 23:23:03 -0400 Subject: [PATCH 138/188] migration: new branch names (#1225) * migration: new branch names * more migration * update channel type * no more var * update * more * more --- .github/workflows/auto_pr_review.yaml | 8 ++++---- .github/workflows/sunnypilot-build-prebuilt.yaml | 6 +++--- ...c3-prep.yaml => sunnypilot-master-dev-prep.yaml} | 12 ++++++------ release/ci/squash_and_merge_prs.py | 2 +- system/version.py | 13 ++++++++----- 5 files changed, 22 insertions(+), 19 deletions(-) rename .github/workflows/{sunnypilot-master-dev-c3-prep.yaml => sunnypilot-master-dev-prep.yaml} (95%) diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index b9664b9066..cedeee1741 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -40,10 +40,10 @@ jobs: runs-on: ubuntu-latest if: (github.event.pull_request.head.repo.fork && (contains(github.event_name, 'pull_request') && github.event.action == 'synchronize')) env: - PR_LABEL: 'dev-c3' + PR_LABEL: 'dev' TRUST_FORK_PR_LABEL: 'trust-fork-pr' steps: - - name: Check if PR has dev-c3 label + - name: Check if PR has dev label id: check-labels uses: actions/github-script@v7 with: @@ -62,11 +62,11 @@ jobs: console.log(`PR #${prNumber} has ${process.env.PR_LABEL} label: ${hasDevC3Label}`); console.log(`PR #${prNumber} has ${process.env.TRUST_FORK_PR_LABEL} label: ${hasTrustLabel}`); - core.setOutput('has-dev-c3', hasDevC3Label ? 'true' : 'false'); + core.setOutput('has-dev', hasDevC3Label ? 'true' : 'false'); core.setOutput('has-trust', hasTrustLabel ? 'true' : 'false'); - name: Remove trust-fork-pr label if present - if: steps.check-labels.outputs.has-dev-c3 == 'true' && steps.check-labels.outputs.has-trust == 'true' + if: steps.check-labels.outputs.has-dev == 'true' && steps.check-labels.outputs.has-trust == 'true' uses: actions/github-script@v7 with: github-token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index d654f5ab46..00ae1e28bf 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -8,14 +8,14 @@ env: PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" # Branch configurations - STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master' }} # vars are set on repo settings. + STAGING_SOURCE_BRANCH: 'master' # Runtime configuration SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}" on: push: - branches: [ master, master-dev-c3-new ] + branches: [ master, master-dev ] tags: [ 'release/*' ] pull_request_target: types: [ labeled ] @@ -138,7 +138,7 @@ jobs: # for security. Only caches from the default branch are shared across all builds. This is by design and cannot be overridden. restore-keys: | scons-${{ runner.os }}-${{ runner.arch }}-${{ env.SOURCE_BRANCH }} - scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_C3_SOURCE_BRANCH }} + scons-${{ runner.os }}-${{ runner.arch }}-${{ env.STAGING_SOURCE_BRANCH }} scons-${{ runner.os }}-${{ runner.arch }} - name: Set environment variables diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-prep.yaml similarity index 95% rename from .github/workflows/sunnypilot-master-dev-c3-prep.yaml rename to .github/workflows/sunnypilot-master-dev-prep.yaml index d4c201824e..122755f2c4 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-prep.yaml @@ -1,9 +1,9 @@ -name: Build dev-c3-new +name: Build dev env: DEFAULT_SOURCE_BRANCH: "master" - DEFAULT_TARGET_BRANCH: "master-dev-c3-new" - PR_LABEL: "dev-c3" + DEFAULT_TARGET_BRANCH: "master-dev" + PR_LABEL: "dev" LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' LFS_PUSH_URL: 'ssh://git@gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git' @@ -25,7 +25,7 @@ on: target_branch: description: 'Target branch to reset and squash into' required: true - default: 'master-dev-c3-new' + default: 'master-dev' type: string cancel_in_progress: description: 'Cancel any in-progress runs of this workflow' @@ -43,7 +43,7 @@ jobs: if: ( (github.event_name == 'workflow_dispatch') || (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev')))) ) steps: - uses: actions/checkout@v4 @@ -55,7 +55,7 @@ jobs: uses: ./.github/workflows/wait-for-action # Path to where you place the action if: ( (github.event_name == 'push' && github.ref == format('refs/heads/{0}', github.event.repository.default_branch)) - || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev-c3' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev-c3')))) + || (contains(github.event_name, 'pull_request') && ((github.event.action == 'labeled' && (github.event.label.name == 'dev' || github.event.label.name == 'trust-fork-pr') && contains(github.event.pull_request.labels.*.name, 'dev')))) ) with: workflow: selfdrive_tests.yaml # The workflow file to monitor diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index 0f20f4f900..24922288be 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -14,7 +14,7 @@ def setup_argument_parser(): parser.add_argument('--pr-data', type=str, help='PR data in JSON format') parser.add_argument('--source-branch', type=str, default='master', help='Source branch for merging') - parser.add_argument('--target-branch', type=str, default='master-dev-c3-new-test', + parser.add_argument('--target-branch', type=str, default='master-dev-test', help='Target branch for merging') parser.add_argument('--squash-script-path', type=str, required=True, help='Path to the squash_and_merge.py script') diff --git a/system/version.py b/system/version.py index 87044b84a8..9719311b7e 100755 --- a/system/version.py +++ b/system/version.py @@ -10,8 +10,8 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.swaglog import cloudlog from openpilot.common.git import get_commit, get_origin, get_branch, get_short_branch, get_commit_date -RELEASE_SP_BRANCHES = ['release-c3'] -TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new'] +RELEASE_SP_BRANCHES = ['release-c3', 'release'] +TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new', 'staging'] MASTER_SP_BRANCHES = ['master'] RELEASE_BRANCHES = ['release3-staging', 'release3', 'release-tici', 'nightly'] + RELEASE_SP_BRANCHES TESTED_BRANCHES = RELEASE_BRANCHES + ['devel', 'devel-staging', 'nightly-dev'] + TESTED_SP_BRANCHES @@ -21,6 +21,9 @@ SP_BRANCH_MIGRATIONS = { ("tici", "dev-c3-new"): "staging-tici", ("tici", "master"): "master-tici", ("tici", "master-dev-c3-new"): "master-tici", + ("tizi", "staging-c3-new"): "staging", + ("tizi", "dev-c3-new"): "dev", + ("tizi", "master-dev-c3-new"): "master-dev", } BUILD_METADATA_FILENAME = "build.json" @@ -131,7 +134,7 @@ class BuildMetadata: @property def development_channel(self) -> bool: - return self.channel.startswith("dev-") or self.channel.endswith("-prebuilt") + return self.channel == "dev" or self.channel.startswith("dev-") or self.channel.endswith("-prebuilt") @property def channel_type(self) -> str: @@ -139,11 +142,11 @@ class BuildMetadata: return "tici" elif self.development_channel: return "development" - elif self.channel.startswith("staging-"): + elif self.tested_channel: return "staging" elif self.master_channel: return "master" - elif self.tested_channel: + elif self.release_channel: return "release" else: return "feature" From 0739d4ac2d9ef05dd1dad3143a957c1c663f627c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 8 Sep 2025 09:01:53 -0700 Subject: [PATCH 139/188] [bot] Update translations (#36089) Update translations Co-authored-by: Vehicle Researcher --- selfdrive/ui/translations/main_ar.ts | 14 -------------- selfdrive/ui/translations/main_de.ts | 14 -------------- selfdrive/ui/translations/main_es.ts | 14 -------------- selfdrive/ui/translations/main_fr.ts | 14 -------------- selfdrive/ui/translations/main_ja.ts | 14 -------------- selfdrive/ui/translations/main_ko.ts | 14 -------------- selfdrive/ui/translations/main_pt-BR.ts | 16 ---------------- selfdrive/ui/translations/main_th.ts | 14 -------------- selfdrive/ui/translations/main_tr.ts | 14 -------------- selfdrive/ui/translations/main_zh-CHS.ts | 16 ---------------- selfdrive/ui/translations/main_zh-CHT.ts | 16 ---------------- 11 files changed, 160 deletions(-) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index ff2e7ab05c..381712770e 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -482,10 +482,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. يتم تنزيل تحديث لنظام تشغيل جهازك ÙÙŠ الخلÙية. سيطلَب منك التحديث عندما يصبح جاهزاً للتثبيت. - - NVMe drive not mounted. - محرك NVMe غير مثبَّت. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. لم يكن openpilot قادراً على تحديد سيارتك. إما أن تكون سيارتك غير مدعومة أو أنه لم يتم التعر٠على وحدة التحكم الإلكتروني (ECUs) Ùيها. يرجى تقديم طلب سحب من أجل Ø¥Ø¶Ø§ÙØ© نسخ برمجيات ثابتة إلى السيارة المناسبة. هل تحتاج إلى أي مساعدة؟ لا تتردد ÙÙŠ التواصل مع doscord.comma.ai. @@ -1052,16 +1048,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 52fe55720d..28b07029eb 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -470,10 +470,6 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Ein Update für das Betriebssystem deines Geräts wird im Hintergrund heruntergeladen. Du wirst aufgefordert, das Update zu installieren, sobald es bereit ist. - - NVMe drive not mounted. - NVMe-Laufwerk nicht gemounted. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. openpilot konnte dein Auto nicht identifizieren. Dein Auto wird entweder nicht unterstützt oder die Steuergeräte (ECUs) werden nicht erkannt. Bitte reiche einen Pull Request ein, um die Firmware-Versionen für das richtige Fahrzeug hinzuzufügen. Hilfe findest du auf discord.comma.ai. @@ -1034,16 +1030,6 @@ Der Firehose-Modus ermöglicht es dir, deine Trainingsdaten-Uploads zu maximiere Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 145eb7ae67..e8d57dddbe 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -478,10 +478,6 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Se está descargando una actualización del sistema operativo de su dispositivo en segundo plano. Se le pedirá que actualice cuando esté listo para instalarse. - - NVMe drive not mounted. - Unidad NVMe no montada. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. openpilot no pudo identificar su automóvil. Su automóvil no es compatible o no se reconocen sus ECU. Por favor haga un pull request para agregar las versiones de firmware del vehículo adecuado. ¿Necesita ayuda? Únase a discord.comma.ai. @@ -1036,16 +1032,6 @@ El Modo Firehose te permite maximizar las subidas de datos de entrenamiento para Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. Graba y almacena el audio del micrófono mientras conduces. El audio se incluirá en el video de la cámara del tablero en comma connect. - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 297d936139..4efb92d0c8 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -472,10 +472,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Une mise à jour du système d'exploitation de votre appareil est en cours de téléchargement en arrière-plan. Vous serez invité à effectuer la mise à jour lorsqu'elle sera prête à être installée. - - NVMe drive not mounted. - Le disque NVMe n'est pas monté. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. openpilot n'a pas pu identifier votre voiture. Votre voiture n'est pas supportée ou ses ECUs ne sont pas reconnues. Veuillez soumettre un pull request pour ajouter les versions de firmware au véhicule approprié. Besoin d'aide ? Rejoignez discord.comma.ai. @@ -1030,16 +1026,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index 7bb5c5364f..4307cee91f 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -472,10 +472,6 @@ Firehoseモードを有効ã«ã™ã‚‹ã¨å­¦ç¿’データを最大é™ã‚¢ãƒƒãƒ—ロー An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. オペレーティングシステムãŒãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã§ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ã•れã¦ã„ã¾ã™ã€‚ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã®æº–å‚™ãŒæ•´ã†ã¨æ›´æ–°ã‚’促ã•れã¾ã™ã€‚ - - NVMe drive not mounted. - SSDドライブ(NVMe)ãŒãƒžã‚¦ãƒ³ãƒˆã•れã¦ã„ã¾ã›ã‚“。 - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. openpilotãŒè»Šä¸¡ã‚’識別ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚è»ŠãŒæœªå¯¾å¿œã¾ãŸã¯ECUãŒèªè­˜ã•れã¦ã„ãªã„å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚該当車両ã®ãƒ•ァームウェアãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’追加ã™ã‚‹ãŸã‚ã«ãƒ—ルリクエストã—ã¦ãã ã•ã„。サãƒãƒ¼ãƒˆãŒå¿…è¦ãªå ´åˆã¯ discord.comma.ai ã«å‚加ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ @@ -1031,16 +1027,6 @@ Firehoseモードを有効ã«ã™ã‚‹ã¨å­¦ç¿’データを最大é™ã‚¢ãƒƒãƒ—ロー Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. é‹è»¢ä¸­ã«ãƒžã‚¤ã‚¯éŸ³å£°ã‚’録音・ä¿å­˜ã—ã¾ã™ã€‚音声㯠comma connect ã®ãƒ‰ãƒ©ã‚¤ãƒ–レコーダー映åƒã«å«ã¾ã‚Œã¾ã™ã€‚ - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 807996f182..9ab43dd9b8 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -472,10 +472,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. 백그ë¼ìš´ë“œì—서 ìš´ì˜ ì²´ì œì— ëŒ€í•œ ì—…ë°ì´íŠ¸ê°€ 다운로드ë˜ê³  있습니다. 설치가 준비ë˜ë©´ ì—…ë°ì´íЏ 메시지가 표시ë©ë‹ˆë‹¤. - - NVMe drive not mounted. - NVMe 드ë¼ì´ë¸Œê°€ 마운트ë˜ì§€ 않았습니다. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. 오픈파ì¼ëŸ¿ì´ ì°¨ëŸ‰ì„ ì‹ë³„í•  수 없습니다. ì§€ì›ë˜ì§€ 않는 차량ì´ê±°ë‚˜ ECUê°€ ì¸ì‹ë˜ì§€ 않습니다. 해당 ì°¨ëŸ‰ì— ë§žëŠ” 펌웨어 ë²„ì „ì„ ì¶”ê°€í•˜ë ¤ë©´ PRì„ ì œì¶œí•˜ì„¸ìš”. ë„ì›€ì´ í•„ìš”í•˜ì‹œë©´ discord.comma.aiì— ì°¸ì—¬í•˜ì„¸ìš”. @@ -1031,16 +1027,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. ìš´ì „ ì¤‘ì— ë§ˆì´í¬ 오디오를 ë…¹ìŒí•˜ê³  저장하십시오. 오디오는 comma connectì˜ ëŒ€ì‹œìº  ë¹„ë””ì˜¤ì— í¬í•¨ë©ë‹ˆë‹¤. - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index 7f41588e8f..77aa5e07c1 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -474,10 +474,6 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. Uma atualização para o sistema operacional do seu dispositivo está sendo baixada em segundo plano. Você será solicitado a atualizar quando estiver pronto para instalar. - - NVMe drive not mounted. - Unidade NVMe não montada. - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. O openpilot não conseguiu identificar o seu carro. Seu carro não é suportado ou seus ECUs não são reconhecidos. Envie um pull request para adicionar as versões de firmware ao veículo adequado. Precisa de ajuda? Junte-se discord.comma.ai. @@ -1036,18 +1032,6 @@ O Modo Firehose permite maximizar o envio de dados de treinamento para melhorar Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. Grave e armazene o áudio do microfone enquanto estiver dirigindo. O áudio será incluído ao vídeo dashcam no comma connect. - - Record Audio Feedback with LKAS button - Gravar feedback de áudio com o botão LKAS - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - Pressione o botão LKAS para gravar e compartilhar feedback de direção com a equipe do openpilot. Quando esta opção estiver desativada, o botão funcionará como um botão de marcador. O evento será destacado no comma connect e o segmento será preservado no armazenamento do seu dispositivo. - -Observe que este recurso é compatível apenas com alguns modelos de carros. - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 34824c2680..9bd6822086 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -472,10 +472,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. à¸à¸³à¸¥à¸±à¸‡à¸”าวน์โหลดอัปเดทสำหรับระบบปà¸à¸´à¸šà¸±à¸•ิà¸à¸²à¸£à¸­à¸¢à¸¹à¹ˆà¹€à¸šà¸·à¹‰à¸­à¸‡à¸«à¸¥à¸±à¸‡ คุณจะได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ือนเมื่อระบบพร้อมสำหรับà¸à¸²à¸£à¸•ิดตั้ง - - NVMe drive not mounted. - ไม่ได้ติดตั้งไดร์ฟ NVMe - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. openpilot ไม่สามารถระบุรถยนต์ของคุณได้ ระบบอาจไม่รองรับรถยนต์ของคุณหรือไม่รู้จัภECU à¸à¸£à¸¸à¸“าส่ง pull request เพื่อเพิ่มรุ่นของเฟิร์มà¹à¸§à¸£à¹Œà¹ƒà¸«à¹‰à¸à¸±à¸šà¸£à¸–ยนต์ที่เหมาะสม หาà¸à¸•้องà¸à¸²à¸£à¸„วามช่วยเหลือให้เข้าร่วม discord.comma.ai @@ -1027,16 +1023,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index db6c4283a8..7c6c692d54 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -469,10 +469,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. - - NVMe drive not mounted. - - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. @@ -1024,16 +1020,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. - - Record Audio Feedback with LKAS button - - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 667d81ddd4..9481e62f10 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -472,10 +472,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. 一个针对您设备的æ“作系统更新正在åŽå°ä¸‹è½½ä¸­ã€‚当更新准备好安装时,您将收到æç¤ºè¿›è¡Œæ›´æ–°ã€‚ - - NVMe drive not mounted. - NVMe固æ€ç¡¬ç›˜æœªè¢«æŒ‚载。 - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. openpilot 无法识别您的车辆。您的车辆å¯èƒ½æœªè¢«æ”¯æŒï¼Œæˆ–是其电控å•å…ƒ (ECU) 未被识别。请æäº¤ä¸€ä¸ª Pull Request 为您的车辆添加正确的固件版本。需è¦å¸®åŠ©å—?请加入 discord.comma.ai。 @@ -1031,18 +1027,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. 在驾驶时录制并存储麦克风音频。该音频将会包å«åœ¨ comma connect 的行车记录仪视频中。 - - Record Audio Feedback with LKAS button - 使用“车é“ä¿æŒâ€æŒ‰é’®å½•制音频å馈 - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - 按下“车é“ä¿æŒâ€æŒ‰é’®ï¼Œå³å¯å½•制并分享驾驶å馈给 openpilot 团队。当此开关ç¦ç”¨æ—¶ï¼Œè¯¥æŒ‰é’®å°†ç”¨ä½œä¹¦ç­¾æŒ‰é’®ã€‚该事件将在 comma connect 中高亮显示,且对应的视频片段将被ä¿ç•™åœ¨æ‚¨çš„设备存储空间中。 - -请注æ„,此功能仅兼容部分车型。 - WiFiPromptWidget diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index a0f1997a00..c3ef55d3d9 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -472,10 +472,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp An update to your device's operating system is downloading in the background. You will be prompted to update when it's ready to install. 一個有關æ“作系統的更新正在後å°ä¸‹è¼‰ä¸­ã€‚ç•¶æ›´æ–°æº–å‚™å¥½å®‰è£æ™‚,您將收到æç¤ºé€²è¡Œæ›´æ–°ã€‚ - - NVMe drive not mounted. - NVMe 固態硬碟未被掛載。 - openpilot was unable to identify your car. Your car is either unsupported or its ECUs are not recognized. Please submit a pull request to add the firmware versions to the proper vehicle. Need help? Join discord.comma.ai. openpilot 無法識別您的車輛。您的車輛å¯èƒ½æœªè¢«æ”¯æ´ï¼Œæˆ–是其電控單元 (ECU) 未被識別。請æäº¤ä¸€å€‹ Pull Request 為您的車輛添加正確的韌體版本。需è¦å¹«åŠ©å—Žï¼Ÿè«‹åŠ å…¥ discord.comma.ai 。 @@ -1031,18 +1027,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. 在駕駛時錄製並儲存麥克風音訊。此音訊將會收錄在 comma connect 的行車記錄器影片中。 - - Record Audio Feedback with LKAS button - 使用「車é“ç¶­æŒã€æŒ‰éˆ•錄製音訊回饋 - - - Press the LKAS button to record and share driving feedback with the openpilot team. When this toggle is disabled, the button acts as a bookmark button. The event will be highlighted in comma connect and the segment will be preserved on your device's storage. - -Note that this feature is only compatible with select cars. - 按下「車é“ç¶­æŒã€æŒ‰éˆ•,å³å¯éŒ„製並分享駕駛回饋給 openpilot 團隊。當此開關åœç”¨æ™‚,該按鈕的功能將轉為書籤按鈕。該事件將會在 comma connect ä¸­è¢«æ¨™è¨»ï¼Œä¸”å°æ‡‰çš„路段影åƒå°‡ä¿ç•™åœ¨æ‚¨çš„è£ç½®å„²å­˜ç©ºé–“中。 - -請注æ„,此功能僅與特定車款相容。 - WiFiPromptWidget From 014213d867976f0f6e52502433f39d6a8787e1c4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 8 Sep 2025 21:09:25 -0400 Subject: [PATCH 140/188] typo --- .../lib/speed_limit_controller/speed_limit_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 1aec76cc98..fff5e26be7 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -265,7 +265,7 @@ class SpeedLimitController: self.state_control() self.update_events(events_sp) - # Update change tracking variablesZ + # Update change tracking variables self.speed_limit_prev = self._speed_limit self.v_cruise_setpoint_prev = self.v_cruise_setpoint self.op_engaged_prev = self.op_engaged From 6278b9000cfb351233a3d39783269a6e74123a89 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 8 Sep 2025 22:27:24 -0400 Subject: [PATCH 141/188] one method state machine --- .../speed_limit_controller.py | 123 +++++++++--------- 1 file changed, 59 insertions(+), 64 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index fff5e26be7..c90f44c769 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -61,16 +61,6 @@ class SpeedLimitController: self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) - # Mapping functions to state transitions - self._state_transition_strategy = { - SpeedLimitControlState.disabled: self.transition_state_from_disabled, - SpeedLimitControlState.inactive: self.transition_state_from_inactive, - SpeedLimitControlState.preActive: self.transition_state_from_preactive, - SpeedLimitControlState.pending: self.transition_state_from_pending, - SpeedLimitControlState.adapting: self.transition_state_from_adapting, - SpeedLimitControlState.active: self.transition_state_from_active, - } - # Solution functions mapped to respective states self.acceleration_solutions = { SpeedLimitControlState.disabled: self.get_current_acceleration_as_target, @@ -178,59 +168,6 @@ class SpeedLimitController: if not self._state_prev == SpeedLimitControlState.preActive and self.state == SpeedLimitControlState.preActive: self.last_preactive_frame = self.frame - def transition_state_from_disabled(self) -> None: - # Wait 2 seconds after long engaged before starting fresh session - if (self.frame - self.last_op_engaged_frame) * DT_MDL >= 2.: - self.state = SpeedLimitControlState.preActive - self.initial_max_set = False - - def transition_state_from_inactive(self) -> None: - pass - - def transition_state_from_preactive(self) -> None: - if self.initial_max_set_confirmed(): - self.initial_max_set = True - if self._speed_limit > 0: - if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting - else: - self.state = SpeedLimitControlState.active - else: - self.state = SpeedLimitControlState.pending - elif (self.frame - self.last_preactive_frame) * DT_MDL >= PRE_ACTIVE_GUARD_PERIOD: - # Timeout - session ended - self.state = SpeedLimitControlState.inactive - - def transition_state_from_pending(self) -> None: - if self._speed_limit > 0: - if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting - else: - self.state = SpeedLimitControlState.active - - def transition_state_from_adapting(self) -> None: - if self.detect_manual_cruise_change(): - self.state = SpeedLimitControlState.inactive - elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.active - - def transition_state_from_active(self) -> None: - if self.detect_manual_cruise_change(): - self.state = SpeedLimitControlState.inactive - elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting - - def state_control(self) -> None: - self._state_prev = self.state - - # If op is disabled or SLC is disabled, go to disabled state - if not self.op_engaged or not self.enabled: - self.state = SpeedLimitControlState.disabled - self.initial_max_set = False - return - - self._state_transition_strategy[self.state]() - def get_current_acceleration_as_target(self) -> float: return self.a_ego @@ -252,6 +189,64 @@ class SpeedLimitController: elif self.speed_limit_changed: events_sp.add(EventNameSP.speedLimitValueChange) + def update_state_machine(self): + self._state_prev = self.state + + if self.state != SpeedLimitControlState.disabled: + if not self.op_engaged or not self.enabled: + self.state = SpeedLimitControlState.disabled + self.initial_max_set = False + + else: + # ACTIVE + if self.state == SpeedLimitControlState.active: + if self.detect_manual_cruise_change(): + self.state = SpeedLimitControlState.inactive + elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: + self.state = SpeedLimitControlState.adapting + + # ADAPTING + elif self.state == SpeedLimitControlState.adapting: + if self.detect_manual_cruise_change(): + self.state = SpeedLimitControlState.inactive + elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: + self.state = SpeedLimitControlState.active + + # PENDING + elif self.state == SpeedLimitControlState.pending: + if self._speed_limit > 0: + if self.v_offset < LIMIT_SPEED_OFFSET_TH: + self.state = SpeedLimitControlState.adapting + else: + self.state = SpeedLimitControlState.active + + # PREACTIVE + elif self.state == SpeedLimitControlState.preActive: + if self.initial_max_set_confirmed(): + self.initial_max_set = True + if self._speed_limit > 0: + if self.v_offset < LIMIT_SPEED_OFFSET_TH: + self.state = SpeedLimitControlState.adapting + else: + self.state = SpeedLimitControlState.active + else: + self.state = SpeedLimitControlState.pending + elif (self.frame - self.last_preactive_frame) * DT_MDL >= PRE_ACTIVE_GUARD_PERIOD: + # Timeout - session ended + self.state = SpeedLimitControlState.inactive + + # INACTIVE + elif self.state == SpeedLimitControlState.inactive: + pass + + # DISABLED + elif self.state == SpeedLimitControlState.disabled: + if self.op_engaged and self.enabled: + # Wait 2 seconds after long engaged before starting fresh session + if (self.frame - self.last_op_engaged_frame) * DT_MDL >= 2.: + self.state = SpeedLimitControlState.preActive + self.initial_max_set = False + def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, speed_limit: float, distance: float, source: Source, events_sp: EventsSP) -> float: self.op_engaged = long_active @@ -262,7 +257,7 @@ class SpeedLimitController: self.update_params() self.update_calculations(v_ego, a_ego, v_cruise_setpoint) - self.state_control() + self.update_state_machine() self.update_events(events_sp) # Update change tracking variables From 44b2e3dff3532ae110732743f46c653a8c683593 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 8 Sep 2025 22:32:07 -0400 Subject: [PATCH 142/188] refactor preactive timeout check --- .../controls/lib/speed_limit_controller/__init__.py | 2 +- .../speed_limit_controller/speed_limit_controller.py | 10 +++++----- .../tests/test_speed_limit_controller.py | 1 + 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py index 8781782f67..8737724439 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py @@ -4,7 +4,7 @@ SpeedLimitControlState = custom.LongitudinalPlanSP.SpeedLimitControlState DEBUG = True PARAMS_UPDATE_PERIOD = 3. # secs. Time between parameter updates. -PRE_ACTIVE_GUARD_PERIOD = 5. # secs. Time to wait after activation before considering temp deactivation signal. +PRE_ACTIVE_GUARD_PERIOD = 5 # secs. Time to wait after activation before considering temp deactivation signal. # Constants for Limit controllers. LIMIT_ADAPT_ACC = -1. # m/s^2 Ideal acceleration for the adapting (braking) phase when approaching speed limits. diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index c90f44c769..0cfb2e1ea4 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -38,7 +38,7 @@ class SpeedLimitController: self.CP = CP self.frame = -1 self.last_op_engaged_frame = 0.0 - self.last_preactive_frame = 0.0 + self.pre_active_timer = 0 self.is_metric = self.params.get_bool("IsMetric") self.enabled = self.params.get_bool("SpeedLimitControl") self.op_engaged = False @@ -165,9 +165,6 @@ class SpeedLimitController: if not self.op_engaged_prev and self.op_engaged: self.last_op_engaged_frame = self.frame - if not self._state_prev == SpeedLimitControlState.preActive and self.state == SpeedLimitControlState.preActive: - self.last_preactive_frame = self.frame - def get_current_acceleration_as_target(self) -> float: return self.a_ego @@ -192,6 +189,8 @@ class SpeedLimitController: def update_state_machine(self): self._state_prev = self.state + self.pre_active_timer = max(0, self.pre_active_timer - 1) + if self.state != SpeedLimitControlState.disabled: if not self.op_engaged or not self.enabled: self.state = SpeedLimitControlState.disabled @@ -231,7 +230,7 @@ class SpeedLimitController: self.state = SpeedLimitControlState.active else: self.state = SpeedLimitControlState.pending - elif (self.frame - self.last_preactive_frame) * DT_MDL >= PRE_ACTIVE_GUARD_PERIOD: + elif self.pre_active_timer <= PRE_ACTIVE_GUARD_PERIOD: # Timeout - session ended self.state = SpeedLimitControlState.inactive @@ -245,6 +244,7 @@ class SpeedLimitController: # Wait 2 seconds after long engaged before starting fresh session if (self.frame - self.last_op_engaged_frame) * DT_MDL >= 2.: self.state = SpeedLimitControlState.preActive + self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL) self.initial_max_set = False def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 7c09495f7a..18b8e22d17 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -37,6 +37,7 @@ class TestSpeedLimitController: self.events_sp = EventsSP() CI = self._setup_platform(TOYOTA.TOYOTA_RAV4_TSS2_2022) self.slc = SpeedLimitController(CI.CP) + self.slc.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL) def teardown_method(self, method): self.reset_state() From f6b855655a966912f5f8b5693f8acc1186b1abe5 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 8 Sep 2025 22:46:07 -0400 Subject: [PATCH 143/188] refactor new session check --- .../lib/speed_limit_controller/__init__.py | 1 + .../speed_limit_controller.py | 14 +++++++------- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py index 8737724439..ee8dfb2225 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py @@ -4,6 +4,7 @@ SpeedLimitControlState = custom.LongitudinalPlanSP.SpeedLimitControlState DEBUG = True PARAMS_UPDATE_PERIOD = 3. # secs. Time between parameter updates. +DISABLED_GUARD_PERIOD = 2 # secs. PRE_ACTIVE_GUARD_PERIOD = 5 # secs. Time to wait after activation before considering temp deactivation signal. # Constants for Limit controllers. diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 0cfb2e1ea4..c63f067e99 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -12,7 +12,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, \ - SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE + SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE, DISABLED_GUARD_PERIOD from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Engage, OffsetType from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP @@ -37,7 +37,7 @@ class SpeedLimitController: self.params = Params() self.CP = CP self.frame = -1 - self.last_op_engaged_frame = 0.0 + self.long_engaged_timer = 0 self.pre_active_timer = 0 self.is_metric = self.params.get_bool("IsMetric") self.enabled = self.params.get_bool("SpeedLimitControl") @@ -162,9 +162,6 @@ class SpeedLimitController: # Update current velocity offset (error) self.v_offset = self.speed_limit_final - self.v_ego - if not self.op_engaged_prev and self.op_engaged: - self.last_op_engaged_frame = self.frame - def get_current_acceleration_as_target(self) -> float: return self.a_ego @@ -189,6 +186,7 @@ class SpeedLimitController: def update_state_machine(self): self._state_prev = self.state + self.long_engaged_timer = max(0, self.long_engaged_timer - 1) self.pre_active_timer = max(0, self.pre_active_timer - 1) if self.state != SpeedLimitControlState.disabled: @@ -241,8 +239,10 @@ class SpeedLimitController: # DISABLED elif self.state == SpeedLimitControlState.disabled: if self.op_engaged and self.enabled: - # Wait 2 seconds after long engaged before starting fresh session - if (self.frame - self.last_op_engaged_frame) * DT_MDL >= 2.: + if not self.op_engaged_prev: + self.pre_active_timer = int(DISABLED_GUARD_PERIOD / DT_MDL) + + elif self.pre_active_timer <= 0: self.state = SpeedLimitControlState.preActive self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL) self.initial_max_set = False From 1365d7925c99d36393e3b927d4ad09eeef371a1e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 8 Sep 2025 22:52:13 -0400 Subject: [PATCH 144/188] directly return statuses --- .../speed_limit_controller.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index c63f067e99..62249ab67d 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -43,6 +43,8 @@ class SpeedLimitController: self.enabled = self.params.get_bool("SpeedLimitControl") self.op_engaged = False self.op_engaged_prev = False + self.is_enabled = False + self.is_active = False self.v_ego = 0. self.a_ego = 0. self.v_offset = 0. @@ -71,14 +73,6 @@ class SpeedLimitController: SpeedLimitControlState.active: self.get_active_state_target_acceleration, } - @property - def is_enabled(self) -> bool: - return self.state in ENABLED_STATES - - @property - def is_active(self) -> bool: - return self.state in ACTIVE_STATES - @property def speed_limit_final(self) -> float: return self._speed_limit + self.speed_limit_offset @@ -247,6 +241,11 @@ class SpeedLimitController: self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL) self.initial_max_set = False + enabled = self.state in ENABLED_STATES + active = self.state in ACTIVE_STATES + + return enabled, active + def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, speed_limit: float, distance: float, source: Source, events_sp: EventsSP) -> float: self.op_engaged = long_active @@ -257,7 +256,7 @@ class SpeedLimitController: self.update_params() self.update_calculations(v_ego, a_ego, v_cruise_setpoint) - self.update_state_machine() + self.is_enabled, self.is_active = self.update_state_machine() self.update_events(events_sp) # Update change tracking variables From 27e112e70ce407e1144f2b27afe42b85ca92ce49 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Mon, 8 Sep 2025 22:53:03 -0400 Subject: [PATCH 145/188] comments --- .../lib/speed_limit_controller/speed_limit_controller.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 62249ab67d..fd759f79d3 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -183,6 +183,7 @@ class SpeedLimitController: self.long_engaged_timer = max(0, self.long_engaged_timer - 1) self.pre_active_timer = max(0, self.pre_active_timer - 1) + # ACTIVE, ADAPTING, PENDING, PRE_ACTIVE, INACTIVE if self.state != SpeedLimitControlState.disabled: if not self.op_engaged or not self.enabled: self.state = SpeedLimitControlState.disabled @@ -211,7 +212,7 @@ class SpeedLimitController: else: self.state = SpeedLimitControlState.active - # PREACTIVE + # PRE_ACTIVE elif self.state == SpeedLimitControlState.preActive: if self.initial_max_set_confirmed(): self.initial_max_set = True From 20eca71fc50b755fb29726fdc79d6718610aed7e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 01:04:07 -0400 Subject: [PATCH 146/188] v_target --- .../lib/speed_limit_controller/speed_limit_controller.py | 8 ++++---- .../tests/test_speed_limit_controller.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index fd759f79d3..9e24168c22 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -93,8 +93,7 @@ class SpeedLimitController: def source(self) -> Source: return self._source - @property - def final_cruise_speed(self) -> float: + def get_v_target_from_control(self) -> float: if self.is_active: # If we have a current valid speed limit, use it if self._speed_limit > 0: @@ -264,7 +263,8 @@ class SpeedLimitController: self.speed_limit_prev = self._speed_limit self.v_cruise_setpoint_prev = self.v_cruise_setpoint self.op_engaged_prev = self.op_engaged - self.frame += 1 - return self.final_cruise_speed + v_target = self.get_v_target_from_control() + + return v_target diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 18b8e22d17..f4275ef761 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -81,7 +81,7 @@ class TestSpeedLimitController: assert self.slc.state == SpeedLimitControlState.disabled assert not self.slc.is_enabled assert not self.slc.is_active - assert self.slc.final_cruise_speed == V_CRUISE_UNSET + assert V_CRUISE_UNSET == self.slc.get_v_target_from_control() def test_disabled(self): self.params.put_bool("SpeedLimitControl", False) From bf64fa29f7a4f2207dadac3627eb81c1d8f3ad88 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 01:43:38 -0400 Subject: [PATCH 147/188] refactor speed limit resolver --- cereal/custom.capnp | 7 ++ sunnypilot/mapd/live_map_data/debug.py | 5 +- .../controls/lib/longitudinal_planner.py | 9 ++- .../lib/speed_limit_controller/common.py | 6 -- .../speed_limit_controller.py | 11 +-- .../speed_limit_resolver.py | 77 ++++++++++--------- .../tests/test_speed_limit_controller.py | 44 ++++++----- .../tests/test_speed_limit_resolver.py | 52 +++++++------ 8 files changed, 113 insertions(+), 98 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index ba4f4beb3f..f1894c14d1 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -144,6 +144,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { speedLimit @3 :Float32; speedLimitOffset @4 :Float32; distToSpeedLimit @5 :Float32; + source @6 :SpeedLimitSource; } enum SpeedLimitControlState { @@ -154,6 +155,12 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { adapting @4; # Reducing speed to match new speed limit. active @5; # Cruising at speed limit. } + + enum SpeedLimitSource { + none @0; + car @1; + map @2; + } } struct OnroadEventSP @0xda96579883444c35 { diff --git a/sunnypilot/mapd/live_map_data/debug.py b/sunnypilot/mapd/live_map_data/debug.py index da9f4d7771..e8a52cda08 100644 --- a/sunnypilot/mapd/live_map_data/debug.py +++ b/sunnypilot/mapd/live_map_data/debug.py @@ -41,11 +41,10 @@ def live_map_data_sp_thread_debug(gps_location_service): _sub_master.update() v_ego = _sub_master['carState'].vEgo - long_spl = _sub_master['longitudinalPlanSP'].speedLimit _policy = Policy.car_state_priority _resolver = SpeedLimitResolver(_policy) - _speed_limit, _distance, _source = _resolver.resolve(v_ego, long_spl, _sub_master) - print(_speed_limit, _distance, _source, " <-> ", long_spl) + _resolver.update(v_ego, _sub_master) + print(_resolver.speed_limit, _resolver.distance, _resolver.source) def main(): diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index c7e16ed976..8368268977 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -52,9 +52,9 @@ class LongitudinalPlannerSP: self.events_sp.clear() # Speed Limit Control - _speed_limit, _distance, _source = self.resolver.resolve(v_ego, sm) + self.resolver.update(v_ego, sm) v_cruise_slc = self.slc.update(sm['carControl'].longActive, v_ego, a_ego, sm['carState'].vCruiseCluster, - _speed_limit, _distance, _source, self.events_sp) + self.resolver.speed_limit, self.resolver.distance, self.resolver.source, self.events_sp) v_cruise_final = min(v_cruise, v_cruise_slc) @@ -84,8 +84,9 @@ class LongitudinalPlannerSP: slc.state = self.slc.state slc.enabled = self.slc.is_enabled slc.active = self.slc.is_active - slc.speedLimit = float(self.slc.speed_limit) + slc.speedLimit = float(self.resolver.speed_limit) slc.speedLimitOffset = float(self.slc.speed_limit_offset) - slc.distToSpeedLimit = float(self.slc.distance) + slc.distToSpeedLimit = float(self.resolver.distance) + slc.source = self.resolver.source pm.send('longitudinalPlanSP', plan_sp_send) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py index 414981a80b..7e5d117d21 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py @@ -1,12 +1,6 @@ from enum import IntEnum -class Source(IntEnum): - none = 0 - car_state = 1 - map_data = 2 - - class Policy(IntEnum): map_data_only = 0 car_state_only = 1 diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 9e24168c22..c8bb599de0 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -14,11 +14,12 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, \ SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE, DISABLED_GUARD_PERIOD from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Engage, OffsetType +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Engage, OffsetType from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.selfdrive.modeld.constants import ModelConstants EventNameSP = custom.OnroadEventSP.EventName +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource ACTIVE_STATES = (SpeedLimitControlState.active, SpeedLimitControlState.adapting) ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pending, *ACTIVE_STATES) @@ -27,7 +28,7 @@ ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pendi class SpeedLimitController: _speed_limit: float _distance: float - _source: Source + _source: SpeedLimitSource v_ego: float a_ego: float v_offset: float @@ -55,7 +56,7 @@ class SpeedLimitController: self.speed_limit_prev = 0. self.last_valid_speed_limit_final = 0. self._distance = 0. - self._source = Source.none + self._source = SpeedLimitSource.none self.state = SpeedLimitControlState.disabled self._state_prev = SpeedLimitControlState.disabled self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise @@ -90,7 +91,7 @@ class SpeedLimitController: return self._distance @property - def source(self) -> Source: + def source(self) -> SpeedLimitSource: return self._source def get_v_target_from_control(self) -> float: @@ -247,7 +248,7 @@ class SpeedLimitController: return enabled, active def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, - speed_limit: float, distance: float, source: Source, events_sp: EventsSP) -> float: + speed_limit: float, distance: float, source: SpeedLimitSource, events_sp: EventsSP) -> float: self.op_engaged = long_active self._speed_limit = speed_limit diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index 64a78e4d36..941b601715 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -1,17 +1,25 @@ import time import numpy as np -from cereal import messaging +import cereal.messaging as messaging +from cereal import custom from openpilot.common.gps import get_gps_location_service from openpilot.common.params import Params from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Policy +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy + +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource + +ALL_SOURCES = tuple(SpeedLimitSource.schema.enumerants.values()) class SpeedLimitResolver: - _limit_solutions: dict[Source, float] # Store for speed limit solutions from different sources - _distance_solutions: dict[Source, float] # Store for distance to current speed limit start for different sources + _limit_solutions: dict[SpeedLimitSource, float] # Store for speed limit solutions from different sources + _distance_solutions: dict[SpeedLimitSource, float] # Store for distance to current speed limit start for different sources _v_ego: float + speed_limit: float + distance: float + source: SpeedLimitSource def __init__(self, policy: Policy): self._gps_location_service = get_gps_location_service(Params()) @@ -20,40 +28,34 @@ class SpeedLimitResolver: self._policy = policy self._policy_to_sources_map = { - Policy.car_state_only: [Source.car_state], - Policy.car_state_priority: [Source.car_state, Source.map_data], - Policy.map_data_priority: [Source.map_data, Source.car_state], - Policy.map_data_only: [Source.map_data], - Policy.combined: [Source.car_state, Source.map_data], + Policy.car_state_only: [SpeedLimitSource.car], + Policy.car_state_priority: [SpeedLimitSource.car, SpeedLimitSource.map], + Policy.map_data_priority: [SpeedLimitSource.map, SpeedLimitSource.car], + Policy.map_data_only: [SpeedLimitSource.map], + Policy.combined: [SpeedLimitSource.car, SpeedLimitSource.map], } - for source in Source: + for source in ALL_SOURCES: self._reset_limit_sources(source) def change_policy(self, policy: Policy) -> None: self._policy = policy - def _reset_limit_sources(self, source: Source) -> None: + def _reset_limit_sources(self, source: SpeedLimitSource) -> None: self._limit_solutions[source] = 0. self._distance_solutions[source] = 0. - def resolve(self, v_ego: float, sm: messaging.SubMaster) -> tuple[float, float, Source]: - self._v_ego = v_ego - - self._resolve_limit_sources(sm) - return self._consolidate() - def _resolve_limit_sources(self, sm: messaging.SubMaster) -> None: """Get limit solutions from each data source""" self._get_from_car_state(sm) self._get_from_map_data(sm) def _get_from_car_state(self, sm: messaging.SubMaster) -> None: - self._reset_limit_sources(Source.car_state) - self._limit_solutions[Source.car_state] = sm['carStateSP'].speedLimit - self._distance_solutions[Source.car_state] = 0. + self._reset_limit_sources(SpeedLimitSource.car) + self._limit_solutions[SpeedLimitSource.car] = sm['carStateSP'].speedLimit + self._distance_solutions[SpeedLimitSource.car] = 0. def _get_from_map_data(self, sm: messaging.SubMaster) -> None: - self._reset_limit_sources(Source.map_data) + self._reset_limit_sources(SpeedLimitSource.map) self._process_map_data(sm) def _process_map_data(self, sm: messaging.SubMaster) -> None: @@ -76,39 +78,44 @@ class SpeedLimitResolver: distance_since_fix = self._v_ego * (time.monotonic() - gps_data.unixTimestampMillis * 1e-3) distance_to_speed_limit_ahead = max(0., map_data.speedLimitAheadDistance - distance_since_fix) - self._limit_solutions[Source.map_data] = speed_limit - self._distance_solutions[Source.map_data] = 0. + self._limit_solutions[SpeedLimitSource.map] = speed_limit + self._distance_solutions[SpeedLimitSource.map] = 0. if 0. < next_speed_limit < self._v_ego: adapt_time = (next_speed_limit - self._v_ego) / LIMIT_ADAPT_ACC adapt_distance = self._v_ego * adapt_time + 0.5 * LIMIT_ADAPT_ACC * adapt_time ** 2 if distance_to_speed_limit_ahead <= adapt_distance: - self._limit_solutions[Source.map_data] = next_speed_limit - self._distance_solutions[Source.map_data] = distance_to_speed_limit_ahead + self._limit_solutions[SpeedLimitSource.map] = next_speed_limit + self._distance_solutions[SpeedLimitSource.map] = distance_to_speed_limit_ahead - def _consolidate(self) -> tuple[float, float, Source]: + def _consolidate(self) -> tuple[float, float, SpeedLimitSource]: source = self._get_source_solution_according_to_policy() - self.speed_limit = self._limit_solutions[source] if source else 0. - self.distance = self._distance_solutions[source] if source else 0. - self.source = source or Source.none + speed_limit = self._limit_solutions[source] if source else 0. + distance = self._distance_solutions[source] if source else 0. - return self.speed_limit, self.distance, self.source + return speed_limit, distance, source - def _get_source_solution_according_to_policy(self) -> Source | None: + def _get_source_solution_according_to_policy(self) -> SpeedLimitSource: sources_for_policy = self._policy_to_sources_map[self._policy] if self._policy != Policy.combined: # They are ordered in the order of preference, so we pick the first that's non zero for source in sources_for_policy: if self._limit_solutions[source] > 0.: - return Source(source) + return source limits = np.array([self._limit_solutions[source] for source in sources_for_policy], dtype=float) - sources = np.array([source.value for source in sources_for_policy], dtype=int) + sources = np.array([source for source in sources_for_policy], dtype=int) if len(limits) > 0: min_idx = np.argmin(limits) - return Source(sources[min_idx]) + return sources[min_idx] - return None + return SpeedLimitSource.none + + def update(self, v_ego: float, sm: messaging.SubMaster) -> None: + self._v_ego = v_ego + self._resolve_limit_sources(sm) + + self.speed_limit, self.distance, self.source = self._consolidate() diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index f4275ef761..3f09a977b5 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -6,6 +6,8 @@ See the LICENSE.md file in the root directory for more details. """ import pytest +from cereal import custom + from opendbc.car.car_helpers import interfaces from opendbc.car.toyota.values import CAR as TOYOTA from openpilot.common.constants import CV @@ -13,12 +15,14 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, OffsetType +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import OffsetType from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitControlState, REQUIRED_INITIAL_MAX_SET_SPEED, \ PRE_ACTIVE_GUARD_PERIOD from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource + ALL_STATES = tuple(SpeedLimitControlState.schema.enumerants.values()) SPEED_LIMITS = { @@ -67,7 +71,7 @@ class TestSpeedLimitController: self.slc.speed_limit_prev = 0. self.slc.last_valid_speed_limit_offsetted = 0. self.slc._distance = 0. - self.slc._source = Source.none + self.slc._source = SpeedLimitSource.none self.slc.v_cruise_setpoint = 0. self.slc.v_cruise_setpoint_prev = 0. self.events_sp.clear() @@ -86,58 +90,58 @@ class TestSpeedLimitController: def test_disabled(self): self.params.put_bool("SpeedLimitControl", False) for _ in range(int(10. / DT_MDL)): - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.disabled def test_transition_disabled_to_preactive(self): for _ in range(int(3. / DT_MDL)): - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.preActive assert self.slc.is_enabled and not self.slc.is_active def test_preactive_to_active_with_max_speed_confirmation(self): self.slc.state = SpeedLimitControlState.preActive - v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.active assert self.slc.is_enabled and self.slc.is_active assert v_cruise_slc == SPEED_LIMITS['city'] def test_preactive_timeout_to_inactive(self): self.slc.state = SpeedLimitControlState.preActive - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) for _ in range(int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)): - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.inactive def test_preactive_to_pending_no_speed_limit(self): self.slc.state = SpeedLimitControlState.preActive - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, Source.none, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, SpeedLimitSource.none, self.events_sp) assert self.slc.state == SpeedLimitControlState.pending assert self.slc.is_enabled and not self.slc.is_active def test_pending_to_active_when_speed_limit_available(self): self.slc.state = SpeedLimitControlState.pending - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.active def test_pending_to_adapting_when_below_speed_limit(self): self.slc.state = SpeedLimitControlState.pending - _ = self.slc.update(True, SPEED_LIMITS['city'] + 5, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'] + 5, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.adapting assert self.slc.is_enabled and self.slc.is_active def test_active_to_adapting_transition(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) - _ = self.slc.update(True, SPEED_LIMITS['city'] + 2, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'] + 2, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.adapting def test_adapting_to_active_transition(self): self.slc.state = SpeedLimitControlState.adapting self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.active def test_manual_cruise_change_detection(self): @@ -146,7 +150,7 @@ class TestSpeedLimitController: self.slc.v_cruise_setpoint_prev = expected_cruise different_cruise = SPEED_LIMITS['highway'] + 5 - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, different_cruise, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, different_cruise, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.inactive @pytest.mark.parametrize("offset_type, offset_value, speed_limit, expected_offset", [ @@ -166,7 +170,7 @@ class TestSpeedLimitController: speed_limits = [SPEED_LIMITS['city'], SPEED_LIMITS['highway'], SPEED_LIMITS['residential']] for _, speed_limit in enumerate(speed_limits): - _ = self.slc.update(True, speed_limit, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, speed_limit, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state in ACTIVE_STATES def test_invalid_speed_limits_handling(self): @@ -176,7 +180,7 @@ class TestSpeedLimitController: invalid_limits = [-10, 0, 200 * CV.MPH_TO_MS] for invalid_limit in invalid_limits: - v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, invalid_limit, 0, Source.car_state, self.events_sp) + v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, invalid_limit, 0, SpeedLimitSource.car, self.events_sp) assert isinstance(v_cruise_slc, (int, float)) assert v_cruise_slc == V_CRUISE_UNSET or v_cruise_slc > 0 @@ -185,14 +189,14 @@ class TestSpeedLimitController: old_speed_limit = SPEED_LIMITS['city'] self.slc.last_valid_speed_limit_final = old_speed_limit - v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, Source.car_state, self.events_sp) + v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state in ACTIVE_STATES assert v_cruise_slc == old_speed_limit def test_different_speed_limit_sources(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) - for source in (Source.car_state, Source.map_data): + for source in (SpeedLimitSource.car, SpeedLimitSource.map): v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, source, self.events_sp) assert v_cruise_slc != V_CRUISE_UNSET @@ -204,14 +208,14 @@ class TestSpeedLimitController: current_speed = SPEED_LIMITS['highway'] target_speed = SPEED_LIMITS['city'] - v_cruise_slc = self.slc.update(True, current_speed, 0, REQUIRED_INITIAL_MAX_SET_SPEED, target_speed, distance, Source.map_data, self.events_sp) + v_cruise_slc = self.slc.update(True, current_speed, 0, REQUIRED_INITIAL_MAX_SET_SPEED, target_speed, distance, SpeedLimitSource.map, self.events_sp) assert self.slc.state == SpeedLimitControlState.adapting assert v_cruise_slc == target_speed # TODO-SP: assert expected accel, need to enable self.acceleration_solutions def test_long_disengaged_to_disabled(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) - v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.disabled assert v_cruise_slc == V_CRUISE_UNSET @@ -232,7 +236,7 @@ class TestSpeedLimitController: initial_state = state - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED,SPEED_LIMITS['city'], 0, Source.car_state, self.events_sp) + _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED,SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state in ALL_STATES # Sanity check diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py index 457439022a..738c144066 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py @@ -4,11 +4,13 @@ import time import pytest from pytest_mock import MockerFixture +from cereal import custom from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE -# from selfdrive.controls.lib.speed_limit_controller_tbd import SpeedLimitResolver as OriginalSpeedLimitResolver -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver as RefactoredSpeedLimitResolver -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Source, Policy +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver, ALL_SOURCES +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy + +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource def create_mock(properties, mocker: MockerFixture): @@ -52,22 +54,22 @@ def setup_sm_mock(mocker: MockerFixture): parametrized_policies = pytest.mark.parametrize( "policy, sm_key, function_key", [ - (Policy.car_state_only, 'carStateSP', 'car_state'), - (Policy.car_state_priority, 'carStateSP', 'car_state'), - (Policy.map_data_only, 'liveMapDataSP', 'map_data'), - (Policy.map_data_priority, 'liveMapDataSP', 'map_data'), + (Policy.car_state_only, 'carStateSP', SpeedLimitSource.car), + (Policy.car_state_priority, 'carStateSP', SpeedLimitSource.car), + (Policy.map_data_only, 'liveMapDataSP', SpeedLimitSource.map), + (Policy.map_data_priority, 'liveMapDataSP', SpeedLimitSource.map), ], ids=lambda val: val.name if hasattr(val, 'name') else str(val) ) -@pytest.mark.parametrize("resolver_class", [RefactoredSpeedLimitResolver], ids=["Refactored"]) +@pytest.mark.parametrize("resolver_class", [SpeedLimitResolver]) class TestSpeedLimitResolverValidation: @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) def test_initial_state(self, resolver_class, policy): resolver = resolver_class(policy) - for source in Source: + for source in ALL_SOURCES: if source in resolver._limit_solutions: assert resolver._limit_solutions[source] == 0. assert resolver._distance_solutions[source] == 0. @@ -79,22 +81,22 @@ class TestSpeedLimitResolverValidation: source_speed_limit = sm_mock[sm_key].speedLimit # Assert the resolver - speed_limit, _, source = resolver.resolve(source_speed_limit, sm_mock) - assert speed_limit == source_speed_limit - assert source == Source[function_key] + resolver.update(source_speed_limit, sm_mock) + assert resolver.speed_limit == source_speed_limit + assert resolver.source == ALL_SOURCES[function_key] def test_resolver_combined(self, resolver_class, mocker: MockerFixture): resolver = resolver_class(Policy.combined) sm_mock = setup_sm_mock(mocker) - socket_to_source = {'carStateSP': Source.car_state, 'liveMapDataSP': Source.map_data} + socket_to_source = {'carStateSP': SpeedLimitSource.car, 'liveMapDataSP': SpeedLimitSource.map} minimum_key, minimum_speed_limit = min( ((key, sm_mock[key].speedLimit) for key in socket_to_source.keys()), key=lambda x: x[1]) # Assert the resolver - speed_limit, _, source = resolver.resolve(minimum_speed_limit, sm_mock) - assert speed_limit == minimum_speed_limit - assert source == socket_to_source[minimum_key] + resolver.update(minimum_speed_limit, sm_mock) + assert resolver.speed_limit == minimum_speed_limit + assert resolver.source == socket_to_source[minimum_key] @parametrized_policies def test_parser(self, resolver_class, policy, sm_key, function_key, mocker: MockerFixture): @@ -103,9 +105,9 @@ class TestSpeedLimitResolverValidation: source_speed_limit = sm_mock[sm_key].speedLimit # Assert the parsing - speed_limit, _, source = resolver.resolve(source_speed_limit, sm_mock) - assert resolver._limit_solutions[Source[function_key]] == source_speed_limit - assert resolver._distance_solutions[Source[function_key]] == 0. + resolver.update(source_speed_limit, sm_mock) + assert resolver._limit_solutions[ALL_SOURCES[function_key]] == source_speed_limit + assert resolver._distance_solutions[ALL_SOURCES[function_key]] == 0. @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) def test_resolve_interaction_in_update(self, resolver_class, policy, mocker: MockerFixture): @@ -113,12 +115,12 @@ class TestSpeedLimitResolverValidation: resolver = resolver_class(policy) sm_mock = setup_sm_mock(mocker) - _speed_limit, _distance, _source = resolver.resolve(v_ego, sm_mock) + resolver.update(v_ego, sm_mock) # After resolution - assert _speed_limit is not None - assert _distance is not None - assert _source is not None + assert resolver.speed_limit is not None + assert resolver.distance is not None + assert resolver.source is not None @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) def test_old_map_data_ignored(self, resolver_class, policy, mocker: MockerFixture): @@ -126,5 +128,5 @@ class TestSpeedLimitResolverValidation: sm_mock = mocker.MagicMock() sm_mock['gpsLocation'].unixTimestampMillis = (time.monotonic() - 2 * LIMIT_MAX_MAP_DATA_AGE) * 1e3 resolver._get_from_map_data(sm_mock) - assert resolver._limit_solutions[Source.map_data] == 0. - assert resolver._distance_solutions[Source.map_data] == 0. + assert resolver._limit_solutions[SpeedLimitSource.map] == 0. + assert resolver._distance_solutions[SpeedLimitSource.map] == 0. From 51d2d7d5f5540c1857f255f0fec22988fa9ce276 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 01:43:44 -0400 Subject: [PATCH 148/188] turn off debug --- .../selfdrive/controls/lib/speed_limit_controller/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py index ee8dfb2225..ff82a7dde0 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py @@ -2,7 +2,7 @@ from cereal import custom SpeedLimitControlState = custom.LongitudinalPlanSP.SpeedLimitControlState -DEBUG = True +DEBUG = False PARAMS_UPDATE_PERIOD = 3. # secs. Time between parameter updates. DISABLED_GUARD_PERIOD = 2 # secs. PRE_ACTIVE_GUARD_PERIOD = 5 # secs. Time to wait after activation before considering temp deactivation signal. From 29ff34821cb18e53f50909fd8b67b94530b46fe8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 02:04:24 -0400 Subject: [PATCH 149/188] more resolver refactor --- sunnypilot/mapd/live_map_data/debug.py | 4 +-- .../controls/lib/longitudinal_planner.py | 13 +--------- .../speed_limit_resolver.py | 26 ++++++++++++++----- .../tests/test_speed_limit_resolver.py | 18 ++++++++----- 4 files changed, 34 insertions(+), 27 deletions(-) diff --git a/sunnypilot/mapd/live_map_data/debug.py b/sunnypilot/mapd/live_map_data/debug.py index e8a52cda08..a79e51bcf5 100644 --- a/sunnypilot/mapd/live_map_data/debug.py +++ b/sunnypilot/mapd/live_map_data/debug.py @@ -41,8 +41,8 @@ def live_map_data_sp_thread_debug(gps_location_service): _sub_master.update() v_ego = _sub_master['carState'].vEgo - _policy = Policy.car_state_priority - _resolver = SpeedLimitResolver(_policy) + _resolver = SpeedLimitResolver() + _resolver.policy = Policy.car_state_priority _resolver.update(v_ego, _sub_master) print(_resolver.speed_limit, _resolver.distance, _resolver.source) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 8368268977..e6ede838ea 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -8,9 +8,7 @@ See the LICENSE.md file in the root directory for more details. from cereal import messaging, custom from opendbc.car import structs from openpilot.common.params import Params -from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP @@ -23,10 +21,8 @@ class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.events_sp = EventsSP() self.params = Params() - self.frame = -1 - self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) - self.resolver = SpeedLimitResolver(self.policy) + self.resolver = SpeedLimitResolver() self.dec = DynamicExperimentalController(CP, mpc) self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None @@ -37,11 +33,6 @@ class LongitudinalPlannerSP: # If we don't have a generation set, we assume it's default model. Which as of today are mlsim. return bool(self.generation is None or self.generation >= 11) - def update_params(self): - if self.frame % int(3. / DT_MDL) == 0: - self.policy = Policy(self.params.get("SpeedLimitControlPolicy", return_default=True)) - self.resolver.change_policy(self.policy) - def get_mpc_mode(self) -> str | None: if not self.dec.active(): return None @@ -63,8 +54,6 @@ class LongitudinalPlannerSP: def update(self, sm: messaging.SubMaster) -> None: self.dec.update(sm) - self.frame += 1 - def publish_longitudinal_plan_sp(self, sm: messaging.SubMaster, pm: messaging.PubMaster) -> None: plan_sp_send = messaging.new_message('longitudinalPlanSP') diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index 941b601715..d92df09f7f 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -5,7 +5,8 @@ import cereal.messaging as messaging from cereal import custom from openpilot.common.gps import get_gps_location_service from openpilot.common.params import Params -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC +from openpilot.common.realtime import DT_MDL +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC, PARAMS_UPDATE_PERIOD from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource @@ -21,12 +22,15 @@ class SpeedLimitResolver: distance: float source: SpeedLimitSource - def __init__(self, policy: Policy): - self._gps_location_service = get_gps_location_service(Params()) + def __init__(self): + self.params = Params() + self.frame = -1 + + self._gps_location_service = get_gps_location_service(self.params) self._limit_solutions = {} self._distance_solutions = {} - self._policy = policy + self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) self._policy_to_sources_map = { Policy.car_state_only: [SpeedLimitSource.car], Policy.car_state_priority: [SpeedLimitSource.car, SpeedLimitSource.map], @@ -37,8 +41,13 @@ class SpeedLimitResolver: for source in ALL_SOURCES: self._reset_limit_sources(source) + def update_params(self): + if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0: + self.policy = Policy(self.params.get("SpeedLimitControlPolicy", return_default=True)) + self.change_policy(self.policy) + def change_policy(self, policy: Policy) -> None: - self._policy = policy + self.policy = policy def _reset_limit_sources(self, source: SpeedLimitSource) -> None: self._limit_solutions[source] = 0. @@ -97,9 +106,9 @@ class SpeedLimitResolver: return speed_limit, distance, source def _get_source_solution_according_to_policy(self) -> SpeedLimitSource: - sources_for_policy = self._policy_to_sources_map[self._policy] + sources_for_policy = self._policy_to_sources_map[self.policy] - if self._policy != Policy.combined: + if self.policy != Policy.combined: # They are ordered in the order of preference, so we pick the first that's non zero for source in sources_for_policy: if self._limit_solutions[source] > 0.: @@ -116,6 +125,9 @@ class SpeedLimitResolver: def update(self, v_ego: float, sm: messaging.SubMaster) -> None: self._v_ego = v_ego + self.update_params() self._resolve_limit_sources(sm) self.speed_limit, self.distance, self.source = self._consolidate() + + self.frame += 1 diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py index 738c144066..e01a60164d 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py @@ -68,7 +68,8 @@ class TestSpeedLimitResolverValidation: @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) def test_initial_state(self, resolver_class, policy): - resolver = resolver_class(policy) + resolver = resolver_class() + resolver.policy = policy for source in ALL_SOURCES: if source in resolver._limit_solutions: assert resolver._limit_solutions[source] == 0. @@ -76,7 +77,8 @@ class TestSpeedLimitResolverValidation: @parametrized_policies def test_resolver(self, resolver_class, policy, sm_key, function_key, mocker: MockerFixture): - resolver = resolver_class(policy) + resolver = resolver_class() + resolver.policy = policy sm_mock = setup_sm_mock(mocker) source_speed_limit = sm_mock[sm_key].speedLimit @@ -86,7 +88,8 @@ class TestSpeedLimitResolverValidation: assert resolver.source == ALL_SOURCES[function_key] def test_resolver_combined(self, resolver_class, mocker: MockerFixture): - resolver = resolver_class(Policy.combined) + resolver = resolver_class() + resolver.policy = Policy.combined sm_mock = setup_sm_mock(mocker) socket_to_source = {'carStateSP': SpeedLimitSource.car, 'liveMapDataSP': SpeedLimitSource.map} minimum_key, minimum_speed_limit = min( @@ -100,7 +103,8 @@ class TestSpeedLimitResolverValidation: @parametrized_policies def test_parser(self, resolver_class, policy, sm_key, function_key, mocker: MockerFixture): - resolver = resolver_class(policy) + resolver = resolver_class() + resolver.policy = policy sm_mock = setup_sm_mock(mocker) source_speed_limit = sm_mock[sm_key].speedLimit @@ -112,7 +116,8 @@ class TestSpeedLimitResolverValidation: @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) def test_resolve_interaction_in_update(self, resolver_class, policy, mocker: MockerFixture): v_ego = 50 - resolver = resolver_class(policy) + resolver = resolver_class() + resolver.policy = policy sm_mock = setup_sm_mock(mocker) resolver.update(v_ego, sm_mock) @@ -124,7 +129,8 @@ class TestSpeedLimitResolverValidation: @pytest.mark.parametrize("policy", list(Policy), ids=lambda policy: policy.name) def test_old_map_data_ignored(self, resolver_class, policy, mocker: MockerFixture): - resolver = resolver_class(policy) + resolver = resolver_class() + resolver.policy = policy sm_mock = mocker.MagicMock() sm_mock['gpsLocation'].unixTimestampMillis = (time.monotonic() - 2 * LIMIT_MAX_MAP_DATA_AGE) * 1e3 resolver._get_from_map_data(sm_mock) From 1fd2a8ca1d6e547e73af5c991a51a8da8aa60a36 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 02:08:14 -0400 Subject: [PATCH 150/188] no longer needed --- .../lib/speed_limit_controller/speed_limit_controller.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index c8bb599de0..6688fed3f2 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -82,10 +82,6 @@ class SpeedLimitController: def speed_limit_offset(self) -> float: return self.get_offset(self.offset_type, self.offset_value) - @property - def speed_limit(self) -> float: - return self._speed_limit - @property def distance(self) -> float: return self._distance From e217604b20e7c3122a30ecbbe4486d0bb5fef2f4 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 02:14:07 -0400 Subject: [PATCH 151/188] lint --- .../lib/speed_limit_controller/speed_limit_resolver.py | 2 +- .../tests/test_speed_limit_controller.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index d92df09f7f..d76ffcd8eb 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -115,7 +115,7 @@ class SpeedLimitResolver: return source limits = np.array([self._limit_solutions[source] for source in sources_for_policy], dtype=float) - sources = np.array([source for source in sources_for_policy], dtype=int) + sources = np.array(sources_for_policy, dtype=int) if len(limits) > 0: min_idx = np.argmin(limits) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 3f09a977b5..66aead5c68 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -215,7 +215,8 @@ class TestSpeedLimitController: def test_long_disengaged_to_disabled(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) - v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], + 0, SpeedLimitSource.car, self.events_sp) assert self.slc.state == SpeedLimitControlState.disabled assert v_cruise_slc == V_CRUISE_UNSET From 8ae9974988cda2fbbbd6707656c2265ca3731486 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 02:38:15 -0400 Subject: [PATCH 152/188] more lint --- .../speed_limit_controller.py | 8 +++---- .../speed_limit_resolver.py | 21 +++++++++---------- .../tests/test_speed_limit_controller.py | 2 +- .../tests/test_speed_limit_resolver.py | 2 +- 4 files changed, 16 insertions(+), 17 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 6688fed3f2..ba9110205e 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -19,7 +19,7 @@ from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.selfdrive.modeld.constants import ModelConstants EventNameSP = custom.OnroadEventSP.EventName -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource ACTIVE_STATES = (SpeedLimitControlState.active, SpeedLimitControlState.adapting) ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pending, *ACTIVE_STATES) @@ -28,7 +28,7 @@ ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pendi class SpeedLimitController: _speed_limit: float _distance: float - _source: SpeedLimitSource + _source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource v_ego: float a_ego: float v_offset: float @@ -87,7 +87,7 @@ class SpeedLimitController: return self._distance @property - def source(self) -> SpeedLimitSource: + def source(self) -> custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource: return self._source def get_v_target_from_control(self) -> float: @@ -244,7 +244,7 @@ class SpeedLimitController: return enabled, active def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, - speed_limit: float, distance: float, source: SpeedLimitSource, events_sp: EventsSP) -> float: + speed_limit: float, distance: float, source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource, events_sp: EventsSP) -> float: self.op_engaged = long_active self._speed_limit = speed_limit diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index d76ffcd8eb..d641c3b570 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -9,26 +9,26 @@ from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC, PARAMS_UPDATE_PERIOD from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource ALL_SOURCES = tuple(SpeedLimitSource.schema.enumerants.values()) class SpeedLimitResolver: - _limit_solutions: dict[SpeedLimitSource, float] # Store for speed limit solutions from different sources - _distance_solutions: dict[SpeedLimitSource, float] # Store for distance to current speed limit start for different sources + _limit_solutions: dict[custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource, float] + _distance_solutions: dict[custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource, float] _v_ego: float speed_limit: float distance: float - source: SpeedLimitSource + source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource def __init__(self): self.params = Params() self.frame = -1 self._gps_location_service = get_gps_location_service(self.params) - self._limit_solutions = {} - self._distance_solutions = {} + self._limit_solutions = {} # Store for speed limit solutions from different sources + self._distance_solutions = {} # Store for distance to current speed limit start for different sources self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) self._policy_to_sources_map = { @@ -49,7 +49,7 @@ class SpeedLimitResolver: def change_policy(self, policy: Policy) -> None: self.policy = policy - def _reset_limit_sources(self, source: SpeedLimitSource) -> None: + def _reset_limit_sources(self, source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource) -> None: self._limit_solutions[source] = 0. self._distance_solutions[source] = 0. @@ -98,21 +98,20 @@ class SpeedLimitResolver: self._limit_solutions[SpeedLimitSource.map] = next_speed_limit self._distance_solutions[SpeedLimitSource.map] = distance_to_speed_limit_ahead - def _consolidate(self) -> tuple[float, float, SpeedLimitSource]: + def _consolidate(self) -> tuple[float, float, custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource]: source = self._get_source_solution_according_to_policy() speed_limit = self._limit_solutions[source] if source else 0. distance = self._distance_solutions[source] if source else 0. return speed_limit, distance, source - def _get_source_solution_according_to_policy(self) -> SpeedLimitSource: + def _get_source_solution_according_to_policy(self) -> custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource: sources_for_policy = self._policy_to_sources_map[self.policy] if self.policy != Policy.combined: # They are ordered in the order of preference, so we pick the first that's non zero for source in sources_for_policy: - if self._limit_solutions[source] > 0.: - return source + return source if self._limit_solutions[source] > 0. else SpeedLimitSource.none limits = np.array([self._limit_solutions[source] for source in sources_for_policy], dtype=float) sources = np.array(sources_for_policy, dtype=int) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 66aead5c68..20f9ba2494 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -21,7 +21,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import S from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource ALL_STATES = tuple(SpeedLimitControlState.schema.enumerants.values()) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py index e01a60164d..b7dcdd45a4 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py @@ -10,7 +10,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import L from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver, ALL_SOURCES from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource def create_mock(properties, mocker: MockerFixture): From c928e32c04c962287251612872c85d2d2819c613 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 02:53:37 -0400 Subject: [PATCH 153/188] fix --- .../controls/lib/longitudinal_planner.py | 2 -- .../lib/speed_limit_controller/common.py | 4 --- .../speed_limit_controller.py | 28 ++++++------------- .../speed_limit_resolver.py | 14 +++++----- .../tests/test_speed_limit_controller.py | 2 +- .../tests/test_speed_limit_resolver.py | 2 +- 6 files changed, 17 insertions(+), 35 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index e6ede838ea..1a3372d7e7 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -7,7 +7,6 @@ See the LICENSE.md file in the root directory for more details. from cereal import messaging, custom from opendbc.car import structs -from openpilot.common.params import Params from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver @@ -20,7 +19,6 @@ DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimen class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.events_sp = EventsSP() - self.params = Params() self.resolver = SpeedLimitResolver() diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py index 7e5d117d21..15e8ab8ad2 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py @@ -9,10 +9,6 @@ class Policy(IntEnum): combined = 4 -class Engage(IntEnum): - auto = 0 - - class OffsetType(IntEnum): off = 0 fixed = 1 diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index ba9110205e..399a80a638 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -14,12 +14,12 @@ from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, \ SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE, DISABLED_GUARD_PERIOD from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Engage, OffsetType +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import OffsetType from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.selfdrive.modeld.constants import ModelConstants EventNameSP = custom.OnroadEventSP.EventName -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource ACTIVE_STATES = (SpeedLimitControlState.active, SpeedLimitControlState.adapting) ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pending, *ACTIVE_STATES) @@ -28,7 +28,7 @@ ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pendi class SpeedLimitController: _speed_limit: float _distance: float - _source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource + _source: custom.LongitudinalPlanSP.SpeedLimitSource v_ego: float a_ego: float v_offset: float @@ -82,14 +82,6 @@ class SpeedLimitController: def speed_limit_offset(self) -> float: return self.get_offset(self.offset_type, self.offset_value) - @property - def distance(self) -> float: - return self._distance - - @property - def source(self) -> custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource: - return self._source - def get_v_target_from_control(self) -> float: if self.is_active: # If we have a current valid speed limit, use it @@ -129,10 +121,6 @@ class SpeedLimitController: self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) self.is_metric = self.params.get_bool("IsMetric") - @staticmethod - def read_engage_type_param() -> Engage: - return Engage.auto - def initial_max_set_confirmed(self) -> bool: return bool(abs(self.v_cruise_setpoint - REQUIRED_INITIAL_MAX_SET_SPEED) <= CRUISE_SPEED_TOLERANCE) @@ -144,10 +132,8 @@ class SpeedLimitController: return False - def update_calculations(self, v_ego: float, a_ego: float, v_cruise_setpoint: float) -> None: + def update_calculations(self, v_cruise_setpoint: float) -> None: self.v_cruise_setpoint = v_cruise_setpoint if not np.isnan(v_cruise_setpoint) else 0.0 - self.v_ego = v_ego - self.a_ego = a_ego # Update current velocity offset (error) self.v_offset = self.speed_limit_final - self.v_ego @@ -244,15 +230,17 @@ class SpeedLimitController: return enabled, active def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, - speed_limit: float, distance: float, source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource, events_sp: EventsSP) -> float: + speed_limit: float, distance: float, source: custom.LongitudinalPlanSP.SpeedLimitSource, events_sp: EventsSP) -> float: self.op_engaged = long_active + self.v_ego = v_ego + self.a_ego = a_ego self._speed_limit = speed_limit self._distance = distance self._source = source self.update_params() - self.update_calculations(v_ego, a_ego, v_cruise_setpoint) + self.update_calculations(v_cruise_setpoint) self.is_enabled, self.is_active = self.update_state_machine() self.update_events(events_sp) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py index d641c3b570..23c25dfd39 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py @@ -9,18 +9,18 @@ from openpilot.common.realtime import DT_MDL from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC, PARAMS_UPDATE_PERIOD from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource ALL_SOURCES = tuple(SpeedLimitSource.schema.enumerants.values()) class SpeedLimitResolver: - _limit_solutions: dict[custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource, float] - _distance_solutions: dict[custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource, float] + _limit_solutions: dict[custom.LongitudinalPlanSP.SpeedLimitSource, float] + _distance_solutions: dict[custom.LongitudinalPlanSP.SpeedLimitSource, float] _v_ego: float speed_limit: float distance: float - source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource + source: custom.LongitudinalPlanSP.SpeedLimitSource def __init__(self): self.params = Params() @@ -49,7 +49,7 @@ class SpeedLimitResolver: def change_policy(self, policy: Policy) -> None: self.policy = policy - def _reset_limit_sources(self, source: custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource) -> None: + def _reset_limit_sources(self, source: custom.LongitudinalPlanSP.SpeedLimitSource) -> None: self._limit_solutions[source] = 0. self._distance_solutions[source] = 0. @@ -98,14 +98,14 @@ class SpeedLimitResolver: self._limit_solutions[SpeedLimitSource.map] = next_speed_limit self._distance_solutions[SpeedLimitSource.map] = distance_to_speed_limit_ahead - def _consolidate(self) -> tuple[float, float, custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource]: + def _consolidate(self) -> tuple[float, float, custom.LongitudinalPlanSP.SpeedLimitSource]: source = self._get_source_solution_according_to_policy() speed_limit = self._limit_solutions[source] if source else 0. distance = self._distance_solutions[source] if source else 0. return speed_limit, distance, source - def _get_source_solution_according_to_policy(self) -> custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource: + def _get_source_solution_according_to_policy(self) -> custom.LongitudinalPlanSP.SpeedLimitSource: sources_for_policy = self._policy_to_sources_map[self.policy] if self.policy != Policy.combined: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py index 20f9ba2494..66aead5c68 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py @@ -21,7 +21,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import S from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource ALL_STATES = tuple(SpeedLimitControlState.schema.enumerants.values()) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py index b7dcdd45a4..e01a60164d 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py @@ -10,7 +10,7 @@ from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import L from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver, ALL_SOURCES from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy -SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitControl.SpeedLimitSource +SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource def create_mock(properties, mocker: MockerFixture): From 183896f01a9491a2a25e5120a780816b5cb6ac24 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 11:05:55 -0400 Subject: [PATCH 154/188] move around --- .../speed_limit_controller.py | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 399a80a638..d692a41b5f 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -78,10 +78,18 @@ class SpeedLimitController: def speed_limit_final(self) -> float: return self._speed_limit + self.speed_limit_offset + @property + def speed_limit_changed(self) -> bool: + return bool(self._speed_limit != self.speed_limit_prev) + @property def speed_limit_offset(self) -> float: return self.get_offset(self.offset_type, self.offset_value) + @property + def v_cruise_setpoint_changed(self) -> bool: + return bool(self.v_cruise_setpoint != self.v_cruise_setpoint_prev) + def get_v_target_from_control(self) -> float: if self.is_active: # If we have a current valid speed limit, use it @@ -96,14 +104,6 @@ class SpeedLimitController: # Fallback return V_CRUISE_UNSET - @property - def v_cruise_setpoint_changed(self) -> bool: - return bool(self.v_cruise_setpoint != self.v_cruise_setpoint_prev) - - @property - def speed_limit_changed(self) -> bool: - return bool(self._speed_limit != self.speed_limit_prev) - def get_offset(self, offset_type: OffsetType, offset_value: int) -> float: if offset_type == OffsetType.off: return 0 @@ -150,15 +150,6 @@ class SpeedLimitController: def get_active_state_target_acceleration(self) -> float: return self.v_offset / float(ModelConstants.T_IDXS[CONTROL_N]) - def update_events(self, events_sp: EventsSP) -> None: - if self.is_active: - if self.state == SpeedLimitControlState.preActive: - events_sp.add(EventNameSP.speedLimitPreActive) - elif self._state_prev not in ACTIVE_STATES: - events_sp.add(EventNameSP.speedLimitActive) - elif self.speed_limit_changed: - events_sp.add(EventNameSP.speedLimitValueChange) - def update_state_machine(self): self._state_prev = self.state @@ -229,6 +220,15 @@ class SpeedLimitController: return enabled, active + def update_events(self, events_sp: EventsSP) -> None: + if self.is_active: + if self.state == SpeedLimitControlState.preActive: + events_sp.add(EventNameSP.speedLimitPreActive) + elif self._state_prev not in ACTIVE_STATES: + events_sp.add(EventNameSP.speedLimitActive) + elif self.speed_limit_changed: + events_sp.add(EventNameSP.speedLimitValueChange) + def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, speed_limit: float, distance: float, source: custom.LongitudinalPlanSP.SpeedLimitSource, events_sp: EventsSP) -> float: self.op_engaged = long_active From ea53111afc011530cbda17574e0e6726a11b3bc2 Mon Sep 17 00:00:00 2001 From: Jimmy <9859727+Quantizr@users.noreply.github.com> Date: Tue, 9 Sep 2025 07:56:07 -1000 Subject: [PATCH 155/188] basic jotpluggler (#36045) * jotpluggler! * demo, executable, fontfile * calc max and min, numpy, cloudlog * mypy things * simplified data.py * multiprocessed data ingest * allow verrryyy long search results * stream in multiprocessed segments * bug fixes * simplify and speed up timeseries * small fixes * rewrite layout * resizable layouts * cleanup * downsampling * deque for consistency * use item_visible_handler * only build visible UI * don't delete item handlers, add locks, don't expand large lists * delete item handlers after a frame * small data tree improvements * seperate datatree into its own file * reset when loading new segments * fix plot window resizing and recursive split resizing logic --- pyproject.toml | 1 + tools/jotpluggler/data.py | 311 ++++++++++++++++++++++++++++++++++ tools/jotpluggler/datatree.py | 266 +++++++++++++++++++++++++++++ tools/jotpluggler/layout.py | 262 ++++++++++++++++++++++++++++ tools/jotpluggler/pluggle.py | 247 +++++++++++++++++++++++++++ tools/jotpluggler/views.py | 195 +++++++++++++++++++++ uv.lock | 17 ++ 7 files changed, 1299 insertions(+) create mode 100644 tools/jotpluggler/data.py create mode 100644 tools/jotpluggler/datatree.py create mode 100644 tools/jotpluggler/layout.py create mode 100755 tools/jotpluggler/pluggle.py create mode 100644 tools/jotpluggler/views.py diff --git a/pyproject.toml b/pyproject.toml index 9ed1d8a4cb..88c4d06739 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -124,6 +124,7 @@ dev = [ tools = [ "metadrive-simulator @ https://github.com/commaai/metadrive/releases/download/MetaDrive-minimal-0.4.2.4/metadrive_simulator-0.4.2.4-py3-none-any.whl ; (platform_machine != 'aarch64')", + "dearpygui>=2.1.0", ] [project.urls] diff --git a/tools/jotpluggler/data.py b/tools/jotpluggler/data.py new file mode 100644 index 0000000000..41c305718e --- /dev/null +++ b/tools/jotpluggler/data.py @@ -0,0 +1,311 @@ +import numpy as np +import threading +import multiprocessing +import bisect +from collections import defaultdict +import tqdm +from openpilot.common.swaglog import cloudlog +from openpilot.tools.lib.logreader import _LogFileReader, LogReader + + +def flatten_dict(d: dict, sep: str = "/", prefix: str = None) -> dict: + result = {} + stack: list[tuple] = [(d, prefix)] + + while stack: + obj, current_prefix = stack.pop() + + if isinstance(obj, dict): + for key, val in obj.items(): + new_prefix = key if current_prefix is None else f"{current_prefix}{sep}{key}" + if isinstance(val, (dict, list)): + stack.append((val, new_prefix)) + else: + result[new_prefix] = val + elif isinstance(obj, list): + for i, item in enumerate(obj): + new_prefix = f"{current_prefix}{sep}{i}" + if isinstance(item, (dict, list)): + stack.append((item, new_prefix)) + else: + result[new_prefix] = item + else: + if current_prefix is not None: + result[current_prefix] = obj + return result + + +def extract_field_types(schema, prefix, field_types_dict): + stack = [(schema, prefix)] + + while stack: + current_schema, current_prefix = stack.pop() + + for field in current_schema.fields_list: + field_name = field.proto.name + field_path = f"{current_prefix}/{field_name}" + field_proto = field.proto + field_which = field_proto.which() + + field_type = field_proto.slot.type.which() if field_which == 'slot' else field_which + field_types_dict[field_path] = field_type + + if field_which == 'slot': + slot_type = field_proto.slot.type + type_which = slot_type.which() + + if type_which == 'list': + element_type = slot_type.list.elementType.which() + list_path = f"{field_path}/*" + field_types_dict[list_path] = element_type + + if element_type == 'struct': + stack.append((field.schema.elementType, list_path)) + + elif type_which == 'struct': + stack.append((field.schema, field_path)) + + elif field_which == 'group': + stack.append((field.schema, field_path)) + + +def _convert_to_optimal_dtype(values_list, capnp_type): + if not values_list: + return np.array([]) + + dtype_mapping = { + 'bool': np.bool_, 'int8': np.int8, 'int16': np.int16, 'int32': np.int32, 'int64': np.int64, + 'uint8': np.uint8, 'uint16': np.uint16, 'uint32': np.uint32, 'uint64': np.uint64, + 'float32': np.float32, 'float64': np.float64, 'text': object, 'data': object, + 'enum': object, 'anyPointer': object, + } + + target_dtype = dtype_mapping.get(capnp_type) + return np.array(values_list, dtype=target_dtype) if target_dtype else np.array(values_list) + + +def _match_field_type(field_path, field_types): + if field_path in field_types: + return field_types[field_path] + + path_parts = field_path.split('/') + template_parts = [p if not p.isdigit() else '*' for p in path_parts] + template_path = '/'.join(template_parts) + return field_types.get(template_path) + + +def msgs_to_time_series(msgs): + """Extract scalar fields and return (time_series_data, start_time, end_time).""" + collected_data = defaultdict(lambda: {'timestamps': [], 'columns': defaultdict(list), 'sparse_fields': set()}) + field_types = {} + extracted_schemas = set() + min_time = max_time = None + + for msg in msgs: + typ = msg.which() + timestamp = msg.logMonoTime * 1e-9 + if typ != 'initData': + if min_time is None: + min_time = timestamp + max_time = timestamp + + sub_msg = getattr(msg, typ) + if not hasattr(sub_msg, 'to_dict') or typ in ('qcomGnss', 'ubloxGnss'): + continue + + if hasattr(sub_msg, 'schema') and typ not in extracted_schemas: + extract_field_types(sub_msg.schema, typ, field_types) + extracted_schemas.add(typ) + + msg_dict = sub_msg.to_dict(verbose=True) + flat_dict = flatten_dict(msg_dict) + flat_dict['_valid'] = msg.valid + + type_data = collected_data[typ] + columns, sparse_fields = type_data['columns'], type_data['sparse_fields'] + known_fields = set(columns.keys()) + missing_fields = known_fields - flat_dict.keys() + + for field, value in flat_dict.items(): + if field not in known_fields and type_data['timestamps']: + sparse_fields.add(field) + columns[field].append(value) + if value is None: + sparse_fields.add(field) + + for field in missing_fields: + columns[field].append(None) + sparse_fields.add(field) + + type_data['timestamps'].append(timestamp) + + final_result = {} + for typ, data in collected_data.items(): + if not data['timestamps']: + continue + + typ_result = {'t': np.array(data['timestamps'], dtype=np.float64)} + sparse_fields = data['sparse_fields'] + + for field_name, values in data['columns'].items(): + if len(values) < len(data['timestamps']): + values = [None] * (len(data['timestamps']) - len(values)) + values + sparse_fields.add(field_name) + + if field_name in sparse_fields: + typ_result[field_name] = np.array(values, dtype=object) + else: + capnp_type = _match_field_type(f"{typ}/{field_name}", field_types) + typ_result[field_name] = _convert_to_optimal_dtype(values, capnp_type) + + final_result[typ] = typ_result + + return final_result, min_time or 0.0, max_time or 0.0 + + +def _process_segment(segment_identifier: str): + try: + lr = _LogFileReader(segment_identifier, sort_by_time=True) + return msgs_to_time_series(lr) + except Exception as e: + cloudlog.warning(f"Warning: Failed to process segment {segment_identifier}: {e}") + return {}, 0.0, 0.0 + + +class DataManager: + def __init__(self): + self._segments = [] + self._segment_starts = [] + self._start_time = 0.0 + self._duration = 0.0 + self._paths = set() + self._observers = [] + self._loading = False + self._lock = threading.RLock() + + def load_route(self, route: str) -> None: + if self._loading: + return + self._reset() + threading.Thread(target=self._load_async, args=(route,), daemon=True).start() + + def get_timeseries(self, path: str): + with self._lock: + msg_type, field = path.split('/', 1) + times, values = [], [] + + for segment in self._segments: + if msg_type in segment and field in segment[msg_type]: + times.append(segment[msg_type]['t']) + values.append(segment[msg_type][field]) + + if not times: + return [], [] + + combined_times = np.concatenate(times) - self._start_time + if len(values) > 1 and any(arr.dtype != values[0].dtype for arr in values): + values = [arr.astype(object) for arr in values] + + return combined_times, np.concatenate(values) + + def get_value_at(self, path: str, time: float): + with self._lock: + MAX_LOOKBACK = 5.0 # seconds + absolute_time = self._start_time + time + message_type, field = path.split('/', 1) + current_index = bisect.bisect_right(self._segment_starts, absolute_time) - 1 + for index in (current_index, current_index - 1): + if not 0 <= index < len(self._segments): + continue + segment = self._segments[index].get(message_type) + if not segment or field not in segment: + continue + times = segment['t'] + if len(times) == 0 or (index != current_index and absolute_time - times[-1] > MAX_LOOKBACK): + continue + position = np.searchsorted(times, absolute_time, 'right') - 1 + if position >= 0 and absolute_time - times[position] <= MAX_LOOKBACK: + return segment[field][position] + return None + + def get_all_paths(self): + with self._lock: + return sorted(self._paths) + + def get_duration(self): + with self._lock: + return self._duration + + def is_plottable(self, path: str): + data = self.get_timeseries(path) + if data is None: + return False + _, values = data + return np.issubdtype(values.dtype, np.number) or np.issubdtype(values.dtype, np.bool_) + + def add_observer(self, callback): + with self._lock: + self._observers.append(callback) + + def remove_observer(self, callback): + with self._lock: + if callback in self._observers: + self._observers.remove(callback) + + def _reset(self): + with self._lock: + self._loading = True + self._segments.clear() + self._segment_starts.clear() + self._paths.clear() + self._start_time = self._duration = 0.0 + observers = self._observers.copy() + + for callback in observers: + callback({'reset': True}) + + def _load_async(self, route: str): + try: + lr = LogReader(route, sort_by_time=True) + if not lr.logreader_identifiers: + cloudlog.warning(f"Warning: No log segments found for route: {route}") + return + + num_processes = max(1, multiprocessing.cpu_count() // 2) + with multiprocessing.Pool(processes=num_processes) as pool, tqdm.tqdm(total=len(lr.logreader_identifiers), desc="Processing Segments") as pbar: + for segment_result, start_time, end_time in pool.imap(_process_segment, lr.logreader_identifiers): + pbar.update(1) + if segment_result: + self._add_segment(segment_result, start_time, end_time) + except Exception: + cloudlog.exception(f"Error loading route {route}:") + finally: + self._finalize_loading() + + def _add_segment(self, segment_data: dict, start_time: float, end_time: float): + with self._lock: + self._segments.append(segment_data) + self._segment_starts.append(start_time) + + if len(self._segments) == 1: + self._start_time = start_time + self._duration = end_time - self._start_time + + for msg_type, data in segment_data.items(): + for field in data.keys(): + if field != 't': + self._paths.add(f"{msg_type}/{field}") + + observers = self._observers.copy() + + for callback in observers: + callback({'segment_added': True, 'duration': self._duration, 'segment_count': len(self._segments)}) + + def _finalize_loading(self): + with self._lock: + self._loading = False + observers = self._observers.copy() + duration = self._duration + + for callback in observers: + callback({'loading_complete': True, 'duration': duration}) diff --git a/tools/jotpluggler/datatree.py b/tools/jotpluggler/datatree.py new file mode 100644 index 0000000000..7bd026cf98 --- /dev/null +++ b/tools/jotpluggler/datatree.py @@ -0,0 +1,266 @@ +import os +import re +import threading +import numpy as np +from collections import deque +import dearpygui.dearpygui as dpg + + +class DataTreeNode: + def __init__(self, name: str, full_path: str = "", parent=None): + self.name = name + self.full_path = full_path + self.parent = parent + self.children: dict[str, DataTreeNode] = {} + self.is_leaf = False + self.child_count = 0 + self.is_plottable: bool | None = None + self.ui_created = False + self.children_ui_created = False + self.ui_tag: str | None = None + + +class DataTree: + MAX_NODES_PER_FRAME = 50 + + def __init__(self, data_manager, playback_manager): + self.data_manager = data_manager + self.playback_manager = playback_manager + self.current_search = "" + self.data_tree = DataTreeNode(name="root") + self._build_queue: deque[tuple[DataTreeNode, str | None, str | int]] = deque() + self._all_paths_cache: set[str] = set() + self._item_handlers: set[str] = set() + self._avg_char_width = None + self._queued_search = None + self._new_data = False + self._ui_lock = threading.RLock() + self.data_manager.add_observer(self._on_data_loaded) + + def create_ui(self, parent_tag: str): + with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1): + dpg.add_text("Available Data") + dpg.add_separator() + dpg.add_input_text(tag="search_input", width=-1, hint="Search fields...", callback=self.search_data) + dpg.add_separator() + with dpg.group(tag="data_tree_container"): + pass + + def _on_data_loaded(self, data: dict): + with self._ui_lock: + if data.get('segment_added'): + self._new_data = True + elif data.get('reset'): + self._all_paths_cache = set() + self._new_data = True + + + def _populate_tree(self): + self._clear_ui() + self.data_tree = self._add_paths_to_tree(self._all_paths_cache, incremental=False) + if self.data_tree: + self._request_children_build(self.data_tree) + + def _add_paths_to_tree(self, paths, incremental=False): + search_term = self.current_search.strip().lower() + filtered_paths = [path for path in paths if self._should_show_path(path, search_term)] + target_tree = self.data_tree if incremental else DataTreeNode(name="root") + + if not filtered_paths: + return target_tree + + parent_nodes_to_recheck = set() + for path in sorted(filtered_paths): + parts = path.split('/') + current_node = target_tree + current_path_prefix = "" + + for i, part in enumerate(parts): + current_path_prefix = f"{current_path_prefix}/{part}" if current_path_prefix else part + if i < len(parts) - 1: + parent_nodes_to_recheck.add(current_node) # for incremental changes from new data + if part not in current_node.children: + current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix, parent=current_node) + current_node = current_node.children[part] + + if not current_node.is_leaf: + current_node.is_leaf = True + + self._calculate_child_counts(target_tree) + if incremental: + for p_node in parent_nodes_to_recheck: + p_node.children_ui_created = False + self._request_children_build(p_node) + return target_tree + + def update_frame(self, font): + with self._ui_lock: + if self._avg_char_width is None and dpg.is_dearpygui_running(): + self._avg_char_width = self.calculate_avg_char_width(font) + + if self._new_data: + current_paths = set(self.data_manager.get_all_paths()) + new_paths = current_paths - self._all_paths_cache + all_paths_empty = not self._all_paths_cache + self._all_paths_cache = current_paths + if all_paths_empty: + self._populate_tree() + elif new_paths: + self._add_paths_to_tree(new_paths, incremental=True) + self._new_data = False + return + + if self._queued_search is not None: + self.current_search = self._queued_search + self._all_paths_cache = set(self.data_manager.get_all_paths()) + self._populate_tree() + self._queued_search = None + return + + nodes_processed = 0 + while self._build_queue and nodes_processed < self.MAX_NODES_PER_FRAME: + child_node, parent_tag, before_tag = self._build_queue.popleft() + if not child_node.ui_created: + if child_node.is_leaf: + self._create_leaf_ui(child_node, parent_tag, before_tag) + else: + self._create_tree_node_ui(child_node, parent_tag, before_tag) + nodes_processed += 1 + + def search_data(self): + self._queued_search = dpg.get_value("search_input") + + def _clear_ui(self): + for handler_tag in self._item_handlers: + dpg.configure_item(handler_tag, show=False) + dpg.set_frame_callback(dpg.get_frame_count() + 1, callback=self._delete_handlers, user_data=list(self._item_handlers)) + self._item_handlers.clear() + + if dpg.does_item_exist("data_tree_container"): + dpg.delete_item("data_tree_container", children_only=True) + + self._build_queue.clear() + + def _delete_handlers(self, sender, app_data, user_data): + for handler in user_data: + dpg.delete_item(handler) + + def _calculate_child_counts(self, node: DataTreeNode): + if node.is_leaf: + node.child_count = 0 + else: + node.child_count = len(node.children) + for child in node.children.values(): + self._calculate_child_counts(child) + + def _create_tree_node_ui(self, node: DataTreeNode, parent_tag: str, before: str | int): + tag = f"tree_{node.full_path}" + node.ui_tag = tag + label = f"{node.name} ({node.child_count} fields)" + search_term = self.current_search.strip().lower() + expand = bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_descendant_paths(node)) + if expand and node.parent and node.parent.child_count > 100 and node.child_count > 2: # don't fully autoexpand large lists (only affects procLog rn) + label += " (+)" + expand = False + + with dpg.tree_node( + label=label, parent=parent_tag, tag=tag, default_open=expand, open_on_arrow=True, open_on_double_click=True, before=before, delay_search=True + ): + with dpg.item_handler_registry() as handler_tag: + dpg.add_item_toggled_open_handler(callback=lambda s, a, u: self._request_children_build(node)) + dpg.add_item_visible_handler(callback=lambda s, a, u: self._request_children_build(node)) + dpg.bind_item_handler_registry(tag, handler_tag) + self._item_handlers.add(handler_tag) + + node.ui_created = True + + def _create_leaf_ui(self, node: DataTreeNode, parent_tag: str, before: str | int): + with dpg.group(parent=parent_tag, tag=f"leaf_{node.full_path}", before=before, delay_search=True) as draggable_group: + with dpg.table(header_row=False, policy=dpg.mvTable_SizingStretchProp, delay_search=True): + dpg.add_table_column(init_width_or_weight=0.5) + dpg.add_table_column(init_width_or_weight=0.5) + with dpg.table_row(): + dpg.add_text(node.name) + dpg.add_text("N/A", tag=f"value_{node.full_path}") + + if node.is_plottable is None: + node.is_plottable = self.data_manager.is_plottable(node.full_path) + if node.is_plottable: + with dpg.drag_payload(parent=draggable_group, drag_data=node.full_path, payload_type="TIMESERIES_PAYLOAD"): + dpg.add_text(f"Plot: {node.full_path}") + + with dpg.item_handler_registry() as handler_tag: + dpg.add_item_visible_handler(callback=self._on_item_visible, user_data=node.full_path) + dpg.bind_item_handler_registry(draggable_group, handler_tag) + self._item_handlers.add(handler_tag) + + node.ui_created = True + node.ui_tag = f"value_{node.full_path}" + + def _on_item_visible(self, sender, app_data, user_data): + with self._ui_lock: + path = user_data + value_tag = f"value_{path}" + value_column_width = dpg.get_item_rect_size("sidebar_window")[0] // 2 + value = self.data_manager.get_value_at(path, self.playback_manager.current_time_s) + if value is not None: + formatted_value = self.format_and_truncate(value, value_column_width, self._avg_char_width) + dpg.set_value(value_tag, formatted_value) + else: + dpg.set_value(value_tag, "N/A") + + def _request_children_build(self, node: DataTreeNode): + with self._ui_lock: + if not node.children_ui_created and (node.name == "root" or (node.ui_tag is not None and dpg.get_value(node.ui_tag))): # check root or node expanded + parent_tag = "data_tree_container" if node.name == "root" else node.ui_tag + sorted_children = sorted(node.children.values(), key=self._natural_sort_key) + next_existing: list[int | str] = [0] * len(sorted_children) + current_before_tag: int | str = 0 + + for i in range(len(sorted_children) - 1, -1, -1): # calculate "before_tag" for correct ordering when incrementally building tree + child = sorted_children[i] + next_existing[i] = current_before_tag + if child.ui_created: + candidate_tag = f"leaf_{child.full_path}" if child.is_leaf else f"tree_{child.full_path}" + if dpg.does_item_exist(candidate_tag): + current_before_tag = candidate_tag + + for i, child_node in enumerate(sorted_children): + if not child_node.ui_created: + before_tag = next_existing[i] + self._build_queue.append((child_node, parent_tag, before_tag)) + node.children_ui_created = True + + def _should_show_path(self, path: str, search_term: str) -> bool: + if 'DEPRECATED' in path and not os.environ.get('SHOW_DEPRECATED'): + return False + return not search_term or search_term in path.lower() + + def _natural_sort_key(self, node: DataTreeNode): + node_type_key = node.is_leaf + parts = [int(p) if p.isdigit() else p.lower() for p in re.split(r'(\d+)', node.name) if p] + return (node_type_key, parts) + + def _get_descendant_paths(self, node: DataTreeNode): + for child_name, child_node in node.children.items(): + child_name_lower = child_name.lower() + if child_node.is_leaf: + yield child_name_lower + else: + for path in self._get_descendant_paths(child_node): + yield f"{child_name_lower}/{path}" + + @staticmethod + def calculate_avg_char_width(font): + sample_text = "abcdefghijklmnopqrstuvwxyz0123456789" + if size := dpg.get_text_size(sample_text, font=font): + return size[0] / len(sample_text) + return None + + @staticmethod + def format_and_truncate(value, available_width: float, avg_char_width: float) -> str: + s = f"{value:.5f}" if np.issubdtype(type(value), np.floating) else str(value) + max_chars = int(available_width / avg_char_width) - 3 + if len(s) > max_chars: + return s[: max(0, max_chars)] + "..." + return s diff --git a/tools/jotpluggler/layout.py b/tools/jotpluggler/layout.py new file mode 100644 index 0000000000..0c40116e66 --- /dev/null +++ b/tools/jotpluggler/layout.py @@ -0,0 +1,262 @@ +import dearpygui.dearpygui as dpg +from openpilot.tools.jotpluggler.data import DataManager +from openpilot.tools.jotpluggler.views import TimeSeriesPanel + +GRIP_SIZE = 4 +MIN_PANE_SIZE = 60 + + +class PlotLayoutManager: + def __init__(self, data_manager: DataManager, playback_manager, worker_manager, scale: float = 1.0): + self.data_manager = data_manager + self.playback_manager = playback_manager + self.worker_manager = worker_manager + self.scale = scale + self.container_tag = "plot_layout_container" + self.active_panels: list = [] + + self.grip_size = int(GRIP_SIZE * self.scale) + self.min_pane_size = int(MIN_PANE_SIZE * self.scale) + + initial_panel = TimeSeriesPanel(data_manager, playback_manager, worker_manager) + self.layout: dict = {"type": "panel", "panel": initial_panel} + + def create_ui(self, parent_tag: str): + if dpg.does_item_exist(self.container_tag): + dpg.delete_item(self.container_tag) + + with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True): + container_width, container_height = dpg.get_item_rect_size(self.container_tag) + self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height) + + def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): + if layout["type"] == "panel": + self._create_panel_ui(layout, parent_tag, path) + else: + self._create_split_ui(layout, parent_tag, path, width, height) + + def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]): + panel_tag = self._path_to_tag(path, "panel") + panel = layout["panel"] + self.active_panels.append(panel) + + with dpg.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): + with dpg.group(horizontal=True): + dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(panel, "title", v)) + dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale)) + dpg.add_button(label="Clear", callback=lambda: self.clear_panel(panel), width=int(40 * self.scale)) + dpg.add_button(label="Delete", callback=lambda: self.delete_panel(path), width=int(40 * self.scale)) + dpg.add_button(label="Split H", callback=lambda: self.split_panel(path, 0), width=int(40 * self.scale)) + dpg.add_button(label="Split V", callback=lambda: self.split_panel(path, 1), width=int(40 * self.scale)) + + dpg.add_separator() + + content_tag = self._path_to_tag(path, "content") + with dpg.child_window(tag=content_tag, border=False, height=-1, width=-1, no_scrollbar=True): + panel.create_ui(content_tag) + + def _create_split_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): + split_tag = self._path_to_tag(path, "split") + orientation, _, pane_sizes = self._get_split_geometry(layout, (width, height)) + + with dpg.group(tag=split_tag, parent=parent_tag, horizontal=orientation == 0): + for i, child_layout in enumerate(layout["children"]): + child_path = path + [i] + container_tag = self._path_to_tag(child_path, "container") + pane_width, pane_height = [(pane_sizes[i], -1), (-1, pane_sizes[i])][orientation] # fill 2nd dim up to the border + with dpg.child_window(tag=container_tag, width=pane_width, height=pane_height, border=False, no_scrollbar=True): + child_width, child_height = [(pane_sizes[i], height), (width, pane_sizes[i])][orientation] + self._create_ui_recursive(child_layout, container_tag, child_path, child_width, child_height) + if i < len(layout["children"]) - 1: + self._create_grip(split_tag, path, i, orientation) + + def clear_panel(self, panel): + panel.clear() + + def delete_panel(self, panel_path: list[int]): + if not panel_path: # Root deletion + old_panel = self.layout["panel"] + old_panel.destroy_ui() + self.active_panels.remove(old_panel) + new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager, self.worker_manager) + self.layout = {"type": "panel", "panel": new_panel} + self._rebuild_ui_at_path([]) + return + + parent, child_index = self._get_parent_and_index(panel_path) + layout_to_delete = parent["children"][child_index] + self._cleanup_ui_recursive(layout_to_delete, panel_path) + + parent["children"].pop(child_index) + parent["proportions"].pop(child_index) + + if len(parent["children"]) == 1: # remove parent and collapse + remaining_child = parent["children"][0] + if len(panel_path) == 1: # parent is at root level - promote remaining child to root + self.layout = remaining_child + self._rebuild_ui_at_path([]) + else: # replace parent with remaining child in grandparent + grandparent_path = panel_path[:-2] + parent_index = panel_path[-2] + self._replace_layout_at_path(grandparent_path + [parent_index], remaining_child) + self._rebuild_ui_at_path(grandparent_path + [parent_index]) + else: # redistribute proportions + equal_prop = 1.0 / len(parent["children"]) + parent["proportions"] = [equal_prop] * len(parent["children"]) + self._rebuild_ui_at_path(panel_path[:-1]) + + def split_panel(self, panel_path: list[int], orientation: int): + current_layout = self._get_layout_at_path(panel_path) + existing_panel = current_layout["panel"] + new_panel = TimeSeriesPanel(self.data_manager, self.playback_manager, self.worker_manager) + parent, child_index = self._get_parent_and_index(panel_path) + + if parent is None: # Root split + self.layout = { + "type": "split", + "orientation": orientation, + "children": [{"type": "panel", "panel": existing_panel}, {"type": "panel", "panel": new_panel}], + "proportions": [0.5, 0.5], + } + self._rebuild_ui_at_path([]) + elif parent["type"] == "split" and parent["orientation"] == orientation: # Same orientation - insert into existing split + parent["children"].insert(child_index + 1, {"type": "panel", "panel": new_panel}) + parent["proportions"] = [1.0 / len(parent["children"])] * len(parent["children"]) + self._rebuild_ui_at_path(panel_path[:-1]) + else: # Different orientation - create new split level + new_split = {"type": "split", "orientation": orientation, "children": [current_layout, {"type": "panel", "panel": new_panel}], "proportions": [0.5, 0.5]} + self._replace_layout_at_path(panel_path, new_split) + self._rebuild_ui_at_path(panel_path) + + def _rebuild_ui_at_path(self, path: list[int]): + layout = self._get_layout_at_path(path) + if path: + container_tag = self._path_to_tag(path, "container") + else: # Root update + container_tag = self.container_tag + + self._cleanup_ui_recursive(layout, path) + dpg.delete_item(container_tag, children_only=True) + width, height = dpg.get_item_rect_size(container_tag) + self._create_ui_recursive(layout, container_tag, path, width, height) + + def _cleanup_ui_recursive(self, layout: dict, path: list[int]): + if layout["type"] == "panel": + panel = layout["panel"] + panel.destroy_ui() + if panel in self.active_panels: + self.active_panels.remove(panel) + else: + for i in range(len(layout["children"]) - 1): + handler_tag = f"{self._path_to_tag(path, f'grip_{i}')}_handler" + if dpg.does_item_exist(handler_tag): + dpg.delete_item(handler_tag) + + for i, child in enumerate(layout["children"]): + self._cleanup_ui_recursive(child, path + [i]) + + def update_all_panels(self): + for panel in self.active_panels: + panel.update() + + def on_viewport_resize(self): + self._resize_splits_recursive(self.layout, []) + + def _resize_splits_recursive(self, layout: dict, path: list[int], width: int | None = None, height: int | None = None): + if layout["type"] == "split": + split_tag = self._path_to_tag(path, "split") + if dpg.does_item_exist(split_tag): + available_sizes = (width, height) if width and height else dpg.get_item_rect_size(dpg.get_item_parent(split_tag)) + orientation, _, pane_sizes = self._get_split_geometry(layout, available_sizes) + size_properties = ("width", "height") + + for i, child_layout in enumerate(layout["children"]): + child_path = path + [i] + container_tag = self._path_to_tag(child_path, "container") + if dpg.does_item_exist(container_tag): + dpg.configure_item(container_tag, **{size_properties[orientation]: pane_sizes[i]}) + child_width, child_height = [(pane_sizes[i], available_sizes[1]), (available_sizes[0], pane_sizes[i])][orientation] + self._resize_splits_recursive(child_layout, child_path, child_width, child_height) + + def _get_split_geometry(self, layout: dict, available_size: tuple[int, int]) -> tuple[int, int, list[int]]: + orientation = layout["orientation"] + num_grips = len(layout["children"]) - 1 + usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * self.grip_size)) + pane_sizes = [max(self.min_pane_size, int(usable_size * prop)) for prop in layout["proportions"]] + return orientation, usable_size, pane_sizes + + def _get_layout_at_path(self, path: list[int]) -> dict: + current = self.layout + for index in path: + current = current["children"][index] + return current + + def _get_parent_and_index(self, path: list[int]) -> tuple: + return (None, -1) if not path else (self._get_layout_at_path(path[:-1]), path[-1]) + + def _replace_layout_at_path(self, path: list[int], new_layout: dict): + if not path: + self.layout = new_layout + else: + parent, index = self._get_parent_and_index(path) + parent["children"][index] = new_layout + + def _path_to_tag(self, path: list[int], prefix: str = "") -> str: + path_str = "_".join(map(str, path)) if path else "root" + return f"{prefix}_{path_str}" if prefix else path_str + + def _create_grip(self, parent_tag: str, path: list[int], grip_index: int, orientation: int): + grip_tag = self._path_to_tag(path, f"grip_{grip_index}") + width, height = [(self.grip_size, -1), (-1, self.grip_size)][orientation] + + with dpg.child_window(tag=grip_tag, parent=parent_tag, width=width, height=height, no_scrollbar=True, border=False): + button_tag = dpg.add_button(label="", width=-1, height=-1) + + with dpg.item_handler_registry(tag=f"{grip_tag}_handler"): + user_data = (path, grip_index, orientation) + dpg.add_item_active_handler(callback=self._on_grip_drag, user_data=user_data) + dpg.add_item_deactivated_handler(callback=self._on_grip_end, user_data=user_data) + dpg.bind_item_handler_registry(button_tag, f"{grip_tag}_handler") + + def _on_grip_drag(self, sender, app_data, user_data): + path, grip_index, orientation = user_data + layout = self._get_layout_at_path(path) + + if "_drag_data" not in layout: + layout["_drag_data"] = {"initial_proportions": layout["proportions"][:], "start_mouse": dpg.get_mouse_pos(local=False)[orientation]} + return + + drag_data = layout["_drag_data"] + split_tag = self._path_to_tag(path, "split") + if not dpg.does_item_exist(split_tag): + return + + _, usable_size, _ = self._get_split_geometry(layout, dpg.get_item_rect_size(split_tag)) + current_coord = dpg.get_mouse_pos(local=False)[orientation] + delta = current_coord - drag_data["start_mouse"] + delta_prop = delta / usable_size + + left_idx = grip_index + right_idx = left_idx + 1 + initial = drag_data["initial_proportions"] + min_prop = self.min_pane_size / usable_size + + new_left = max(min_prop, initial[left_idx] + delta_prop) + new_right = max(min_prop, initial[right_idx] - delta_prop) + + total_available = initial[left_idx] + initial[right_idx] + if new_left + new_right > total_available: + if new_left > new_right: + new_left = total_available - new_right + else: + new_right = total_available - new_left + + layout["proportions"] = initial[:] + layout["proportions"][left_idx] = new_left + layout["proportions"][right_idx] = new_right + + self._resize_splits_recursive(layout, path) + + def _on_grip_end(self, sender, app_data, user_data): + path, _, _ = user_data + self._get_layout_at_path(path).pop("_drag_data", None) diff --git a/tools/jotpluggler/pluggle.py b/tools/jotpluggler/pluggle.py new file mode 100755 index 0000000000..9868b998ed --- /dev/null +++ b/tools/jotpluggler/pluggle.py @@ -0,0 +1,247 @@ +#!/usr/bin/env python3 +import argparse +import os +import pyautogui +import subprocess +import dearpygui.dearpygui as dpg +import multiprocessing +import uuid +import signal +from openpilot.common.basedir import BASEDIR +from openpilot.tools.jotpluggler.data import DataManager +from openpilot.tools.jotpluggler.datatree import DataTree +from openpilot.tools.jotpluggler.layout import PlotLayoutManager + +DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" + + +class WorkerManager: + def __init__(self, max_workers=None): + self.pool = multiprocessing.Pool(max_workers or min(4, multiprocessing.cpu_count()), initializer=WorkerManager.worker_initializer) + self.active_tasks = {} + + def submit_task(self, func, args_list, callback=None, task_id=None): + task_id = task_id or str(uuid.uuid4()) + + if task_id in self.active_tasks: + try: + self.active_tasks[task_id].terminate() + except Exception: + pass + + def handle_success(result): + self.active_tasks.pop(task_id, None) + if callback: + try: + callback(result) + except Exception as e: + print(f"Callback for task {task_id} failed: {e}") + + def handle_error(error): + self.active_tasks.pop(task_id, None) + print(f"Task {task_id} failed: {error}") + + async_result = self.pool.starmap_async(func, args_list, callback=handle_success, error_callback=handle_error) + self.active_tasks[task_id] = async_result + return task_id + + @staticmethod + def worker_initializer(): + signal.signal(signal.SIGINT, signal.SIG_IGN) + + def shutdown(self): + for task in self.active_tasks.values(): + try: + task.terminate() + except Exception: + pass + self.pool.terminate() + self.pool.join() + + +class PlaybackManager: + def __init__(self): + self.is_playing = False + self.current_time_s = 0.0 + self.duration_s = 0.0 + + def set_route_duration(self, duration: float): + self.duration_s = duration + self.seek(min(self.current_time_s, duration)) + + def toggle_play_pause(self): + if not self.is_playing and self.current_time_s >= self.duration_s: + self.seek(0.0) + self.is_playing = not self.is_playing + + def seek(self, time_s: float): + self.is_playing = False + self.current_time_s = max(0.0, min(time_s, self.duration_s)) + + def update_time(self, delta_t: float): + if self.is_playing: + self.current_time_s = min(self.current_time_s + delta_t, self.duration_s) + if self.current_time_s >= self.duration_s: + self.is_playing = False + return self.current_time_s + + +class MainController: + def __init__(self, scale: float = 1.0): + self.scale = scale + self.data_manager = DataManager() + self.playback_manager = PlaybackManager() + self.worker_manager = WorkerManager() + self._create_global_themes() + self.data_tree = DataTree(self.data_manager, self.playback_manager) + self.plot_layout_manager = PlotLayoutManager(self.data_manager, self.playback_manager, self.worker_manager, scale=self.scale) + self.data_manager.add_observer(self.on_data_loaded) + + def _create_global_themes(self): + with dpg.theme(tag="global_line_theme"): + with dpg.theme_component(dpg.mvLineSeries): + scaled_thickness = max(1.0, self.scale) + dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots) + + with dpg.theme(tag="global_timeline_theme"): + with dpg.theme_component(dpg.mvInfLineSeries): + scaled_thickness = max(1.0, self.scale) + dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots) + dpg.add_theme_color(dpg.mvPlotCol_Line, (255, 0, 0, 128), category=dpg.mvThemeCat_Plots) + + + def on_data_loaded(self, data: dict): + duration = data.get('duration', 0.0) + self.playback_manager.set_route_duration(duration) + + if data.get('reset'): + self.playback_manager.current_time_s = 0.0 + self.playback_manager.duration_s = 0.0 + self.playback_manager.is_playing = False + dpg.set_value("load_status", "Loading...") + dpg.set_value("timeline_slider", 0.0) + dpg.configure_item("timeline_slider", max_value=0.0) + dpg.configure_item("play_pause_button", label="Play") + dpg.configure_item("load_button", enabled=True) + elif data.get('loading_complete'): + num_paths = len(self.data_manager.get_all_paths()) + dpg.set_value("load_status", f"Loaded {num_paths} data paths") + dpg.configure_item("load_button", enabled=True) + elif data.get('segment_added'): + segment_count = data.get('segment_count', 0) + dpg.set_value("load_status", f"Loading... {segment_count} segments processed") + + dpg.configure_item("timeline_slider", max_value=duration) + + def setup_ui(self): + with dpg.window(tag="Primary Window"): + with dpg.group(horizontal=True): + # Left panel - Data tree + with dpg.child_window(label="Sidebar", width=300 * self.scale, tag="sidebar_window", border=True, resizable_x=True): + with dpg.group(horizontal=True): + dpg.add_input_text(tag="route_input", width=-75 * self.scale, hint="Enter route name...") + dpg.add_button(label="Load", callback=self.load_route, tag="load_button", width=-1) + dpg.add_text("Ready to load route", tag="load_status") + dpg.add_separator() + self.data_tree.create_ui("sidebar_window") + + # Right panel - Plots and timeline + with dpg.group(tag="right_panel"): + with dpg.child_window(label="Plot Window", border=True, height=-(30 + 13 * self.scale), tag="main_plot_area"): + self.plot_layout_manager.create_ui("main_plot_area") + + with dpg.child_window(label="Timeline", border=True): + with dpg.table(header_row=False, borders_innerH=False, borders_innerV=False, borders_outerH=False, borders_outerV=False): + dpg.add_table_column(width_fixed=True, init_width_or_weight=int(50 * self.scale)) # Play button + dpg.add_table_column(width_stretch=True) # Timeline slider + dpg.add_table_column(width_fixed=True, init_width_or_weight=int(50 * self.scale)) # FPS counter + with dpg.table_row(): + dpg.add_button(label="Play", tag="play_pause_button", callback=self.toggle_play_pause, width=int(50 * self.scale)) + dpg.add_slider_float(tag="timeline_slider", default_value=0.0, label="", width=-1, callback=self.timeline_drag) + dpg.add_text("", tag="fps_counter") + with dpg.item_handler_registry(tag="plot_resize_handler"): + dpg.add_item_resize_handler(callback=self.on_plot_resize) + dpg.bind_item_handler_registry("right_panel", "plot_resize_handler") + + dpg.set_primary_window("Primary Window", True) + + def on_plot_resize(self, sender, app_data, user_data): + self.plot_layout_manager.on_viewport_resize() + + def load_route(self): + route_name = dpg.get_value("route_input").strip() + if route_name: + dpg.set_value("load_status", "Loading route...") + dpg.configure_item("load_button", enabled=False) + self.data_manager.load_route(route_name) + + def toggle_play_pause(self, sender): + self.playback_manager.toggle_play_pause() + label = "Pause" if self.playback_manager.is_playing else "Play" + dpg.configure_item(sender, label=label) + + def timeline_drag(self, sender, app_data): + self.playback_manager.seek(app_data) + dpg.configure_item("play_pause_button", label="Play") + + def update_frame(self, font): + self.data_tree.update_frame(font) + + new_time = self.playback_manager.update_time(dpg.get_delta_time()) + if not dpg.is_item_active("timeline_slider"): + dpg.set_value("timeline_slider", new_time) + + self.plot_layout_manager.update_all_panels() + + dpg.set_value("fps_counter", f"{dpg.get_frame_rate():.1f} FPS") + + def shutdown(self): + self.worker_manager.shutdown() + + +def main(route_to_load=None): + dpg.create_context() + + # TODO: find better way of calculating display scaling + try: + w, h = next(tuple(map(int, l.split()[0].split('x'))) for l in subprocess.check_output(['xrandr']).decode().split('\n') if '*' in l) # actual resolution + scale = pyautogui.size()[0] / w # scaled resolution + except Exception: + scale = 1 + + with dpg.font_registry(): + default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/Inter-Regular.ttf"), int(13 * scale)) + dpg.bind_font(default_font) + + viewport_width, viewport_height = int(1200 * scale), int(800 * scale) + mouse_x, mouse_y = pyautogui.position() # TODO: find better way of creating the window where the user is (default dpg behavior annoying on multiple displays) + dpg.create_viewport( + title='JotPluggler', width=viewport_width, height=viewport_height, x_pos=mouse_x - viewport_width // 2, y_pos=mouse_y - viewport_height // 2 + ) + dpg.setup_dearpygui() + + controller = MainController(scale=scale) + controller.setup_ui() + + if route_to_load: + dpg.set_value("route_input", route_to_load) + controller.load_route() + + dpg.show_viewport() + + # Main loop + try: + while dpg.is_dearpygui_running(): + controller.update_frame(default_font) + dpg.render_dearpygui_frame() + finally: + controller.shutdown() + dpg.destroy_context() + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A tool for visualizing openpilot logs.") + parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") + parser.add_argument("route", nargs='?', default=None, help="Optional route name to load on startup.") + args = parser.parse_args() + route = DEMO_ROUTE if args.demo else args.route + main(route_to_load=route) diff --git a/tools/jotpluggler/views.py b/tools/jotpluggler/views.py new file mode 100644 index 0000000000..bcadd4f387 --- /dev/null +++ b/tools/jotpluggler/views.py @@ -0,0 +1,195 @@ +import uuid +import threading +import numpy as np +from collections import deque +import dearpygui.dearpygui as dpg +from abc import ABC, abstractmethod + + +class ViewPanel(ABC): + """Abstract base class for all view panels that can be displayed in a plot container""" + + def __init__(self, panel_id: str = None): + self.panel_id = panel_id or str(uuid.uuid4()) + self.title = "Untitled Panel" + + @abstractmethod + def clear(self): + pass + + @abstractmethod + def create_ui(self, parent_tag: str): + pass + + @abstractmethod + def destroy_ui(self): + pass + + @abstractmethod + def get_panel_type(self) -> str: + pass + + @abstractmethod + def update(self): + pass + + +class TimeSeriesPanel(ViewPanel): + def __init__(self, data_manager, playback_manager, worker_manager, panel_id: str | None = None): + super().__init__(panel_id) + self.data_manager = data_manager + self.playback_manager = playback_manager + self.worker_manager = worker_manager + self.title = "Time Series Plot" + self.plot_tag = f"plot_{self.panel_id}" + self.x_axis_tag = f"{self.plot_tag}_x_axis" + self.y_axis_tag = f"{self.plot_tag}_y_axis" + self.timeline_indicator_tag = f"{self.plot_tag}_timeline" + self._ui_created = False + self._series_data: dict[str, tuple[list, list]] = {} + self._last_plot_duration = 0 + self._update_lock = threading.RLock() + self.results_deque: deque[tuple[str, list, list]] = deque() + self._new_data = False + + def create_ui(self, parent_tag: str): + self.data_manager.add_observer(self.on_data_loaded) + with dpg.plot(height=-1, width=-1, tag=self.plot_tag, parent=parent_tag, drop_callback=self._on_series_drop, payload_type="TIMESERIES_PAYLOAD"): + dpg.add_plot_legend() + dpg.add_plot_axis(dpg.mvXAxis, no_label=True, tag=self.x_axis_tag) + dpg.add_plot_axis(dpg.mvYAxis, no_label=True, tag=self.y_axis_tag) + timeline_series_tag = dpg.add_inf_line_series(x=[0], label="Timeline", parent=self.y_axis_tag, tag=self.timeline_indicator_tag) + dpg.bind_item_theme(timeline_series_tag, "global_timeline_theme") + + for series_path in list(self._series_data.keys()): + self.add_series(series_path) + self._ui_created = True + + def update(self): + with self._update_lock: + if not self._ui_created: + return + + if self._new_data: # handle new data in main thread + self._new_data = False + for series_path in list(self._series_data.keys()): + self.add_series(series_path, update=True) + + while self.results_deque: # handle downsampled results in main thread + results = self.results_deque.popleft() + for series_path, downsampled_time, downsampled_values in results: + series_tag = f"series_{self.panel_id}_{series_path}" + if dpg.does_item_exist(series_tag): + dpg.set_value(series_tag, [downsampled_time, downsampled_values]) + + # update timeline + current_time_s = self.playback_manager.current_time_s + dpg.set_value(self.timeline_indicator_tag, [[current_time_s], [0]]) + + # update timeseries legend label + for series_path, (time_array, value_array) in self._series_data.items(): + position = np.searchsorted(time_array, current_time_s, side='right') - 1 + if position >= 0 and (current_time_s - time_array[position]) <= 1.0: + value = value_array[position] + formatted_value = f"{value:.5f}" if np.issubdtype(type(value), np.floating) else str(value) + series_tag = f"series_{self.panel_id}_{series_path}" + if dpg.does_item_exist(series_tag): + dpg.configure_item(series_tag, label=f"{series_path}: {formatted_value}") + + # downsample if plot zoom changed significantly + plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0] + if plot_duration > self._last_plot_duration * 2 or plot_duration < self._last_plot_duration * 0.5: + self._downsample_all_series(plot_duration) + + def _downsample_all_series(self, plot_duration): + plot_width = dpg.get_item_rect_size(self.plot_tag)[0] + if plot_width <= 0 or plot_duration <= 0: + return + + self._last_plot_duration = plot_duration + target_points_per_second = plot_width / plot_duration + work_items = [] + for series_path, (time_array, value_array) in self._series_data.items(): + if len(time_array) == 0: + continue + series_duration = time_array[-1] - time_array[0] if len(time_array) > 1 else 1 + points_per_second = len(time_array) / series_duration + if points_per_second > target_points_per_second * 2: + target_points = max(int(target_points_per_second * series_duration), plot_width) + work_items.append((series_path, time_array, value_array, target_points)) + elif dpg.does_item_exist(f"series_{self.panel_id}_{series_path}"): + dpg.set_value(f"series_{self.panel_id}_{series_path}", [time_array, value_array]) + + if work_items: + self.worker_manager.submit_task( + TimeSeriesPanel._downsample_worker, work_items, callback=lambda results: self.results_deque.append(results), task_id=f"downsample_{self.panel_id}" + ) + + def add_series(self, series_path: str, update: bool = False): + with self._update_lock: + if update or series_path not in self._series_data: + self._series_data[series_path] = self.data_manager.get_timeseries(series_path) + + time_array, value_array = self._series_data[series_path] + series_tag = f"series_{self.panel_id}_{series_path}" + if dpg.does_item_exist(series_tag): + dpg.set_value(series_tag, [time_array, value_array]) + else: + line_series_tag = dpg.add_line_series(x=time_array, y=value_array, label=series_path, parent=self.y_axis_tag, tag=series_tag) + dpg.bind_item_theme(line_series_tag, "global_line_theme") + dpg.fit_axis_data(self.x_axis_tag) + dpg.fit_axis_data(self.y_axis_tag) + plot_duration = dpg.get_axis_limits(self.x_axis_tag)[1] - dpg.get_axis_limits(self.x_axis_tag)[0] + self._downsample_all_series(plot_duration) + + def destroy_ui(self): + with self._update_lock: + self.data_manager.remove_observer(self.on_data_loaded) + if dpg.does_item_exist(self.plot_tag): + dpg.delete_item(self.plot_tag) + self._ui_created = False + + def get_panel_type(self) -> str: + return "timeseries" + + def clear(self): + with self._update_lock: + for series_path in list(self._series_data.keys()): + self.remove_series(series_path) + + def remove_series(self, series_path: str): + with self._update_lock: + if series_path in self._series_data: + if dpg.does_item_exist(f"series_{self.panel_id}_{series_path}"): + dpg.delete_item(f"series_{self.panel_id}_{series_path}") + del self._series_data[series_path] + + def on_data_loaded(self, data: dict): + self._new_data = True + + def _on_series_drop(self, sender, app_data, user_data): + self.add_series(app_data) + + @staticmethod + def _downsample_worker(series_path, time_array, value_array, target_points): + if len(time_array) <= target_points: + return series_path, time_array, value_array + + step = len(time_array) / target_points + indices = [] + + for i in range(target_points): + start_idx = int(i * step) + end_idx = int((i + 1) * step) + if start_idx == end_idx: + indices.append(start_idx) + else: + bucket_values = value_array[start_idx:end_idx] + min_idx = start_idx + np.argmin(bucket_values) + max_idx = start_idx + np.argmax(bucket_values) + if min_idx != max_idx: + indices.extend([min(min_idx, max_idx), max(min_idx, max_idx)]) + else: + indices.append(min_idx) + indices = sorted(set(indices)) + return series_path, time_array[indices], value_array[indices] diff --git a/uv.lock b/uv.lock index 9977ff1bf7..25d2b626cf 100644 --- a/uv.lock +++ b/uv.lock @@ -451,6 +451,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d2/fc/c0a3f4c4eaa5a22fbef91713474666e13d0ea2a69c84532579490a9f2cc8/dbus_next-0.2.3-py3-none-any.whl", hash = "sha256:58948f9aff9db08316734c0be2a120f6dc502124d9642f55e90ac82ffb16a18b", size = 57885, upload-time = "2021-07-25T22:11:25.466Z" }, ] +[[package]] +name = "dearpygui" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/fe/66293fc40254a29f060efd3398f2b1001ed79263ae1837db9ec42caa8f1d/dearpygui-2.1.0-cp311-cp311-macosx_10_6_x86_64.whl", hash = "sha256:03e5dc0b3dd2f7965e50bbe41f3316a814408064b582586de994d93afedb125c", size = 2100924, upload-time = "2025-07-07T14:20:00.602Z" }, + { url = "https://files.pythonhosted.org/packages/c4/4d/9fa1c3156ba7bbf4dc89e2e322998752fccfdc3575923a98dd6a4da48911/dearpygui-2.1.0-cp311-cp311-macosx_13_0_arm64.whl", hash = "sha256:b5b37710c3fa135c48e2347f39ecd1f415146e86db5d404707a0bf72d16bd304", size = 1874441, upload-time = "2025-07-07T14:20:09.165Z" }, + { url = "https://files.pythonhosted.org/packages/5a/3c/af5673b50699e1734296a0b5bcef39bb6989175b001ad1f9b0e7888ad90d/dearpygui-2.1.0-cp311-cp311-manylinux1_x86_64.whl", hash = "sha256:b0cfd7ac7eaa090fc22d6aa60fc4b527fc631cee10c348e4d8df92bb39af03d2", size = 2636574, upload-time = "2025-07-07T14:20:14.951Z" }, + { url = "https://files.pythonhosted.org/packages/7f/db/ed4db0bb3d88e7a8c405472641419086bef9632c4b8b0489dc0c43519c0d/dearpygui-2.1.0-cp311-cp311-win_amd64.whl", hash = "sha256:a9af54f96d3ef30c5db9d12cdf3266f005507396fb0da2e12e6b22b662161070", size = 1810266, upload-time = "2025-07-07T14:19:51.565Z" }, + { url = "https://files.pythonhosted.org/packages/55/9d/20a55786cc9d9266395544463d5db3be3528f7d5244bc52ba760de5dcc2d/dearpygui-2.1.0-cp312-cp312-macosx_10_6_x86_64.whl", hash = "sha256:1270ceb9cdb8ecc047c42477ccaa075b7864b314a5d09191f9280a24c8aa90a0", size = 2101499, upload-time = "2025-07-07T14:20:01.701Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/39d820796b7ac4d0ebf93306c1f031bf3516b159408286f1fb495c6babeb/dearpygui-2.1.0-cp312-cp312-macosx_13_0_arm64.whl", hash = "sha256:ce9969eb62057b9d4c88a8baaed13b5fbe4058caa9faf5b19fec89da75aece3d", size = 1874385, upload-time = "2025-07-07T14:20:11.226Z" }, + { url = "https://files.pythonhosted.org/packages/fc/26/c29998ffeb5eb8d638f307851e51a81c8bd4aeaf89ad660fc67ea4d1ac1a/dearpygui-2.1.0-cp312-cp312-manylinux1_x86_64.whl", hash = "sha256:a3ca8cf788db63ef7e2e8d6f277631b607d548b37606f080ca1b42b1f0a9b183", size = 2635863, upload-time = "2025-07-07T14:20:17.186Z" }, + { url = "https://files.pythonhosted.org/packages/28/9c/3ab33927f1d8c839c5b7033a33d44fc9f0aeb00c264fc9772cb7555a03c4/dearpygui-2.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:43f0e4db9402f44fc3683a1f5c703564819de18cc15a042de7f1ed1c8cb5d148", size = 1810460, upload-time = "2025-07-07T14:19:53.13Z" }, +] + [[package]] name = "dictdiffer" version = "0.9.0" @@ -1246,6 +1261,7 @@ dependencies = [ { name = "cffi" }, { name = "crcmod" }, { name = "cython" }, + { name = "dearpygui" }, { name = "future-fstrings" }, { name = "inputs" }, { name = "json-rpc" }, @@ -1337,6 +1353,7 @@ requires-dist = [ { name = "crcmod" }, { name = "cython" }, { name = "dbus-next", marker = "extra == 'dev'" }, + { name = "dearpygui", specifier = ">=2.1.0" }, { name = "dictdiffer", marker = "extra == 'dev'" }, { name = "future-fstrings" }, { name = "hypothesis", marker = "extra == 'testing'", specifier = "==6.47.*" }, From 2ac51c182bfd5ee587865dd0e161d6bdedef5710 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 16:37:23 -0400 Subject: [PATCH 156/188] fix events --- .../lib/speed_limit_controller/speed_limit_controller.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index d692a41b5f..7b488c5d75 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -221,10 +221,10 @@ class SpeedLimitController: return enabled, active def update_events(self, events_sp: EventsSP) -> None: - if self.is_active: - if self.state == SpeedLimitControlState.preActive: - events_sp.add(EventNameSP.speedLimitPreActive) - elif self._state_prev not in ACTIVE_STATES: + if self.state == SpeedLimitControlState.preActive: + events_sp.add(EventNameSP.speedLimitPreActive) + elif self.is_active: + if self._state_prev not in ACTIVE_STATES: events_sp.add(EventNameSP.speedLimitActive) elif self.speed_limit_changed: events_sp.add(EventNameSP.speedLimitValueChange) From 6b217a0d10ebfe49627432774572629ca1294fe8 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 16:47:36 -0400 Subject: [PATCH 157/188] update event --- cereal/custom.capnp | 2 +- .../speed_limit_controller.py | 4 ++-- sunnypilot/selfdrive/selfdrived/events.py | 16 ++++++++++------ 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index f1894c14d1..bc6f8af316 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -205,7 +205,7 @@ struct OnroadEventSP @0xda96579883444c35 { speedLimitPreActive @19; speedLimitActive @20; speedLimitConfirmed @21; - speedLimitValueChange @22; + speedLimitChanged @22; } } diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index 7b488c5d75..a6327e978b 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -221,13 +221,13 @@ class SpeedLimitController: return enabled, active def update_events(self, events_sp: EventsSP) -> None: - if self.state == SpeedLimitControlState.preActive: + if self.state == SpeedLimitControlState.preActive and self._state_prev != SpeedLimitControlState.preActive: events_sp.add(EventNameSP.speedLimitPreActive) elif self.is_active: if self._state_prev not in ACTIVE_STATES: events_sp.add(EventNameSP.speedLimitActive) elif self.speed_limit_changed: - events_sp.add(EventNameSP.speedLimitValueChange) + events_sp.add(EventNameSP.speedLimitChanged) def update(self, long_active: bool, v_ego: float, a_ego: float, v_cruise_setpoint: float, speed_limit: float, distance: float, source: custom.LongitudinalPlanSP.SpeedLimitSource, events_sp: EventsSP) -> float: diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index 956f425c46..b40e86ad9f 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -165,14 +165,18 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { EventNameSP.speedLimitActive: { ET.WARNING: Alert( - "Set speed changed to match posted speed limit", - "", + "Automatically adjusting", + "to the posted speed limit", AlertStatus.normal, AlertSize.small, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 3.), + Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.), }, - EventNameSP.speedLimitValueChange: { - ET.WARNING: speed_limit_adjust_alert, + EventNameSP.speedLimitChanged: { + ET.WARNING: Alert( + "Set speed changed", + "", + AlertStatus.normal, AlertSize.small, + Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.), }, EventNameSP.speedLimitPreActive: { @@ -180,6 +184,6 @@ EVENTS_SP: dict[int, dict[str, Alert | AlertCallbackType]] = { "Auto Speed Limit Control: Activation Required", "Manually change set speed to 80 MPH to activate", AlertStatus.normal, AlertSize.mid, - Priority.LOW, VisualAlert.none, AudibleAlert.none, 3.), + Priority.LOW, VisualAlert.none, AudibleAlert.none, 5.), }, } From 2bfdd0d61db30cc5e97c144ce299bec81adc8144 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Tue, 9 Sep 2025 18:02:31 -0400 Subject: [PATCH 158/188] PlotJuggler: Updated layout for torque controller (#36123) * PlotJuggler: Updated layout for torque controller * yeah, no --- pyproject.toml | 2 +- .../plotjuggler/layouts/torque-controller.xml | 305 +++++++++--------- 2 files changed, 156 insertions(+), 151 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 88c4d06739..1655b565e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -176,7 +176,7 @@ quiet-level = 3 # if you've got a short variable name that's getting flagged, add it here ignore-words-list = "bu,ro,te,ue,alo,hda,ois,nam,nams,ned,som,parm,setts,inout,warmup,bumb,nd,sie,preints,whit,indexIn,ws,uint,grey,deque,stdio,amin,BA,LITE,atEnd,UIs,errorString,arange,FocusIn,od,tim,relA,hist,copyable,jupyter,thead,TGE,abl,lite" builtin = "clear,rare,informal,code,names,en-GB_to_en-US" -skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.ts, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*" +skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, ./opendbc/*, ./opendbc_repo/*, ./rednose/*, ./rednose_repo/*, ./teleoprtc/*, ./teleoprtc_repo/*, *.ts, uv.lock, *.onnx, ./cereal/gen/*, */c_generated_code/*, docs/assets/*, tools/plotjuggler/layouts/*" [tool.mypy] python_version = "3.11" diff --git a/tools/plotjuggler/layouts/torque-controller.xml b/tools/plotjuggler/layouts/torque-controller.xml index 606df03611..8e9a1a8526 100644 --- a/tools/plotjuggler/layouts/torque-controller.xml +++ b/tools/plotjuggler/layouts/torque-controller.xml @@ -1,77 +1,45 @@ - + - + - - + + - - + + - - + + - - + + - - + + - + - + - - + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + @@ -79,115 +47,134 @@ - + - - + + - - + + - - + + - - + + - - + + - - + + - + - - - - + + + + - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + + - - + + - - - - + + + + - - + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + @@ -199,25 +186,34 @@ + + + + + + + + + + + + + - + - return (0) - /carState/canValid + return value * 3.6 + /carState/vEgo - + - return (value * v1 ^ 2) - (v2 * 9.81) - /controlsState/curvature - - /carState/vEgo - /liveParameters/roll - + return value * 2.23694 + /carState/vEgo @@ -228,15 +224,24 @@ /liveParameters/roll - + - return value * 2.23694 - /carState/vEgo + return (value * v1 ^ 2) - (v2 * 9.81) + /controlsState/curvature + + /carState/vEgo + /liveParameters/roll + - + - return value * 3.6 - /carState/vEgo + return value + 0.2 + /carParams/steerActuatorDelay + + + + return (0) + /carState/canValid From d0171084b51c2ee9eb2d3211adc5201edfb498f1 Mon Sep 17 00:00:00 2001 From: YassineYousfi Date: Tue, 9 Sep 2025 15:40:39 -0700 Subject: [PATCH 159/188] Update RELEASES.md 0.10.1 --- RELEASES.md | 1 + 1 file changed, 1 insertion(+) diff --git a/RELEASES.md b/RELEASES.md index d89ee24628..90d99c0015 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -4,6 +4,7 @@ Version 0.10.1 (2025-09-08) * World Model: removed global localization inputs * World Model: 2x the number of parameters * World Model: trained on 4x the number of segments + * Driving Vision Model: trained on 4x the number of segments * Record driving feedback using LKAS button * Honda City 2023 support thanks to drFritz! From 8f8561b88df1aba1a9066752bfc8e2e1c72e449a Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 23:00:43 -0400 Subject: [PATCH 160/188] already happens while in enabled --- .../lib/speed_limit_controller/speed_limit_controller.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py index a6327e978b..57c3284f0a 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py @@ -91,7 +91,7 @@ class SpeedLimitController: return bool(self.v_cruise_setpoint != self.v_cruise_setpoint_prev) def get_v_target_from_control(self) -> float: - if self.is_active: + if self.is_enabled: # If we have a current valid speed limit, use it if self._speed_limit > 0: self.last_valid_speed_limit_final = self.speed_limit_final From c5d95f0e8fb12f0bfabd1ae8241de23667f293e2 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 23:34:26 -0400 Subject: [PATCH 161/188] add carstateSP --- sunnypilot/mapd/live_map_data/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sunnypilot/mapd/live_map_data/debug.py b/sunnypilot/mapd/live_map_data/debug.py index a79e51bcf5..794f2ef8ff 100644 --- a/sunnypilot/mapd/live_map_data/debug.py +++ b/sunnypilot/mapd/live_map_data/debug.py @@ -37,7 +37,7 @@ def live_map_data_sp_thread(): def live_map_data_sp_thread_debug(gps_location_service): - _sub_master = messaging.SubMaster(['carState', 'livePose', 'liveMapDataSP', 'longitudinalPlanSP', gps_location_service]) + _sub_master = messaging.SubMaster(['carState', 'livePose', 'liveMapDataSP', 'longitudinalPlanSP', 'carStateSP', gps_location_service]) _sub_master.update() v_ego = _sub_master['carState'].vEgo From 3c0d16ae69beba3c9014fe4b28d3f59cd13fd122 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 9 Sep 2025 23:41:32 -0400 Subject: [PATCH 162/188] less --- .../selfdrive/controls/lib/speed_limit_controller/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py index ff82a7dde0..ce4ae1c63c 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py @@ -2,7 +2,6 @@ from cereal import custom SpeedLimitControlState = custom.LongitudinalPlanSP.SpeedLimitControlState -DEBUG = False PARAMS_UPDATE_PERIOD = 3. # secs. Time between parameter updates. DISABLED_GUARD_PERIOD = 2 # secs. PRE_ACTIVE_GUARD_PERIOD = 5 # secs. Time to wait after activation before considering temp deactivation signal. From 6b131753380e21b22701827194cc3d083ec3f073 Mon Sep 17 00:00:00 2001 From: Jimmy <9859727+Quantizr@users.noreply.github.com> Date: Tue, 9 Sep 2025 21:09:08 -1000 Subject: [PATCH 163/188] jotpluggler: better handle sparse message data and bools (#36124) * better handle sparse message data * fix plotting of of bools * add type for msg._valid * fix typing * add assert in case something changes in future --- tools/jotpluggler/data.py | 105 ++++++++++++++++++++++++++----------- tools/jotpluggler/views.py | 18 +++---- 2 files changed, 82 insertions(+), 41 deletions(-) diff --git a/tools/jotpluggler/data.py b/tools/jotpluggler/data.py index 41c305718e..100dfe544d 100644 --- a/tools/jotpluggler/data.py +++ b/tools/jotpluggler/data.py @@ -3,7 +3,7 @@ import threading import multiprocessing import bisect from collections import defaultdict -import tqdm +from tqdm import tqdm from openpilot.common.swaglog import cloudlog from openpilot.tools.lib.logreader import _LogFileReader, LogReader @@ -70,9 +70,6 @@ def extract_field_types(schema, prefix, field_types_dict): def _convert_to_optimal_dtype(values_list, capnp_type): - if not values_list: - return np.array([]) - dtype_mapping = { 'bool': np.bool_, 'int8': np.int8, 'int16': np.int16, 'int32': np.int32, 'int64': np.int64, 'uint8': np.uint8, 'uint16': np.uint16, 'uint32': np.uint32, 'uint64': np.uint64, @@ -80,8 +77,8 @@ def _convert_to_optimal_dtype(values_list, capnp_type): 'enum': object, 'anyPointer': object, } - target_dtype = dtype_mapping.get(capnp_type) - return np.array(values_list, dtype=target_dtype) if target_dtype else np.array(values_list) + target_dtype = dtype_mapping.get(capnp_type, object) + return np.array(values_list, dtype=target_dtype) def _match_field_type(field_path, field_types): @@ -94,6 +91,21 @@ def _match_field_type(field_path, field_types): return field_types.get(template_path) +def _get_field_times_values(segment, field_name): + if field_name not in segment: + return None, None + + field_data = segment[field_name] + segment_times = segment['t'] + + if field_data['sparse']: + if len(field_data['t_index']) == 0: + return None, None + return segment_times[field_data['t_index']], field_data['values'] + else: + return segment_times, field_data['values'] + + def msgs_to_time_series(msgs): """Extract scalar fields and return (time_series_data, start_time, end_time).""" collected_data = defaultdict(lambda: {'timestamps': [], 'columns': defaultdict(list), 'sparse_fields': set()}) @@ -110,16 +122,22 @@ def msgs_to_time_series(msgs): max_time = timestamp sub_msg = getattr(msg, typ) - if not hasattr(sub_msg, 'to_dict') or typ in ('qcomGnss', 'ubloxGnss'): + if not hasattr(sub_msg, 'to_dict'): continue if hasattr(sub_msg, 'schema') and typ not in extracted_schemas: extract_field_types(sub_msg.schema, typ, field_types) extracted_schemas.add(typ) - msg_dict = sub_msg.to_dict(verbose=True) + try: + msg_dict = sub_msg.to_dict(verbose=True) + except Exception as e: + cloudlog.warning(f"Failed to convert sub_msg.to_dict() for message of type: {typ}: {e}") + continue + flat_dict = flatten_dict(msg_dict) flat_dict['_valid'] = msg.valid + field_types[f"{typ}/_valid"] = 'bool' type_data = collected_data[typ] columns, sparse_fields = type_data['columns'], type_data['sparse_fields'] @@ -152,11 +170,26 @@ def msgs_to_time_series(msgs): values = [None] * (len(data['timestamps']) - len(values)) + values sparse_fields.add(field_name) - if field_name in sparse_fields: - typ_result[field_name] = np.array(values, dtype=object) - else: - capnp_type = _match_field_type(f"{typ}/{field_name}", field_types) - typ_result[field_name] = _convert_to_optimal_dtype(values, capnp_type) + capnp_type = _match_field_type(f"{typ}/{field_name}", field_types) + + if field_name in sparse_fields: # extract non-None values and their indices + non_none_indices = [] + non_none_values = [] + for i, value in enumerate(values): + if value is not None: + non_none_indices.append(i) + non_none_values.append(value) + + if non_none_values: # check if indices > uint16 max, currently would require a 1000+ Hz signal since indices are within segments + assert max(non_none_indices) <= 65535, f"Sparse field {typ}/{field_name} has timestamp indices exceeding uint16 max. Max: {max(non_none_indices)}" + + typ_result[field_name] = { + 'values': _convert_to_optimal_dtype(non_none_values, capnp_type), + 'sparse': True, + 't_index': np.array(non_none_indices, dtype=np.uint16), + } + else: # dense representation + typ_result[field_name] = {'values': _convert_to_optimal_dtype(values, capnp_type), 'sparse': False} final_result[typ] = typ_result @@ -195,22 +228,31 @@ class DataManager: times, values = [], [] for segment in self._segments: - if msg_type in segment and field in segment[msg_type]: - times.append(segment[msg_type]['t']) - values.append(segment[msg_type][field]) + if msg_type in segment: + field_times, field_values = _get_field_times_values(segment[msg_type], field) + if field_times is not None: + times.append(field_times) + values.append(field_values) if not times: - return [], [] + return np.array([]), np.array([]) combined_times = np.concatenate(times) - self._start_time - if len(values) > 1 and any(arr.dtype != values[0].dtype for arr in values): - values = [arr.astype(object) for arr in values] - return combined_times, np.concatenate(values) + if len(values) > 1: + first_dtype = values[0].dtype + if all(arr.dtype == first_dtype for arr in values): # check if all arrays have compatible dtypes + combined_values = np.concatenate(values) + else: + combined_values = np.concatenate([arr.astype(object) for arr in values]) + else: + combined_values = values[0] if values else np.array([]) + + return combined_times, combined_values def get_value_at(self, path: str, time: float): with self._lock: - MAX_LOOKBACK = 5.0 # seconds + MAX_LOOKBACK = 5.0 # seconds absolute_time = self._start_time + time message_type, field = path.split('/', 1) current_index = bisect.bisect_right(self._segment_starts, absolute_time) - 1 @@ -218,14 +260,14 @@ class DataManager: if not 0 <= index < len(self._segments): continue segment = self._segments[index].get(message_type) - if not segment or field not in segment: + if not segment: continue - times = segment['t'] - if len(times) == 0 or (index != current_index and absolute_time - times[-1] > MAX_LOOKBACK): + times, values = _get_field_times_values(segment, field) + if times is None or len(times) == 0 or (index != current_index and absolute_time - times[-1] > MAX_LOOKBACK): continue position = np.searchsorted(times, absolute_time, 'right') - 1 if position >= 0 and absolute_time - times[position] <= MAX_LOOKBACK: - return segment[field][position] + return values[position] return None def get_all_paths(self): @@ -237,10 +279,9 @@ class DataManager: return self._duration def is_plottable(self, path: str): - data = self.get_timeseries(path) - if data is None: + _, values = self.get_timeseries(path) + if len(values) == 0: return False - _, values = data return np.issubdtype(values.dtype, np.number) or np.issubdtype(values.dtype, np.bool_) def add_observer(self, callback): @@ -272,7 +313,7 @@ class DataManager: return num_processes = max(1, multiprocessing.cpu_count() // 2) - with multiprocessing.Pool(processes=num_processes) as pool, tqdm.tqdm(total=len(lr.logreader_identifiers), desc="Processing Segments") as pbar: + with multiprocessing.Pool(processes=num_processes) as pool, tqdm(total=len(lr.logreader_identifiers), desc="Processing Segments") as pbar: for segment_result, start_time, end_time in pool.imap(_process_segment, lr.logreader_identifiers): pbar.update(1) if segment_result: @@ -292,9 +333,9 @@ class DataManager: self._duration = end_time - self._start_time for msg_type, data in segment_data.items(): - for field in data.keys(): - if field != 't': - self._paths.add(f"{msg_type}/{field}") + for field_name in data.keys(): + if field_name != 't': + self._paths.add(f"{msg_type}/{field_name}") observers = self._observers.copy() diff --git a/tools/jotpluggler/views.py b/tools/jotpluggler/views.py index bcadd4f387..4af9a102ac 100644 --- a/tools/jotpluggler/views.py +++ b/tools/jotpluggler/views.py @@ -46,10 +46,10 @@ class TimeSeriesPanel(ViewPanel): self.y_axis_tag = f"{self.plot_tag}_y_axis" self.timeline_indicator_tag = f"{self.plot_tag}_timeline" self._ui_created = False - self._series_data: dict[str, tuple[list, list]] = {} + self._series_data: dict[str, tuple[np.ndarray, np.ndarray]] = {} self._last_plot_duration = 0 self._update_lock = threading.RLock() - self.results_deque: deque[tuple[str, list, list]] = deque() + self._results_deque: deque[tuple[str, list, list]] = deque() self._new_data = False def create_ui(self, parent_tag: str): @@ -75,12 +75,12 @@ class TimeSeriesPanel(ViewPanel): for series_path in list(self._series_data.keys()): self.add_series(series_path, update=True) - while self.results_deque: # handle downsampled results in main thread - results = self.results_deque.popleft() + while self._results_deque: # handle downsampled results in main thread + results = self._results_deque.popleft() for series_path, downsampled_time, downsampled_values in results: series_tag = f"series_{self.panel_id}_{series_path}" if dpg.does_item_exist(series_tag): - dpg.set_value(series_tag, [downsampled_time, downsampled_values]) + dpg.set_value(series_tag, (downsampled_time, downsampled_values.astype(float))) # update timeline current_time_s = self.playback_manager.current_time_s @@ -118,11 +118,11 @@ class TimeSeriesPanel(ViewPanel): target_points = max(int(target_points_per_second * series_duration), plot_width) work_items.append((series_path, time_array, value_array, target_points)) elif dpg.does_item_exist(f"series_{self.panel_id}_{series_path}"): - dpg.set_value(f"series_{self.panel_id}_{series_path}", [time_array, value_array]) + dpg.set_value(f"series_{self.panel_id}_{series_path}", (time_array, value_array.astype(float))) if work_items: self.worker_manager.submit_task( - TimeSeriesPanel._downsample_worker, work_items, callback=lambda results: self.results_deque.append(results), task_id=f"downsample_{self.panel_id}" + TimeSeriesPanel._downsample_worker, work_items, callback=lambda results: self._results_deque.append(results), task_id=f"downsample_{self.panel_id}" ) def add_series(self, series_path: str, update: bool = False): @@ -133,9 +133,9 @@ class TimeSeriesPanel(ViewPanel): time_array, value_array = self._series_data[series_path] series_tag = f"series_{self.panel_id}_{series_path}" if dpg.does_item_exist(series_tag): - dpg.set_value(series_tag, [time_array, value_array]) + dpg.set_value(series_tag, (time_array, value_array.astype(float))) else: - line_series_tag = dpg.add_line_series(x=time_array, y=value_array, label=series_path, parent=self.y_axis_tag, tag=series_tag) + line_series_tag = dpg.add_line_series(x=time_array, y=value_array.astype(float), label=series_path, parent=self.y_axis_tag, tag=series_tag) dpg.bind_item_theme(line_series_tag, "global_line_theme") dpg.fit_axis_data(self.x_axis_tag) dpg.fit_axis_data(self.y_axis_tag) From 10580aca922789c58fae1a6922193df9b2fdf311 Mon Sep 17 00:00:00 2001 From: Maxime Desroches Date: Wed, 10 Sep 2025 15:06:25 -0700 Subject: [PATCH 164/188] ci: adjust power draw bounds (#36130) * consider min * bounds --- system/hardware/tici/tests/test_power_draw.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/system/hardware/tici/tests/test_power_draw.py b/system/hardware/tici/tests/test_power_draw.py index 46b45460b5..4fbde81673 100644 --- a/system/hardware/tici/tests/test_power_draw.py +++ b/system/hardware/tici/tests/test_power_draw.py @@ -31,9 +31,9 @@ class Proc: PROCS = [ - Proc(['camerad'], 1.75, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), + Proc(['camerad'], 1.65, atol=0.4, msgs=['roadCameraState', 'wideRoadCameraState', 'driverCameraState']), Proc(['modeld'], 1.24, atol=0.2, msgs=['modelV2']), - Proc(['dmonitoringmodeld'], 0.7, msgs=['driverStateV2']), + Proc(['dmonitoringmodeld'], 0.65, atol=0.35, msgs=['driverStateV2']), Proc(['encoderd'], 0.23, msgs=[]), ] From 0e1b573f89ab425e724ee0b7278f2d31bfd7029e Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Thu, 11 Sep 2025 05:32:44 -0400 Subject: [PATCH 165/188] Honda: Add Honda Odyssey 2021-25 to release (#36132) * bump opendbc * regen CARS.md * add to RELEASES.md * forgot this was originally VG's PR * correctly typo the typo * follow recent DBC cleanup --- RELEASES.md | 3 ++- docs/CARS.md | 7 ++++--- opendbc_repo | 2 +- tools/sim/lib/simulated_car.py | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 90d99c0015..529bcb6b09 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -6,7 +6,8 @@ Version 0.10.1 (2025-09-08) * World Model: trained on 4x the number of segments * Driving Vision Model: trained on 4x the number of segments * Record driving feedback using LKAS button -* Honda City 2023 support thanks to drFritz! +* Honda City 2023 support thanks to vanillagorillaa and drFritz! +* Honda Odyssey 2021-25 support thanks to vanillagorillaa and MVL! Version 0.10.0 (2025-08-05) ======================== diff --git a/docs/CARS.md b/docs/CARS.md index 7269f25737..4c0569a1b8 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 321 Supported Cars +# 322 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -83,7 +83,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Civic Hatchback 2017-18|Honda Sensing|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|

Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hatchback 2019-21|All|openpilot available[1](#footnotes)|0 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hatchback 2022-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Honda|Civic Hatchback Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Civic Hatchback Hybrid 2025-26|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hatchback Hybrid (Europe only) 2023|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Civic Hybrid 2025|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|CR-V 2015-16|Touring Trim|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -99,6 +99,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|Odyssey 2021-25|All|openpilot available[1](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Pilot 2016-22|Honda Sensing|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Pilot 2023-25|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch C connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| @@ -247,7 +248,7 @@ A supported vehicle is one that just works when you install a comma device. All |Tesla[11](#footnotes)|Model 3 (with HW3) 2019-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Tesla[11](#footnotes)|Model 3 (with HW4) 2024-25[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Tesla[11](#footnotes)|Model Y (with HW3) 2020-23[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla A connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Tesla[11](#footnotes)|Model Y (with HW4) 2024[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Tesla[11](#footnotes)|Model Y (with HW4) 2024-25[10](#footnotes)|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Tesla B connector
- 1 USB-C coupler
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Toyota|Alphard 2019-20|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Toyota|Alphard Hybrid 2021|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Toyota|Avalon 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Toyota A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| diff --git a/opendbc_repo b/opendbc_repo index 7afc25d8d4..435ef8203b 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 7afc25d8d4096bb31e25c0b7ae0b961ea05f5394 +Subproject commit 435ef8203be39c225e95ba8b10b7db7272b99fcc diff --git a/tools/sim/lib/simulated_car.py b/tools/sim/lib/simulated_car.py index 2681b26904..68ff3050db 100644 --- a/tools/sim/lib/simulated_car.py +++ b/tools/sim/lib/simulated_car.py @@ -11,7 +11,7 @@ from openpilot.tools.sim.lib.common import SimulatorState class SimulatedCar: """Simulates a honda civic 2022 (panda state + can messages) to OpenPilot""" - packer = CANPacker("honda_civic_ex_2022_can_generated") + packer = CANPacker("honda_bosch_radarless_generated") def __init__(self): self.pm = messaging.PubMaster(['can', 'pandaStates']) @@ -23,7 +23,7 @@ class SimulatedCar: @staticmethod def get_car_can_parser(): - dbc_f = 'honda_civic_ex_2022_can_generated' + dbc_f = 'honda_bosch_radarless_generated' checks = [] return CANParser(dbc_f, checks, 0) From 4ccd17903b6528d73ebd7ab5d4e30eca3685d2d0 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Thu, 11 Sep 2025 05:59:15 -0400 Subject: [PATCH 166/188] correction to Honda release notes (#36133) correction to release notes --- RELEASES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 529bcb6b09..b868bb7d5e 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,7 +7,7 @@ Version 0.10.1 (2025-09-08) * Driving Vision Model: trained on 4x the number of segments * Record driving feedback using LKAS button * Honda City 2023 support thanks to vanillagorillaa and drFritz! -* Honda Odyssey 2021-25 support thanks to vanillagorillaa and MVL! +* Honda Odyssey 2021-25 support thanks to MVL! Version 0.10.0 (2025-08-05) ======================== From 3c28188d7a6b433211d665f5482720275088d7d3 Mon Sep 17 00:00:00 2001 From: Jason Young <46612682+jyoung8607@users.noreply.github.com> Date: Thu, 11 Sep 2025 07:04:15 -0400 Subject: [PATCH 167/188] Honda: Add Honda N-Box 2018 to release (#36134) * bump opendbc * regen CARS.md * add to RELEASES.md --- RELEASES.md | 1 + docs/CARS.md | 3 ++- opendbc_repo | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index b868bb7d5e..746dea9fd7 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -7,6 +7,7 @@ Version 0.10.1 (2025-09-08) * Driving Vision Model: trained on 4x the number of segments * Record driving feedback using LKAS button * Honda City 2023 support thanks to vanillagorillaa and drFritz! +* Honda N-Box 2018 support thanks to miettal! * Honda Odyssey 2021-25 support thanks to MVL! Version 0.10.0 (2025-08-05) diff --git a/docs/CARS.md b/docs/CARS.md index 4c0569a1b8..bd6a9c920c 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,7 +4,7 @@ A supported vehicle is one that just works when you install a comma device. All supported cars provide a better experience than any stock system. Supported vehicles reference the US market unless otherwise specified. -# 322 Supported Cars +# 323 Supported Cars |Make|Model|Supported Package|ACC|No ACC accel below|No ALC below|Steering Torque|Resume from stop|Hardware Needed
 |Video|Setup Video| |---|---|---|:---:|:---:|:---:|:---:|:---:|:---:|:---:|:---:| @@ -98,6 +98,7 @@ A supported vehicle is one that just works when you install a comma device. All |Honda|HR-V 2023-25|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch B connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Insight 2019-22|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Inspire 2018|All|openpilot available[1](#footnotes)|0 mph|3 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Honda|N-Box 2018|All|openpilot available[1](#footnotes)|0 mph|11 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Odyssey 2018-20|Honda Sensing|openpilot|26 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Odyssey 2021-25|All|openpilot available[1](#footnotes)|0 mph|43 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Honda Bosch A connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| |Honda|Passport 2019-25|All|openpilot|26 mph|12 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Honda Nidec connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| diff --git a/opendbc_repo b/opendbc_repo index 435ef8203b..4170d7d876 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 435ef8203be39c225e95ba8b10b7db7272b99fcc +Subproject commit 4170d7d876a87904dab4b351aa8139ec3d400430 From 994170ddb593dfcdc7ec613b4eab2ca2c7c10309 Mon Sep 17 00:00:00 2001 From: Jimmy <9859727+Quantizr@users.noreply.github.com> Date: Thu, 11 Sep 2025 07:45:36 -1000 Subject: [PATCH 168/188] fix qcom decoder compilation on mac with platform check (#36131) --- tools/replay/SConscript | 4 +++- tools/replay/framereader.cc | 7 ++++++- tools/replay/framereader.h | 5 +++++ 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tools/replay/SConscript b/tools/replay/SConscript index 99c8263a8c..136c4119f6 100644 --- a/tools/replay/SConscript +++ b/tools/replay/SConscript @@ -12,7 +12,9 @@ else: base_libs.append('OpenCL') replay_lib_src = ["replay.cc", "consoleui.cc", "camera.cc", "filereader.cc", "logreader.cc", "framereader.cc", - "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "api.cc", "qcom_decoder.cc"] + "route.cc", "util.cc", "seg_mgr.cc", "timeline.cc", "api.cc"] +if arch != "Darwin": + replay_lib_src.append("qcom_decoder.cc") replay_lib = replay_env.Library("replay", replay_lib_src, LIBS=base_libs, FRAMEWORKS=base_frameworks) Export('replay_lib') replay_libs = [replay_lib, 'avutil', 'avcodec', 'avformat', 'bz2', 'zstd', 'curl', 'yuv', 'ncurses'] + base_libs diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index f2b1faf2c4..e9cd090446 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -39,9 +39,12 @@ struct DecoderManager { } std::unique_ptr decoder; + #ifndef __APPLE__ if (Hardware::TICI() && hw_decoder) { decoder = std::make_unique(); - } else { + } else + #endif + { decoder = std::make_unique(); } @@ -264,6 +267,7 @@ bool FFmpegVideoDecoder::copyBuffer(AVFrame *f, VisionBuf *buf) { return true; } +#ifndef __APPLE__ bool QcomVideoDecoder::open(AVCodecParameters *codecpar, bool hw_decoder) { if (codecpar->codec_id != AV_CODEC_ID_HEVC) { rError("Hardware decoder only supports HEVC codec"); @@ -305,3 +309,4 @@ bool QcomVideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { } return result; } +#endif diff --git a/tools/replay/framereader.h b/tools/replay/framereader.h index 1fb3cdfeb1..d8e86fce0f 100644 --- a/tools/replay/framereader.h +++ b/tools/replay/framereader.h @@ -6,7 +6,10 @@ #include "msgq/visionipc/visionbuf.h" #include "tools/replay/filereader.h" #include "tools/replay/util.h" + +#ifndef __APPLE__ #include "tools/replay/qcom_decoder.h" +#endif extern "C" { #include @@ -65,6 +68,7 @@ private: AVBufferRef *hw_device_ctx = nullptr; }; +#ifndef __APPLE__ class QcomVideoDecoder : public VideoDecoder { public: QcomVideoDecoder() {}; @@ -75,3 +79,4 @@ public: private: MsmVidc msm_vidc = MsmVidc(); }; +#endif From 67238d5045a3b0cb1bd675acd02f7ee638dc656a Mon Sep 17 00:00:00 2001 From: vanillagorillaa <31773928+vanillagorillaa@users.noreply.github.com> Date: Thu, 11 Sep 2025 12:47:23 -0500 Subject: [PATCH 169/188] Update release notes (#36137) Update RELEASES.md --- RELEASES.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index 746dea9fd7..f9656cfb07 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -8,7 +8,7 @@ Version 0.10.1 (2025-09-08) * Record driving feedback using LKAS button * Honda City 2023 support thanks to vanillagorillaa and drFritz! * Honda N-Box 2018 support thanks to miettal! -* Honda Odyssey 2021-25 support thanks to MVL! +* Honda Odyssey 2021-25 support thanks to csouers and MVL! Version 0.10.0 (2025-08-05) ======================== From fa498221da86eb76e6342cd9897efd509671f077 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 11 Sep 2025 10:48:32 -0700 Subject: [PATCH 170/188] still thinking about this one --- RELEASES.md | 1 - 1 file changed, 1 deletion(-) diff --git a/RELEASES.md b/RELEASES.md index f9656cfb07..189aa7ad54 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -5,7 +5,6 @@ Version 0.10.1 (2025-09-08) * World Model: 2x the number of parameters * World Model: trained on 4x the number of segments * Driving Vision Model: trained on 4x the number of segments -* Record driving feedback using LKAS button * Honda City 2023 support thanks to vanillagorillaa and drFritz! * Honda N-Box 2018 support thanks to miettal! * Honda Odyssey 2021-25 support thanks to csouers and MVL! From 572c03dbace3d9e0454c3d4f1561e31333821aeb Mon Sep 17 00:00:00 2001 From: Jimmy <9859727+Quantizr@users.noreply.github.com> Date: Thu, 11 Sep 2025 07:48:45 -1000 Subject: [PATCH 171/188] jotpluggler: fix flashing while searching (#36128) * modify in place instead of recreating nodes * don't delete DataTreeNodes and simplify code * faster: more efficient state tracking, better handler deletion --- tools/jotpluggler/datatree.py | 266 ++++++++++++++++++++-------------- 1 file changed, 160 insertions(+), 106 deletions(-) diff --git a/tools/jotpluggler/datatree.py b/tools/jotpluggler/datatree.py index 7bd026cf98..c18ab61892 100644 --- a/tools/jotpluggler/datatree.py +++ b/tools/jotpluggler/datatree.py @@ -2,7 +2,6 @@ import os import re import threading import numpy as np -from collections import deque import dearpygui.dearpygui as dpg @@ -12,8 +11,9 @@ class DataTreeNode: self.full_path = full_path self.parent = parent self.children: dict[str, DataTreeNode] = {} + self.filtered_children: dict[str, DataTreeNode] = {} + self.created_children: dict[str, DataTreeNode] = {} self.is_leaf = False - self.child_count = 0 self.is_plottable: bool | None = None self.ui_created = False self.children_ui_created = False @@ -28,13 +28,17 @@ class DataTree: self.playback_manager = playback_manager self.current_search = "" self.data_tree = DataTreeNode(name="root") - self._build_queue: deque[tuple[DataTreeNode, str | None, str | int]] = deque() - self._all_paths_cache: set[str] = set() - self._item_handlers: set[str] = set() + self._build_queue: dict[str, tuple[DataTreeNode, DataTreeNode, str | int]] = {} # full_path -> (node, parent, before_tag) + self._current_created_paths: set[str] = set() + self._current_filtered_paths: set[str] = set() + self._path_to_node: dict[str, DataTreeNode] = {} # full_path -> node + self._expanded_tags: set[str] = set() + self._item_handlers: dict[str, str] = {} # ui_tag -> handler_tag self._avg_char_width = None self._queued_search = None self._new_data = False self._ui_lock = threading.RLock() + self._handlers_to_delete = [] self.data_manager.add_observer(self._on_data_loaded) def create_ui(self, parent_tag: str): @@ -48,31 +52,102 @@ class DataTree: def _on_data_loaded(self, data: dict): with self._ui_lock: - if data.get('segment_added'): - self._new_data = True - elif data.get('reset'): - self._all_paths_cache = set() + if data.get('segment_added') or data.get('reset'): self._new_data = True + def update_frame(self, font): + if self._handlers_to_delete: # we need to do everything in main thread, frame callbacks are flaky + dpg.render_dearpygui_frame() # wait a frame to ensure queued callbacks are done + with self._ui_lock: + for handler in self._handlers_to_delete: + dpg.delete_item(handler) + self._handlers_to_delete.clear() - def _populate_tree(self): - self._clear_ui() - self.data_tree = self._add_paths_to_tree(self._all_paths_cache, incremental=False) - if self.data_tree: - self._request_children_build(self.data_tree) + with self._ui_lock: + if self._avg_char_width is None and dpg.is_dearpygui_running(): + self._avg_char_width = self.calculate_avg_char_width(font) - def _add_paths_to_tree(self, paths, incremental=False): + if self._new_data: + self._process_path_change() + self._new_data = False + return + + if self._queued_search is not None: + self.current_search = self._queued_search + self._process_path_change() + self._queued_search = None + return + + nodes_processed = 0 + while self._build_queue and nodes_processed < self.MAX_NODES_PER_FRAME: + child_node, parent, before_tag = self._build_queue.pop(next(iter(self._build_queue))) + parent_tag = "data_tree_container" if parent.name == "root" else parent.ui_tag + if not child_node.ui_created: + if child_node.is_leaf: + self._create_leaf_ui(child_node, parent_tag, before_tag) + else: + self._create_tree_node_ui(child_node, parent_tag, before_tag) + parent.created_children[child_node.name] = parent.children[child_node.name] + self._current_created_paths.add(child_node.full_path) + nodes_processed += 1 + + def _process_path_change(self): + self._build_queue.clear() search_term = self.current_search.strip().lower() - filtered_paths = [path for path in paths if self._should_show_path(path, search_term)] - target_tree = self.data_tree if incremental else DataTreeNode(name="root") - - if not filtered_paths: - return target_tree - - parent_nodes_to_recheck = set() - for path in sorted(filtered_paths): + all_paths = set(self.data_manager.get_all_paths()) + new_filtered_leafs = {path for path in all_paths if self._should_show_path(path, search_term)} + new_filtered_paths = set(new_filtered_leafs) + for path in new_filtered_leafs: parts = path.split('/') - current_node = target_tree + for i in range(1, len(parts)): + prefix = '/'.join(parts[:i]) + new_filtered_paths.add(prefix) + created_paths_to_remove = self._current_created_paths - new_filtered_paths + filtered_paths_to_remove = self._current_filtered_paths - new_filtered_leafs + + if created_paths_to_remove or filtered_paths_to_remove: + self._remove_paths_from_tree(created_paths_to_remove, filtered_paths_to_remove) + self._apply_expansion_to_tree(self.data_tree, search_term) + + paths_to_add = new_filtered_leafs - self._current_created_paths + if paths_to_add: + self._add_paths_to_tree(paths_to_add) + self._apply_expansion_to_tree(self.data_tree, search_term) + self._current_filtered_paths = new_filtered_paths + + def _remove_paths_from_tree(self, created_paths_to_remove, filtered_paths_to_remove): + for path in sorted(created_paths_to_remove, reverse=True): + current_node = self._path_to_node[path] + + if len(current_node.created_children) == 0: + self._current_created_paths.remove(current_node.full_path) + if item_handler_tag := self._item_handlers.get(current_node.ui_tag): + dpg.configure_item(item_handler_tag, show=False) + self._handlers_to_delete.append(item_handler_tag) + del self._item_handlers[current_node.ui_tag] + dpg.delete_item(current_node.ui_tag) + current_node.ui_created = False + current_node.ui_tag = None + current_node.children_ui_created = False + del current_node.parent.created_children[current_node.name] + del current_node.parent.filtered_children[current_node.name] + + for path in filtered_paths_to_remove: + parts = path.split('/') + current_node = self._path_to_node[path] + + part_array_index = -1 + while len(current_node.filtered_children) == 0 and part_array_index >= -len(parts): + current_node = current_node.parent + if parts[part_array_index] in current_node.filtered_children: + del current_node.filtered_children[parts[part_array_index]] + part_array_index -= 1 + + def _add_paths_to_tree(self, paths): + parent_nodes_to_recheck = set() + for path in sorted(paths): + parts = path.split('/') + current_node = self.data_tree current_path_prefix = "" for i, part in enumerate(parts): @@ -81,101 +156,81 @@ class DataTree: parent_nodes_to_recheck.add(current_node) # for incremental changes from new data if part not in current_node.children: current_node.children[part] = DataTreeNode(name=part, full_path=current_path_prefix, parent=current_node) + self._path_to_node[current_path_prefix] = current_node.children[part] + current_node.filtered_children[part] = current_node.children[part] current_node = current_node.children[part] if not current_node.is_leaf: current_node.is_leaf = True - self._calculate_child_counts(target_tree) - if incremental: - for p_node in parent_nodes_to_recheck: - p_node.children_ui_created = False - self._request_children_build(p_node) - return target_tree + for p_node in parent_nodes_to_recheck: + p_node.children_ui_created = False + self._request_children_build(p_node) - def update_frame(self, font): - with self._ui_lock: - if self._avg_char_width is None and dpg.is_dearpygui_running(): - self._avg_char_width = self.calculate_avg_char_width(font) + def _get_node_label_and_expand(self, node: DataTreeNode, search_term: str): + label = f"{node.name} ({len(node.filtered_children)} fields)" + expand = len(search_term) > 0 and any(search_term in path for path in self._get_descendant_paths(node)) + if expand and node.parent and len(node.parent.filtered_children) > 100 and len(node.filtered_children) > 2: + label += " (+)" # symbol for large lists which aren't fully expanded for performance (only affects procLog rn) + expand = False + return label, expand - if self._new_data: - current_paths = set(self.data_manager.get_all_paths()) - new_paths = current_paths - self._all_paths_cache - all_paths_empty = not self._all_paths_cache - self._all_paths_cache = current_paths - if all_paths_empty: - self._populate_tree() - elif new_paths: - self._add_paths_to_tree(new_paths, incremental=True) - self._new_data = False - return + def _apply_expansion_to_tree(self, node: DataTreeNode, search_term: str): + if node.ui_created and not node.is_leaf and node.ui_tag and dpg.does_item_exist(node.ui_tag): + label, expand = self._get_node_label_and_expand(node, search_term) + if expand: + self._expanded_tags.add(node.ui_tag) + dpg.set_value(node.ui_tag, expand) + elif node.ui_tag in self._expanded_tags: # not expanded and was expanded + self._expanded_tags.remove(node.ui_tag) + dpg.set_value(node.ui_tag, expand) + dpg.delete_item(node.ui_tag, children_only=True) # delete children (not visible since collapsed) + self._reset_ui_state_recursive(node) + node.children_ui_created = False + dpg.set_item_label(node.ui_tag, label) + for child in node.created_children.values(): + self._apply_expansion_to_tree(child, search_term) - if self._queued_search is not None: - self.current_search = self._queued_search - self._all_paths_cache = set(self.data_manager.get_all_paths()) - self._populate_tree() - self._queued_search = None - return - - nodes_processed = 0 - while self._build_queue and nodes_processed < self.MAX_NODES_PER_FRAME: - child_node, parent_tag, before_tag = self._build_queue.popleft() - if not child_node.ui_created: - if child_node.is_leaf: - self._create_leaf_ui(child_node, parent_tag, before_tag) - else: - self._create_tree_node_ui(child_node, parent_tag, before_tag) - nodes_processed += 1 + def _reset_ui_state_recursive(self, node: DataTreeNode): + for child in node.created_children.values(): + if child.ui_tag is not None: + if item_handler_tag := self._item_handlers.get(child.ui_tag): + self._handlers_to_delete.append(item_handler_tag) + dpg.configure_item(item_handler_tag, show=False) + del self._item_handlers[child.ui_tag] + self._reset_ui_state_recursive(child) + child.ui_created = False + child.ui_tag = None + child.children_ui_created = False + self._current_created_paths.remove(child.full_path) + node.created_children.clear() def search_data(self): - self._queued_search = dpg.get_value("search_input") - - def _clear_ui(self): - for handler_tag in self._item_handlers: - dpg.configure_item(handler_tag, show=False) - dpg.set_frame_callback(dpg.get_frame_count() + 1, callback=self._delete_handlers, user_data=list(self._item_handlers)) - self._item_handlers.clear() - - if dpg.does_item_exist("data_tree_container"): - dpg.delete_item("data_tree_container", children_only=True) - - self._build_queue.clear() - - def _delete_handlers(self, sender, app_data, user_data): - for handler in user_data: - dpg.delete_item(handler) - - def _calculate_child_counts(self, node: DataTreeNode): - if node.is_leaf: - node.child_count = 0 - else: - node.child_count = len(node.children) - for child in node.children.values(): - self._calculate_child_counts(child) + with self._ui_lock: + self._queued_search = dpg.get_value("search_input") def _create_tree_node_ui(self, node: DataTreeNode, parent_tag: str, before: str | int): - tag = f"tree_{node.full_path}" - node.ui_tag = tag - label = f"{node.name} ({node.child_count} fields)" + node.ui_tag = f"tree_{node.full_path}" search_term = self.current_search.strip().lower() - expand = bool(search_term) and len(search_term) > 1 and any(search_term in path for path in self._get_descendant_paths(node)) - if expand and node.parent and node.parent.child_count > 100 and node.child_count > 2: # don't fully autoexpand large lists (only affects procLog rn) - label += " (+)" - expand = False + label, expand = self._get_node_label_and_expand(node, search_term) + if expand: + self._expanded_tags.add(node.ui_tag) + elif node.ui_tag in self._expanded_tags: + self._expanded_tags.remove(node.ui_tag) with dpg.tree_node( - label=label, parent=parent_tag, tag=tag, default_open=expand, open_on_arrow=True, open_on_double_click=True, before=before, delay_search=True + label=label, parent=parent_tag, tag=node.ui_tag, default_open=expand, open_on_arrow=True, open_on_double_click=True, before=before, delay_search=True ): with dpg.item_handler_registry() as handler_tag: dpg.add_item_toggled_open_handler(callback=lambda s, a, u: self._request_children_build(node)) dpg.add_item_visible_handler(callback=lambda s, a, u: self._request_children_build(node)) - dpg.bind_item_handler_registry(tag, handler_tag) - self._item_handlers.add(handler_tag) - + dpg.bind_item_handler_registry(node.ui_tag, handler_tag) + self._item_handlers[node.ui_tag] = handler_tag node.ui_created = True def _create_leaf_ui(self, node: DataTreeNode, parent_tag: str, before: str | int): - with dpg.group(parent=parent_tag, tag=f"leaf_{node.full_path}", before=before, delay_search=True) as draggable_group: + node.ui_tag = f"leaf_{node.full_path}" + with dpg.group(parent=parent_tag, tag=node.ui_tag, before=before, delay_search=True): with dpg.table(header_row=False, policy=dpg.mvTable_SizingStretchProp, delay_search=True): dpg.add_table_column(init_width_or_weight=0.5) dpg.add_table_column(init_width_or_weight=0.5) @@ -186,21 +241,21 @@ class DataTree: if node.is_plottable is None: node.is_plottable = self.data_manager.is_plottable(node.full_path) if node.is_plottable: - with dpg.drag_payload(parent=draggable_group, drag_data=node.full_path, payload_type="TIMESERIES_PAYLOAD"): + with dpg.drag_payload(parent=node.ui_tag, drag_data=node.full_path, payload_type="TIMESERIES_PAYLOAD"): dpg.add_text(f"Plot: {node.full_path}") with dpg.item_handler_registry() as handler_tag: dpg.add_item_visible_handler(callback=self._on_item_visible, user_data=node.full_path) - dpg.bind_item_handler_registry(draggable_group, handler_tag) - self._item_handlers.add(handler_tag) - + dpg.bind_item_handler_registry(node.ui_tag, handler_tag) + self._item_handlers[node.ui_tag] = handler_tag node.ui_created = True - node.ui_tag = f"value_{node.full_path}" def _on_item_visible(self, sender, app_data, user_data): with self._ui_lock: path = user_data value_tag = f"value_{path}" + if not dpg.does_item_exist(value_tag): + return value_column_width = dpg.get_item_rect_size("sidebar_window")[0] // 2 value = self.data_manager.get_value_at(path, self.playback_manager.current_time_s) if value is not None: @@ -212,8 +267,7 @@ class DataTree: def _request_children_build(self, node: DataTreeNode): with self._ui_lock: if not node.children_ui_created and (node.name == "root" or (node.ui_tag is not None and dpg.get_value(node.ui_tag))): # check root or node expanded - parent_tag = "data_tree_container" if node.name == "root" else node.ui_tag - sorted_children = sorted(node.children.values(), key=self._natural_sort_key) + sorted_children = sorted(node.filtered_children.values(), key=self._natural_sort_key) next_existing: list[int | str] = [0] * len(sorted_children) current_before_tag: int | str = 0 @@ -228,7 +282,7 @@ class DataTree: for i, child_node in enumerate(sorted_children): if not child_node.ui_created: before_tag = next_existing[i] - self._build_queue.append((child_node, parent_tag, before_tag)) + self._build_queue[child_node.full_path] = (child_node, node, before_tag) node.children_ui_created = True def _should_show_path(self, path: str, search_term: str) -> bool: @@ -242,7 +296,7 @@ class DataTree: return (node_type_key, parts) def _get_descendant_paths(self, node: DataTreeNode): - for child_name, child_node in node.children.items(): + for child_name, child_node in node.filtered_children.items(): child_name_lower = child_name.lower() if child_node.is_leaf: yield child_name_lower From 70c0592e84a2cc5a8795b0d09d60eec46329c490 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 11 Sep 2025 11:03:59 -0700 Subject: [PATCH 172/188] CI: re-enable macOS build (#36120) * CI: re-enable macOS build * Update selfdrive_tests.yaml with new env variable --- .github/workflows/selfdrive_tests.yaml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index cdafbbfede..beb426c669 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -91,7 +91,6 @@ jobs: build_mac: name: build macOS - if: false # temp disable since homebrew install is getting stuck runs-on: ${{ ((github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot'))) && 'namespace-profile-macos-8x14' || 'macos-latest' }} steps: - uses: actions/checkout@v4 @@ -109,8 +108,8 @@ jobs: - name: Install dependencies run: ./tools/mac_setup.sh env: - # package install has DeprecationWarnings - PYTHONWARNINGS: default + PYTHONWARNINGS: default # package install has DeprecationWarnings + HOMEBREW_DISPLAY_INSTALL_TIMES: 1 - run: git lfs pull - name: Getting scons cache uses: ./.github/workflows/auto-cache From b7f8dd11a559f9cf7a427990e6d158d96a10a707 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Thu, 11 Sep 2025 21:44:43 +0200 Subject: [PATCH 173/188] SL: bugfix parameter handling in sunnylink restore and remote setting (#1234) * refactor: improve parameter handling in sunnylink for robustness - Updated `get_param_as_byte` to return `None` for nonexistent parameters. - Enhanced param compression and encoding in `sunnylinkd`. * refactor: centralize parameter restoration with new helper function - Added `save_param_from_base64_encoded_string` to handle param decoding and saving. - Updated backup manager and sunnylinkd to use the new method. - Improved code readability and reduced duplication in parameter handling logic. * don't bother * clean --- sunnypilot/sunnylink/athena/sunnylinkd.py | 26 +++++++--------- sunnypilot/sunnylink/backups/manager.py | 29 +++-------------- sunnypilot/sunnylink/utils.py | 38 +++++++++++++++++++++-- 3 files changed, 53 insertions(+), 40 deletions(-) diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index 363fa1defc..25a77c367b 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -23,7 +23,7 @@ from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutExce import cereal.messaging as messaging from sunnypilot.sunnylink.api import SunnylinkApi -from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte +from sunnypilot.sunnylink.utils import sunnylink_need_register, sunnylink_ready, get_param_as_byte, save_param_from_base64_encoded_string SUNNYLINK_ATHENA_HOST = os.getenv('SUNNYLINK_ATHENA_HOST', 'wss://ws.stg.api.sunnypilot.ai') HANDLER_THREADS = int(os.getenv('HANDLER_THREADS', "4")) @@ -184,14 +184,18 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s try: param_keys_validated = [key for key in params_keys if key in getParamsAllKeys()] - params_dict: dict[str, list[dict[str, str | bool | int ]]] = {"params": [ - { + params_dict: dict[str, list[dict[str, str | bool | int]]] = {"params": []} + for key in param_keys_validated: + value = get_param_as_byte(key) + if value is None: + continue + + params_dict["params"].append({ "key": key, - "value": base64.b64encode(gzip.compress(get_param_as_byte(key)) if compression else get_param_as_byte(key)).decode('utf-8'), + "value": base64.b64encode(gzip.compress(value) if compression else value).decode('utf-8'), "type": int(params.get_type(key).value), "is_compressed": compression - } for key in param_keys_validated - ]} + }) response = {str(param.get('key')): str(param.get('value')) for param in params_dict.get("params", [])} response |= {"params": json.dumps(params_dict.get("params", []))} # Upcoming for settings v1 @@ -204,15 +208,9 @@ def getParams(params_keys: list[str], compression: bool = False) -> str | dict[s @dispatcher.add_method def saveParams(params_to_update: dict[str, str], compression: bool = False) -> None: - params = Params() - params_dict = {key: base64.b64decode(value) for key, value in params_to_update.items()} - - if compression: - params_dict = {key: gzip.decompress(value) for key, value in params_dict.items()} - - for key, value in params_dict.items(): + for key, value in params_to_update.items(): try: - params.put(key, value) + save_param_from_base64_encoded_string(key, value, compression) except Exception as e: cloudlog.error(f"sunnylinkd.saveParams.exception {e}") diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index 315300c73c..e52b547afe 100644 --- a/sunnypilot/sunnylink/backups/manager.py +++ b/sunnypilot/sunnylink/backups/manager.py @@ -12,7 +12,7 @@ from enum import Enum from typing import Any from openpilot.common.git import get_branch -from openpilot.common.params import Params, ParamKeyType, ParamKeyFlag +from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_version @@ -20,7 +20,7 @@ from openpilot.system.version import get_version from cereal import messaging, custom from sunnypilot.sunnylink.api import SunnylinkApi from sunnypilot.sunnylink.backups.utils import decrypt_compressed_data, encrypt_compress_data, SnakeCaseEncoder -from sunnypilot.sunnylink.utils import get_param_as_byte +from sunnypilot.sunnylink.utils import get_param_as_byte, save_param_from_base64_encoded_string class OperationType(Enum): @@ -173,8 +173,7 @@ class BackupManagerSP: self._update_progress(75.0, OperationType.RESTORE) # Apply configuration - all_values_encoded = self._get_metadata_value(backup_metadata, "all_values_encoded", "false") - self._apply_config(config_data, str(all_values_encoded).lower() == "true") + self._apply_config(config_data) self.restore_status = custom.BackupManagerSP.Status.completed self._update_progress(100.0, OperationType.RESTORE) @@ -187,7 +186,7 @@ class BackupManagerSP: self._report_status() return False - def _apply_config(self, config_data: dict[str, str], all_values_encoded: bool = False) -> None: + def _apply_config(self, config_data: dict[str, str]) -> None: """Applies configuration data from a backup, but only for parameters marked as backupable.""" backupable_params = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)] backupable_set_lower = {p.lower() for p in backupable_params} @@ -199,26 +198,8 @@ class BackupManagerSP: if param.lower() in backupable_set_lower: # Find real param name (with correct casing) real_param = next(p for p in backupable_params if p.lower() == param.lower()) - param_type = self.params.get_type(real_param) try: - value = base64.b64decode(encoded_value) if all_values_encoded else encoded_value - - if param_type != ParamKeyType.BYTES: - value = value.decode('utf-8') # type: ignore - - if param_type == ParamKeyType.STRING: - value = value - elif param_type == ParamKeyType.BOOL: - value = value.lower() in ('true', '1', 'yes') # type: ignore - elif param_type == ParamKeyType.INT: - value = int(value) # type: ignore - elif param_type == ParamKeyType.FLOAT: - value = float(value) # type: ignore - elif param_type == ParamKeyType.TIME: - value = str(value) - elif param_type == ParamKeyType.JSON: - value = json.loads(value) - self.params.put(real_param, value) + save_param_from_base64_encoded_string(real_param, encoded_value) restored_count += 1 except Exception as e: cloudlog.error(f"Failed to restore param {param}: {str(e)}") diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 569afd26b6..1310b91f0e 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -1,3 +1,5 @@ +import base64 +import gzip import json from sunnypilot.sunnylink.api import SunnylinkApi, UNREGISTERED_SUNNYLINK_DONGLE_ID from openpilot.common.params import Params, ParamKeyType @@ -58,13 +60,45 @@ def get_api_token(): print(f"API Token: {token}") -def get_param_as_byte(param_name: str) -> bytes: +def get_param_as_byte(param_name: str) -> bytes | None: + """Get a parameter as bytes. Returns None if the parameter does not exist.""" params = Params() param = params.get(param_name) - param_type = params.get_type(param_name) + if param is None: + return None + param_type = params.get_type(param_name) if param_type == ParamKeyType.BYTES: return bytes(param) elif param_type == ParamKeyType.JSON: return json.dumps(param).encode('utf-8') return str(param).encode('utf-8') + + +def save_param_from_base64_encoded_string(param_name: str, base64_encoded_data: str, is_compressed=False) -> None: + """Save a parameter from bytes. Overwrites the parameter if it already exists.""" + params = Params() + # Find real param name (with correct casing) + param_type = params.get_type(param_name) + value = base64.b64decode(base64_encoded_data) + + if is_compressed: + value = gzip.decompress(value) + + # We convert to string anything that isn't bytes first. We later transform further. + if param_type != ParamKeyType.BYTES: + value = value.decode('utf-8') # type: ignore + + if param_type == ParamKeyType.STRING: + value = value + elif param_type == ParamKeyType.BOOL: + value = value.lower() in ('true', '1', 'yes') # type: ignore + elif param_type == ParamKeyType.INT: + value = int(value) # type: ignore + elif param_type == ParamKeyType.FLOAT: + value = float(value) # type: ignore + elif param_type == ParamKeyType.TIME: + value = str(value) # type: ignore + elif param_type == ParamKeyType.JSON: + value = json.loads(value) + params.put(param_name, value) From 2c04a27a2a0c3d3bdf9dfc6079d724651e369b49 Mon Sep 17 00:00:00 2001 From: Adeeb Shihadeh Date: Thu, 11 Sep 2025 14:03:37 -0700 Subject: [PATCH 174/188] ubloxd: cleanup unused files --- system/ubloxd/glonass_fix.patch | 13 ----- system/ubloxd/tests/ubloxd.py | 89 --------------------------------- 2 files changed, 102 deletions(-) delete mode 100644 system/ubloxd/glonass_fix.patch delete mode 100755 system/ubloxd/tests/ubloxd.py diff --git a/system/ubloxd/glonass_fix.patch b/system/ubloxd/glonass_fix.patch deleted file mode 100644 index 7eb973a348..0000000000 --- a/system/ubloxd/glonass_fix.patch +++ /dev/null @@ -1,13 +0,0 @@ -diff --git a/system/ubloxd/generated/glonass.cpp b/system/ubloxd/generated/glonass.cpp -index 5b17bc327..b5c6aa610 100644 ---- a/system/ubloxd/generated/glonass.cpp -+++ b/system/ubloxd/generated/glonass.cpp -@@ -17,7 +17,7 @@ glonass_t::glonass_t(kaitai::kstream* p__io, kaitai::kstruct* p__parent, glonass - void glonass_t::_read() { - m_idle_chip = m__io->read_bits_int_be(1); - m_string_number = m__io->read_bits_int_be(4); -- m__io->align_to_byte(); -+ //m__io->align_to_byte(); - switch (string_number()) { - case 4: { - m_data = new string_4_t(m__io, this, m__root); diff --git a/system/ubloxd/tests/ubloxd.py b/system/ubloxd/tests/ubloxd.py deleted file mode 100755 index c17387114f..0000000000 --- a/system/ubloxd/tests/ubloxd.py +++ /dev/null @@ -1,89 +0,0 @@ -#!/usr/bin/env python3 -# type: ignore - -from openpilot.selfdrive.locationd.test import ublox -import struct - -baudrate = 460800 -rate = 100 # send new data every 100ms - - -def configure_ublox(dev): - # configure ports and solution parameters and rate - dev.configure_port(port=ublox.PORT_USB, inMask=1, outMask=1) # enable only UBX on USB - dev.configure_port(port=0, inMask=0, outMask=0) # disable DDC - - payload = struct.pack(' Date: Fri, 12 Sep 2025 01:00:05 -0400 Subject: [PATCH 175/188] UI: Developer UI (#1233) --- common/params_keys.h | 1 + selfdrive/ui/qt/onroad/alerts.cc | 9 + selfdrive/ui/qt/onroad/annotated_camera.h | 1 + selfdrive/ui/qt/onroad/driver_monitoring.cc | 5 + selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/visuals_panel.cc | 12 + .../qt/offroad/settings/visuals_panel.h | 1 + .../sunnypilot/qt/onroad/annotated_camera.cc | 5 + .../sunnypilot/qt/onroad/annotated_camera.h | 3 + .../qt/onroad/developer_ui/developer_ui.cc | 227 ++++++++++++++++++ .../qt/onroad/developer_ui/developer_ui.h | 31 +++ .../qt/onroad/developer_ui/ui_elements.h | 19 ++ selfdrive/ui/sunnypilot/qt/onroad/hud.cc | 190 +++++++++++++++ selfdrive/ui/sunnypilot/qt/onroad/hud.h | 39 ++- selfdrive/ui/sunnypilot/ui.cc | 16 +- selfdrive/ui/sunnypilot/ui.h | 4 + selfdrive/ui/sunnypilot/ui_scene.h | 12 + selfdrive/ui/ui.h | 5 + 18 files changed, 578 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h create mode 100644 selfdrive/ui/sunnypilot/ui_scene.h diff --git a/common/params_keys.h b/common/params_keys.h index afb6b348eb..fc7842720b 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -146,6 +146,7 @@ inline static std::unordered_map keys = { {"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}}, {"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}}, {"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}}, + {"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}}, {"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}}, {"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}}, {"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}}, diff --git a/selfdrive/ui/qt/onroad/alerts.cc b/selfdrive/ui/qt/onroad/alerts.cc index d6829c6b08..2e8f3612eb 100644 --- a/selfdrive/ui/qt/onroad/alerts.cc +++ b/selfdrive/ui/qt/onroad/alerts.cc @@ -4,6 +4,9 @@ #include #include "selfdrive/ui/qt/util.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#endif void OnroadAlerts::updateState(const UIState &s) { Alert a = getAlert(*(s.sm), s.scene.started_frame); @@ -73,6 +76,12 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) { } QRect r = QRect(0 + margin, height() - h + margin, width() - margin*2, h - margin*2); +#ifdef SUNNYPILOT + const int dev_ui_info = uiStateSP()->scene.dev_ui_info; + const int adjustment = dev_ui_info > 1 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 30 : 0; + r = QRect(0 + margin, height() - h + margin - adjustment, width() - margin*2, h - margin*2); +#endif + QPainter p(this); // draw background + gradient diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h index e3ca837907..5d9d21ab6b 100644 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/qt/onroad/annotated_camera.h @@ -12,6 +12,7 @@ #include "selfdrive/ui/sunnypilot/qt/onroad/model.h" #define ExperimentalButton ExperimentalButtonSP #define ModelRenderer ModelRendererSP +#define HudRenderer HudRendererSP #else #include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/hud.h" diff --git a/selfdrive/ui/qt/onroad/driver_monitoring.cc b/selfdrive/ui/qt/onroad/driver_monitoring.cc index 49f2c950b4..e67c483047 100644 --- a/selfdrive/ui/qt/onroad/driver_monitoring.cc +++ b/selfdrive/ui/qt/onroad/driver_monitoring.cc @@ -73,6 +73,11 @@ void DriverMonitorRenderer::draw(QPainter &painter, const QRect &surface_rect) { float y = surface_rect.height() - offset; float opacity = is_active ? 0.65f : 0.2f; +#ifdef SUNNYPILOT + const int dev_ui_info = uiStateSP()->scene.dev_ui_info; + y -= dev_ui_info > 1 ? 50 : 0; +#endif + drawIcon(painter, QPoint(x, y), dm_img, QColor(0, 0, 0, 70), opacity); QPointF keypoints[std::size(DEFAULT_FACE_KPTS_3D)]; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 2f3c8ddd8d..807bf02478 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -39,6 +39,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/visuals_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", "sunnypilot/qt/onroad/buttons.cc", + "sunnypilot/qt/onroad/developer_ui/developer_ui.cc", "sunnypilot/qt/onroad/hud.cc", "sunnypilot/qt/onroad/model.cc", "sunnypilot/qt/onroad/onroad_home.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index dd2f05416d..c3aaf12d22 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -72,6 +72,15 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { list->addItem(chevron_info_settings); param_watcher->addParam("ChevronInfo"); + // Visuals: Developer UI Info (Dev UI) + std::vector dev_ui_settings_texts{tr("Off"), tr("Right"), tr("Right &&\nBottom")}; + dev_ui_settings = new ButtonParamControlSP( + "DevUIInfo", tr("Developer UI"), tr("Display real-time parameters and metrics from various sources."), + "", + dev_ui_settings_texts, + 380); + list->addItem(dev_ui_settings); + sunnypilotScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnypilotScroller); @@ -90,4 +99,7 @@ void VisualsPanel::paramsRefresh() { if (chevron_info_settings) { chevron_info_settings->refresh(); } + if (dev_ui_settings) { + dev_ui_settings->refresh(); + } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h index f342662c22..30ff31c301 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -28,4 +28,5 @@ protected: std::map toggles; ParamWatcher * param_watcher; ButtonParamControlSP *chevron_info_settings; + ButtonParamControlSP *dev_ui_settings; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc index 3721a3d198..1d5567161a 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc @@ -14,3 +14,8 @@ AnnotatedCameraWidgetSP::AnnotatedCameraWidgetSP(VisionStreamType type, QWidget void AnnotatedCameraWidgetSP::updateState(const UIState &s) { AnnotatedCameraWidget::updateState(s); } + +void AnnotatedCameraWidgetSP::showEvent(QShowEvent *event) { + AnnotatedCameraWidget::showEvent(event); + ui_update_params_sp(uiState()); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h index 46ce7d4be3..8c0a385657 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h @@ -15,4 +15,7 @@ class AnnotatedCameraWidgetSP : public AnnotatedCameraWidget { public: explicit AnnotatedCameraWidgetSP(VisionStreamType type, QWidget *parent = nullptr); void updateState(const UIState &s) override; + +protected: + void showEvent(QShowEvent *event) override; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc new file mode 100644 index 0000000000..292ba6f7bb --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ +#include + +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" + + +// Add Relative Distance to Primary Lead Car +// Unit: Meters +UiElement DeveloperUi::getDRel(bool lead_status, float lead_d_rel) { + QString value = lead_status ? QString::number(lead_d_rel, 'f', 0) : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lead_status) { + // Orange if close, Red if very close + if (lead_d_rel < 5) { + color = QColor(255, 0, 0, 255); + } else if (lead_d_rel < 15) { + color = QColor(255, 188, 0, 255); + } + } + + return UiElement(value, "REL DIST", "m", color); +} + +// Add Relative Velocity vs Primary Lead Car +// Unit: kph if metric, else mph +UiElement DeveloperUi::getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit) { + QString value = lead_status ? QString::number(lead_v_rel * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lead_status) { + // Red if approaching faster than 10mph + // Orange if approaching (negative) + if (lead_v_rel < -4.4704) { + color = QColor(255, 0, 0, 255); + } else if (lead_v_rel < 0) { + color = QColor(255, 188, 0, 255); + } + } + + return UiElement(value, "REL SPEED", speed_unit, color); +} + +// Add Real Steering Angle +// Unit: Degrees +UiElement DeveloperUi::getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override) { + QString value = QString("%1%2%3").arg(QString::number(angle_steers, 'f', 1)).arg("°").arg(""); + QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); + + // Red if large steering angle + // Orange if moderate steering angle + if (std::fabs(angle_steers) > 180) { + color = QColor(255, 0, 0, 255); + } else if (std::fabs(angle_steers) > 90) { + color = QColor(255, 188, 0, 255); + } + + return UiElement(value, "REAL STEER", "", color); +} + +// Add Actual Lateral Acceleration (roll compensated) when using Torque +// Unit: m/s² +UiElement DeveloperUi::getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override) { + double actualLateralAccel = (curvature * pow(v_ego, 2)) - (roll * 9.81); + + QString value = QString::number(actualLateralAccel, 'f', 2); + QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); + + return UiElement(value, "ACTUAL L.A.", "m/s²", color); +} + +// Add Desired Steering Angle when using PID +// Unit: Degrees +UiElement DeveloperUi::getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers) { + QString value = lat_active ? QString("%1%2%3").arg(QString::number(steer_angle_desired, 'f', 1)).arg("°").arg("") : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lat_active) { + // Red if large steering angle + // Orange if moderate steering angle + if (std::fabs(angle_steers) > 180) { + color = QColor(255, 0, 0, 255); + } else if (std::fabs(angle_steers) > 90) { + color = QColor(255, 188, 0, 255); + } else { + color = QColor(0, 255, 0, 255); + } + } + + return UiElement(value, "DESIRED STEER", "", color); +} + +// Add Device Memory (RAM) Usage +// Unit: Percent +UiElement DeveloperUi::getMemoryUsagePercent(int memory_usage_percent) { + QString value = QString("%1%2").arg(QString::number(memory_usage_percent, 'd', 0)).arg("%"); + QColor color = (memory_usage_percent > 85) ? QColor(255, 188, 0, 255) : QColor(255, 255, 255, 255); + + return UiElement(value, "RAM", "", color); +} + +// Add Vehicle Current Acceleration +// Unit: m/s² +UiElement DeveloperUi::getAEgo(float a_ego) { + QString value = QString::number(a_ego, 'f', 1); + QColor color = QColor(255, 255, 255, 255); + + return UiElement(value, "ACC.", "m/s²", color); +} + +// Add Relative Velocity to Primary Lead Car +// Unit: kph if metric, else mph +UiElement DeveloperUi::getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit) { + QString value = lead_status ? QString::number((lead_v_rel + v_ego) * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lead_status) { + // Red if approaching faster than 10mph + // Orange if approaching (negative) + if (lead_v_rel < -4.4704) { + color = QColor(255, 0, 0, 255); + } else if (lead_v_rel < 0) { + color = QColor(255, 188, 0, 255); + } + } + + return UiElement(value, "L.S.", speed_unit, color); +} + +// Add Friction Coefficient Raw from torqued +// Unit: None +UiElement DeveloperUi::getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid) { + QString value = QString::number(friction_coefficient_filtered, 'f', 3); + QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); + + return UiElement(value, "FRIC.", "", color); +} + +// Add Lateral Acceleration Factor Raw from torqued +// Unit: m/s² +UiElement DeveloperUi::getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid) { + QString value = QString::number(lat_accel_factor_filtered, 'f', 3); + QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); + + return UiElement(value, "L.A.", "m/s²", color); +} + +// Add Steering Torque from Car EPS +// Unit: Newton Meters +UiElement DeveloperUi::getSteeringTorqueEps(float steering_torque_eps) { + QString value = QString::number(std::fabs(steering_torque_eps), 'f', 1); + QColor color = QColor(255, 255, 255, 255); + + return UiElement(value, "E.T.", "N·dm", color); +} + +// Add Bearing Degree and Direction from Car (Compass) +// Unit: Meters +UiElement DeveloperUi::getBearingDeg(float bearing_accuracy_deg, float bearing_deg) { + QString value = (bearing_accuracy_deg != 180.00) ? QString("%1%2%3").arg(QString::number(bearing_deg, 'd', 0)).arg("°").arg("") : "-"; + QColor color = QColor(255, 255, 255, 255); + QString dir_value; + + if (bearing_accuracy_deg != 180.00) { + if (((bearing_deg >= 337.5) && (bearing_deg <= 360)) || ((bearing_deg >= 0) && (bearing_deg <= 22.5))) { + dir_value = "N"; + } else if ((bearing_deg > 22.5) && (bearing_deg < 67.5)) { + dir_value = "NE"; + } else if ((bearing_deg >= 67.5) && (bearing_deg <= 112.5)) { + dir_value = "E"; + } else if ((bearing_deg > 112.5) && (bearing_deg < 157.5)) { + dir_value = "SE"; + } else if ((bearing_deg >= 157.5) && (bearing_deg <= 202.5)) { + dir_value = "S"; + } else if ((bearing_deg > 202.5) && (bearing_deg < 247.5)) { + dir_value = "SW"; + } else if ((bearing_deg >= 247.5) && (bearing_deg <= 292.5)) { + dir_value = "W"; + } else if ((bearing_deg > 292.5) && (bearing_deg < 337.5)) { + dir_value = "NW"; + } + } else { + dir_value = "OFF"; + } + + return UiElement(QString("%1 | %2").arg(dir_value).arg(value), "B.D.", "", color); +} + +// Add Altitude of Current Location +// Unit: Meters +UiElement DeveloperUi::getAltitude(float gps_accuracy, float altitude) { + QString value = (gps_accuracy != 0.00) ? QString::number(altitude, 'f', 1) : "-"; + QColor color = QColor(255, 255, 255, 255); + + return UiElement(value, "ALT.", "m", color); +} + +// Add Actuators Output +// Unit: Degree (angle) or m/s² (torque) +UiElement DeveloperUi::getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, + cereal::CarControl::Actuators::Reader &actuators, + float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override) { + QString label; + QString value; + QString unit; + + if (steerControlType == cereal::CarParams::SteerControlType::ANGLE) { + label = "DESIRED STEER"; + value = QString("%1%2%3").arg(QString::number(actuators.getSteeringAngleDeg(), 'f', 1)).arg("°").arg(""); + } else { + label = "DESIRED L.A."; + double desiredLateralAccel = (desiredCurvature * pow(v_ego, 2)) - (roll * 9.81); + value = QString::number(desiredLateralAccel, 'f', 2); + unit = "m/s²"; + } + + value = lat_active ? value : "-"; + QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); + + return UiElement(value, label, unit, color); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h new file mode 100644 index 0000000000..0c5c472209 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ +#pragma once + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h" + +class DeveloperUi { + +public: + static UiElement getDRel(bool lead_status, float lead_d_rel); + static UiElement getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit); + static UiElement getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override); + static UiElement getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override); + static UiElement getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers); + static UiElement getMemoryUsagePercent(int memory_usage_percent); + static UiElement getAEgo(float a_ego); + static UiElement getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit); + static UiElement getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid); + static UiElement getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid); + static UiElement getSteeringTorqueEps(float steering_torque_eps); + static UiElement getBearingDeg(float bearing_accuracy_deg, float bearing_deg); + static UiElement getAltitude(float gps_accuracy, float altitude); + static UiElement getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, + cereal::CarControl::Actuators::Reader &actuators, + float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override); +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h new file mode 100644 index 0000000000..3711e5ac05 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ +#pragma once + +#include + +struct UiElement { + QString value{}; + QString label{}; + QString units{}; + QColor color{}; + + explicit UiElement(const QString &value = "", const QString &label = "", const QString &units = "", const QColor &color = QColor(255, 255, 255, 255)) + : value(value), label(label), units(units), color(color) {} +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc index 233ca59f98..15722cc9f8 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc @@ -7,12 +7,202 @@ #include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" +#include "selfdrive/ui/qt/util.h" + + HudRendererSP::HudRendererSP() {} void HudRendererSP::updateState(const UIState &s) { HudRenderer::updateState(s); + + const SubMaster &sm = *(s.sm); + const bool cs_alive = sm.alive("controlsState"); + const auto cs = sm["controlsState"].getControlsState(); + const auto car_state = sm["carState"].getCarState(); + const auto car_control = sm["carControl"].getCarControl(); + const auto radar_state = sm["radarState"].getRadarState(); + const auto is_gps_location_external = sm.rcv_frame("gpsLocationExternal") > 1; + const auto gpsLocation = is_gps_location_external ? sm["gpsLocationExternal"].getGpsLocationExternal() : sm["gpsLocation"].getGpsLocation(); + const auto ltp = sm["liveTorqueParameters"].getLiveTorqueParameters(); + const auto car_params = sm["carParams"].getCarParams(); + + static int reverse_delay = 0; + bool reverse_allowed = false; + if (int(car_state.getGearShifter()) != 4) { + reverse_delay = 0; + reverse_allowed = false; + } else { + reverse_delay += 50; + if (reverse_delay >= 1000) { + reverse_allowed = true; + } + } + + reversing = reverse_allowed; + is_metric = s.scene.is_metric; + + // Handle older routes where vEgoCluster is not set + v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0; + float v_ego = v_ego_cluster_seen ? car_state.getVEgoCluster() : car_state.getVEgo(); + speed = cs_alive ? std::max(0.0, v_ego) : 0.0; + speed *= is_metric ? MS_TO_KPH : MS_TO_MPH; + + latActive = car_control.getLatActive(); + steerOverride = car_state.getSteeringPressed(); + + devUiInfo = s.scene.dev_ui_info; + + speedUnit = is_metric ? tr("km/h") : tr("mph"); + lead_d_rel = radar_state.getLeadOne().getDRel(); + lead_v_rel = radar_state.getLeadOne().getVRel(); + lead_status = radar_state.getLeadOne().getStatus(); + steerControlType = car_params.getSteerControlType(); + actuators = car_control.getActuators(); + torqueLateral = steerControlType == cereal::CarParams::SteerControlType::TORQUE; + angleSteers = car_state.getSteeringAngleDeg(); + desiredCurvature = cs.getDesiredCurvature(); + curvature = cs.getCurvature(); + roll = sm["liveParameters"].getLiveParameters().getRoll(); + memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent(); + gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not. + altitude = gpsLocation.getAltitude(); + vEgo = car_state.getVEgo(); + aEgo = car_state.getAEgo(); + steeringTorqueEps = car_state.getSteeringTorqueEps(); + bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg(); + bearingDeg = gpsLocation.getBearingDeg(); + torquedUseParams = ltp.getUseParams(); + latAccelFactorFiltered = ltp.getLatAccelFactorFiltered(); + frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered(); + liveValid = ltp.getLiveValid(); } void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { HudRenderer::draw(p, surface_rect); + if (!reversing) { + // Bottom Dev UI + if (devUiInfo == 2) { + QRect rect_bottom(surface_rect.left(), surface_rect.bottom() - 60, surface_rect.width(), 61); + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 0, 0, 100)); + p.drawRect(rect_bottom); + drawBottomDevUI(p, rect_bottom.left(), rect_bottom.center().y()); + } + + // Right Dev UI + if (devUiInfo != 0) { + QRect rect_right(surface_rect.right() - (UI_BORDER_SIZE * 2), UI_BORDER_SIZE * 1.5, 184, 170); + drawRightDevUI(p, surface_rect.right() - 184 - UI_BORDER_SIZE * 2, UI_BORDER_SIZE * 2 + rect_right.height()); + } + } +} + +void HudRendererSP::drawText(QPainter &p, int x, int y, const QString &text, QColor color) { + QRect real_rect = p.fontMetrics().boundingRect(text); + real_rect.moveCenter({x, y - real_rect.height() / 2}); + p.setPen(color); + p.drawText(real_rect.x(), real_rect.bottom(), text); +} + +int HudRendererSP::drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { + + p.setFont(InterFont(28, QFont::Bold)); + x += 92; + y += 80; + drawText(p, x, y, label); + + p.setFont(InterFont(30 * 2, QFont::Bold)); + y += 65; + drawText(p, x, y, value, color); + + p.setFont(InterFont(28, QFont::Bold)); + + if (units.length() > 0) { + p.save(); + x += 120; + y -= 25; + p.translate(x, y); + p.rotate(-90); + drawText(p, 0, 0, units); + p.restore(); + } + + return 130; +} + +void HudRendererSP::drawRightDevUI(QPainter &p, int x, int y) { + int rh = 5; + int ry = y; + + UiElement dRelElement = DeveloperUi::getDRel(lead_status, lead_d_rel); + rh += drawRightDevUIElement(p, x, ry, dRelElement.value, dRelElement.label, dRelElement.units, dRelElement.color); + ry = y + rh; + + UiElement vRelElement = DeveloperUi::getVRel(lead_status, lead_v_rel, is_metric, speedUnit); + rh += drawRightDevUIElement(p, x, ry, vRelElement.value, vRelElement.label, vRelElement.units, vRelElement.color); + ry = y + rh; + + UiElement steeringAngleDegElement = DeveloperUi::getSteeringAngleDeg(angleSteers, latActive, steerOverride); + rh += drawRightDevUIElement(p, x, ry, steeringAngleDegElement.value, steeringAngleDegElement.label, steeringAngleDegElement.units, steeringAngleDegElement.color); + ry = y + rh; + + UiElement actuatorsOutputLateralElement = DeveloperUi::getActuatorsOutputLateral(steerControlType, actuators, desiredCurvature, vEgo, roll, latActive, steerOverride); + rh += drawRightDevUIElement(p, x, ry, actuatorsOutputLateralElement.value, actuatorsOutputLateralElement.label, actuatorsOutputLateralElement.units, actuatorsOutputLateralElement.color); + ry = y + rh; + + UiElement actualLateralAccelElement = DeveloperUi::getActualLateralAccel(curvature, vEgo, roll, latActive, steerOverride); + rh += drawRightDevUIElement(p, x, ry, actualLateralAccelElement.value, actualLateralAccelElement.label, actualLateralAccelElement.units, actualLateralAccelElement.color); +} + +int HudRendererSP::drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { + p.setFont(InterFont(38, QFont::Bold)); + QFontMetrics fm(p.font()); + QRect init_rect = fm.boundingRect(label + " "); + QRect real_rect = fm.boundingRect(init_rect, 0, label + " "); + real_rect.moveCenter({x, y}); + + QRect init_rect2 = fm.boundingRect(value); + QRect real_rect2 = fm.boundingRect(init_rect2, 0, value); + real_rect2.moveTop(real_rect.top()); + real_rect2.moveLeft(real_rect.right() + 10); + + QRect init_rect3 = fm.boundingRect(units); + QRect real_rect3 = fm.boundingRect(init_rect3, 0, units); + real_rect3.moveTop(real_rect.top()); + real_rect3.moveLeft(real_rect2.right() + 10); + + p.setPen(QColorConstants::White); + p.drawText(real_rect, Qt::AlignLeft | Qt::AlignVCenter, label); + + p.setPen(color); + p.drawText(real_rect2, Qt::AlignRight | Qt::AlignVCenter, value); + p.drawText(real_rect3, Qt::AlignLeft | Qt::AlignVCenter, units); + return 430; +} + +void HudRendererSP::drawBottomDevUI(QPainter &p, int x, int y) { + int rw = 90; + + UiElement aEgoElement = DeveloperUi::getAEgo(aEgo); + rw += drawBottomDevUIElement(p, rw, y, aEgoElement.value, aEgoElement.label, aEgoElement.units, aEgoElement.color); + + UiElement vEgoLeadElement = DeveloperUi::getVEgoLead(lead_status, lead_v_rel, vEgo, is_metric, speedUnit); + rw += drawBottomDevUIElement(p, rw, y, vEgoLeadElement.value, vEgoLeadElement.label, vEgoLeadElement.units, vEgoLeadElement.color); + + if (torqueLateral && torquedUseParams) { + UiElement frictionCoefficientFilteredElement = DeveloperUi::getFrictionCoefficientFiltered(frictionCoefficientFiltered, liveValid); + rw += drawBottomDevUIElement(p, rw, y, frictionCoefficientFilteredElement.value, frictionCoefficientFilteredElement.label, frictionCoefficientFilteredElement.units, frictionCoefficientFilteredElement.color); + + UiElement latAccelFactorFilteredElement = DeveloperUi::getLatAccelFactorFiltered(latAccelFactorFiltered, liveValid); + rw += drawBottomDevUIElement(p, rw, y, latAccelFactorFilteredElement.value, latAccelFactorFilteredElement.label, latAccelFactorFilteredElement.units, latAccelFactorFilteredElement.color); + } else { + UiElement steeringTorqueEpsElement = DeveloperUi::getSteeringTorqueEps(steeringTorqueEps); + rw += drawBottomDevUIElement(p, rw, y, steeringTorqueEpsElement.value, steeringTorqueEpsElement.label, steeringTorqueEpsElement.units, steeringTorqueEpsElement.color); + + UiElement bearingDegElement = DeveloperUi::getBearingDeg(bearingAccuracyDeg, bearingDeg); + rw += drawBottomDevUIElement(p, rw, y, bearingDegElement.value, bearingDegElement.label, bearingDegElement.units, bearingDegElement.color); + } + + UiElement altitudeElement = DeveloperUi::getAltitude(gpsAccuracy, altitude); + rw += drawBottomDevUIElement(p, rw, y, altitudeElement.value, altitudeElement.label, altitudeElement.units, altitudeElement.color); } diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h index 1e98cd3a52..d869d989df 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.h @@ -7,9 +7,8 @@ #pragma once -#include - #include "selfdrive/ui/qt/onroad/hud.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" class HudRendererSP : public HudRenderer { Q_OBJECT @@ -18,4 +17,40 @@ public: HudRendererSP(); void updateState(const UIState &s) override; void draw(QPainter &p, const QRect &surface_rect) override; + +private: + Params params; + void drawText(QPainter &p, int x, int y, const QString &text, QColor color = QColorConstants::White); + void drawRightDevUI(QPainter &p, int x, int y); + int drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); + int drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); + void drawBottomDevUI(QPainter &p, int x, int y); + + bool lead_status; + float lead_d_rel; + float lead_v_rel; + bool torqueLateral; + float angleSteers; + float desiredCurvature; + float curvature; + float roll; + int memoryUsagePercent; + int devUiInfo; + float gpsAccuracy; + float altitude; + float vEgo; + float aEgo; + float steeringTorqueEps; + float bearingAccuracyDeg; + float bearingDeg; + bool torquedUseParams; + float latAccelFactorFiltered; + float frictionCoefficientFiltered; + bool liveValid; + QString speedUnit; + bool latActive; + bool steerOverride; + bool reversing; + cereal::CarParams::SteerControlType steerControlType; + cereal::CarControl::Actuators::Reader actuators; }; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index b2701356cc..1277195df1 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -18,13 +18,22 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP" + "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", + "carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters", + "carStateSP", "liveParameters" }); // update timer timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIStateSP::update); timer->start(1000 / UI_FREQ); + + // Param watcher for UIScene param updates + param_watcher = new ParamWatcher(this); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + ui_update_params_sp(this); + }); + param_watcher->addParam("DevUIInfo"); } // This method overrides completely the update method from the parent class intentionally. @@ -39,6 +48,11 @@ void UIStateSP::update() { emit uiUpdate(*this); } +void ui_update_params_sp(UIStateSP *s) { + auto params = Params(); + s->scene.dev_ui_info = std::atoi(params.get("DevUIInfo").c_str()); +} + DeviceSP::DeviceSP(QObject *parent) : Device(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); QObject::connect(this, &Device::displayPowerChanged, this, &DeviceSP::handleDisplayPowerChanged); diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h index cf8de1c4bb..393f997cbd 100644 --- a/selfdrive/ui/sunnypilot/ui.h +++ b/selfdrive/ui/sunnypilot/ui.h @@ -13,6 +13,7 @@ #include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h" #include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h" #include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/util.h" class UIStateSP : public UIState { Q_OBJECT @@ -73,6 +74,7 @@ private slots: private: std::vector sunnylinkRoles = {}; std::vector sunnylinkUsers = {}; + ParamWatcher *param_watcher; }; UIStateSP *uiStateSP(); @@ -92,3 +94,5 @@ private: DeviceSP *deviceSP(); inline DeviceSP *device() { return deviceSP(); } + +void ui_update_params_sp(UIStateSP *s); diff --git a/selfdrive/ui/sunnypilot/ui_scene.h b/selfdrive/ui/sunnypilot/ui_scene.h new file mode 100644 index 0000000000..93e0cd6c91 --- /dev/null +++ b/selfdrive/ui/sunnypilot/ui_scene.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +typedef struct UISceneSP : UIScene { + int dev_ui_info = 0; +} UISceneSP; diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index e78b573b66..5b3872b3d4 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -66,6 +66,11 @@ typedef struct UIScene { uint64_t started_frame; } UIScene; +#ifdef SUNNYPILOT +#include "sunnypilot/ui_scene.h" +#define UIScene UISceneSP +#endif + class UIState : public QObject { Q_OBJECT From c9dbf97649a27117be6d5955a49e2d4253337288 Mon Sep 17 00:00:00 2001 From: Jimmy <9859727+Quantizr@users.noreply.github.com> Date: Thu, 11 Sep 2025 20:31:32 -1000 Subject: [PATCH 176/188] jotpluggler: add icons, use monospace font, and fix ui quirks (#36141) * use play/pause icons * use monospace font * x button for delete * add icons for splitting * many scaling + scrollbar fixes and niceties * simplify texture loading code --- tools/jotpluggler/assets/pause.png | 3 +++ tools/jotpluggler/assets/play.png | 3 +++ tools/jotpluggler/assets/split_h.png | 3 +++ tools/jotpluggler/assets/split_v.png | 3 +++ tools/jotpluggler/assets/x.png | 3 +++ tools/jotpluggler/datatree.py | 31 +++++++++++---------------- tools/jotpluggler/layout.py | 32 ++++++++++++++++++---------- tools/jotpluggler/pluggle.py | 25 +++++++++++++--------- 8 files changed, 64 insertions(+), 39 deletions(-) create mode 100644 tools/jotpluggler/assets/pause.png create mode 100644 tools/jotpluggler/assets/play.png create mode 100644 tools/jotpluggler/assets/split_h.png create mode 100644 tools/jotpluggler/assets/split_v.png create mode 100644 tools/jotpluggler/assets/x.png diff --git a/tools/jotpluggler/assets/pause.png b/tools/jotpluggler/assets/pause.png new file mode 100644 index 0000000000..8040099831 --- /dev/null +++ b/tools/jotpluggler/assets/pause.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:3ea96d8193eb9067a5efdc5d88a3099730ecafa40efcd09d7402bb3efd723603 +size 2305 diff --git a/tools/jotpluggler/assets/play.png b/tools/jotpluggler/assets/play.png new file mode 100644 index 0000000000..b1556cf0ab --- /dev/null +++ b/tools/jotpluggler/assets/play.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:53097ac5403b725ff1841dfa186ea770b4bb3714205824bde36ec3c2a0fb5dba +size 2758 diff --git a/tools/jotpluggler/assets/split_h.png b/tools/jotpluggler/assets/split_h.png new file mode 100644 index 0000000000..4fd88806e1 --- /dev/null +++ b/tools/jotpluggler/assets/split_h.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:54dd035ff898d881509fa686c402a61af8ef5fb408b92414722da01f773b0d33 +size 2900 diff --git a/tools/jotpluggler/assets/split_v.png b/tools/jotpluggler/assets/split_v.png new file mode 100644 index 0000000000..752e62a4ae --- /dev/null +++ b/tools/jotpluggler/assets/split_v.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:adbd4e5df1f58694dca9dde46d1d95b4e7471684e42e6bca9f41ea5d346e67c5 +size 3669 diff --git a/tools/jotpluggler/assets/x.png b/tools/jotpluggler/assets/x.png new file mode 100644 index 0000000000..3b2eabd447 --- /dev/null +++ b/tools/jotpluggler/assets/x.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a6d9c90cb0dd906e0b15e1f7f3fd9f0dfad3c3b0b34eeed7a7882768dc5f3961 +size 2053 diff --git a/tools/jotpluggler/datatree.py b/tools/jotpluggler/datatree.py index c18ab61892..3390fed2e1 100644 --- a/tools/jotpluggler/datatree.py +++ b/tools/jotpluggler/datatree.py @@ -34,7 +34,7 @@ class DataTree: self._path_to_node: dict[str, DataTreeNode] = {} # full_path -> node self._expanded_tags: set[str] = set() self._item_handlers: dict[str, str] = {} # ui_tag -> handler_tag - self._avg_char_width = None + self._char_width = None self._queued_search = None self._new_data = False self._ui_lock = threading.RLock() @@ -43,12 +43,13 @@ class DataTree: def create_ui(self, parent_tag: str): with dpg.child_window(parent=parent_tag, border=False, width=-1, height=-1): - dpg.add_text("Available Data") + dpg.add_text("Timeseries List") dpg.add_separator() dpg.add_input_text(tag="search_input", width=-1, hint="Search fields...", callback=self.search_data) dpg.add_separator() - with dpg.group(tag="data_tree_container"): - pass + with dpg.child_window(border=False, width=-1, height=-1): + with dpg.group(tag="data_tree_container"): + pass def _on_data_loaded(self, data: dict): with self._ui_lock: @@ -64,8 +65,9 @@ class DataTree: self._handlers_to_delete.clear() with self._ui_lock: - if self._avg_char_width is None and dpg.is_dearpygui_running(): - self._avg_char_width = self.calculate_avg_char_width(font) + if self._char_width is None: + if size := dpg.get_text_size(" ", font=font): + self._char_width = size[0] if self._new_data: self._process_path_change() @@ -256,10 +258,10 @@ class DataTree: value_tag = f"value_{path}" if not dpg.does_item_exist(value_tag): return - value_column_width = dpg.get_item_rect_size("sidebar_window")[0] // 2 + value_column_width = dpg.get_item_rect_size(f"leaf_{path}")[0] // 2 value = self.data_manager.get_value_at(path, self.playback_manager.current_time_s) if value is not None: - formatted_value = self.format_and_truncate(value, value_column_width, self._avg_char_width) + formatted_value = self.format_and_truncate(value, value_column_width, self._char_width) dpg.set_value(value_tag, formatted_value) else: dpg.set_value(value_tag, "N/A") @@ -305,16 +307,9 @@ class DataTree: yield f"{child_name_lower}/{path}" @staticmethod - def calculate_avg_char_width(font): - sample_text = "abcdefghijklmnopqrstuvwxyz0123456789" - if size := dpg.get_text_size(sample_text, font=font): - return size[0] / len(sample_text) - return None - - @staticmethod - def format_and_truncate(value, available_width: float, avg_char_width: float) -> str: + def format_and_truncate(value, available_width: float, char_width: float) -> str: s = f"{value:.5f}" if np.issubdtype(type(value), np.floating) else str(value) - max_chars = int(available_width / avg_char_width) - 3 + max_chars = int(available_width / char_width) if len(s) > max_chars: - return s[: max(0, max_chars)] + "..." + return s[: max(0, max_chars - 3)] + "..." return s diff --git a/tools/jotpluggler/layout.py b/tools/jotpluggler/layout.py index 0c40116e66..917c156f9f 100644 --- a/tools/jotpluggler/layout.py +++ b/tools/jotpluggler/layout.py @@ -25,29 +25,33 @@ class PlotLayoutManager: if dpg.does_item_exist(self.container_tag): dpg.delete_item(self.container_tag) - with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True): + with dpg.child_window(tag=self.container_tag, parent=parent_tag, border=False, width=-1, height=-1, no_scrollbar=True, no_scroll_with_mouse=True): container_width, container_height = dpg.get_item_rect_size(self.container_tag) self._create_ui_recursive(self.layout, self.container_tag, [], container_width, container_height) def _create_ui_recursive(self, layout: dict, parent_tag: str, path: list[int], width: int, height: int): if layout["type"] == "panel": - self._create_panel_ui(layout, parent_tag, path) + self._create_panel_ui(layout, parent_tag, path, width, height) else: self._create_split_ui(layout, parent_tag, path, width, height) - def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int]): + def _create_panel_ui(self, layout: dict, parent_tag: str, path: list[int], width: int, height:int): panel_tag = self._path_to_tag(path, "panel") panel = layout["panel"] self.active_panels.append(panel) + text_size = int(13 * self.scale) + bar_height = (text_size+24) if width < int(279 * self.scale + 80) else (text_size+8) # adjust height to allow for scrollbar - with dpg.child_window(tag=panel_tag, parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): + with dpg.child_window(parent=parent_tag, border=True, width=-1, height=-1, no_scrollbar=True): with dpg.group(horizontal=True): - dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(panel, "title", v)) - dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale)) - dpg.add_button(label="Clear", callback=lambda: self.clear_panel(panel), width=int(40 * self.scale)) - dpg.add_button(label="Delete", callback=lambda: self.delete_panel(path), width=int(40 * self.scale)) - dpg.add_button(label="Split H", callback=lambda: self.split_panel(path, 0), width=int(40 * self.scale)) - dpg.add_button(label="Split V", callback=lambda: self.split_panel(path, 1), width=int(40 * self.scale)) + with dpg.child_window(tag=panel_tag, width=-(text_size + 16), height=bar_height, horizontal_scrollbar=True, no_scroll_with_mouse=True, border=False): + with dpg.group(horizontal=True): + dpg.add_input_text(default_value=panel.title, width=int(100 * self.scale), callback=lambda s, v: setattr(panel, "title", v)) + dpg.add_combo(items=["Time Series"], default_value="Time Series", width=int(100 * self.scale)) + dpg.add_button(label="Clear", callback=lambda: self.clear_panel(panel), width=int(40 * self.scale)) + dpg.add_image_button(texture_tag="split_h_texture", callback=lambda: self.split_panel(path, 0), width=text_size, height=text_size) + dpg.add_image_button(texture_tag="split_v_texture", callback=lambda: self.split_panel(path, 1), width=text_size, height=text_size) + dpg.add_image_button(texture_tag="x_texture", callback=lambda: self.delete_panel(path), width=text_size, height=text_size) dpg.add_separator() @@ -177,11 +181,17 @@ class PlotLayoutManager: dpg.configure_item(container_tag, **{size_properties[orientation]: pane_sizes[i]}) child_width, child_height = [(pane_sizes[i], available_sizes[1]), (available_sizes[0], pane_sizes[i])][orientation] self._resize_splits_recursive(child_layout, child_path, child_width, child_height) + else: # leaf node/panel - adjust bar height to allow for scrollbar + panel_tag = self._path_to_tag(path, "panel") + if width is not None and width < int(279 * self.scale + 80): # scaled widths of the elements in top bar + fixed 8 padding on left and right of each item + dpg.configure_item(panel_tag, height=(int(13*self.scale) + 24)) + else: + dpg.configure_item(panel_tag, height=(int(13*self.scale) + 8)) def _get_split_geometry(self, layout: dict, available_size: tuple[int, int]) -> tuple[int, int, list[int]]: orientation = layout["orientation"] num_grips = len(layout["children"]) - 1 - usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * self.grip_size)) + usable_size = max(self.min_pane_size, available_size[orientation] - (num_grips * (self.grip_size + 8 * (2-orientation)))) # approximate, scaling is weird pane_sizes = [max(self.min_pane_size, int(usable_size * prop)) for prop in layout["proportions"]] return orientation, usable_size, pane_sizes diff --git a/tools/jotpluggler/pluggle.py b/tools/jotpluggler/pluggle.py index 9868b998ed..582a44454e 100755 --- a/tools/jotpluggler/pluggle.py +++ b/tools/jotpluggler/pluggle.py @@ -73,9 +73,10 @@ class PlaybackManager: if not self.is_playing and self.current_time_s >= self.duration_s: self.seek(0.0) self.is_playing = not self.is_playing + texture_tag = "pause_texture" if self.is_playing else "play_texture" + dpg.configure_item("play_pause_button", texture_tag=texture_tag) def seek(self, time_s: float): - self.is_playing = False self.current_time_s = max(0.0, min(time_s, self.duration_s)) def update_time(self, delta_t: float): @@ -83,6 +84,7 @@ class PlaybackManager: self.current_time_s = min(self.current_time_s + delta_t, self.duration_s) if self.current_time_s >= self.duration_s: self.is_playing = False + dpg.configure_item("play_pause_button", texture_tag="play_texture") return self.current_time_s @@ -109,7 +111,6 @@ class MainController: dpg.add_theme_style(dpg.mvPlotStyleVar_LineWeight, scaled_thickness, category=dpg.mvThemeCat_Plots) dpg.add_theme_color(dpg.mvPlotCol_Line, (255, 0, 0, 128), category=dpg.mvThemeCat_Plots) - def on_data_loaded(self, data: dict): duration = data.get('duration', 0.0) self.playback_manager.set_route_duration(duration) @@ -121,7 +122,7 @@ class MainController: dpg.set_value("load_status", "Loading...") dpg.set_value("timeline_slider", 0.0) dpg.configure_item("timeline_slider", max_value=0.0) - dpg.configure_item("play_pause_button", label="Play") + dpg.configure_item("play_pause_button", texture_tag="play_texture") dpg.configure_item("load_button", enabled=True) elif data.get('loading_complete'): num_paths = len(self.data_manager.get_all_paths()) @@ -134,6 +135,12 @@ class MainController: dpg.configure_item("timeline_slider", max_value=duration) def setup_ui(self): + with dpg.texture_registry(): + script_dir = os.path.dirname(os.path.realpath(__file__)) + for image in ["play", "pause", "x", "split_h", "split_v"]: + texture = dpg.load_image(os.path.join(script_dir, "assets", f"{image}.png")) + dpg.add_static_texture(width=texture[0], height=texture[1], default_value=texture[3], tag=f"{image}_texture") + with dpg.window(tag="Primary Window"): with dpg.group(horizontal=True): # Left panel - Data tree @@ -147,16 +154,17 @@ class MainController: # Right panel - Plots and timeline with dpg.group(tag="right_panel"): - with dpg.child_window(label="Plot Window", border=True, height=-(30 + 13 * self.scale), tag="main_plot_area"): + with dpg.child_window(label="Plot Window", border=True, height=-(32 + 13 * self.scale), tag="main_plot_area"): self.plot_layout_manager.create_ui("main_plot_area") with dpg.child_window(label="Timeline", border=True): with dpg.table(header_row=False, borders_innerH=False, borders_innerV=False, borders_outerH=False, borders_outerV=False): - dpg.add_table_column(width_fixed=True, init_width_or_weight=int(50 * self.scale)) # Play button + btn_size = int(13 * self.scale) + dpg.add_table_column(width_fixed=True, init_width_or_weight=(btn_size + 8)) # Play button dpg.add_table_column(width_stretch=True) # Timeline slider dpg.add_table_column(width_fixed=True, init_width_or_weight=int(50 * self.scale)) # FPS counter with dpg.table_row(): - dpg.add_button(label="Play", tag="play_pause_button", callback=self.toggle_play_pause, width=int(50 * self.scale)) + dpg.add_image_button(texture_tag="play_texture", tag="play_pause_button", callback=self.toggle_play_pause, width=btn_size, height=btn_size) dpg.add_slider_float(tag="timeline_slider", default_value=0.0, label="", width=-1, callback=self.timeline_drag) dpg.add_text("", tag="fps_counter") with dpg.item_handler_registry(tag="plot_resize_handler"): @@ -177,12 +185,9 @@ class MainController: def toggle_play_pause(self, sender): self.playback_manager.toggle_play_pause() - label = "Pause" if self.playback_manager.is_playing else "Play" - dpg.configure_item(sender, label=label) def timeline_drag(self, sender, app_data): self.playback_manager.seek(app_data) - dpg.configure_item("play_pause_button", label="Play") def update_frame(self, font): self.data_tree.update_frame(font) @@ -210,7 +215,7 @@ def main(route_to_load=None): scale = 1 with dpg.font_registry(): - default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/Inter-Regular.ttf"), int(13 * scale)) + default_font = dpg.add_font(os.path.join(BASEDIR, "selfdrive/assets/fonts/JetBrainsMono-Medium.ttf"), int(13 * scale)) dpg.bind_font(default_font) viewport_width, viewport_height = int(1200 * scale), int(800 * scale) From 810a2d9448df89ef84ecc1c794b63fad55350d69 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Fri, 12 Sep 2025 09:03:17 +0200 Subject: [PATCH 177/188] Revert & Reapply "UI: Developer UI" temporarily due to QT version mismatch (#1237) * Revert "UI: Developer UI (#1233)" This reverts commit 1bb4ca2547448f2189f67cb54db150e78bc4d487. * Reapply "UI: Developer UI (#1233)" This reverts commit b0a77049dacbdd59bc21d154820ba8cadf4ad201. * QColorConstants is not on device's QT version. Thanks @kumar for the fix --- selfdrive/ui/sunnypilot/qt/onroad/hud.cc | 2 +- selfdrive/ui/sunnypilot/qt/onroad/hud.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc index 15722cc9f8..9ead933d04 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc @@ -171,7 +171,7 @@ int HudRendererSP::drawBottomDevUIElement(QPainter &p, int x, int y, const QStri real_rect3.moveTop(real_rect.top()); real_rect3.moveLeft(real_rect2.right() + 10); - p.setPen(QColorConstants::White); + p.setPen(Qt::white); p.drawText(real_rect, Qt::AlignLeft | Qt::AlignVCenter, label); p.setPen(color); diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h index d869d989df..968789bc15 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.h @@ -20,7 +20,7 @@ public: private: Params params; - void drawText(QPainter &p, int x, int y, const QString &text, QColor color = QColorConstants::White); + void drawText(QPainter &p, int x, int y, const QString &text, QColor color = Qt::white); void drawRightDevUI(QPainter &p, int x, int y); int drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); int drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); From 1be13fdc55a24a72bc33703c7343c9421fdd0842 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Fri, 12 Sep 2025 08:37:24 -0400 Subject: [PATCH 178/188] Revert "UI: Developer UI" (#1238) * Revert "Revert & Reapply "UI: Developer UI" temporarily due to QT version mismatch (#1237)" This reverts commit 810a2d9448df89ef84ecc1c794b63fad55350d69. * Revert "UI: Developer UI (#1233)" This reverts commit 1bb4ca2547448f2189f67cb54db150e78bc4d487. --- common/params_keys.h | 1 - selfdrive/ui/qt/onroad/alerts.cc | 9 - selfdrive/ui/qt/onroad/annotated_camera.h | 1 - selfdrive/ui/qt/onroad/driver_monitoring.cc | 5 - selfdrive/ui/sunnypilot/SConscript | 1 - .../qt/offroad/settings/visuals_panel.cc | 12 - .../qt/offroad/settings/visuals_panel.h | 1 - .../sunnypilot/qt/onroad/annotated_camera.cc | 5 - .../sunnypilot/qt/onroad/annotated_camera.h | 3 - .../qt/onroad/developer_ui/developer_ui.cc | 227 ------------------ .../qt/onroad/developer_ui/developer_ui.h | 31 --- .../qt/onroad/developer_ui/ui_elements.h | 19 -- selfdrive/ui/sunnypilot/qt/onroad/hud.cc | 190 --------------- selfdrive/ui/sunnypilot/qt/onroad/hud.h | 39 +-- selfdrive/ui/sunnypilot/ui.cc | 16 +- selfdrive/ui/sunnypilot/ui.h | 4 - selfdrive/ui/sunnypilot/ui_scene.h | 12 - selfdrive/ui/ui.h | 5 - 18 files changed, 3 insertions(+), 578 deletions(-) delete mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc delete mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h delete mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h delete mode 100644 selfdrive/ui/sunnypilot/ui_scene.h diff --git a/common/params_keys.h b/common/params_keys.h index fc7842720b..afb6b348eb 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -146,7 +146,6 @@ inline static std::unordered_map keys = { {"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}}, {"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}}, {"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}}, - {"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}}, {"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}}, {"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}}, {"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}}, diff --git a/selfdrive/ui/qt/onroad/alerts.cc b/selfdrive/ui/qt/onroad/alerts.cc index 2e8f3612eb..d6829c6b08 100644 --- a/selfdrive/ui/qt/onroad/alerts.cc +++ b/selfdrive/ui/qt/onroad/alerts.cc @@ -4,9 +4,6 @@ #include #include "selfdrive/ui/qt/util.h" -#ifdef SUNNYPILOT -#include "selfdrive/ui/sunnypilot/ui.h" -#endif void OnroadAlerts::updateState(const UIState &s) { Alert a = getAlert(*(s.sm), s.scene.started_frame); @@ -76,12 +73,6 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) { } QRect r = QRect(0 + margin, height() - h + margin, width() - margin*2, h - margin*2); -#ifdef SUNNYPILOT - const int dev_ui_info = uiStateSP()->scene.dev_ui_info; - const int adjustment = dev_ui_info > 1 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 30 : 0; - r = QRect(0 + margin, height() - h + margin - adjustment, width() - margin*2, h - margin*2); -#endif - QPainter p(this); // draw background + gradient diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h index 5d9d21ab6b..e3ca837907 100644 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/qt/onroad/annotated_camera.h @@ -12,7 +12,6 @@ #include "selfdrive/ui/sunnypilot/qt/onroad/model.h" #define ExperimentalButton ExperimentalButtonSP #define ModelRenderer ModelRendererSP -#define HudRenderer HudRendererSP #else #include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/hud.h" diff --git a/selfdrive/ui/qt/onroad/driver_monitoring.cc b/selfdrive/ui/qt/onroad/driver_monitoring.cc index e67c483047..49f2c950b4 100644 --- a/selfdrive/ui/qt/onroad/driver_monitoring.cc +++ b/selfdrive/ui/qt/onroad/driver_monitoring.cc @@ -73,11 +73,6 @@ void DriverMonitorRenderer::draw(QPainter &painter, const QRect &surface_rect) { float y = surface_rect.height() - offset; float opacity = is_active ? 0.65f : 0.2f; -#ifdef SUNNYPILOT - const int dev_ui_info = uiStateSP()->scene.dev_ui_info; - y -= dev_ui_info > 1 ? 50 : 0; -#endif - drawIcon(painter, QPoint(x, y), dm_img, QColor(0, 0, 0, 70), opacity); QPointF keypoints[std::size(DEFAULT_FACE_KPTS_3D)]; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 807bf02478..2f3c8ddd8d 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -39,7 +39,6 @@ qt_src = [ "sunnypilot/qt/offroad/settings/visuals_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", "sunnypilot/qt/onroad/buttons.cc", - "sunnypilot/qt/onroad/developer_ui/developer_ui.cc", "sunnypilot/qt/onroad/hud.cc", "sunnypilot/qt/onroad/model.cc", "sunnypilot/qt/onroad/onroad_home.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index c3aaf12d22..dd2f05416d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -72,15 +72,6 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { list->addItem(chevron_info_settings); param_watcher->addParam("ChevronInfo"); - // Visuals: Developer UI Info (Dev UI) - std::vector dev_ui_settings_texts{tr("Off"), tr("Right"), tr("Right &&\nBottom")}; - dev_ui_settings = new ButtonParamControlSP( - "DevUIInfo", tr("Developer UI"), tr("Display real-time parameters and metrics from various sources."), - "", - dev_ui_settings_texts, - 380); - list->addItem(dev_ui_settings); - sunnypilotScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnypilotScroller); @@ -99,7 +90,4 @@ void VisualsPanel::paramsRefresh() { if (chevron_info_settings) { chevron_info_settings->refresh(); } - if (dev_ui_settings) { - dev_ui_settings->refresh(); - } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h index 30ff31c301..f342662c22 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -28,5 +28,4 @@ protected: std::map toggles; ParamWatcher * param_watcher; ButtonParamControlSP *chevron_info_settings; - ButtonParamControlSP *dev_ui_settings; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc index 1d5567161a..3721a3d198 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc @@ -14,8 +14,3 @@ AnnotatedCameraWidgetSP::AnnotatedCameraWidgetSP(VisionStreamType type, QWidget void AnnotatedCameraWidgetSP::updateState(const UIState &s) { AnnotatedCameraWidget::updateState(s); } - -void AnnotatedCameraWidgetSP::showEvent(QShowEvent *event) { - AnnotatedCameraWidget::showEvent(event); - ui_update_params_sp(uiState()); -} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h index 8c0a385657..46ce7d4be3 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h @@ -15,7 +15,4 @@ class AnnotatedCameraWidgetSP : public AnnotatedCameraWidget { public: explicit AnnotatedCameraWidgetSP(VisionStreamType type, QWidget *parent = nullptr); void updateState(const UIState &s) override; - -protected: - void showEvent(QShowEvent *event) override; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc deleted file mode 100644 index 292ba6f7bb..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc +++ /dev/null @@ -1,227 +0,0 @@ -/** - * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - * - * This file is part of sunnypilot and is licensed under the MIT License. - * See the LICENSE.md file in the root directory for more details. - */ -#include - -#include "common/util.h" -#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" - - -// Add Relative Distance to Primary Lead Car -// Unit: Meters -UiElement DeveloperUi::getDRel(bool lead_status, float lead_d_rel) { - QString value = lead_status ? QString::number(lead_d_rel, 'f', 0) : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lead_status) { - // Orange if close, Red if very close - if (lead_d_rel < 5) { - color = QColor(255, 0, 0, 255); - } else if (lead_d_rel < 15) { - color = QColor(255, 188, 0, 255); - } - } - - return UiElement(value, "REL DIST", "m", color); -} - -// Add Relative Velocity vs Primary Lead Car -// Unit: kph if metric, else mph -UiElement DeveloperUi::getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit) { - QString value = lead_status ? QString::number(lead_v_rel * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lead_status) { - // Red if approaching faster than 10mph - // Orange if approaching (negative) - if (lead_v_rel < -4.4704) { - color = QColor(255, 0, 0, 255); - } else if (lead_v_rel < 0) { - color = QColor(255, 188, 0, 255); - } - } - - return UiElement(value, "REL SPEED", speed_unit, color); -} - -// Add Real Steering Angle -// Unit: Degrees -UiElement DeveloperUi::getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override) { - QString value = QString("%1%2%3").arg(QString::number(angle_steers, 'f', 1)).arg("°").arg(""); - QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); - - // Red if large steering angle - // Orange if moderate steering angle - if (std::fabs(angle_steers) > 180) { - color = QColor(255, 0, 0, 255); - } else if (std::fabs(angle_steers) > 90) { - color = QColor(255, 188, 0, 255); - } - - return UiElement(value, "REAL STEER", "", color); -} - -// Add Actual Lateral Acceleration (roll compensated) when using Torque -// Unit: m/s² -UiElement DeveloperUi::getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override) { - double actualLateralAccel = (curvature * pow(v_ego, 2)) - (roll * 9.81); - - QString value = QString::number(actualLateralAccel, 'f', 2); - QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); - - return UiElement(value, "ACTUAL L.A.", "m/s²", color); -} - -// Add Desired Steering Angle when using PID -// Unit: Degrees -UiElement DeveloperUi::getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers) { - QString value = lat_active ? QString("%1%2%3").arg(QString::number(steer_angle_desired, 'f', 1)).arg("°").arg("") : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lat_active) { - // Red if large steering angle - // Orange if moderate steering angle - if (std::fabs(angle_steers) > 180) { - color = QColor(255, 0, 0, 255); - } else if (std::fabs(angle_steers) > 90) { - color = QColor(255, 188, 0, 255); - } else { - color = QColor(0, 255, 0, 255); - } - } - - return UiElement(value, "DESIRED STEER", "", color); -} - -// Add Device Memory (RAM) Usage -// Unit: Percent -UiElement DeveloperUi::getMemoryUsagePercent(int memory_usage_percent) { - QString value = QString("%1%2").arg(QString::number(memory_usage_percent, 'd', 0)).arg("%"); - QColor color = (memory_usage_percent > 85) ? QColor(255, 188, 0, 255) : QColor(255, 255, 255, 255); - - return UiElement(value, "RAM", "", color); -} - -// Add Vehicle Current Acceleration -// Unit: m/s² -UiElement DeveloperUi::getAEgo(float a_ego) { - QString value = QString::number(a_ego, 'f', 1); - QColor color = QColor(255, 255, 255, 255); - - return UiElement(value, "ACC.", "m/s²", color); -} - -// Add Relative Velocity to Primary Lead Car -// Unit: kph if metric, else mph -UiElement DeveloperUi::getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit) { - QString value = lead_status ? QString::number((lead_v_rel + v_ego) * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; - QColor color = QColor(255, 255, 255, 255); - - if (lead_status) { - // Red if approaching faster than 10mph - // Orange if approaching (negative) - if (lead_v_rel < -4.4704) { - color = QColor(255, 0, 0, 255); - } else if (lead_v_rel < 0) { - color = QColor(255, 188, 0, 255); - } - } - - return UiElement(value, "L.S.", speed_unit, color); -} - -// Add Friction Coefficient Raw from torqued -// Unit: None -UiElement DeveloperUi::getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid) { - QString value = QString::number(friction_coefficient_filtered, 'f', 3); - QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); - - return UiElement(value, "FRIC.", "", color); -} - -// Add Lateral Acceleration Factor Raw from torqued -// Unit: m/s² -UiElement DeveloperUi::getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid) { - QString value = QString::number(lat_accel_factor_filtered, 'f', 3); - QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); - - return UiElement(value, "L.A.", "m/s²", color); -} - -// Add Steering Torque from Car EPS -// Unit: Newton Meters -UiElement DeveloperUi::getSteeringTorqueEps(float steering_torque_eps) { - QString value = QString::number(std::fabs(steering_torque_eps), 'f', 1); - QColor color = QColor(255, 255, 255, 255); - - return UiElement(value, "E.T.", "N·dm", color); -} - -// Add Bearing Degree and Direction from Car (Compass) -// Unit: Meters -UiElement DeveloperUi::getBearingDeg(float bearing_accuracy_deg, float bearing_deg) { - QString value = (bearing_accuracy_deg != 180.00) ? QString("%1%2%3").arg(QString::number(bearing_deg, 'd', 0)).arg("°").arg("") : "-"; - QColor color = QColor(255, 255, 255, 255); - QString dir_value; - - if (bearing_accuracy_deg != 180.00) { - if (((bearing_deg >= 337.5) && (bearing_deg <= 360)) || ((bearing_deg >= 0) && (bearing_deg <= 22.5))) { - dir_value = "N"; - } else if ((bearing_deg > 22.5) && (bearing_deg < 67.5)) { - dir_value = "NE"; - } else if ((bearing_deg >= 67.5) && (bearing_deg <= 112.5)) { - dir_value = "E"; - } else if ((bearing_deg > 112.5) && (bearing_deg < 157.5)) { - dir_value = "SE"; - } else if ((bearing_deg >= 157.5) && (bearing_deg <= 202.5)) { - dir_value = "S"; - } else if ((bearing_deg > 202.5) && (bearing_deg < 247.5)) { - dir_value = "SW"; - } else if ((bearing_deg >= 247.5) && (bearing_deg <= 292.5)) { - dir_value = "W"; - } else if ((bearing_deg > 292.5) && (bearing_deg < 337.5)) { - dir_value = "NW"; - } - } else { - dir_value = "OFF"; - } - - return UiElement(QString("%1 | %2").arg(dir_value).arg(value), "B.D.", "", color); -} - -// Add Altitude of Current Location -// Unit: Meters -UiElement DeveloperUi::getAltitude(float gps_accuracy, float altitude) { - QString value = (gps_accuracy != 0.00) ? QString::number(altitude, 'f', 1) : "-"; - QColor color = QColor(255, 255, 255, 255); - - return UiElement(value, "ALT.", "m", color); -} - -// Add Actuators Output -// Unit: Degree (angle) or m/s² (torque) -UiElement DeveloperUi::getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, - cereal::CarControl::Actuators::Reader &actuators, - float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override) { - QString label; - QString value; - QString unit; - - if (steerControlType == cereal::CarParams::SteerControlType::ANGLE) { - label = "DESIRED STEER"; - value = QString("%1%2%3").arg(QString::number(actuators.getSteeringAngleDeg(), 'f', 1)).arg("°").arg(""); - } else { - label = "DESIRED L.A."; - double desiredLateralAccel = (desiredCurvature * pow(v_ego, 2)) - (roll * 9.81); - value = QString::number(desiredLateralAccel, 'f', 2); - unit = "m/s²"; - } - - value = lat_active ? value : "-"; - QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); - - return UiElement(value, label, unit, color); -} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h deleted file mode 100644 index 0c5c472209..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - * - * This file is part of sunnypilot and is licensed under the MIT License. - * See the LICENSE.md file in the root directory for more details. - */ -#pragma once - -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h" - -class DeveloperUi { - -public: - static UiElement getDRel(bool lead_status, float lead_d_rel); - static UiElement getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit); - static UiElement getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override); - static UiElement getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override); - static UiElement getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers); - static UiElement getMemoryUsagePercent(int memory_usage_percent); - static UiElement getAEgo(float a_ego); - static UiElement getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit); - static UiElement getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid); - static UiElement getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid); - static UiElement getSteeringTorqueEps(float steering_torque_eps); - static UiElement getBearingDeg(float bearing_accuracy_deg, float bearing_deg); - static UiElement getAltitude(float gps_accuracy, float altitude); - static UiElement getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, - cereal::CarControl::Actuators::Reader &actuators, - float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override); -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h deleted file mode 100644 index 3711e5ac05..0000000000 --- a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - * - * This file is part of sunnypilot and is licensed under the MIT License. - * See the LICENSE.md file in the root directory for more details. - */ -#pragma once - -#include - -struct UiElement { - QString value{}; - QString label{}; - QString units{}; - QColor color{}; - - explicit UiElement(const QString &value = "", const QString &label = "", const QString &units = "", const QColor &color = QColor(255, 255, 255, 255)) - : value(value), label(label), units(units), color(color) {} -}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc index 9ead933d04..233ca59f98 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc @@ -7,202 +7,12 @@ #include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" -#include "selfdrive/ui/qt/util.h" - - HudRendererSP::HudRendererSP() {} void HudRendererSP::updateState(const UIState &s) { HudRenderer::updateState(s); - - const SubMaster &sm = *(s.sm); - const bool cs_alive = sm.alive("controlsState"); - const auto cs = sm["controlsState"].getControlsState(); - const auto car_state = sm["carState"].getCarState(); - const auto car_control = sm["carControl"].getCarControl(); - const auto radar_state = sm["radarState"].getRadarState(); - const auto is_gps_location_external = sm.rcv_frame("gpsLocationExternal") > 1; - const auto gpsLocation = is_gps_location_external ? sm["gpsLocationExternal"].getGpsLocationExternal() : sm["gpsLocation"].getGpsLocation(); - const auto ltp = sm["liveTorqueParameters"].getLiveTorqueParameters(); - const auto car_params = sm["carParams"].getCarParams(); - - static int reverse_delay = 0; - bool reverse_allowed = false; - if (int(car_state.getGearShifter()) != 4) { - reverse_delay = 0; - reverse_allowed = false; - } else { - reverse_delay += 50; - if (reverse_delay >= 1000) { - reverse_allowed = true; - } - } - - reversing = reverse_allowed; - is_metric = s.scene.is_metric; - - // Handle older routes where vEgoCluster is not set - v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0; - float v_ego = v_ego_cluster_seen ? car_state.getVEgoCluster() : car_state.getVEgo(); - speed = cs_alive ? std::max(0.0, v_ego) : 0.0; - speed *= is_metric ? MS_TO_KPH : MS_TO_MPH; - - latActive = car_control.getLatActive(); - steerOverride = car_state.getSteeringPressed(); - - devUiInfo = s.scene.dev_ui_info; - - speedUnit = is_metric ? tr("km/h") : tr("mph"); - lead_d_rel = radar_state.getLeadOne().getDRel(); - lead_v_rel = radar_state.getLeadOne().getVRel(); - lead_status = radar_state.getLeadOne().getStatus(); - steerControlType = car_params.getSteerControlType(); - actuators = car_control.getActuators(); - torqueLateral = steerControlType == cereal::CarParams::SteerControlType::TORQUE; - angleSteers = car_state.getSteeringAngleDeg(); - desiredCurvature = cs.getDesiredCurvature(); - curvature = cs.getCurvature(); - roll = sm["liveParameters"].getLiveParameters().getRoll(); - memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent(); - gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not. - altitude = gpsLocation.getAltitude(); - vEgo = car_state.getVEgo(); - aEgo = car_state.getAEgo(); - steeringTorqueEps = car_state.getSteeringTorqueEps(); - bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg(); - bearingDeg = gpsLocation.getBearingDeg(); - torquedUseParams = ltp.getUseParams(); - latAccelFactorFiltered = ltp.getLatAccelFactorFiltered(); - frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered(); - liveValid = ltp.getLiveValid(); } void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { HudRenderer::draw(p, surface_rect); - if (!reversing) { - // Bottom Dev UI - if (devUiInfo == 2) { - QRect rect_bottom(surface_rect.left(), surface_rect.bottom() - 60, surface_rect.width(), 61); - p.setPen(Qt::NoPen); - p.setBrush(QColor(0, 0, 0, 100)); - p.drawRect(rect_bottom); - drawBottomDevUI(p, rect_bottom.left(), rect_bottom.center().y()); - } - - // Right Dev UI - if (devUiInfo != 0) { - QRect rect_right(surface_rect.right() - (UI_BORDER_SIZE * 2), UI_BORDER_SIZE * 1.5, 184, 170); - drawRightDevUI(p, surface_rect.right() - 184 - UI_BORDER_SIZE * 2, UI_BORDER_SIZE * 2 + rect_right.height()); - } - } -} - -void HudRendererSP::drawText(QPainter &p, int x, int y, const QString &text, QColor color) { - QRect real_rect = p.fontMetrics().boundingRect(text); - real_rect.moveCenter({x, y - real_rect.height() / 2}); - p.setPen(color); - p.drawText(real_rect.x(), real_rect.bottom(), text); -} - -int HudRendererSP::drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { - - p.setFont(InterFont(28, QFont::Bold)); - x += 92; - y += 80; - drawText(p, x, y, label); - - p.setFont(InterFont(30 * 2, QFont::Bold)); - y += 65; - drawText(p, x, y, value, color); - - p.setFont(InterFont(28, QFont::Bold)); - - if (units.length() > 0) { - p.save(); - x += 120; - y -= 25; - p.translate(x, y); - p.rotate(-90); - drawText(p, 0, 0, units); - p.restore(); - } - - return 130; -} - -void HudRendererSP::drawRightDevUI(QPainter &p, int x, int y) { - int rh = 5; - int ry = y; - - UiElement dRelElement = DeveloperUi::getDRel(lead_status, lead_d_rel); - rh += drawRightDevUIElement(p, x, ry, dRelElement.value, dRelElement.label, dRelElement.units, dRelElement.color); - ry = y + rh; - - UiElement vRelElement = DeveloperUi::getVRel(lead_status, lead_v_rel, is_metric, speedUnit); - rh += drawRightDevUIElement(p, x, ry, vRelElement.value, vRelElement.label, vRelElement.units, vRelElement.color); - ry = y + rh; - - UiElement steeringAngleDegElement = DeveloperUi::getSteeringAngleDeg(angleSteers, latActive, steerOverride); - rh += drawRightDevUIElement(p, x, ry, steeringAngleDegElement.value, steeringAngleDegElement.label, steeringAngleDegElement.units, steeringAngleDegElement.color); - ry = y + rh; - - UiElement actuatorsOutputLateralElement = DeveloperUi::getActuatorsOutputLateral(steerControlType, actuators, desiredCurvature, vEgo, roll, latActive, steerOverride); - rh += drawRightDevUIElement(p, x, ry, actuatorsOutputLateralElement.value, actuatorsOutputLateralElement.label, actuatorsOutputLateralElement.units, actuatorsOutputLateralElement.color); - ry = y + rh; - - UiElement actualLateralAccelElement = DeveloperUi::getActualLateralAccel(curvature, vEgo, roll, latActive, steerOverride); - rh += drawRightDevUIElement(p, x, ry, actualLateralAccelElement.value, actualLateralAccelElement.label, actualLateralAccelElement.units, actualLateralAccelElement.color); -} - -int HudRendererSP::drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { - p.setFont(InterFont(38, QFont::Bold)); - QFontMetrics fm(p.font()); - QRect init_rect = fm.boundingRect(label + " "); - QRect real_rect = fm.boundingRect(init_rect, 0, label + " "); - real_rect.moveCenter({x, y}); - - QRect init_rect2 = fm.boundingRect(value); - QRect real_rect2 = fm.boundingRect(init_rect2, 0, value); - real_rect2.moveTop(real_rect.top()); - real_rect2.moveLeft(real_rect.right() + 10); - - QRect init_rect3 = fm.boundingRect(units); - QRect real_rect3 = fm.boundingRect(init_rect3, 0, units); - real_rect3.moveTop(real_rect.top()); - real_rect3.moveLeft(real_rect2.right() + 10); - - p.setPen(Qt::white); - p.drawText(real_rect, Qt::AlignLeft | Qt::AlignVCenter, label); - - p.setPen(color); - p.drawText(real_rect2, Qt::AlignRight | Qt::AlignVCenter, value); - p.drawText(real_rect3, Qt::AlignLeft | Qt::AlignVCenter, units); - return 430; -} - -void HudRendererSP::drawBottomDevUI(QPainter &p, int x, int y) { - int rw = 90; - - UiElement aEgoElement = DeveloperUi::getAEgo(aEgo); - rw += drawBottomDevUIElement(p, rw, y, aEgoElement.value, aEgoElement.label, aEgoElement.units, aEgoElement.color); - - UiElement vEgoLeadElement = DeveloperUi::getVEgoLead(lead_status, lead_v_rel, vEgo, is_metric, speedUnit); - rw += drawBottomDevUIElement(p, rw, y, vEgoLeadElement.value, vEgoLeadElement.label, vEgoLeadElement.units, vEgoLeadElement.color); - - if (torqueLateral && torquedUseParams) { - UiElement frictionCoefficientFilteredElement = DeveloperUi::getFrictionCoefficientFiltered(frictionCoefficientFiltered, liveValid); - rw += drawBottomDevUIElement(p, rw, y, frictionCoefficientFilteredElement.value, frictionCoefficientFilteredElement.label, frictionCoefficientFilteredElement.units, frictionCoefficientFilteredElement.color); - - UiElement latAccelFactorFilteredElement = DeveloperUi::getLatAccelFactorFiltered(latAccelFactorFiltered, liveValid); - rw += drawBottomDevUIElement(p, rw, y, latAccelFactorFilteredElement.value, latAccelFactorFilteredElement.label, latAccelFactorFilteredElement.units, latAccelFactorFilteredElement.color); - } else { - UiElement steeringTorqueEpsElement = DeveloperUi::getSteeringTorqueEps(steeringTorqueEps); - rw += drawBottomDevUIElement(p, rw, y, steeringTorqueEpsElement.value, steeringTorqueEpsElement.label, steeringTorqueEpsElement.units, steeringTorqueEpsElement.color); - - UiElement bearingDegElement = DeveloperUi::getBearingDeg(bearingAccuracyDeg, bearingDeg); - rw += drawBottomDevUIElement(p, rw, y, bearingDegElement.value, bearingDegElement.label, bearingDegElement.units, bearingDegElement.color); - } - - UiElement altitudeElement = DeveloperUi::getAltitude(gpsAccuracy, altitude); - rw += drawBottomDevUIElement(p, rw, y, altitudeElement.value, altitudeElement.label, altitudeElement.units, altitudeElement.color); } diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h index 968789bc15..1e98cd3a52 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.h @@ -7,8 +7,9 @@ #pragma once +#include + #include "selfdrive/ui/qt/onroad/hud.h" -#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" class HudRendererSP : public HudRenderer { Q_OBJECT @@ -17,40 +18,4 @@ public: HudRendererSP(); void updateState(const UIState &s) override; void draw(QPainter &p, const QRect &surface_rect) override; - -private: - Params params; - void drawText(QPainter &p, int x, int y, const QString &text, QColor color = Qt::white); - void drawRightDevUI(QPainter &p, int x, int y); - int drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); - int drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); - void drawBottomDevUI(QPainter &p, int x, int y); - - bool lead_status; - float lead_d_rel; - float lead_v_rel; - bool torqueLateral; - float angleSteers; - float desiredCurvature; - float curvature; - float roll; - int memoryUsagePercent; - int devUiInfo; - float gpsAccuracy; - float altitude; - float vEgo; - float aEgo; - float steeringTorqueEps; - float bearingAccuracyDeg; - float bearingDeg; - bool torquedUseParams; - float latAccelFactorFiltered; - float frictionCoefficientFiltered; - bool liveValid; - QString speedUnit; - bool latActive; - bool steerOverride; - bool reversing; - cereal::CarParams::SteerControlType steerControlType; - cereal::CarControl::Actuators::Reader actuators; }; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index 1277195df1..b2701356cc 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -18,22 +18,13 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", - "carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters", - "carStateSP", "liveParameters" + "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP" }); // update timer timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIStateSP::update); timer->start(1000 / UI_FREQ); - - // Param watcher for UIScene param updates - param_watcher = new ParamWatcher(this); - connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { - ui_update_params_sp(this); - }); - param_watcher->addParam("DevUIInfo"); } // This method overrides completely the update method from the parent class intentionally. @@ -48,11 +39,6 @@ void UIStateSP::update() { emit uiUpdate(*this); } -void ui_update_params_sp(UIStateSP *s) { - auto params = Params(); - s->scene.dev_ui_info = std::atoi(params.get("DevUIInfo").c_str()); -} - DeviceSP::DeviceSP(QObject *parent) : Device(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); QObject::connect(this, &Device::displayPowerChanged, this, &DeviceSP::handleDisplayPowerChanged); diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h index 393f997cbd..cf8de1c4bb 100644 --- a/selfdrive/ui/sunnypilot/ui.h +++ b/selfdrive/ui/sunnypilot/ui.h @@ -13,7 +13,6 @@ #include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h" #include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h" #include "selfdrive/ui/ui.h" -#include "selfdrive/ui/qt/util.h" class UIStateSP : public UIState { Q_OBJECT @@ -74,7 +73,6 @@ private slots: private: std::vector sunnylinkRoles = {}; std::vector sunnylinkUsers = {}; - ParamWatcher *param_watcher; }; UIStateSP *uiStateSP(); @@ -94,5 +92,3 @@ private: DeviceSP *deviceSP(); inline DeviceSP *device() { return deviceSP(); } - -void ui_update_params_sp(UIStateSP *s); diff --git a/selfdrive/ui/sunnypilot/ui_scene.h b/selfdrive/ui/sunnypilot/ui_scene.h deleted file mode 100644 index 93e0cd6c91..0000000000 --- a/selfdrive/ui/sunnypilot/ui_scene.h +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. - * - * This file is part of sunnypilot and is licensed under the MIT License. - * See the LICENSE.md file in the root directory for more details. - */ - -#pragma once - -typedef struct UISceneSP : UIScene { - int dev_ui_info = 0; -} UISceneSP; diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 5b3872b3d4..e78b573b66 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -66,11 +66,6 @@ typedef struct UIScene { uint64_t started_frame; } UIScene; -#ifdef SUNNYPILOT -#include "sunnypilot/ui_scene.h" -#define UIScene UISceneSP -#endif - class UIState : public QObject { Q_OBJECT From 4f44d6e6436041a4f09a4d5c530405fc019c2492 Mon Sep 17 00:00:00 2001 From: Nayan Date: Fri, 12 Sep 2025 10:58:59 -0400 Subject: [PATCH 179/188] Reapply "UI: Developer UI" (#1238) (#1239) This reverts commit 1be13fdc55a24a72bc33703c7343c9421fdd0842. Co-authored-by: Jason Wen --- common/params_keys.h | 1 + selfdrive/ui/qt/onroad/alerts.cc | 9 + selfdrive/ui/qt/onroad/annotated_camera.h | 1 + selfdrive/ui/qt/onroad/driver_monitoring.cc | 5 + selfdrive/ui/sunnypilot/SConscript | 1 + .../qt/offroad/settings/visuals_panel.cc | 12 + .../qt/offroad/settings/visuals_panel.h | 1 + .../sunnypilot/qt/onroad/annotated_camera.cc | 5 + .../sunnypilot/qt/onroad/annotated_camera.h | 3 + .../qt/onroad/developer_ui/developer_ui.cc | 227 ++++++++++++++++++ .../qt/onroad/developer_ui/developer_ui.h | 31 +++ .../qt/onroad/developer_ui/ui_elements.h | 19 ++ selfdrive/ui/sunnypilot/qt/onroad/hud.cc | 190 +++++++++++++++ selfdrive/ui/sunnypilot/qt/onroad/hud.h | 39 ++- selfdrive/ui/sunnypilot/ui.cc | 16 +- selfdrive/ui/sunnypilot/ui.h | 4 + selfdrive/ui/sunnypilot/ui_scene.h | 12 + selfdrive/ui/ui.h | 5 + 18 files changed, 578 insertions(+), 3 deletions(-) create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h create mode 100644 selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h create mode 100644 selfdrive/ui/sunnypilot/ui_scene.h diff --git a/common/params_keys.h b/common/params_keys.h index afb6b348eb..fc7842720b 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -146,6 +146,7 @@ inline static std::unordered_map keys = { {"CustomAccLongPressIncrement", {PERSISTENT | BACKUP, INT, "5"}}, {"CustomAccShortPressIncrement", {PERSISTENT | BACKUP, INT, "1"}}, {"DeviceBootMode", {PERSISTENT | BACKUP, INT, "0"}}, + {"DevUIInfo", {PERSISTENT | BACKUP, INT, "0"}}, {"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}}, {"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}}, {"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}}, diff --git a/selfdrive/ui/qt/onroad/alerts.cc b/selfdrive/ui/qt/onroad/alerts.cc index d6829c6b08..2e8f3612eb 100644 --- a/selfdrive/ui/qt/onroad/alerts.cc +++ b/selfdrive/ui/qt/onroad/alerts.cc @@ -4,6 +4,9 @@ #include #include "selfdrive/ui/qt/util.h" +#ifdef SUNNYPILOT +#include "selfdrive/ui/sunnypilot/ui.h" +#endif void OnroadAlerts::updateState(const UIState &s) { Alert a = getAlert(*(s.sm), s.scene.started_frame); @@ -73,6 +76,12 @@ void OnroadAlerts::paintEvent(QPaintEvent *event) { } QRect r = QRect(0 + margin, height() - h + margin, width() - margin*2, h - margin*2); +#ifdef SUNNYPILOT + const int dev_ui_info = uiStateSP()->scene.dev_ui_info; + const int adjustment = dev_ui_info > 1 && alert.size != cereal::SelfdriveState::AlertSize::FULL ? 30 : 0; + r = QRect(0 + margin, height() - h + margin - adjustment, width() - margin*2, h - margin*2); +#endif + QPainter p(this); // draw background + gradient diff --git a/selfdrive/ui/qt/onroad/annotated_camera.h b/selfdrive/ui/qt/onroad/annotated_camera.h index e3ca837907..5d9d21ab6b 100644 --- a/selfdrive/ui/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/qt/onroad/annotated_camera.h @@ -12,6 +12,7 @@ #include "selfdrive/ui/sunnypilot/qt/onroad/model.h" #define ExperimentalButton ExperimentalButtonSP #define ModelRenderer ModelRendererSP +#define HudRenderer HudRendererSP #else #include "selfdrive/ui/qt/onroad/buttons.h" #include "selfdrive/ui/qt/onroad/hud.h" diff --git a/selfdrive/ui/qt/onroad/driver_monitoring.cc b/selfdrive/ui/qt/onroad/driver_monitoring.cc index 49f2c950b4..e67c483047 100644 --- a/selfdrive/ui/qt/onroad/driver_monitoring.cc +++ b/selfdrive/ui/qt/onroad/driver_monitoring.cc @@ -73,6 +73,11 @@ void DriverMonitorRenderer::draw(QPainter &painter, const QRect &surface_rect) { float y = surface_rect.height() - offset; float opacity = is_active ? 0.65f : 0.2f; +#ifdef SUNNYPILOT + const int dev_ui_info = uiStateSP()->scene.dev_ui_info; + y -= dev_ui_info > 1 ? 50 : 0; +#endif + drawIcon(painter, QPoint(x, y), dm_img, QColor(0, 0, 0, 70), opacity); QPointF keypoints[std::size(DEFAULT_FACE_KPTS_3D)]; diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index 2f3c8ddd8d..807bf02478 100644 --- a/selfdrive/ui/sunnypilot/SConscript +++ b/selfdrive/ui/sunnypilot/SConscript @@ -39,6 +39,7 @@ qt_src = [ "sunnypilot/qt/offroad/settings/visuals_panel.cc", "sunnypilot/qt/onroad/annotated_camera.cc", "sunnypilot/qt/onroad/buttons.cc", + "sunnypilot/qt/onroad/developer_ui/developer_ui.cc", "sunnypilot/qt/onroad/hud.cc", "sunnypilot/qt/onroad/model.cc", "sunnypilot/qt/onroad/onroad_home.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index dd2f05416d..c3aaf12d22 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -72,6 +72,15 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { list->addItem(chevron_info_settings); param_watcher->addParam("ChevronInfo"); + // Visuals: Developer UI Info (Dev UI) + std::vector dev_ui_settings_texts{tr("Off"), tr("Right"), tr("Right &&\nBottom")}; + dev_ui_settings = new ButtonParamControlSP( + "DevUIInfo", tr("Developer UI"), tr("Display real-time parameters and metrics from various sources."), + "", + dev_ui_settings_texts, + 380); + list->addItem(dev_ui_settings); + sunnypilotScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnypilotScroller); @@ -90,4 +99,7 @@ void VisualsPanel::paramsRefresh() { if (chevron_info_settings) { chevron_info_settings->refresh(); } + if (dev_ui_settings) { + dev_ui_settings->refresh(); + } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h index f342662c22..30ff31c301 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -28,4 +28,5 @@ protected: std::map toggles; ParamWatcher * param_watcher; ButtonParamControlSP *chevron_info_settings; + ButtonParamControlSP *dev_ui_settings; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc index 3721a3d198..1d5567161a 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.cc @@ -14,3 +14,8 @@ AnnotatedCameraWidgetSP::AnnotatedCameraWidgetSP(VisionStreamType type, QWidget void AnnotatedCameraWidgetSP::updateState(const UIState &s) { AnnotatedCameraWidget::updateState(s); } + +void AnnotatedCameraWidgetSP::showEvent(QShowEvent *event) { + AnnotatedCameraWidget::showEvent(event); + ui_update_params_sp(uiState()); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h index 46ce7d4be3..8c0a385657 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/annotated_camera.h @@ -15,4 +15,7 @@ class AnnotatedCameraWidgetSP : public AnnotatedCameraWidget { public: explicit AnnotatedCameraWidgetSP(VisionStreamType type, QWidget *parent = nullptr); void updateState(const UIState &s) override; + +protected: + void showEvent(QShowEvent *event) override; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc new file mode 100644 index 0000000000..292ba6f7bb --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.cc @@ -0,0 +1,227 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ +#include + +#include "common/util.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" + + +// Add Relative Distance to Primary Lead Car +// Unit: Meters +UiElement DeveloperUi::getDRel(bool lead_status, float lead_d_rel) { + QString value = lead_status ? QString::number(lead_d_rel, 'f', 0) : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lead_status) { + // Orange if close, Red if very close + if (lead_d_rel < 5) { + color = QColor(255, 0, 0, 255); + } else if (lead_d_rel < 15) { + color = QColor(255, 188, 0, 255); + } + } + + return UiElement(value, "REL DIST", "m", color); +} + +// Add Relative Velocity vs Primary Lead Car +// Unit: kph if metric, else mph +UiElement DeveloperUi::getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit) { + QString value = lead_status ? QString::number(lead_v_rel * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lead_status) { + // Red if approaching faster than 10mph + // Orange if approaching (negative) + if (lead_v_rel < -4.4704) { + color = QColor(255, 0, 0, 255); + } else if (lead_v_rel < 0) { + color = QColor(255, 188, 0, 255); + } + } + + return UiElement(value, "REL SPEED", speed_unit, color); +} + +// Add Real Steering Angle +// Unit: Degrees +UiElement DeveloperUi::getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override) { + QString value = QString("%1%2%3").arg(QString::number(angle_steers, 'f', 1)).arg("°").arg(""); + QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); + + // Red if large steering angle + // Orange if moderate steering angle + if (std::fabs(angle_steers) > 180) { + color = QColor(255, 0, 0, 255); + } else if (std::fabs(angle_steers) > 90) { + color = QColor(255, 188, 0, 255); + } + + return UiElement(value, "REAL STEER", "", color); +} + +// Add Actual Lateral Acceleration (roll compensated) when using Torque +// Unit: m/s² +UiElement DeveloperUi::getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override) { + double actualLateralAccel = (curvature * pow(v_ego, 2)) - (roll * 9.81); + + QString value = QString::number(actualLateralAccel, 'f', 2); + QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); + + return UiElement(value, "ACTUAL L.A.", "m/s²", color); +} + +// Add Desired Steering Angle when using PID +// Unit: Degrees +UiElement DeveloperUi::getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers) { + QString value = lat_active ? QString("%1%2%3").arg(QString::number(steer_angle_desired, 'f', 1)).arg("°").arg("") : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lat_active) { + // Red if large steering angle + // Orange if moderate steering angle + if (std::fabs(angle_steers) > 180) { + color = QColor(255, 0, 0, 255); + } else if (std::fabs(angle_steers) > 90) { + color = QColor(255, 188, 0, 255); + } else { + color = QColor(0, 255, 0, 255); + } + } + + return UiElement(value, "DESIRED STEER", "", color); +} + +// Add Device Memory (RAM) Usage +// Unit: Percent +UiElement DeveloperUi::getMemoryUsagePercent(int memory_usage_percent) { + QString value = QString("%1%2").arg(QString::number(memory_usage_percent, 'd', 0)).arg("%"); + QColor color = (memory_usage_percent > 85) ? QColor(255, 188, 0, 255) : QColor(255, 255, 255, 255); + + return UiElement(value, "RAM", "", color); +} + +// Add Vehicle Current Acceleration +// Unit: m/s² +UiElement DeveloperUi::getAEgo(float a_ego) { + QString value = QString::number(a_ego, 'f', 1); + QColor color = QColor(255, 255, 255, 255); + + return UiElement(value, "ACC.", "m/s²", color); +} + +// Add Relative Velocity to Primary Lead Car +// Unit: kph if metric, else mph +UiElement DeveloperUi::getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit) { + QString value = lead_status ? QString::number((lead_v_rel + v_ego) * (is_metric ? MS_TO_KPH : MS_TO_MPH), 'f', 0) : "-"; + QColor color = QColor(255, 255, 255, 255); + + if (lead_status) { + // Red if approaching faster than 10mph + // Orange if approaching (negative) + if (lead_v_rel < -4.4704) { + color = QColor(255, 0, 0, 255); + } else if (lead_v_rel < 0) { + color = QColor(255, 188, 0, 255); + } + } + + return UiElement(value, "L.S.", speed_unit, color); +} + +// Add Friction Coefficient Raw from torqued +// Unit: None +UiElement DeveloperUi::getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid) { + QString value = QString::number(friction_coefficient_filtered, 'f', 3); + QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); + + return UiElement(value, "FRIC.", "", color); +} + +// Add Lateral Acceleration Factor Raw from torqued +// Unit: m/s² +UiElement DeveloperUi::getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid) { + QString value = QString::number(lat_accel_factor_filtered, 'f', 3); + QColor color = live_valid ? QColor(0, 255, 0, 255) : QColor(255, 255, 255, 255); + + return UiElement(value, "L.A.", "m/s²", color); +} + +// Add Steering Torque from Car EPS +// Unit: Newton Meters +UiElement DeveloperUi::getSteeringTorqueEps(float steering_torque_eps) { + QString value = QString::number(std::fabs(steering_torque_eps), 'f', 1); + QColor color = QColor(255, 255, 255, 255); + + return UiElement(value, "E.T.", "N·dm", color); +} + +// Add Bearing Degree and Direction from Car (Compass) +// Unit: Meters +UiElement DeveloperUi::getBearingDeg(float bearing_accuracy_deg, float bearing_deg) { + QString value = (bearing_accuracy_deg != 180.00) ? QString("%1%2%3").arg(QString::number(bearing_deg, 'd', 0)).arg("°").arg("") : "-"; + QColor color = QColor(255, 255, 255, 255); + QString dir_value; + + if (bearing_accuracy_deg != 180.00) { + if (((bearing_deg >= 337.5) && (bearing_deg <= 360)) || ((bearing_deg >= 0) && (bearing_deg <= 22.5))) { + dir_value = "N"; + } else if ((bearing_deg > 22.5) && (bearing_deg < 67.5)) { + dir_value = "NE"; + } else if ((bearing_deg >= 67.5) && (bearing_deg <= 112.5)) { + dir_value = "E"; + } else if ((bearing_deg > 112.5) && (bearing_deg < 157.5)) { + dir_value = "SE"; + } else if ((bearing_deg >= 157.5) && (bearing_deg <= 202.5)) { + dir_value = "S"; + } else if ((bearing_deg > 202.5) && (bearing_deg < 247.5)) { + dir_value = "SW"; + } else if ((bearing_deg >= 247.5) && (bearing_deg <= 292.5)) { + dir_value = "W"; + } else if ((bearing_deg > 292.5) && (bearing_deg < 337.5)) { + dir_value = "NW"; + } + } else { + dir_value = "OFF"; + } + + return UiElement(QString("%1 | %2").arg(dir_value).arg(value), "B.D.", "", color); +} + +// Add Altitude of Current Location +// Unit: Meters +UiElement DeveloperUi::getAltitude(float gps_accuracy, float altitude) { + QString value = (gps_accuracy != 0.00) ? QString::number(altitude, 'f', 1) : "-"; + QColor color = QColor(255, 255, 255, 255); + + return UiElement(value, "ALT.", "m", color); +} + +// Add Actuators Output +// Unit: Degree (angle) or m/s² (torque) +UiElement DeveloperUi::getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, + cereal::CarControl::Actuators::Reader &actuators, + float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override) { + QString label; + QString value; + QString unit; + + if (steerControlType == cereal::CarParams::SteerControlType::ANGLE) { + label = "DESIRED STEER"; + value = QString("%1%2%3").arg(QString::number(actuators.getSteeringAngleDeg(), 'f', 1)).arg("°").arg(""); + } else { + label = "DESIRED L.A."; + double desiredLateralAccel = (desiredCurvature * pow(v_ego, 2)) - (roll * 9.81); + value = QString::number(desiredLateralAccel, 'f', 2); + unit = "m/s²"; + } + + value = lat_active ? value : "-"; + QColor color = lat_active ? (steer_override ? QColor(0x91, 0x9b, 0x95, 0xff) : QColor(0, 255, 0, 255)) : QColor(255, 255, 255, 255); + + return UiElement(value, label, unit, color); +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h new file mode 100644 index 0000000000..0c5c472209 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h @@ -0,0 +1,31 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ +#pragma once + +#include "selfdrive/ui/qt/util.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h" + +class DeveloperUi { + +public: + static UiElement getDRel(bool lead_status, float lead_d_rel); + static UiElement getVRel(bool lead_status, float lead_v_rel, bool is_metric, const QString &speed_unit); + static UiElement getSteeringAngleDeg(float angle_steers, bool lat_active, bool steer_override); + static UiElement getActualLateralAccel(float curvature, float v_ego, float roll, bool lat_active, bool steer_override); + static UiElement getSteeringAngleDesiredDeg(bool lat_active, float steer_angle_desired, float angle_steers); + static UiElement getMemoryUsagePercent(int memory_usage_percent); + static UiElement getAEgo(float a_ego); + static UiElement getVEgoLead(bool lead_status, float lead_v_rel, float v_ego, bool is_metric, const QString &speed_unit); + static UiElement getFrictionCoefficientFiltered(float friction_coefficient_filtered, bool live_valid); + static UiElement getLatAccelFactorFiltered(float lat_accel_factor_filtered, bool live_valid); + static UiElement getSteeringTorqueEps(float steering_torque_eps); + static UiElement getBearingDeg(float bearing_accuracy_deg, float bearing_deg); + static UiElement getAltitude(float gps_accuracy, float altitude); + static UiElement getActuatorsOutputLateral(cereal::CarParams::SteerControlType steerControlType, + cereal::CarControl::Actuators::Reader &actuators, + float desiredCurvature, float v_ego, float roll, bool lat_active, bool steer_override); +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h new file mode 100644 index 0000000000..3711e5ac05 --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/onroad/developer_ui/ui_elements.h @@ -0,0 +1,19 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ +#pragma once + +#include + +struct UiElement { + QString value{}; + QString label{}; + QString units{}; + QColor color{}; + + explicit UiElement(const QString &value = "", const QString &label = "", const QString &units = "", const QColor &color = QColor(255, 255, 255, 255)) + : value(value), label(label), units(units), color(color) {} +}; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc index 233ca59f98..9ead933d04 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc @@ -7,12 +7,202 @@ #include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" +#include "selfdrive/ui/qt/util.h" + + HudRendererSP::HudRendererSP() {} void HudRendererSP::updateState(const UIState &s) { HudRenderer::updateState(s); + + const SubMaster &sm = *(s.sm); + const bool cs_alive = sm.alive("controlsState"); + const auto cs = sm["controlsState"].getControlsState(); + const auto car_state = sm["carState"].getCarState(); + const auto car_control = sm["carControl"].getCarControl(); + const auto radar_state = sm["radarState"].getRadarState(); + const auto is_gps_location_external = sm.rcv_frame("gpsLocationExternal") > 1; + const auto gpsLocation = is_gps_location_external ? sm["gpsLocationExternal"].getGpsLocationExternal() : sm["gpsLocation"].getGpsLocation(); + const auto ltp = sm["liveTorqueParameters"].getLiveTorqueParameters(); + const auto car_params = sm["carParams"].getCarParams(); + + static int reverse_delay = 0; + bool reverse_allowed = false; + if (int(car_state.getGearShifter()) != 4) { + reverse_delay = 0; + reverse_allowed = false; + } else { + reverse_delay += 50; + if (reverse_delay >= 1000) { + reverse_allowed = true; + } + } + + reversing = reverse_allowed; + is_metric = s.scene.is_metric; + + // Handle older routes where vEgoCluster is not set + v_ego_cluster_seen = v_ego_cluster_seen || car_state.getVEgoCluster() != 0.0; + float v_ego = v_ego_cluster_seen ? car_state.getVEgoCluster() : car_state.getVEgo(); + speed = cs_alive ? std::max(0.0, v_ego) : 0.0; + speed *= is_metric ? MS_TO_KPH : MS_TO_MPH; + + latActive = car_control.getLatActive(); + steerOverride = car_state.getSteeringPressed(); + + devUiInfo = s.scene.dev_ui_info; + + speedUnit = is_metric ? tr("km/h") : tr("mph"); + lead_d_rel = radar_state.getLeadOne().getDRel(); + lead_v_rel = radar_state.getLeadOne().getVRel(); + lead_status = radar_state.getLeadOne().getStatus(); + steerControlType = car_params.getSteerControlType(); + actuators = car_control.getActuators(); + torqueLateral = steerControlType == cereal::CarParams::SteerControlType::TORQUE; + angleSteers = car_state.getSteeringAngleDeg(); + desiredCurvature = cs.getDesiredCurvature(); + curvature = cs.getCurvature(); + roll = sm["liveParameters"].getLiveParameters().getRoll(); + memoryUsagePercent = sm["deviceState"].getDeviceState().getMemoryUsagePercent(); + gpsAccuracy = is_gps_location_external ? gpsLocation.getHorizontalAccuracy() : 1.0; // External reports accuracy, internal does not. + altitude = gpsLocation.getAltitude(); + vEgo = car_state.getVEgo(); + aEgo = car_state.getAEgo(); + steeringTorqueEps = car_state.getSteeringTorqueEps(); + bearingAccuracyDeg = gpsLocation.getBearingAccuracyDeg(); + bearingDeg = gpsLocation.getBearingDeg(); + torquedUseParams = ltp.getUseParams(); + latAccelFactorFiltered = ltp.getLatAccelFactorFiltered(); + frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered(); + liveValid = ltp.getLiveValid(); } void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { HudRenderer::draw(p, surface_rect); + if (!reversing) { + // Bottom Dev UI + if (devUiInfo == 2) { + QRect rect_bottom(surface_rect.left(), surface_rect.bottom() - 60, surface_rect.width(), 61); + p.setPen(Qt::NoPen); + p.setBrush(QColor(0, 0, 0, 100)); + p.drawRect(rect_bottom); + drawBottomDevUI(p, rect_bottom.left(), rect_bottom.center().y()); + } + + // Right Dev UI + if (devUiInfo != 0) { + QRect rect_right(surface_rect.right() - (UI_BORDER_SIZE * 2), UI_BORDER_SIZE * 1.5, 184, 170); + drawRightDevUI(p, surface_rect.right() - 184 - UI_BORDER_SIZE * 2, UI_BORDER_SIZE * 2 + rect_right.height()); + } + } +} + +void HudRendererSP::drawText(QPainter &p, int x, int y, const QString &text, QColor color) { + QRect real_rect = p.fontMetrics().boundingRect(text); + real_rect.moveCenter({x, y - real_rect.height() / 2}); + p.setPen(color); + p.drawText(real_rect.x(), real_rect.bottom(), text); +} + +int HudRendererSP::drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { + + p.setFont(InterFont(28, QFont::Bold)); + x += 92; + y += 80; + drawText(p, x, y, label); + + p.setFont(InterFont(30 * 2, QFont::Bold)); + y += 65; + drawText(p, x, y, value, color); + + p.setFont(InterFont(28, QFont::Bold)); + + if (units.length() > 0) { + p.save(); + x += 120; + y -= 25; + p.translate(x, y); + p.rotate(-90); + drawText(p, 0, 0, units); + p.restore(); + } + + return 130; +} + +void HudRendererSP::drawRightDevUI(QPainter &p, int x, int y) { + int rh = 5; + int ry = y; + + UiElement dRelElement = DeveloperUi::getDRel(lead_status, lead_d_rel); + rh += drawRightDevUIElement(p, x, ry, dRelElement.value, dRelElement.label, dRelElement.units, dRelElement.color); + ry = y + rh; + + UiElement vRelElement = DeveloperUi::getVRel(lead_status, lead_v_rel, is_metric, speedUnit); + rh += drawRightDevUIElement(p, x, ry, vRelElement.value, vRelElement.label, vRelElement.units, vRelElement.color); + ry = y + rh; + + UiElement steeringAngleDegElement = DeveloperUi::getSteeringAngleDeg(angleSteers, latActive, steerOverride); + rh += drawRightDevUIElement(p, x, ry, steeringAngleDegElement.value, steeringAngleDegElement.label, steeringAngleDegElement.units, steeringAngleDegElement.color); + ry = y + rh; + + UiElement actuatorsOutputLateralElement = DeveloperUi::getActuatorsOutputLateral(steerControlType, actuators, desiredCurvature, vEgo, roll, latActive, steerOverride); + rh += drawRightDevUIElement(p, x, ry, actuatorsOutputLateralElement.value, actuatorsOutputLateralElement.label, actuatorsOutputLateralElement.units, actuatorsOutputLateralElement.color); + ry = y + rh; + + UiElement actualLateralAccelElement = DeveloperUi::getActualLateralAccel(curvature, vEgo, roll, latActive, steerOverride); + rh += drawRightDevUIElement(p, x, ry, actualLateralAccelElement.value, actualLateralAccelElement.label, actualLateralAccelElement.units, actualLateralAccelElement.color); +} + +int HudRendererSP::drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { + p.setFont(InterFont(38, QFont::Bold)); + QFontMetrics fm(p.font()); + QRect init_rect = fm.boundingRect(label + " "); + QRect real_rect = fm.boundingRect(init_rect, 0, label + " "); + real_rect.moveCenter({x, y}); + + QRect init_rect2 = fm.boundingRect(value); + QRect real_rect2 = fm.boundingRect(init_rect2, 0, value); + real_rect2.moveTop(real_rect.top()); + real_rect2.moveLeft(real_rect.right() + 10); + + QRect init_rect3 = fm.boundingRect(units); + QRect real_rect3 = fm.boundingRect(init_rect3, 0, units); + real_rect3.moveTop(real_rect.top()); + real_rect3.moveLeft(real_rect2.right() + 10); + + p.setPen(Qt::white); + p.drawText(real_rect, Qt::AlignLeft | Qt::AlignVCenter, label); + + p.setPen(color); + p.drawText(real_rect2, Qt::AlignRight | Qt::AlignVCenter, value); + p.drawText(real_rect3, Qt::AlignLeft | Qt::AlignVCenter, units); + return 430; +} + +void HudRendererSP::drawBottomDevUI(QPainter &p, int x, int y) { + int rw = 90; + + UiElement aEgoElement = DeveloperUi::getAEgo(aEgo); + rw += drawBottomDevUIElement(p, rw, y, aEgoElement.value, aEgoElement.label, aEgoElement.units, aEgoElement.color); + + UiElement vEgoLeadElement = DeveloperUi::getVEgoLead(lead_status, lead_v_rel, vEgo, is_metric, speedUnit); + rw += drawBottomDevUIElement(p, rw, y, vEgoLeadElement.value, vEgoLeadElement.label, vEgoLeadElement.units, vEgoLeadElement.color); + + if (torqueLateral && torquedUseParams) { + UiElement frictionCoefficientFilteredElement = DeveloperUi::getFrictionCoefficientFiltered(frictionCoefficientFiltered, liveValid); + rw += drawBottomDevUIElement(p, rw, y, frictionCoefficientFilteredElement.value, frictionCoefficientFilteredElement.label, frictionCoefficientFilteredElement.units, frictionCoefficientFilteredElement.color); + + UiElement latAccelFactorFilteredElement = DeveloperUi::getLatAccelFactorFiltered(latAccelFactorFiltered, liveValid); + rw += drawBottomDevUIElement(p, rw, y, latAccelFactorFilteredElement.value, latAccelFactorFilteredElement.label, latAccelFactorFilteredElement.units, latAccelFactorFilteredElement.color); + } else { + UiElement steeringTorqueEpsElement = DeveloperUi::getSteeringTorqueEps(steeringTorqueEps); + rw += drawBottomDevUIElement(p, rw, y, steeringTorqueEpsElement.value, steeringTorqueEpsElement.label, steeringTorqueEpsElement.units, steeringTorqueEpsElement.color); + + UiElement bearingDegElement = DeveloperUi::getBearingDeg(bearingAccuracyDeg, bearingDeg); + rw += drawBottomDevUIElement(p, rw, y, bearingDegElement.value, bearingDegElement.label, bearingDegElement.units, bearingDegElement.color); + } + + UiElement altitudeElement = DeveloperUi::getAltitude(gpsAccuracy, altitude); + rw += drawBottomDevUIElement(p, rw, y, altitudeElement.value, altitudeElement.label, altitudeElement.units, altitudeElement.color); } diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h index 1e98cd3a52..968789bc15 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.h @@ -7,9 +7,8 @@ #pragma once -#include - #include "selfdrive/ui/qt/onroad/hud.h" +#include "selfdrive/ui/sunnypilot/qt/onroad/developer_ui/developer_ui.h" class HudRendererSP : public HudRenderer { Q_OBJECT @@ -18,4 +17,40 @@ public: HudRendererSP(); void updateState(const UIState &s) override; void draw(QPainter &p, const QRect &surface_rect) override; + +private: + Params params; + void drawText(QPainter &p, int x, int y, const QString &text, QColor color = Qt::white); + void drawRightDevUI(QPainter &p, int x, int y); + int drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); + int drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); + void drawBottomDevUI(QPainter &p, int x, int y); + + bool lead_status; + float lead_d_rel; + float lead_v_rel; + bool torqueLateral; + float angleSteers; + float desiredCurvature; + float curvature; + float roll; + int memoryUsagePercent; + int devUiInfo; + float gpsAccuracy; + float altitude; + float vEgo; + float aEgo; + float steeringTorqueEps; + float bearingAccuracyDeg; + float bearingDeg; + bool torquedUseParams; + float latAccelFactorFiltered; + float frictionCoefficientFiltered; + bool liveValid; + QString speedUnit; + bool latActive; + bool steerOverride; + bool reversing; + cereal::CarParams::SteerControlType steerControlType; + cereal::CarControl::Actuators::Reader actuators; }; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index b2701356cc..1277195df1 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -18,13 +18,22 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { "modelV2", "controlsState", "liveCalibration", "radarState", "deviceState", "pandaStates", "carParams", "driverMonitoringState", "carState", "driverStateV2", "wideRoadCameraState", "managerState", "selfdriveState", "longitudinalPlan", - "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP" + "modelManagerSP", "selfdriveStateSP", "longitudinalPlanSP", "backupManagerSP", + "carControl", "gpsLocationExternal", "gpsLocation", "liveTorqueParameters", + "carStateSP", "liveParameters" }); // update timer timer = new QTimer(this); QObject::connect(timer, &QTimer::timeout, this, &UIStateSP::update); timer->start(1000 / UI_FREQ); + + // Param watcher for UIScene param updates + param_watcher = new ParamWatcher(this); + connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { + ui_update_params_sp(this); + }); + param_watcher->addParam("DevUIInfo"); } // This method overrides completely the update method from the parent class intentionally. @@ -39,6 +48,11 @@ void UIStateSP::update() { emit uiUpdate(*this); } +void ui_update_params_sp(UIStateSP *s) { + auto params = Params(); + s->scene.dev_ui_info = std::atoi(params.get("DevUIInfo").c_str()); +} + DeviceSP::DeviceSP(QObject *parent) : Device(parent) { QObject::connect(uiStateSP(), &UIStateSP::uiUpdate, this, &DeviceSP::update); QObject::connect(this, &Device::displayPowerChanged, this, &DeviceSP::handleDisplayPowerChanged); diff --git a/selfdrive/ui/sunnypilot/ui.h b/selfdrive/ui/sunnypilot/ui.h index cf8de1c4bb..393f997cbd 100644 --- a/selfdrive/ui/sunnypilot/ui.h +++ b/selfdrive/ui/sunnypilot/ui.h @@ -13,6 +13,7 @@ #include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/role_model.h" #include "selfdrive/ui/sunnypilot/qt/network/sunnylink/models/sponsor_role_model.h" #include "selfdrive/ui/ui.h" +#include "selfdrive/ui/qt/util.h" class UIStateSP : public UIState { Q_OBJECT @@ -73,6 +74,7 @@ private slots: private: std::vector sunnylinkRoles = {}; std::vector sunnylinkUsers = {}; + ParamWatcher *param_watcher; }; UIStateSP *uiStateSP(); @@ -92,3 +94,5 @@ private: DeviceSP *deviceSP(); inline DeviceSP *device() { return deviceSP(); } + +void ui_update_params_sp(UIStateSP *s); diff --git a/selfdrive/ui/sunnypilot/ui_scene.h b/selfdrive/ui/sunnypilot/ui_scene.h new file mode 100644 index 0000000000..93e0cd6c91 --- /dev/null +++ b/selfdrive/ui/sunnypilot/ui_scene.h @@ -0,0 +1,12 @@ +/** + * Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + * + * This file is part of sunnypilot and is licensed under the MIT License. + * See the LICENSE.md file in the root directory for more details. + */ + +#pragma once + +typedef struct UISceneSP : UIScene { + int dev_ui_info = 0; +} UISceneSP; diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index e78b573b66..5b3872b3d4 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -66,6 +66,11 @@ typedef struct UIScene { uint64_t started_frame; } UIScene; +#ifdef SUNNYPILOT +#include "sunnypilot/ui_scene.h" +#define UIScene UISceneSP +#endif + class UIState : public QObject { Q_OBJECT From bffb2fb6fabd384a39517e46d841b45b97a58210 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Sun, 14 Sep 2025 20:22:51 +0200 Subject: [PATCH 180/188] bugfix: param store to support the latest param changes (#1244) * bugfix: update parameter handling to use custom CarControlSP.Param and improve param retrieval * bump opendbc --- cereal/custom.capnp | 15 +++++++- opendbc_repo | 2 +- .../selfdrive/controls/controlsd_ext.py | 2 +- .../selfdrive/controls/lib/param_store.py | 36 ++++++++++--------- sunnypilot/sunnylink/utils.py | 18 ++++++++-- 5 files changed, 50 insertions(+), 23 deletions(-) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 5468e0f39d..8d4bc3cb82 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -202,7 +202,20 @@ struct CarControlSP @0xa5cd762cd951a455 { struct Param { key @0 :Text; - value @1 :Text; + type @2 :ParamType; + value @3 :Data; + + valueDEPRECATED @1 :Text; # The data type change may cause issues with backwards compatibility. + } + + enum ParamType { + string @0; + bool @1; + int @2; + float @3; + time @4; + json @5; + bytes @6; } } diff --git a/opendbc_repo b/opendbc_repo index c705ebf987..189dc3f78c 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit c705ebf9872193e97f9498868cead9e9affdee15 +Subproject commit 189dc3f78c3b2eeb0402e69760602aa7149d05c9 diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 7e06ac77c1..a096b7dc8c 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -73,7 +73,7 @@ class ControlsExt: # MADS state CC_SP.mads = sm['selfdriveStateSP'].mads - CC_SP.params = self.param_store.publish() + CC_SP.params = self.param_store.param_list return CC_SP diff --git a/sunnypilot/selfdrive/controls/lib/param_store.py b/sunnypilot/selfdrive/controls/lib/param_store.py index 2ef3473187..65a0175340 100644 --- a/sunnypilot/selfdrive/controls/lib/param_store.py +++ b/sunnypilot/selfdrive/controls/lib/param_store.py @@ -4,39 +4,41 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -import capnp - from cereal import custom - from opendbc.car import structs from openpilot.common.params import Params +from sunnypilot.sunnylink.utils import get_param_as_byte + class ParamStore: keys: list[str] - values: dict[str, str] + _params: dict[str, custom.CarControlSP.Param] def __init__(self, CP: structs.CarParams): universal_params: list[str] = [] brand_params: list[str] = [] self.keys = universal_params + brand_params - self.values = {} - self.cached_params_list: list[capnp.lib.capnp._DynamicStructBuilder] | None = None + self._params = {} self.frame = 0 def update(self, params: Params) -> None: - if self.frame % 300 == 0: - old_values = dict(self.values) - self.values = {k: params.get(k) or "0" for k in self.keys} - if old_values != self.values: - self.cached_params_list = None - self.frame += 1 + if self.frame % 300 != 0: + return - def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]: - if self.cached_params_list is None: - # TODO-SP: Why are we doing a list instead of a dictionary here? - self.cached_params_list = [custom.CarControlSP.Param(key=k, value=self.values[k]) for k in self.keys] - return self.cached_params_list + for key in self.keys: + param_type = params.get_type(key).name.lower() # Using string instead of number because its "loose" dependency, and could change by OP at anytime. + + # Over engineering opportunity: It's possible this conversion is slow, we may check the value as params returns it for cache purposes. Not today. + param_value = get_param_as_byte(key, params) + if (existing_param := self._params.get(key)) is not None and existing_param.value == param_value: + continue + + self._params[key] = custom.CarControlSP.Param(key=key, value=param_value, type=param_type) + + @property + def param_list(self) -> list[custom.CarControlSP.Param]: + return [v for k,v in self._params.items()] diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 1310b91f0e..91c0788795 100644 --- a/sunnypilot/sunnylink/utils.py +++ b/sunnypilot/sunnylink/utils.py @@ -60,9 +60,9 @@ def get_api_token(): print(f"API Token: {token}") -def get_param_as_byte(param_name: str) -> bytes | None: +def get_param_as_byte(param_name: str, params=None) -> bytes | None: """Get a parameter as bytes. Returns None if the parameter does not exist.""" - params = Params() + params = params or Params() # Use existing Params instance if provided param = params.get(param_name) if param is None: return None @@ -85,6 +85,17 @@ def save_param_from_base64_encoded_string(param_name: str, base64_encoded_data: if is_compressed: value = gzip.decompress(value) + # We convert to string anything that isn't bytes first. We later transform further. + param_value = _convert_param_to_type(value, param_type) + params.put(param_name, param_value) + + +def _convert_param_to_type(value: bytes, param_type: ParamKeyType) -> bytes | str | int | float | bool | dict | None: + """ + Convert a byte value to the specified param type. Used internally when getting a Param to convert it to the right type. + If this method looks familiar, it's because on SP we have a similar one in openpilot/sunnypilot/car/__init__.py. + """ + # We convert to string anything that isn't bytes first. We later transform further. if param_type != ParamKeyType.BYTES: value = value.decode('utf-8') # type: ignore @@ -101,4 +112,5 @@ def save_param_from_base64_encoded_string(param_name: str, base64_encoded_data: value = str(value) # type: ignore elif param_type == ParamKeyType.JSON: value = json.loads(value) - params.put(param_name, value) + + return value From b32c6dafee755b2a961c37fa6967cc1fe5292398 Mon Sep 17 00:00:00 2001 From: James Vecellio-Grant <159560811+Discountchubbs@users.noreply.github.com> Date: Sun, 14 Sep 2025 18:45:22 -0700 Subject: [PATCH 181/188] modeld: add laneline helper for plan indices calculation (#1240) * modeld: add laneline_helper for plan X indices calculation * spacing * keep type hints * openpilot * sunnypilot/models/helpers add modeld helpers to helpers * Send it from each fill message --------- Co-authored-by: Jason Wen --- selfdrive/modeld/fill_model_msg.py | 5 +++-- sunnypilot/modeld/fill_model_msg.py | 19 ++----------------- sunnypilot/modeld_v2/fill_model_msg.py | 5 +++-- sunnypilot/models/helpers.py | 24 ++++++++++++++++++++++++ 4 files changed, 32 insertions(+), 21 deletions(-) diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index 82c4c92b1d..7273745c7b 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -3,6 +3,7 @@ import capnp import numpy as np from cereal import log from openpilot.selfdrive.modeld.constants import ModelConstants, Plan, Meta +from openpilot.sunnypilot.models.helpers import plan_x_idxs_helper SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -95,8 +96,8 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # action modelV2.action = action - # times at X_IDXS of edges and lines aren't used - LINE_T_IDXS: list[float] = [] + # times at X_IDXS of edges and lines + LINE_T_IDXS: list[float] = plan_x_idxs_helper(ModelConstants, Plan, net_output_data) # lane lines modelV2.init('laneLines', 4) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py index dadffc8433..a62c451efd 100644 --- a/sunnypilot/modeld/fill_model_msg.py +++ b/sunnypilot/modeld/fill_model_msg.py @@ -3,6 +3,7 @@ import capnp import numpy as np from cereal import log from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan +from openpilot.sunnypilot.models.helpers import plan_x_idxs_helper from openpilot.sunnypilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_lag_adjusted_curvature, MIN_SPEED SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -120,23 +121,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D modelV2_action.desiredCurvature = desired_curvature # times at X_IDXS according to model plan - PLAN_T_IDXS = [np.nan] * ModelConstants.IDX_N - PLAN_T_IDXS[0] = 0.0 - plan_x = net_output_data['plan'][0,:,Plan.POSITION][:,0].tolist() - for xidx in range(1, ModelConstants.IDX_N): - tidx = 0 - # increment tidx until we find an element that's further away than the current xidx - while tidx < ModelConstants.IDX_N - 1 and plan_x[tidx+1] < ModelConstants.X_IDXS[xidx]: - tidx += 1 - if tidx == ModelConstants.IDX_N - 1: - # if the Plan doesn't extend far enough, set plan_t to the max value (10s), then break - PLAN_T_IDXS[xidx] = ModelConstants.T_IDXS[ModelConstants.IDX_N - 1] - break - # interpolate to find `t` for the current xidx - current_x_val = plan_x[tidx] - next_x_val = plan_x[tidx+1] - p = (ModelConstants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs(next_x_val - current_x_val) > 1e-9 else float('nan') - PLAN_T_IDXS[xidx] = p * ModelConstants.T_IDXS[tidx+1] + (1 - p) * ModelConstants.T_IDXS[tidx] + PLAN_T_IDXS: list[float] = plan_x_idxs_helper(ModelConstants, Plan, net_output_data) # lane lines modelV2.init('laneLines', 4) diff --git a/sunnypilot/modeld_v2/fill_model_msg.py b/sunnypilot/modeld_v2/fill_model_msg.py index c7de698f61..ee0eb48684 100644 --- a/sunnypilot/modeld_v2/fill_model_msg.py +++ b/sunnypilot/modeld_v2/fill_model_msg.py @@ -3,6 +3,7 @@ import capnp import numpy as np from cereal import log from openpilot.sunnypilot.modeld_v2.constants import ModelConstants, Plan +from openpilot.sunnypilot.models.helpers import plan_x_idxs_helper from openpilot.selfdrive.controls.lib.drive_helpers import get_curvature_from_plan SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') @@ -118,8 +119,8 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D # action (includes lateral planning now) modelV2.action = action - # times at X_IDXS of edges and lines aren't used - LINE_T_IDXS: list[float] = [] + # times at X_IDXS of edges and lines + LINE_T_IDXS: list[float] = plan_x_idxs_helper(ModelConstants, Plan, net_output_data) # lane lines modelV2.init('laneLines', 4) diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index ecf0a39b72..20b94fb611 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -185,3 +185,27 @@ def load_meta_constants(model_metadata): meta = MetaTombRaider return meta + + +# The following method(s) are modeld helper methods +def plan_x_idxs_helper(constants, plan, model_output) -> list[float]: + # times at X_IDXS according to plan. + LINE_T_IDXS = [np.nan] * constants.IDX_N + LINE_T_IDXS[0] = 0.0 + plan_x = model_output['plan'][0, :, plan.POSITION][:, 0].tolist() + for xidx in range(1, constants.IDX_N): + tidx = 0 + # increment tidx until we find an element that's further away than the current xidx + while tidx < constants.IDX_N - 1 and plan_x[tidx + 1] < constants.X_IDXS[xidx]: + tidx += 1 + if tidx == constants.IDX_N - 1: + # if the plan doesn't extend far enough, set plan_t to the max value (10s), then break + LINE_T_IDXS[xidx] = constants.T_IDXS[constants.IDX_N - 1] + break + # interpolate to find `t` for the current xidx + current_x_val = plan_x[tidx] + next_x_val = plan_x[tidx + 1] + p = (constants.X_IDXS[xidx] - current_x_val) / (next_x_val - current_x_val) if abs( + next_x_val - current_x_val) > 1e-9 else float('nan') + LINE_T_IDXS[xidx] = p * constants.T_IDXS[tidx + 1] + (1 - p) * constants.T_IDXS[tidx] + return LINE_T_IDXS From 747460363fca35025cf7cf306269c95b4e7bc0aa Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Sun, 14 Sep 2025 23:51:29 -0400 Subject: [PATCH 182/188] panda: consolidate supported panda checks (#1248) * panda: fix upstream merge conflicts * move it higher * consolidate checks * update * bump * actual bump --- panda | 2 +- selfdrive/pandad/pandad.py | 26 ++++++++++++++++++++------ 2 files changed, 21 insertions(+), 7 deletions(-) diff --git a/panda b/panda index 7c393d1cd5..69ab12ee2a 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit 7c393d1cd5cc88d3c28af2086bf25ff6f3035574 +Subproject commit 69ab12ee2a2958bb9825bd772ff03be6714b6c0e diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index 361c1f2148..f4064ddcd4 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -29,6 +29,12 @@ def flash_panda(panda_serial: str) -> Panda: HARDWARE.recover_internal_panda() raise + # skip flashing if the detected panda is not supported + supported_panda = check_panda_support(panda) + if not supported_panda: + cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {panda.get_type()}), skipping flash...") + return panda + fw_signature = get_expected_signature(panda) internal_panda = panda.is_internal() @@ -36,12 +42,6 @@ def flash_panda(panda_serial: str) -> Panda: panda_signature = b"" if panda.bootstub else panda.get_signature() cloudlog.warning(f"Panda {panda_serial} connected, version: {panda_version}, signature {panda_signature.hex()[:16]}, expected {fw_signature.hex()[:16]}") - # skip flashing if the detected device is not supported from upstream - hw_type = panda.get_type() - if hw_type not in Panda.SUPPORTED_DEVICES: - cloudlog.warning(f"Panda {panda_serial} is not supported (hw_type: {hw_type}), skipping flash...") - return panda - if panda.bootstub or panda_signature != fw_signature: cloudlog.info("Panda firmware out of date, update required") panda.flash() @@ -67,6 +67,14 @@ def flash_panda(panda_serial: str) -> Panda: return panda +def check_panda_support(panda) -> bool: + hw_type = panda.get_type() + if hw_type in Panda.SUPPORTED_DEVICES: + return True + + return False + + def main() -> None: # signal pandad to close the relay and exit def signal_handler(signum, frame): @@ -140,6 +148,12 @@ def main() -> None: params.put("PandaSignatures", b','.join(p.get_signature() for p in pandas)) for panda in pandas: + # skip health check if the detected panda is not supported + supported_panda = check_panda_support(panda) + if not supported_panda: + cloudlog.warning(f"Panda {panda.get_usb_serial()} is not supported (hw_type: {panda.get_type()}), skipping health check...") + continue + # check health for lost heartbeat health = panda.health() if health["heartbeat_lost"]: From 94f93a9f266e6a51d6a79ac092cefd2f7b0b6551 Mon Sep 17 00:00:00 2001 From: DevTekVE Date: Tue, 16 Sep 2025 10:05:34 +0200 Subject: [PATCH 183/188] ui: Add standstill timer to HUD (#1251) * Add standstill timer to HUD - Introduced a timer displaying elapsed time when the car is at a standstill. - Added settings toggle to enable/disable this feature. - Updated UI elements and related logic to support the standstill timer. * ruff be happy * stop screaming * c stands for not cereal * slight cleanup * a bit more cleanup * unused --------- Co-authored-by: nayan Co-authored-by: Jason Wen --- common/params_keys.h | 1 + .../qt/offroad/settings/visuals_panel.cc | 7 ++++ selfdrive/ui/sunnypilot/qt/onroad/hud.cc | 42 +++++++++++++++++++ selfdrive/ui/sunnypilot/qt/onroad/hud.h | 4 ++ selfdrive/ui/sunnypilot/ui.cc | 2 + selfdrive/ui/sunnypilot/ui_scene.h | 1 + 6 files changed, 57 insertions(+) diff --git a/common/params_keys.h b/common/params_keys.h index 011a3161f9..44d25ec413 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -159,6 +159,7 @@ inline static std::unordered_map keys = { {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params {"Mads", {PERSISTENT | BACKUP, BOOL, "1"}}, diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index c3aaf12d22..e19760f2e1 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -35,6 +35,13 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { "../assets/offroad/icon_monitoring.png", false, }, + { + "StandstillTimer", + tr("Enable Standstill Timer"), + tr("Show a timer on the HUD when the car is at a standstill."), + "../assets/offroad/icon_monitoring.png", + false, + }, }; // Add regular toggles first diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc index 9ead933d04..291a669ac5 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc @@ -75,6 +75,9 @@ void HudRendererSP::updateState(const UIState &s) { latAccelFactorFiltered = ltp.getLatAccelFactorFiltered(); frictionCoefficientFiltered = ltp.getFrictionCoefficientFiltered(); liveValid = ltp.getLiveValid(); + + standstillTimer = s.scene.standstill_timer; + isStandstill = car_state.getStandstill(); } void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { @@ -94,6 +97,11 @@ void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { QRect rect_right(surface_rect.right() - (UI_BORDER_SIZE * 2), UI_BORDER_SIZE * 1.5, 184, 170); drawRightDevUI(p, surface_rect.right() - 184 - UI_BORDER_SIZE * 2, UI_BORDER_SIZE * 2 + rect_right.height()); } + + // Standstill Timer + if (standstillTimer) { + drawStandstillTimer(p, surface_rect.right() / 12 * 10, surface_rect.bottom() / 12 * 1.53); + } } } @@ -206,3 +214,37 @@ void HudRendererSP::drawBottomDevUI(QPainter &p, int x, int y) { UiElement altitudeElement = DeveloperUi::getAltitude(gpsAccuracy, altitude); rw += drawBottomDevUIElement(p, rw, y, altitudeElement.value, altitudeElement.label, altitudeElement.units, altitudeElement.color); } + +void HudRendererSP::drawStandstillTimer(QPainter &p, int x, int y) { + if (isStandstill) { + standstillElapsedTime += 1.0 / UI_FREQ; + + int minute = static_cast(standstillElapsedTime / 60); + int second = static_cast(standstillElapsedTime - (minute * 60)); + + // stop sign for standstill timer + const int size = 190; // size + const float angle = M_PI / 8.0; + + QPolygon octagon; + for (int i = 0; i < 8; i++) { + float curr_angle = angle + i * M_PI / 4.0; + int point_x = x + size / 2 * cos(curr_angle); + int point_y = y + size / 2 * sin(curr_angle); + octagon << QPoint(point_x, point_y); + } + + p.setPen(QPen(Qt::white, 6)); + p.setBrush(QColor(255, 90, 81, 200)); // red pastel + p.drawPolygon(octagon); + + QString time_str = QString("%1:%2").arg(minute, 1, 10, QChar('0')).arg(second, 2, 10, QChar('0')); + p.setFont(InterFont(55, QFont::Bold)); + p.setPen(Qt::white); + QRect timerTextRect = p.fontMetrics().boundingRect(QString(time_str)); + timerTextRect.moveCenter({x, y}); + p.drawText(timerTextRect, Qt::AlignCenter, QString(time_str)); + } else { + standstillElapsedTime = 0.0; + } +} diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h index 968789bc15..75fc520fcf 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.h @@ -25,6 +25,7 @@ private: int drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); int drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); void drawBottomDevUI(QPainter &p, int x, int y); + void drawStandstillTimer(QPainter &p, int x, int y); bool lead_status; float lead_d_rel; @@ -53,4 +54,7 @@ private: bool reversing; cereal::CarParams::SteerControlType steerControlType; cereal::CarControl::Actuators::Reader actuators; + bool standstillTimer; + bool isStandstill; + float standstillElapsedTime; }; diff --git a/selfdrive/ui/sunnypilot/ui.cc b/selfdrive/ui/sunnypilot/ui.cc index 1277195df1..7b10929bc6 100644 --- a/selfdrive/ui/sunnypilot/ui.cc +++ b/selfdrive/ui/sunnypilot/ui.cc @@ -34,6 +34,7 @@ UIStateSP::UIStateSP(QObject *parent) : UIState(parent) { ui_update_params_sp(this); }); param_watcher->addParam("DevUIInfo"); + param_watcher->addParam("StandstillTimer"); } // This method overrides completely the update method from the parent class intentionally. @@ -51,6 +52,7 @@ void UIStateSP::update() { void ui_update_params_sp(UIStateSP *s) { auto params = Params(); s->scene.dev_ui_info = std::atoi(params.get("DevUIInfo").c_str()); + s->scene.standstill_timer = params.getBool("StandstillTimer"); } DeviceSP::DeviceSP(QObject *parent) : Device(parent) { diff --git a/selfdrive/ui/sunnypilot/ui_scene.h b/selfdrive/ui/sunnypilot/ui_scene.h index 93e0cd6c91..c941be675c 100644 --- a/selfdrive/ui/sunnypilot/ui_scene.h +++ b/selfdrive/ui/sunnypilot/ui_scene.h @@ -9,4 +9,5 @@ typedef struct UISceneSP : UIScene { int dev_ui_info = 0; + bool standstill_timer = false; } UISceneSP; From cb94d3b0550e3ee329f17f143fe9a3cbba1e9430 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Tue, 16 Sep 2025 22:51:32 -0400 Subject: [PATCH 184/188] Longitudinal: Smart Cruise Control prerequisites (#1249) * Controls: Vision Turn Speed Control * fix * Data type temp fix * format * more * even more * self contain targets * state cleanup * fix * param updates * no need * use similar state machine * raise exception if not found * new state * entirely internal * use long active * more * rename and expose aTarget * rename to SCC-V * init tests * slight tests * expose toggle * lint * todo * remove lat planner sub and mock sm data * introduce aTarget * rename * rename * update fill_model_msg.py to calculate PLAN_T_IDXS for lanelines and road edges * sync upstream * no SCC-V yet * Revert "no SCC-V yet" This reverts commit b67281bcac0dce0c6c3237afc2584cf29144c83c. * wrap it with SCC main * no SCC-V yet * noqa now * fix * OP long for now, enable for ICBM once merged * type hints * let's get it straight from carcontrol instead * not needed * unused * add source to track * we can do this --------- Co-authored-by: discountchubbs --- cereal/custom.capnp | 9 +++++++++ .../controls/lib/longitudinal_planner.py | 3 +++ sunnypilot/__init__.py | 7 +++++++ .../controls/lib/longitudinal_planner.py | 20 +++++++++++++++++++ .../lib/smart_cruise_control/__init__.py | 0 .../smart_cruise_control.py | 13 ++++++++++++ 6 files changed, 52 insertions(+) create mode 100644 sunnypilot/selfdrive/controls/lib/smart_cruise_control/__init__.py create mode 100644 sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 8d4bc3cb82..e09b32a0a6 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -122,6 +122,8 @@ struct ModelManagerSP @0xaedffd8f31e7b55d { struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { dec @0 :DynamicExperimentalControl; + longitudinalPlanSource @1 :LongitudinalPlanSource; + smartCruiseControl @2 :SmartCruiseControl; struct DynamicExperimentalControl { state @0 :DynamicExperimentalControlState; @@ -133,6 +135,13 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { blended @1; } } + + struct SmartCruiseControl { + } + + enum LongitudinalPlanSource { + cruise @0; + } } struct OnroadEventSP @0xda96579883444c35 { diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 96248b7132..139cdc06e7 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -146,6 +146,9 @@ class LongitudinalPlanner(LongitudinalPlannerSP): clipped_accel_coast_interp = np.interp(v_ego, [MIN_ALLOW_THROTTLE_SPEED, MIN_ALLOW_THROTTLE_SPEED*2], [accel_clip[1], clipped_accel_coast]) accel_clip[1] = min(accel_clip[1], clipped_accel_coast_interp) + # Get new v_cruise and a_desired from Smart Cruise Control + v_cruise, self.a_desired = LongitudinalPlannerSP.update_targets(self, sm, self.v_desired_filter.x, self.a_desired, v_cruise) + if force_slow_decel: v_cruise = 0.0 diff --git a/sunnypilot/__init__.py b/sunnypilot/__init__.py index e69de29bb2..ab5441aa71 100644 --- a/sunnypilot/__init__.py +++ b/sunnypilot/__init__.py @@ -0,0 +1,7 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +PARAMS_UPDATE_PERIOD = 3 # seconds diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 6952a97f1f..ec020000a3 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -8,15 +8,19 @@ See the LICENSE.md file in the root directory for more details. from cereal import messaging, custom from opendbc.car import structs from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl from openpilot.sunnypilot.models.helpers import get_active_bundle DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState +Source = custom.LongitudinalPlanSP.LongitudinalPlanSource class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.dec = DynamicExperimentalController(CP, mpc) + self.scc = SmartCruiseControl() self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None + self.source = Source.cruise @property def mlsim(self) -> bool: @@ -29,6 +33,18 @@ class LongitudinalPlannerSP: return self.dec.mode() + def update_targets(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> tuple[float, float]: + self.scc.update(sm, v_ego, a_ego, v_cruise) + + targets = { + Source.cruise : (v_cruise, a_ego), + } + + self.source = min(targets, key=lambda k: targets[k][0]) + v_target, a_target = targets[self.source] + + return v_target, a_target + def update(self, sm: messaging.SubMaster) -> None: self.dec.update(sm) @@ -38,6 +54,7 @@ class LongitudinalPlannerSP: plan_sp_send.valid = sm.all_checks(service_list=['carState', 'controlsState']) longitudinalPlanSP = plan_sp_send.longitudinalPlanSP + longitudinalPlanSP.longitudinalPlanSource = self.source # Dynamic Experimental Control dec = longitudinalPlanSP.dec @@ -45,4 +62,7 @@ class LongitudinalPlannerSP: dec.enabled = self.dec.enabled() dec.active = self.dec.active() + # Smart Cruise Control + smartCruiseControl = longitudinalPlanSP.smartCruiseControl # noqa: F841 + pm.send('longitudinalPlanSP', plan_sp_send) diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/__init__.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py new file mode 100644 index 0000000000..0a04c243c1 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py @@ -0,0 +1,13 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from cereal import messaging + + +class SmartCruiseControl: + + def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float): + pass From 784e1d665880f65b46cfe3c0c242fe71e22f4a2e Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Wed, 17 Sep 2025 22:55:36 -0400 Subject: [PATCH 185/188] Smart Cruise Control: Vision (SCC-V) (#997) * Controls: Vision Turn Speed Control * fix * Data type temp fix * format * more * even more * self contain targets * state cleanup * fix * param updates * no need * use similar state machine * raise exception if not found * new state * entirely internal * use long active * more * rename and expose aTarget * rename to SCC-V * init tests * slight tests * expose toggle * lint * todo * remove lat planner sub and mock sm data * introduce aTarget * rename * rename * update fill_model_msg.py to calculate PLAN_T_IDXS for lanelines and road edges * sync upstream * no SCC-V yet * Revert "no SCC-V yet" This reverts commit b67281bcac0dce0c6c3237afc2584cf29144c83c. * wrap it with SCC main * leave enabled out of here * wat * enabled and active on cereal * OP long for now, enable for ICBM once merged * need this * unused * let's go hybrid * fix * add override state * update tests * huh * don't math here if not enabled * ui: Smart Cruise Control - Vision (SCC-V) (#1253) * vtsc-ui * slight cleanup * more cleanup * unused * a bit more * pulse like it's hot * draw only enabled and active * let's try this for now * settle * finalize UI * brighter color so we blind devtekve * add long override --------- Co-authored-by: Jason Wen * slight cleanup * more * type hints --------- Co-authored-by: discountchubbs Co-authored-by: Kumar <36933347+rav4kumar@users.noreply.github.com> --- cereal/custom.capnp | 21 ++ common/params_keys.h | 1 + .../qt/offroad/settings/longitudinal_panel.cc | 9 + .../qt/offroad/settings/longitudinal_panel.h | 1 + selfdrive/ui/sunnypilot/qt/onroad/hud.cc | 63 ++++++ selfdrive/ui/sunnypilot/qt/onroad/hud.h | 6 + .../controls/lib/longitudinal_planner.py | 14 +- .../smart_cruise_control.py | 12 +- .../tests/test_vision_controller.py | 104 +++++++++ .../smart_cruise_control/vision_controller.py | 205 ++++++++++++++++++ 10 files changed, 431 insertions(+), 5 deletions(-) create mode 100644 sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py create mode 100644 sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index e09b32a0a6..66aa1b71f8 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -137,10 +137,31 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { } struct SmartCruiseControl { + vision @0 :Vision; + + struct Vision { + state @0 :VisionState; + vTarget @1 :Float32; + aTarget @2 :Float32; + currentLateralAccel @3 :Float32; + maxPredictedLateralAccel @4 :Float32; + enabled @5 :Bool; + active @6 :Bool; + } + + enum VisionState { + disabled @0; # System disabled or inactive. + enabled @1; # No predicted substantial turn on vision range. + entering @2; # A substantial turn is predicted ahead, adapting speed to turn comfort levels. + turning @3; # Actively turning. Managing acceleration to provide a roll on turn feeling. + leaving @4; # Road ahead straightens. Start to allow positive acceleration. + overriding @5; # System overriding with manual control. + } } enum LongitudinalPlanSource { cruise @0; + sccVision @1; } } diff --git a/common/params_keys.h b/common/params_keys.h index 44d25ec413..f0511bf9c2 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -159,6 +159,7 @@ inline static std::unordered_map keys = { {"QuietMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"RainbowMode", {PERSISTENT | BACKUP, BOOL, "0"}}, {"ShowAdvancedControls", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"SmartCruiseControlVision", {PERSISTENT | BACKUP, BOOL, "0"}}, {"StandstillTimer", {PERSISTENT | BACKUP, BOOL, "0"}}, // MADS params diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc index 15ed10bf9a..ac7b57e297 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc @@ -18,6 +18,13 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) { cruisePanelScroller = new ScrollViewSP(list, this); vlayout->addWidget(cruisePanelScroller); + SmartCruiseControlVision = new ParamControl( + "SmartCruiseControlVision", + tr("Smart Cruise Control - Vision"), + tr("Use vision path predictions to estimate the appropriate speed to drive through turns ahead."), + ""); + list->addItem(SmartCruiseControlVision); + customAccIncrement = new CustomAccIncrement("CustomAccIncrementsEnabled", tr("Custom ACC Speed Increments"), "", "", this); list->addItem(customAccIncrement); @@ -75,5 +82,7 @@ void LongitudinalPanel::refresh(bool _offroad) { customAccIncrement->setEnabled(has_longitudinal_control && !is_pcm_cruise && !offroad); customAccIncrement->refresh(); + SmartCruiseControlVision->setEnabled(has_longitudinal_control); + offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h index 58e94f333b..36c35720e7 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h @@ -29,4 +29,5 @@ private: ScrollViewSP *cruisePanelScroller = nullptr; QWidget *cruisePanelScreen = nullptr; CustomAccIncrement *customAccIncrement = nullptr; + ParamControl *SmartCruiseControlVision; }; diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc index 291a669ac5..fb2a69c24b 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.cc +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.cc @@ -4,6 +4,7 @@ * This file is part of sunnypilot and is licensed under the MIT License. * See the LICENSE.md file in the root directory for more details. */ +#include #include "selfdrive/ui/sunnypilot/qt/onroad/hud.h" @@ -25,6 +26,7 @@ void HudRendererSP::updateState(const UIState &s) { const auto gpsLocation = is_gps_location_external ? sm["gpsLocationExternal"].getGpsLocationExternal() : sm["gpsLocation"].getGpsLocation(); const auto ltp = sm["liveTorqueParameters"].getLiveTorqueParameters(); const auto car_params = sm["carParams"].getCarParams(); + const auto lp_sp = sm["longitudinalPlanSP"].getLongitudinalPlanSP(); static int reverse_delay = 0; bool reverse_allowed = false; @@ -78,11 +80,30 @@ void HudRendererSP::updateState(const UIState &s) { standstillTimer = s.scene.standstill_timer; isStandstill = car_state.getStandstill(); + longOverride = car_control.getCruiseControl().getOverride(); + smartCruiseControlVisionEnabled = lp_sp.getSmartCruiseControl().getVision().getEnabled(); + smartCruiseControlVisionActive = lp_sp.getSmartCruiseControl().getVision().getActive(); } void HudRendererSP::draw(QPainter &p, const QRect &surface_rect) { HudRenderer::draw(p, surface_rect); if (!reversing) { + // Smart Cruise Control + int x_offset = -260; + int y1_offset = -80; + // int y2_offset = -140; // reserved for 2 icons + + bool scc_vision_active_pulse = pulseElement(smartCruiseControlVisionFrame); + if ((smartCruiseControlVisionEnabled && !smartCruiseControlVisionActive) || (smartCruiseControlVisionActive && scc_vision_active_pulse)) { + drawSmartCruiseControlOnroadIcon(p, surface_rect, x_offset, y1_offset, "SCC-V"); + } + + if (smartCruiseControlVisionActive) { + smartCruiseControlVisionFrame++; + } else { + smartCruiseControlVisionFrame = 0; + } + // Bottom Dev UI if (devUiInfo == 2) { QRect rect_bottom(surface_rect.left(), surface_rect.bottom() - 60, surface_rect.width(), 61); @@ -112,6 +133,48 @@ void HudRendererSP::drawText(QPainter &p, int x, int y, const QString &text, QCo p.drawText(real_rect.x(), real_rect.bottom(), text); } +bool HudRendererSP::pulseElement(int frame) { + if (frame % UI_FREQ < (UI_FREQ / 2.5)) { + return false; + } + + return true; +} + +void HudRendererSP::drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &surface_rect, int x_offset, int y_offset, std::string name) { + int x = surface_rect.center().x(); + int y = surface_rect.height() / 4; + + QString text = QString::fromStdString(name); + QFont font = InterFont(36, QFont::Bold); + p.setFont(font); + + QFontMetrics fm(font); + + int padding_v = 5; + int box_width = 160; + int box_height = fm.height() + padding_v * 2; + + QRectF bg_rect(x - (box_width / 2) + x_offset, + y - (box_height / 2) + y_offset, + box_width, box_height); + + QPainterPath boxPath; + boxPath.addRoundedRect(bg_rect, 10, 10); + + int text_w = fm.horizontalAdvance(text); + qreal baseline_y = bg_rect.top() + padding_v + fm.ascent(); + qreal text_x = bg_rect.center().x() - (text_w / 2.0); + + QPainterPath textPath; + textPath.addText(QPointF(text_x, baseline_y), font, text); + boxPath = boxPath.subtracted(textPath); + + p.setPen(Qt::NoPen); + p.setBrush(longOverride ? QColor(0x91, 0x9b, 0x95, 0xf1) : QColor(0, 0xff, 0, 0xff)); + p.drawPath(boxPath); +} + int HudRendererSP::drawRightDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color) { p.setFont(InterFont(28, QFont::Bold)); diff --git a/selfdrive/ui/sunnypilot/qt/onroad/hud.h b/selfdrive/ui/sunnypilot/qt/onroad/hud.h index 75fc520fcf..4c92835957 100644 --- a/selfdrive/ui/sunnypilot/qt/onroad/hud.h +++ b/selfdrive/ui/sunnypilot/qt/onroad/hud.h @@ -26,6 +26,8 @@ private: int drawBottomDevUIElement(QPainter &p, int x, int y, const QString &value, const QString &label, const QString &units, QColor &color); void drawBottomDevUI(QPainter &p, int x, int y); void drawStandstillTimer(QPainter &p, int x, int y); + bool pulseElement(int frame); + void drawSmartCruiseControlOnroadIcon(QPainter &p, const QRect &surface_rect, int x_offset, int y_offset, std::string name); bool lead_status; float lead_d_rel; @@ -57,4 +59,8 @@ private: bool standstillTimer; bool isStandstill; float standstillElapsedTime; + bool longOverride; + bool smartCruiseControlVisionEnabled; + bool smartCruiseControlVisionActive; + int smartCruiseControlVisionFrame; }; diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index ec020000a3..f2358a2648 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -37,7 +37,8 @@ class LongitudinalPlannerSP: self.scc.update(sm, v_ego, a_ego, v_cruise) targets = { - Source.cruise : (v_cruise, a_ego), + Source.cruise: (v_cruise, a_ego), + Source.sccVision: (self.scc.vision.output_v_target, self.scc.vision.output_a_target) } self.source = min(targets, key=lambda k: targets[k][0]) @@ -63,6 +64,15 @@ class LongitudinalPlannerSP: dec.active = self.dec.active() # Smart Cruise Control - smartCruiseControl = longitudinalPlanSP.smartCruiseControl # noqa: F841 + smartCruiseControl = longitudinalPlanSP.smartCruiseControl + # Vision Turn Speed Control + sccVision = smartCruiseControl.vision + sccVision.state = self.scc.vision.state + sccVision.vTarget = float(self.scc.vision.output_v_target) + sccVision.aTarget = float(self.scc.vision.output_a_target) + sccVision.currentLateralAccel = float(self.scc.vision.current_lat_acc) + sccVision.maxPredictedLateralAccel = float(self.scc.vision.max_pred_lat_acc) + sccVision.enabled = self.scc.vision.is_enabled + sccVision.active = self.scc.vision.is_active pm.send('longitudinalPlanSP', plan_sp_send) diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py index 0a04c243c1..c66c2c392a 100644 --- a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/smart_cruise_control.py @@ -4,10 +4,16 @@ Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. This file is part of sunnypilot and is licensed under the MIT License. See the LICENSE.md file in the root directory for more details. """ -from cereal import messaging +import cereal.messaging as messaging +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision class SmartCruiseControl: + def __init__(self): + self.vision = SmartCruiseControlVision() - def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float): - pass + def update(self, sm: messaging.SubMaster, v_ego: float, a_ego: float, v_cruise: float) -> None: + long_enabled = sm['carControl'].enabled + long_override = sm['carControl'].cruiseControl.override + + self.vision.update(sm, long_enabled, long_override, v_ego, a_ego, v_cruise) diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py new file mode 100644 index 0000000000..9f6efffb55 --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/tests/test_vision_controller.py @@ -0,0 +1,104 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import numpy as np + +import cereal.messaging as messaging +from cereal import custom, log +from openpilot.common.params import Params +from openpilot.common.realtime import DT_MDL +from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET +from openpilot.selfdrive.modeld.constants import ModelConstants +from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.vision_controller import SmartCruiseControlVision + +VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState + + +def generate_modelV2(): + model = messaging.new_message('modelV2') + position = log.XYZTData.new_message() + speed = 30 + position.x = [float(x) for x in (speed + 0.5) * np.array(ModelConstants.T_IDXS)] + model.modelV2.position = position + orientation = log.XYZTData.new_message() + curvature = 0.05 + orientation.x = [float(curvature) for _ in ModelConstants.T_IDXS] + orientation.y = [0.0 for _ in ModelConstants.T_IDXS] + model.modelV2.orientation = orientation + orientationRate = log.XYZTData.new_message() + orientationRate.z = [float(z) for z in ModelConstants.T_IDXS] + model.modelV2.orientationRate = orientationRate + velocity = log.XYZTData.new_message() + velocity.x = [float(x) for x in (speed + 0.5) * np.ones_like(ModelConstants.T_IDXS)] + velocity.x[0] = float(speed) # always start at current speed + model.modelV2.velocity = velocity + acceleration = log.XYZTData.new_message() + acceleration.x = [float(x) for x in np.zeros_like(ModelConstants.T_IDXS)] + acceleration.y = [float(y) for y in np.zeros_like(ModelConstants.T_IDXS)] + model.modelV2.acceleration = acceleration + + return model + + +def generate_carState(): + car_state = messaging.new_message('carState') + speed = 30 + v_cruise = 50 + car_state.carState.vEgo = float(speed) + car_state.carState.standstill = False + car_state.carState.vCruise = float(v_cruise * 3.6) + + return car_state + + +def generate_controlsState(): + controls_state = messaging.new_message('controlsState') + controls_state.controlsState.curvature = 0.05 + + return controls_state + + +class TestSmartCruiseControlVision: + + def setup_method(self): + self.params = Params() + self.reset_params() + self.scc_v = SmartCruiseControlVision() + + mdl = generate_modelV2() + cs = generate_carState() + controls_state = generate_controlsState() + self.sm = {'modelV2': mdl.modelV2, 'carState': cs.carState, 'controlsState': controls_state.controlsState} + + def reset_params(self): + self.params.put_bool("SmartCruiseControlVision", True) + + def test_initial_state(self): + assert self.scc_v.state == VisionState.disabled + assert not self.scc_v.is_active + assert self.scc_v.output_v_target == V_CRUISE_UNSET + assert self.scc_v.output_a_target == 0. + + def test_system_disabled(self): + self.params.put_bool("SmartCruiseControlVision", False) + self.scc_v.enabled = self.params.get_bool("SmartCruiseControlVision") + + for _ in range(int(10. / DT_MDL)): + self.scc_v.update(self.sm, True, False, 0., 0., 0.) + assert self.scc_v.state == VisionState.disabled + assert not self.scc_v.is_active + + def test_disabled(self): + for _ in range(int(10. / DT_MDL)): + self.scc_v.update(self.sm, False, False, 0., 0., 0.) + assert self.scc_v.state == VisionState.disabled + + def test_transition_disabled_to_enabled(self): + for _ in range(int(10. / DT_MDL)): + self.scc_v.update(self.sm, True, False, 0., 0., 0.) + assert self.scc_v.state == VisionState.enabled + + # TODO-SP: mock modelV2 data to test other states diff --git a/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py new file mode 100644 index 0000000000..f12a00f23e --- /dev/null +++ b/sunnypilot/selfdrive/controls/lib/smart_cruise_control/vision_controller.py @@ -0,0 +1,205 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +import numpy as np + +import cereal.messaging as messaging +from cereal import custom +from openpilot.common.constants import CV +from openpilot.common.params import Params +from openpilot.common.realtime import DT_MDL +from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET +from openpilot.sunnypilot import PARAMS_UPDATE_PERIOD + +VisionState = custom.LongitudinalPlanSP.SmartCruiseControl.VisionState + +ACTIVE_STATES = (VisionState.entering, VisionState.turning, VisionState.leaving) +ENABLED_STATES = (VisionState.enabled, VisionState.overriding, *ACTIVE_STATES) + +_MIN_V = 20 * CV.KPH_TO_MS # Do not operate under 20 km/h + +_ENTERING_PRED_LAT_ACC_TH = 1.3 # Predicted Lat Acc threshold to trigger entering turn state. +_ABORT_ENTERING_PRED_LAT_ACC_TH = 1.1 # Predicted Lat Acc threshold to abort entering state if speed drops. + +_TURNING_LAT_ACC_TH = 1.6 # Lat Acc threshold to trigger turning state. + +_LEAVING_LAT_ACC_TH = 1.3 # Lat Acc threshold to trigger leaving turn state. +_FINISH_LAT_ACC_TH = 1.1 # Lat Acc threshold to trigger the end of the turn cycle. + +_A_LAT_REG_MAX = 2. # Maximum lateral acceleration + +_NO_OVERSHOOT_TIME_HORIZON = 4. # s. Time to use for velocity desired based on a_target when not overshooting. + +# Lookup table for the minimum smooth deceleration during the ENTERING state +# depending on the actual maximum absolute lateral acceleration predicted on the turn ahead. +_ENTERING_SMOOTH_DECEL_V = [-0.2, -1.] # min decel value allowed on ENTERING state +_ENTERING_SMOOTH_DECEL_BP = [1.3, 3.] # absolute value of lat acc ahead + +# Lookup table for the acceleration for the TURNING state +# depending on the current lateral acceleration of the vehicle. +_TURNING_ACC_V = [0.5, 0., -0.4] # acc value +_TURNING_ACC_BP = [1.5, 2.3, 3.] # absolute value of current lat acc + +_LEAVING_ACC = 0.5 # Conformable acceleration to regain speed while leaving a turn. + + +class SmartCruiseControlVision: + v_target: float = 0 + a_target: float = 0. + v_ego: float = 0. + a_ego: float = 0. + output_v_target: float = V_CRUISE_UNSET + output_a_target: float = 0. + + def __init__(self): + self.params = Params() + self.frame = -1 + self.long_enabled = False + self.long_override = False + self.is_enabled = False + self.is_active = False + self.enabled = self.params.get_bool("SmartCruiseControlVision") + self.v_cruise_setpoint = 0. + + self.state = VisionState.disabled + self.current_lat_acc = 0. + self.max_pred_lat_acc = 0. + + def get_a_target_from_control(self) -> float: + return self.a_target + + def get_v_target_from_control(self) -> float: + if self.is_active: + return max(self.v_target, _MIN_V) + self.a_target * _NO_OVERSHOOT_TIME_HORIZON + + return V_CRUISE_UNSET + + def _update_params(self) -> None: + if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0: + self.enabled = self.params.get_bool("SmartCruiseControlVision") + + def _update_calculations(self, sm: messaging.SubMaster) -> None: + if not self.long_enabled: + return + else: + rate_plan = np.array(np.abs(sm['modelV2'].orientationRate.z)) + vel_plan = np.array(sm['modelV2'].velocity.x) + + self.current_lat_acc = self.v_ego ** 2 * abs(sm['controlsState'].curvature) + + # get the maximum lat accel from the model + predicted_lat_accels = rate_plan * vel_plan + self.max_pred_lat_acc = np.amax(predicted_lat_accels) + + # get the maximum curve based on the current velocity + v_ego = max(self.v_ego, 0.1) # ensure a value greater than 0 for calculations + max_curve = self.max_pred_lat_acc / (v_ego**2) + + # Get the target velocity for the maximum curve + self.v_target = (_A_LAT_REG_MAX / max_curve) ** 0.5 + + def _update_state_machine(self) -> tuple[bool, bool]: + # ENABLED, ENTERING, TURNING, LEAVING + if self.state != VisionState.disabled: + # longitudinal and feature disable always have priority in a non-disabled state + if not self.long_enabled or not self.enabled: + self.state = VisionState.disabled + elif self.long_override: + self.state = VisionState.overriding + + else: + # ENABLED + if self.state == VisionState.enabled: + # Do not enter a turn control cycle if the speed is low. + if self.v_ego <= _MIN_V: + pass + # If significant lateral acceleration is predicted ahead, then move to Entering turn state. + elif self.max_pred_lat_acc >= _ENTERING_PRED_LAT_ACC_TH: + self.state = VisionState.entering + + # OVERRIDING + elif self.state == VisionState.overriding: + if not self.long_override: + self.state = VisionState.enabled + + # ENTERING + elif self.state == VisionState.entering: + # Transition to Turning if current lateral acceleration is over the threshold. + if self.current_lat_acc >= _TURNING_LAT_ACC_TH: + self.state = VisionState.turning + # Abort if the predicted lateral acceleration drops + elif self.max_pred_lat_acc < _ABORT_ENTERING_PRED_LAT_ACC_TH: + self.state = VisionState.enabled + + # TURNING + elif self.state == VisionState.turning: + # Transition to Leaving if current lateral acceleration drops below a threshold. + if self.current_lat_acc <= _LEAVING_LAT_ACC_TH: + self.state = VisionState.leaving + + # LEAVING + elif self.state == VisionState.leaving: + # Transition back to Turning if current lateral acceleration goes back over the threshold. + if self.current_lat_acc >= _TURNING_LAT_ACC_TH: + self.state = VisionState.turning + # Finish if current lateral acceleration goes below a threshold. + elif self.current_lat_acc < _FINISH_LAT_ACC_TH: + self.state = VisionState.enabled + + # DISABLED + elif self.state == VisionState.disabled: + if self.long_enabled and self.enabled: + if self.long_override: + self.state = VisionState.overriding + else: + self.state = VisionState.enabled + + enabled = self.state in ENABLED_STATES + active = self.state in ACTIVE_STATES + + return enabled, active + + def _update_solution(self) -> float: + # DISABLED, ENABLED + if self.state not in ACTIVE_STATES: + # when not overshooting, calculate v_turn as the speed at the prediction horizon when following + # the smooth deceleration. + a_target = self.a_ego + # ENTERING + elif self.state == VisionState.entering: + # when not overshooting, target a smooth deceleration in preparation for a sharp turn to come. + a_target = np.interp(self.max_pred_lat_acc, _ENTERING_SMOOTH_DECEL_BP, _ENTERING_SMOOTH_DECEL_V) + # TURNING + elif self.state == VisionState.turning: + # When turning, we provide a target acceleration that is comfortable for the lateral acceleration felt. + a_target = np.interp(self.current_lat_acc, _TURNING_ACC_BP, _TURNING_ACC_V) + # LEAVING + elif self.state == VisionState.leaving: + # When leaving, we provide a comfortable acceleration to regain speed. + a_target = _LEAVING_ACC + else: + raise NotImplementedError(f"SCC-V state not supported: {self.state}") + + return a_target + + def update(self, sm: messaging.SubMaster, long_enabled: bool, long_override: bool, v_ego: float, a_ego: float, + v_cruise_setpoint: float) -> None: + self.long_enabled = long_enabled + self.long_override = long_override + self.v_ego = v_ego + self.a_ego = a_ego + self.v_cruise_setpoint = v_cruise_setpoint + + self._update_params() + self._update_calculations(sm) + + self.is_enabled, self.is_active = self._update_state_machine() + self.a_target = self._update_solution() + + self.output_v_target = self.get_v_target_from_control() + self.output_a_target = self.get_a_target_from_control() + + self.frame += 1 From 1f8941367d52a34c313049f1da6d84d57e2bf940 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 18 Sep 2025 00:06:16 -0400 Subject: [PATCH 186/188] Intelligent Cruise Button Management (ICBM) (#1242) * init * slightly more * check across all * publish on CC_SP * more infra setup * try it out for HKG for now * slight cleanup * oops * legacy * send * actually take over * expose toggle * icbm * need to allow * fix * name * small fixes * actually send it now * set default * use cs is_metric * offroad only lol * allow them all * fix * send desire as-is * use stock method * clean up hysteresis * speed cluster may be more accurate * rename * allow init and resume from pcmCruise * just send it * fix * only allow custom v cruise after no button press at initial enabled * no hysteresis for now * fix tests * fix min check * only apply to non pcm changes * add ICBM * some more ui * bump * slight cleanup * fixup * cleanup * type hints * type hints * bump * bump * bring back hysteresis * fix ui * do not spam if overriding or below allowed speed --- cereal/custom.capnp | 24 +++ common/params_keys.h | 1 + opendbc_repo | 2 +- selfdrive/car/card.py | 2 +- selfdrive/car/cruise.py | 13 +- selfdrive/car/helpers.py | 3 + selfdrive/car/tests/test_cruise_speed.py | 5 +- selfdrive/controls/controlsd.py | 5 +- selfdrive/selfdrived/selfdrived.py | 9 ++ .../qt/offroad/settings/longitudinal_panel.cc | 30 +++- .../qt/offroad/settings/longitudinal_panel.h | 2 + sunnypilot/selfdrive/car/cruise_ext.py | 52 ++++++- .../__init__.py | 0 .../controller.py | 137 ++++++++++++++++++ .../helpers.py | 8 + sunnypilot/selfdrive/car/interfaces.py | 10 ++ .../selfdrive/controls/controlsd_ext.py | 2 + 17 files changed, 289 insertions(+), 16 deletions(-) create mode 100644 sunnypilot/selfdrive/car/intelligent_cruise_button_management/__init__.py create mode 100644 sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py create mode 100644 sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 66aa1b71f8..0791841a55 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -25,6 +25,26 @@ struct ModularAssistiveDrivingSystem { } } +struct IntelligentCruiseButtonManagement { + state @0 :IntelligentCruiseButtonManagementState; + sendButton @1 :SendButtonState; + vTarget @2 :Float32; + + enum IntelligentCruiseButtonManagementState { + inactive @0; # No button press or default state + preActive @1; # Pre-active state before transitioning to increasing or decreasing + increasing @2; # Increasing speed + decreasing @3; # Decreasing speed + holding @4; # Holding steady speed + } + + enum SendButtonState { + none @0; + increase @1; + decrease @2; + } +} + # Same struct as Log.RadarState.LeadData struct LeadData { dRel @0 :Float32; @@ -48,6 +68,7 @@ struct LeadData { struct SelfdriveStateSP @0x81c2f05a394cf4af { mads @0 :ModularAssistiveDrivingSystem; + intelligentCruiseButtonManagement @1 :IntelligentCruiseButtonManagement; } struct ModelManagerSP @0xaedffd8f31e7b55d { @@ -210,6 +231,8 @@ struct OnroadEventSP @0xda96579883444c35 { struct CarParamsSP @0x80ae746ee2596b11 { flags @0 :UInt32; # flags for car specific quirks in sunnypilot safetyParam @1 : Int16; # flags for sunnypilot's custom safety flags + pcmCruiseSpeed @3 :Bool = true; + intelligentCruiseButtonManagementAvailable @4 :Bool; neuralNetworkLateralControl @2 :NeuralNetworkLateralControl; @@ -229,6 +252,7 @@ struct CarControlSP @0xa5cd762cd951a455 { params @1 :List(Param); leadOne @2 :LeadData; leadTwo @3 :LeadData; + intelligentCruiseButtonManagement @4 :IntelligentCruiseButtonManagement; struct Param { key @0 :Text; diff --git a/common/params_keys.h b/common/params_keys.h index f0511bf9c2..80d12c6b95 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -149,6 +149,7 @@ inline static std::unordered_map keys = { {"EnableCopyparty", {PERSISTENT | BACKUP, BOOL}}, {"EnableGithubRunner", {PERSISTENT | BACKUP, BOOL}}, {"GithubRunnerSufficientVoltage", {CLEAR_ON_MANAGER_START , BOOL}}, + {"IntelligentCruiseButtonManagement", {PERSISTENT | BACKUP , BOOL}}, {"InteractivityTimeout", {PERSISTENT | BACKUP, INT, "0"}}, {"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}}, {"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}}, diff --git a/opendbc_repo b/opendbc_repo index 189dc3f78c..f54d85c7d9 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 189dc3f78c3b2eeb0402e69760602aa7149d05c9 +Subproject commit f54d85c7d99d45ada6f6978621b10c8b421b1923 diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index 820c68e895..2e52c00827 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -179,7 +179,7 @@ class Car: self.params.put_nonblocking("CarParamsSPPersistent", cp_sp_bytes) self.mock_carstate = MockCarState() - self.v_cruise_helper = VCruiseHelper(self.CP) + self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_SP) self.is_metric = self.params.get_bool("IsMetric") self.experimental_mode = self.params.get_bool("ExperimentalMode") diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 5ffb43f9e5..c82287d2b1 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -30,8 +30,8 @@ CRUISE_INTERVAL_SIGN = { class VCruiseHelper(VCruiseHelperSP): - def __init__(self, CP): - VCruiseHelperSP.__init__(self) + def __init__(self, CP, CP_SP): + VCruiseHelperSP.__init__(self, CP, CP_SP) self.CP = CP self.v_cruise_kph = V_CRUISE_UNSET self.v_cruise_cluster_kph = V_CRUISE_UNSET @@ -46,10 +46,13 @@ class VCruiseHelper(VCruiseHelperSP): def update_v_cruise(self, CS, enabled, is_metric): self.v_cruise_kph_last = self.v_cruise_kph + self.get_minimum_set_speed(is_metric) + if CS.cruiseState.available: - if not self.CP.pcmCruise: + _enabled = self.update_enabled_state(CS, enabled) + if not self.CP.pcmCruise or (not self.CP_SP.pcmCruiseSpeed and _enabled): # if stock cruise is completely disabled, then we can use our own set speed logic - self._update_v_cruise_non_pcm(CS, enabled, is_metric) + self._update_v_cruise_non_pcm(CS, _enabled, is_metric) self.v_cruise_cluster_kph = self.v_cruise_kph self.update_button_timers(CS, enabled) else: @@ -111,7 +114,7 @@ class VCruiseHelper(VCruiseHelperSP): if CS.gasPressed and button_type in (ButtonType.decelCruise, ButtonType.setCruise): self.v_cruise_kph = max(self.v_cruise_kph, CS.vEgo * CV.MS_TO_KPH) - self.v_cruise_kph = np.clip(round(self.v_cruise_kph, 1), V_CRUISE_MIN, V_CRUISE_MAX) + self.v_cruise_kph = np.clip(round(self.v_cruise_kph, 1), self.v_cruise_min, V_CRUISE_MAX) def update_button_timers(self, CS, enabled): # increment timer for buttons still pressed diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py index 275c84479f..a7abc1976c 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -60,5 +60,8 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct struct_dataclass.params = [structs.CarControlSP.Param(**remove_deprecated(p)) for p in struct_dict.get('params', [])] struct_dataclass.leadOne = structs.LeadData(**remove_deprecated(struct_dict.get('leadOne', {}))) struct_dataclass.leadTwo = structs.LeadData(**remove_deprecated(struct_dict.get('leadTwo', {}))) + struct_dataclass.intelligentCruiseButtonManagement = structs.IntelligentCruiseButtonManagement( + **remove_deprecated(struct_dict.get('intelligentCruiseButtonManagement', {})) + ) return struct_dataclass diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index 4f9444d4bb..8512651de3 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -5,7 +5,7 @@ import numpy as np from parameterized import parameterized_class from cereal import log from openpilot.selfdrive.car.cruise import VCruiseHelper, V_CRUISE_MIN, V_CRUISE_MAX, V_CRUISE_INITIAL, IMPERIAL_INCREMENT -from cereal import car +from cereal import car, custom from openpilot.common.constants import CV from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver @@ -49,7 +49,8 @@ class TestCruiseSpeed: class TestVCruiseHelper: def setup_method(self): self.CP = car.CarParams(pcmCruise=self.pcm_cruise) - self.v_cruise_helper = VCruiseHelper(self.CP) + self.CP_SP = custom.CarParamsSP() + self.v_cruise_helper = VCruiseHelper(self.CP, self.CP_SP) self.reset_cruise_speed_state() def reset_cruise_speed_state(self): diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 9e88a37690..3751efc87a 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -116,7 +116,8 @@ class Controls(ControlsExt, ModelStateBase): CC.latActive = _lat_active and not CS.steerFaultTemporary and not CS.steerFaultPermanent and \ (not standstill or self.CP.steerAtStandstill) - CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and self.CP.openpilotLongitudinalControl + CC.longActive = CC.enabled and not any(e.overrideLongitudinal for e in self.sm['onroadEvents']) and \ + (self.CP.openpilotLongitudinalControl or not self.CP_SP.pcmCruiseSpeed) actuators = CC.actuators actuators.longControlState = self.LoC.long_control_state @@ -168,7 +169,7 @@ class Controls(ControlsExt, ModelStateBase): CC.orientationNED = self.calibrated_pose.orientation.xyz.tolist() CC.angularVelocity = self.calibrated_pose.angular_velocity.xyz.tolist() - CC.cruiseControl.override = CC.enabled and not CC.longActive and self.CP.openpilotLongitudinalControl + CC.cruiseControl.override = CC.enabled and not CC.longActive and (self.CP.openpilotLongitudinalControl or not self.CP_SP.pcmCruiseSpeed) CC.cruiseControl.cancel = CS.cruiseState.enabled and (not CC.enabled or not self.CP.pcmCruise) CC.cruiseControl.resume = CC.enabled and CS.cruiseState.standstill and not self.sm['longitudinalPlan'].shouldStop diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index 749378f3a6..69dc51360b 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -26,6 +26,7 @@ from openpilot.system.version import get_build_metadata from openpilot.sunnypilot.mads.mads import ModularAssistiveDrivingSystem from openpilot.sunnypilot.selfdrive.car.car_specific import CarSpecificEventsSP from openpilot.sunnypilot.selfdrive.car.cruise_helpers import CruiseHelper +from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.controller import IntelligentCruiseButtonManagement from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP REPLAY = "REPLAY" in os.environ @@ -156,6 +157,7 @@ class SelfdriveD(CruiseHelper): self.events_sp_prev = [] self.mads = ModularAssistiveDrivingSystem(self) + self.icbm = IntelligentCruiseButtonManagement(self.CP, self.CP_SP) self.car_events_sp = CarSpecificEventsSP(self.CP, self.params) @@ -442,6 +444,8 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.personalityChanged) self.experimental_mode_switched = False + self.icbm.run(CS, self.sm['carControl'], self.is_metric) + def data_sample(self): _car_state = messaging.recv_one(self.car_state_sock) CS = _car_state.carState if _car_state else self.CS_prev @@ -546,6 +550,11 @@ class SelfdriveD(CruiseHelper): mads.active = self.mads.active mads.available = self.mads.enabled_toggle + icbm = ss_sp.intelligentCruiseButtonManagement + icbm.state = self.icbm.state + icbm.sendButton = self.icbm.cruise_button + icbm.vTarget = self.icbm.v_target + self.pm.send('selfdriveStateSP', ss_sp_msg) # onroadEventsSP - logged every second or on change diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc index ac7b57e297..df4a6077b5 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc @@ -18,6 +18,16 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) { cruisePanelScroller = new ScrollViewSP(list, this); vlayout->addWidget(cruisePanelScroller); + intelligentCruiseButtonManagement = new ParamControlSP( + "IntelligentCruiseButtonManagement", + tr("Intelligent Cruise Button Management (ICBM) (Alpha)"), + tr("When enabled, sunnypilot will attempt to manage the built-in cruise control buttons by emulating button presses for limited longitudinal control."), + "", + this + ); + intelligentCruiseButtonManagement->setConfirmation(true, false); + list->addItem(intelligentCruiseButtonManagement); + SmartCruiseControlVision = new ParamControl( "SmartCruiseControlVision", tr("Smart Cruise Control - Vision"), @@ -42,16 +52,22 @@ void LongitudinalPanel::showEvent(QShowEvent *event) { void LongitudinalPanel::refresh(bool _offroad) { auto cp_bytes = params.get("CarParamsPersistent"); - if (!cp_bytes.empty()) { + auto cp_sp_bytes = params.get("CarParamsSPPersistent"); + if (!cp_bytes.empty() && !cp_sp_bytes.empty()) { AlignedBuffer aligned_buf; + AlignedBuffer aligned_buf_sp; capnp::FlatArrayMessageReader cmsg(aligned_buf.align(cp_bytes.data(), cp_bytes.size())); + capnp::FlatArrayMessageReader cmsg_sp(aligned_buf_sp.align(cp_sp_bytes.data(), cp_sp_bytes.size())); cereal::CarParams::Reader CP = cmsg.getRoot(); + cereal::CarParamsSP::Reader CP_SP = cmsg_sp.getRoot(); has_longitudinal_control = hasLongitudinalControl(CP); is_pcm_cruise = CP.getPcmCruise(); + intelligent_cruise_button_management_available = CP_SP.getIntelligentCruiseButtonManagementAvailable(); } else { has_longitudinal_control = false; is_pcm_cruise = false; + intelligent_cruise_button_management_available = false; } QString accEnabledDescription = tr("Enable custom Short & Long press increments for cruise speed increase/decrease."); @@ -63,7 +79,7 @@ void LongitudinalPanel::refresh(bool _offroad) { customAccIncrement->setDescription(onroadOnlyDescription); customAccIncrement->showDescription(); } else { - if (has_longitudinal_control) { + if (has_longitudinal_control || intelligent_cruise_button_management_available) { if (is_pcm_cruise) { customAccIncrement->setDescription(accPcmCruiseDisabledDescription); customAccIncrement->showDescription(); @@ -75,14 +91,20 @@ void LongitudinalPanel::refresh(bool _offroad) { customAccIncrement->toggleFlipped(false); customAccIncrement->setDescription(accNoLongDescription); customAccIncrement->showDescription(); + params.remove("IntelligentCruiseButtonManagement"); + intelligentCruiseButtonManagement->toggleFlipped(false); } } + bool icbm_allowed = intelligent_cruise_button_management_available && !has_longitudinal_control; + intelligentCruiseButtonManagement->setEnabled(icbm_allowed && offroad); + // enable toggle when long is available and is not PCM cruise - customAccIncrement->setEnabled(has_longitudinal_control && !is_pcm_cruise && !offroad); + bool cai_allowed = (has_longitudinal_control && !is_pcm_cruise) || icbm_allowed; + customAccIncrement->setEnabled(cai_allowed && !offroad); customAccIncrement->refresh(); - SmartCruiseControlVision->setEnabled(has_longitudinal_control); + SmartCruiseControlVision->setEnabled(has_longitudinal_control || icbm_allowed); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h index 36c35720e7..af89822595 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h @@ -23,6 +23,7 @@ private: Params params; bool has_longitudinal_control = false; bool is_pcm_cruise = false; + bool intelligent_cruise_button_management_available = false;; bool offroad = false; QStackedLayout *main_layout = nullptr; @@ -30,4 +31,5 @@ private: QWidget *cruisePanelScreen = nullptr; CustomAccIncrement *customAccIncrement = nullptr; ParamControl *SmartCruiseControlVision; + ParamControl *intelligentCruiseButtonManagement = nullptr; }; diff --git a/sunnypilot/selfdrive/car/cruise_ext.py b/sunnypilot/selfdrive/car/cruise_ext.py index f443aeee0e..716e3e1c93 100644 --- a/sunnypilot/selfdrive/car/cruise_ext.py +++ b/sunnypilot/selfdrive/car/cruise_ext.py @@ -7,19 +7,46 @@ See the LICENSE.md file in the root directory for more details. import numpy as np from cereal import car +from opendbc.car import structs from openpilot.common.params import Params +from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.helpers import get_minimum_set_speed ButtonType = car.CarState.ButtonEvent.Type +CRUISE_BUTTON_TIMER = {ButtonType.decelCruise: 0, ButtonType.accelCruise: 0, + ButtonType.setCruise: 0, ButtonType.resumeCruise: 0, + ButtonType.cancel: 0, ButtonType.mainCruise: 0} + +V_CRUISE_MIN = 8 + + +def update_manual_button_timers(CS: car.CarState, button_timers: dict[car.CarState.ButtonEvent.Type, int]) -> None: + # increment timer for buttons still pressed + for k in button_timers: + if button_timers[k] > 0: + button_timers[k] += 1 + + for b in CS.buttonEvents: + if b.type.raw in button_timers: + # Start/end timer and store current state on change of button pressed + button_timers[b.type.raw] = 1 if b.pressed else 0 + + class VCruiseHelperSP: - def __init__(self) -> None: + def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP) -> None: + self.CP = CP + self.CP_SP = CP_SP self.params = Params() + self.v_cruise_min = 0 + self.enabled_prev = False self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") self.short_increment = self.params.get("CustomAccShortPressIncrement", return_default=True) self.long_increment = self.params.get("CustomAccLongPressIncrement", return_default=True) + self.enable_button_timers = CRUISE_BUTTON_TIMER + def read_custom_set_speed_params(self) -> None: self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") self.short_increment = self.params.get("CustomAccShortPressIncrement", return_default=True) @@ -39,3 +66,26 @@ class VCruiseHelperSP: v_cruise_delta = v_cruise_delta * actual_increment return round_to_nearest, v_cruise_delta + + def get_minimum_set_speed(self, is_metric: bool) -> None: + if self.CP_SP.pcmCruiseSpeed: + self.v_cruise_min = V_CRUISE_MIN + return + + self.v_cruise_min = get_minimum_set_speed(is_metric) + + def update_enabled_state(self, CS: car.CarState, enabled: bool) -> bool: + # special enabled state for non pcmCruiseSpeed, unchanged for non pcmCruise + if not self.CP_SP.pcmCruiseSpeed: + update_manual_button_timers(CS, self.enable_button_timers) + button_pressed = any(self.enable_button_timers[k] > 0 for k in self.enable_button_timers) + + if enabled and not self.enabled_prev: + self.enabled_prev = not button_pressed + enabled = False + elif not enabled: + self.enabled_prev = enabled + + return enabled and self.enabled_prev + + return enabled diff --git a/sunnypilot/selfdrive/car/intelligent_cruise_button_management/__init__.py b/sunnypilot/selfdrive/car/intelligent_cruise_button_management/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py b/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py new file mode 100644 index 0000000000..0b0957b828 --- /dev/null +++ b/sunnypilot/selfdrive/car/intelligent_cruise_button_management/controller.py @@ -0,0 +1,137 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +from cereal import car, custom +from opendbc.car import structs, apply_hysteresis +from openpilot.common.constants import CV +from openpilot.common.realtime import DT_CTRL +from openpilot.sunnypilot.selfdrive.car.intelligent_cruise_button_management.helpers import get_minimum_set_speed +from openpilot.sunnypilot.selfdrive.car.cruise_ext import CRUISE_BUTTON_TIMER, update_manual_button_timers + +LongitudinalPlanSource = custom.LongitudinalPlanSP.LongitudinalPlanSource +State = custom.IntelligentCruiseButtonManagement.IntelligentCruiseButtonManagementState +SendButtonState = custom.IntelligentCruiseButtonManagement.SendButtonState + +ALLOWED_SPEED_THRESHOLD = 1.8 # m/s, ~4 MPH +HYST_GAP = 0.75 +INACTIVE_TIMER = 0.4 + + +SEND_BUTTONS = { + State.increasing: SendButtonState.increase, + State.decreasing: SendButtonState.decrease, +} + + +class IntelligentCruiseButtonManagement: + def __init__(self, CP: structs.CarParams, CP_SP: structs.CarParamsSP): + self.CP = CP + self.CP_SP = CP_SP + + self.v_target = 0 + self.v_cruise_cluster = 0 + self.v_cruise_min = 0 + self.cruise_button = SendButtonState.none + self.state = State.inactive + self.pre_active_timer = 0 + + self.is_ready = False + self.is_ready_prev = False + self.v_target_ms_last = 0.0 + self.is_metric = False + + self.cruise_button_timers = CRUISE_BUTTON_TIMER + + @property + def v_cruise_equal(self) -> bool: + return self.v_target == self.v_cruise_cluster + + def update_calculations(self, CS: car.CarState) -> None: + speed_conv = CV.MS_TO_KPH if self.is_metric else CV.MS_TO_MPH + ms_conv = CV.KPH_TO_MS if self.is_metric else CV.MPH_TO_MS + v_cruise_ms = CS.vCruise * CV.KPH_TO_MS + + # all targets in m/s + v_targets = { + LongitudinalPlanSource.cruise: v_cruise_ms + } + source = min(v_targets, key=lambda k: v_targets[k]) + v_target_ms = v_targets[source] + + self.v_target_ms_last = apply_hysteresis(v_target_ms, self.v_target_ms_last, HYST_GAP * ms_conv) + + self.v_target = round(self.v_target_ms_last * speed_conv) + self.v_cruise_min = get_minimum_set_speed(self.is_metric) + self.v_cruise_cluster = round(CS.cruiseState.speedCluster * speed_conv) + + def update_state_machine(self) -> custom.IntelligentCruiseButtonManagement.SendButtonState: + self.pre_active_timer = max(0, self.pre_active_timer - 1) + + # HOLDING, ACCELERATING, DECELERATING, PRE_ACTIVE + if self.state != State.inactive: + if not self.is_ready: + self.state = State.inactive + + else: + # PRE_ACTIVE + if self.state == State.preActive: + if self.pre_active_timer <= 0: + if self.v_cruise_equal: + self.state = State.holding + + elif self.v_target > self.v_cruise_cluster: + self.state = State.increasing + + elif self.v_target < self.v_cruise_cluster and self.v_cruise_cluster > self.v_cruise_min: + self.state = State.decreasing + + # HOLDING + elif self.state == State.holding: + if not self.v_cruise_equal: + self.state = State.preActive + + # ACCELERATING + elif self.state == State.increasing: + if self.v_target <= self.v_cruise_cluster: + self.state = State.holding + + # DECELERATING + elif self.state == State.decreasing: + if self.v_target >= self.v_cruise_cluster or self.v_cruise_cluster <= self.v_cruise_min: + self.state = State.holding + + # INACTIVE + elif self.state == State.inactive: + if self.is_ready and not self.is_ready_prev: + self.pre_active_timer = int(INACTIVE_TIMER / DT_CTRL) + self.state = State.preActive + + send_button = SEND_BUTTONS.get(self.state, SendButtonState.none) + + return send_button + + def update_readiness(self, CS: car.CarState, CC: car.CarControl) -> None: + update_manual_button_timers(CS, self.cruise_button_timers) + + allowed_speed = CS.vEgo > ALLOWED_SPEED_THRESHOLD + ready = CS.cruiseState.enabled and allowed_speed and not CC.cruiseControl.override and not CC.cruiseControl.cancel and \ + not CC.cruiseControl.resume + button_pressed = any(self.cruise_button_timers[k] > 0 for k in self.cruise_button_timers) + + self.is_ready = ready and not button_pressed + + def run(self, CS: car.CarState, CC: car.CarControl, is_metric: bool) -> None: + if self.CP_SP.pcmCruiseSpeed: + return + + self.is_metric = is_metric + + self.update_calculations(CS) + self.update_readiness(CS, CC) + + self.cruise_button = self.update_state_machine() + + self.is_ready_prev = self.is_ready diff --git a/sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py b/sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py new file mode 100644 index 0000000000..eb4bbdecb5 --- /dev/null +++ b/sunnypilot/selfdrive/car/intelligent_cruise_button_management/helpers.py @@ -0,0 +1,8 @@ +""" +Copyright (c) 2021-, Haibin Wen, sunnypilot, and a number of other contributors. + +This file is part of sunnypilot and is licensed under the MIT License. +See the LICENSE.md file in the root directory for more details. +""" +def get_minimum_set_speed(is_metric: bool) -> int: + return 30 if is_metric else 20 diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index cd7a24b63b..55244caa57 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -43,11 +43,21 @@ def _initialize_neural_network_lateral_control(CI: CarInterfaceBase, CP: structs CP_SP.neuralNetworkLateralControl.fuzzyFingerprint = not exact_match +def _initialize_intelligent_cruise_button_management(CP: structs.CarParams, CP_SP: structs.CarParamsSP, params: Params = None) -> None: + if params is None: + params = Params() + + icbm_enabled = params.get_bool("IntelligentCruiseButtonManagement") + if icbm_enabled and CP_SP.intelligentCruiseButtonManagementAvailable and not CP.openpilotLongitudinalControl: + CP_SP.pcmCruiseSpeed = False + + def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None: CP = CI.CP CP_SP = CI.CP_SP _initialize_neural_network_lateral_control(CI, CP, CP_SP, params) + _initialize_intelligent_cruise_button_management(CP, CP_SP, params) def initialize_params(params) -> list[dict[str, Any]]: diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index a096b7dc8c..e0f9326bf8 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -75,6 +75,8 @@ class ControlsExt: CC_SP.params = self.param_store.param_list + CC_SP.intelligentCruiseButtonManagement = sm['selfdriveStateSP'].intelligentCruiseButtonManagement + return CC_SP @staticmethod From c95cff27e8b3149dde69aadbb6c4d5e446342647 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 18 Sep 2025 00:08:23 -0400 Subject: [PATCH 187/188] Speed Limit Control -> Speed Limit Assist --- cereal/custom.capnp | 9 +- common/params_keys.h | 4 +- .../qt/offroad/settings/longitudinal_panel.cc | 8 + .../qt/offroad/settings/longitudinal_panel.h | 1 + .../controls/lib/longitudinal_planner.py | 4 +- .../__init__.py | 4 +- .../common.py | 0 .../speed_limit_assist.py} | 72 +++---- .../speed_limit_resolver.py | 8 +- .../tests/__init__.py | 0 .../tests/test_speed_limit_assist.py} | 198 +++++++++--------- .../tests/test_speed_limit_resolver.py | 6 +- sunnypilot/selfdrive/selfdrived/events.py | 2 +- 13 files changed, 163 insertions(+), 153 deletions(-) rename sunnypilot/selfdrive/controls/lib/{speed_limit_controller => speed_limit_assist}/__init__.py (91%) rename sunnypilot/selfdrive/controls/lib/{speed_limit_controller => speed_limit_assist}/common.py (100%) rename sunnypilot/selfdrive/controls/lib/{speed_limit_controller/speed_limit_controller.py => speed_limit_assist/speed_limit_assist.py} (75%) rename sunnypilot/selfdrive/controls/lib/{speed_limit_controller => speed_limit_assist}/speed_limit_resolver.py (92%) rename sunnypilot/selfdrive/controls/lib/{speed_limit_controller => speed_limit_assist}/tests/__init__.py (100%) rename sunnypilot/selfdrive/controls/lib/{speed_limit_controller/tests/test_speed_limit_controller.py => speed_limit_assist/tests/test_speed_limit_assist.py} (53%) rename sunnypilot/selfdrive/controls/lib/{speed_limit_controller => speed_limit_assist}/tests/test_speed_limit_resolver.py (93%) diff --git a/cereal/custom.capnp b/cereal/custom.capnp index 55264c8c21..f34e0b1828 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -124,7 +124,7 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { dec @0 :DynamicExperimentalControl; longitudinalPlanSource @1 :LongitudinalPlanSource; smartCruiseControl @2 :SmartCruiseControl; - slc @3 :SpeedLimitControl; + speedLimitAssist @3 :SpeedLimitAssist; events @4 :List(OnroadEventSP.Event); struct DynamicExperimentalControl { @@ -161,8 +161,8 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { } } - struct SpeedLimitControl { - state @0 :SpeedLimitControlState; + struct SpeedLimitAssist { + state @0 :SpeedLimitAssistState; enabled @1 :Bool; active @2 :Bool; speedLimit @3 :Float32; @@ -174,9 +174,10 @@ struct LongitudinalPlanSP @0xf35cc4560bbf6ec2 { enum LongitudinalPlanSource { cruise @0; sccVision @1; + speedLimitAssist @2; } - enum SpeedLimitControlState { + enum SpeedLimitAssistState { disabled @0; inactive @1; # No speed limit set or not enabled by parameter. preActive @2; diff --git a/common/params_keys.h b/common/params_keys.h index 4f53927383..0eaf6c2331 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -226,8 +226,8 @@ inline static std::unordered_map keys = { {"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING}}, // Speed Limit Control - {"SpeedLimitControl", {PERSISTENT | BACKUP, BOOL, "0"}}, - {"SpeedLimitControlPolicy", {PERSISTENT | BACKUP, INT, "3"}}, + {"SpeedLimitAssist", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"SpeedLimitAssistPolicy", {PERSISTENT | BACKUP, INT, "3"}}, {"SpeedLimitEngageType", {PERSISTENT | BACKUP, INT, "0"}}, {"SpeedLimitOffsetType", {PERSISTENT | BACKUP, INT, "0"}}, {"SpeedLimitValueOffset", {PERSISTENT | BACKUP, INT, "0"}}, diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc index ac7b57e297..7b2ab508ab 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc @@ -25,6 +25,13 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) { ""); list->addItem(SmartCruiseControlVision); + speedLimitAssist = new ParamControl( + "SpeedLimitAssist", + tr("Speed Limit Assist (SLA)"), + tr("When you engage ACC, you will be prompted to set the cruising speed to the speed limit of the road adjusted by the Offset and Source Policy specified, or the current driving speed. The maximum cruising speed will always be the MAX set speed."), + ""); + list->addItem(speedLimitAssist); + customAccIncrement = new CustomAccIncrement("CustomAccIncrementsEnabled", tr("Custom ACC Speed Increments"), "", "", this); list->addItem(customAccIncrement); @@ -83,6 +90,7 @@ void LongitudinalPanel::refresh(bool _offroad) { customAccIncrement->refresh(); SmartCruiseControlVision->setEnabled(has_longitudinal_control); + speedLimitAssist->setEnabled(has_longitudinal_control); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h index 36c35720e7..f497472ac2 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h @@ -30,4 +30,5 @@ private: QWidget *cruisePanelScreen = nullptr; CustomAccIncrement *customAccIncrement = nullptr; ParamControl *SmartCruiseControlVision; + ParamControl *speedLimitAssist; }; diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 97329e4659..e20e80f99c 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -9,8 +9,8 @@ from cereal import messaging, custom from opendbc.car import structs from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController from openpilot.sunnypilot.selfdrive.controls.lib.smart_cruise_control.smart_cruise_control import SmartCruiseControl -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.speed_limit_assist import SpeedLimitAssist +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.speed_limit_resolver import SpeedLimitResolver from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.sunnypilot.models.helpers import get_active_bundle diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/__init__.py similarity index 91% rename from sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py rename to sunnypilot/selfdrive/controls/lib/speed_limit_assist/__init__.py index ce4ae1c63c..2ac5b6a6f0 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/__init__.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/__init__.py @@ -1,6 +1,6 @@ from cereal import custom -SpeedLimitControlState = custom.LongitudinalPlanSP.SpeedLimitControlState +SpeedLimitAssistState = custom.LongitudinalPlanSP.SpeedLimitAssistState PARAMS_UPDATE_PERIOD = 3. # secs. Time between parameter updates. DISABLED_GUARD_PERIOD = 2 # secs. @@ -14,7 +14,7 @@ LIMIT_MIN_SPEED = 8.33 # m/s, Minimum speed limit to provide as solution on lim LIMIT_SPEED_OFFSET_TH = -1. # m/s Maximum offset between speed limit and current speed for adapting state. LIMIT_MAX_MAP_DATA_AGE = 10. # s Maximum time to hold to map data, then consider it invalid inside limits controllers. -# Speed Limit Control Auto mode constants +# Speed Limit Assist Auto mode constants REQUIRED_INITIAL_MAX_SET_SPEED = 35.7632 # m/s 80 MPH # TODO-SP: customizable with params CRUISE_SPEED_TOLERANCE = 0.44704 # m/s ±1 MPH tolerance # TODO-SP: metric vs imperial FALLBACK_CRUISE_SPEED = 255.0 # m/s fallback when no speed limit available diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/common.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/speed_limit_controller/common.py rename to sunnypilot/selfdrive/controls/lib/speed_limit_assist/common.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/speed_limit_assist.py similarity index 75% rename from sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py rename to sunnypilot/selfdrive/controls/lib/speed_limit_assist/speed_limit_assist.py index 57c3284f0a..de9ebecaac 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/speed_limit_assist.py @@ -11,21 +11,21 @@ from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, \ - SpeedLimitControlState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE, DISABLED_GUARD_PERIOD +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist import PARAMS_UPDATE_PERIOD, LIMIT_SPEED_OFFSET_TH, \ + SpeedLimitAssistState, PRE_ACTIVE_GUARD_PERIOD, REQUIRED_INITIAL_MAX_SET_SPEED, CRUISE_SPEED_TOLERANCE, DISABLED_GUARD_PERIOD from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import OffsetType +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.common import OffsetType from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP from openpilot.selfdrive.modeld.constants import ModelConstants EventNameSP = custom.OnroadEventSP.EventName SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource -ACTIVE_STATES = (SpeedLimitControlState.active, SpeedLimitControlState.adapting) -ENABLED_STATES = (SpeedLimitControlState.preActive, SpeedLimitControlState.pending, *ACTIVE_STATES) +ACTIVE_STATES = (SpeedLimitAssistState.active, SpeedLimitAssistState.adapting) +ENABLED_STATES = (SpeedLimitAssistState.preActive, SpeedLimitAssistState.pending, *ACTIVE_STATES) -class SpeedLimitController: +class SpeedLimitAssist: _speed_limit: float _distance: float _source: custom.LongitudinalPlanSP.SpeedLimitSource @@ -41,7 +41,7 @@ class SpeedLimitController: self.long_engaged_timer = 0 self.pre_active_timer = 0 self.is_metric = self.params.get_bool("IsMetric") - self.enabled = self.params.get_bool("SpeedLimitControl") + self.enabled = self.params.get_bool("SpeedLimitAssist") self.op_engaged = False self.op_engaged_prev = False self.is_enabled = False @@ -57,8 +57,8 @@ class SpeedLimitController: self.last_valid_speed_limit_final = 0. self._distance = 0. self._source = SpeedLimitSource.none - self.state = SpeedLimitControlState.disabled - self._state_prev = SpeedLimitControlState.disabled + self.state = SpeedLimitAssistState.disabled + self._state_prev = SpeedLimitAssistState.disabled self.pcm_cruise_op_long = CP.openpilotLongitudinalControl and CP.pcmCruise self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) @@ -66,12 +66,12 @@ class SpeedLimitController: # Solution functions mapped to respective states self.acceleration_solutions = { - SpeedLimitControlState.disabled: self.get_current_acceleration_as_target, - SpeedLimitControlState.inactive: self.get_current_acceleration_as_target, - SpeedLimitControlState.preActive: self.get_current_acceleration_as_target, - SpeedLimitControlState.pending: self.get_current_acceleration_as_target, - SpeedLimitControlState.adapting: self.get_adapting_state_target_acceleration, - SpeedLimitControlState.active: self.get_active_state_target_acceleration, + SpeedLimitAssistState.disabled: self.get_current_acceleration_as_target, + SpeedLimitAssistState.inactive: self.get_current_acceleration_as_target, + SpeedLimitAssistState.preActive: self.get_current_acceleration_as_target, + SpeedLimitAssistState.pending: self.get_current_acceleration_as_target, + SpeedLimitAssistState.adapting: self.get_adapting_state_target_acceleration, + SpeedLimitAssistState.active: self.get_active_state_target_acceleration, } @property @@ -116,7 +116,7 @@ class SpeedLimitController: def update_params(self) -> None: if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0: - self.enabled = self.params.get_bool("SpeedLimitControl") + self.enabled = self.params.get_bool("SpeedLimitAssist") self.offset_type = OffsetType(self.params.get("SpeedLimitOffsetType", return_default=True)) self.offset_value = self.params.get("SpeedLimitValueOffset", return_default=True) self.is_metric = self.params.get_bool("IsMetric") @@ -157,61 +157,61 @@ class SpeedLimitController: self.pre_active_timer = max(0, self.pre_active_timer - 1) # ACTIVE, ADAPTING, PENDING, PRE_ACTIVE, INACTIVE - if self.state != SpeedLimitControlState.disabled: + if self.state != SpeedLimitAssistState.disabled: if not self.op_engaged or not self.enabled: - self.state = SpeedLimitControlState.disabled + self.state = SpeedLimitAssistState.disabled self.initial_max_set = False else: # ACTIVE - if self.state == SpeedLimitControlState.active: + if self.state == SpeedLimitAssistState.active: if self.detect_manual_cruise_change(): - self.state = SpeedLimitControlState.inactive + self.state = SpeedLimitAssistState.inactive elif self._speed_limit > 0 and self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting + self.state = SpeedLimitAssistState.adapting # ADAPTING - elif self.state == SpeedLimitControlState.adapting: + elif self.state == SpeedLimitAssistState.adapting: if self.detect_manual_cruise_change(): - self.state = SpeedLimitControlState.inactive + self.state = SpeedLimitAssistState.inactive elif self.v_offset >= LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.active + self.state = SpeedLimitAssistState.active # PENDING - elif self.state == SpeedLimitControlState.pending: + elif self.state == SpeedLimitAssistState.pending: if self._speed_limit > 0: if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting + self.state = SpeedLimitAssistState.adapting else: - self.state = SpeedLimitControlState.active + self.state = SpeedLimitAssistState.active # PRE_ACTIVE - elif self.state == SpeedLimitControlState.preActive: + elif self.state == SpeedLimitAssistState.preActive: if self.initial_max_set_confirmed(): self.initial_max_set = True if self._speed_limit > 0: if self.v_offset < LIMIT_SPEED_OFFSET_TH: - self.state = SpeedLimitControlState.adapting + self.state = SpeedLimitAssistState.adapting else: - self.state = SpeedLimitControlState.active + self.state = SpeedLimitAssistState.active else: - self.state = SpeedLimitControlState.pending + self.state = SpeedLimitAssistState.pending elif self.pre_active_timer <= PRE_ACTIVE_GUARD_PERIOD: # Timeout - session ended - self.state = SpeedLimitControlState.inactive + self.state = SpeedLimitAssistState.inactive # INACTIVE - elif self.state == SpeedLimitControlState.inactive: + elif self.state == SpeedLimitAssistState.inactive: pass # DISABLED - elif self.state == SpeedLimitControlState.disabled: + elif self.state == SpeedLimitAssistState.disabled: if self.op_engaged and self.enabled: if not self.op_engaged_prev: self.pre_active_timer = int(DISABLED_GUARD_PERIOD / DT_MDL) elif self.pre_active_timer <= 0: - self.state = SpeedLimitControlState.preActive + self.state = SpeedLimitAssistState.preActive self.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL) self.initial_max_set = False @@ -221,7 +221,7 @@ class SpeedLimitController: return enabled, active def update_events(self, events_sp: EventsSP) -> None: - if self.state == SpeedLimitControlState.preActive and self._state_prev != SpeedLimitControlState.preActive: + if self.state == SpeedLimitAssistState.preActive and self._state_prev != SpeedLimitAssistState.preActive: events_sp.add(EventNameSP.speedLimitPreActive) elif self.is_active: if self._state_prev not in ACTIVE_STATES: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/speed_limit_resolver.py similarity index 92% rename from sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py rename to sunnypilot/selfdrive/controls/lib/speed_limit_assist/speed_limit_resolver.py index 23c25dfd39..ddc3252992 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/speed_limit_resolver.py @@ -6,8 +6,8 @@ from cereal import custom from openpilot.common.gps import get_gps_location_service from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC, PARAMS_UPDATE_PERIOD -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist import LIMIT_MAX_MAP_DATA_AGE, LIMIT_ADAPT_ACC, PARAMS_UPDATE_PERIOD +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.common import Policy SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource @@ -30,7 +30,7 @@ class SpeedLimitResolver: self._limit_solutions = {} # Store for speed limit solutions from different sources self._distance_solutions = {} # Store for distance to current speed limit start for different sources - self.policy = self.params.get("SpeedLimitControlPolicy", return_default=True) + self.policy = self.params.get("SpeedLimitAssistPolicy", return_default=True) self._policy_to_sources_map = { Policy.car_state_only: [SpeedLimitSource.car], Policy.car_state_priority: [SpeedLimitSource.car, SpeedLimitSource.map], @@ -43,7 +43,7 @@ class SpeedLimitResolver: def update_params(self): if self.frame % int(PARAMS_UPDATE_PERIOD / DT_MDL) == 0: - self.policy = Policy(self.params.get("SpeedLimitControlPolicy", return_default=True)) + self.policy = Policy(self.params.get("SpeedLimitAssistPolicy", return_default=True)) self.change_policy(self.policy) def change_policy(self, policy: Policy) -> None: diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/__init__.py b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/__init__.py similarity index 100% rename from sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/__init__.py rename to sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/__init__.py diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/test_speed_limit_assist.py similarity index 53% rename from sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py rename to sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/test_speed_limit_assist.py index 66aead5c68..fa17494d2e 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_controller.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/test_speed_limit_assist.py @@ -15,15 +15,15 @@ from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.car.cruise import V_CRUISE_UNSET from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfaces -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import OffsetType -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import SpeedLimitControlState, REQUIRED_INITIAL_MAX_SET_SPEED, \ +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.common import OffsetType +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist import SpeedLimitAssistState, REQUIRED_INITIAL_MAX_SET_SPEED, \ PRE_ACTIVE_GUARD_PERIOD -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_controller import SpeedLimitController, ACTIVE_STATES +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.speed_limit_assist import SpeedLimitAssist, ACTIVE_STATES from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource -ALL_STATES = tuple(SpeedLimitControlState.schema.enumerants.values()) +ALL_STATES = tuple(SpeedLimitAssistState.schema.enumerants.values()) SPEED_LIMITS = { 'residential': 25 * CV.MPH_TO_MS, # 25 mph @@ -33,15 +33,15 @@ SPEED_LIMITS = { } -class TestSpeedLimitController: +class TestSpeedLimitAssist: def setup_method(self): self.params = Params() self.reset_custom_params() self.events_sp = EventsSP() CI = self._setup_platform(TOYOTA.TOYOTA_RAV4_TSS2_2022) - self.slc = SpeedLimitController(CI.CP) - self.slc.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL) + self.sla = SpeedLimitAssist(CI.CP) + self.sla.pre_active_timer = int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL) def teardown_method(self, method): self.reset_state() @@ -55,103 +55,103 @@ class TestSpeedLimitController: return CI def reset_custom_params(self): - self.params.put_bool("SpeedLimitControl", True) + self.params.put_bool("SpeedLimitAssist", True) self.params.put_bool("IsMetric", False) self.params.put("SpeedLimitOffsetType", 0) self.params.put("SpeedLimitValueOffset", 0) def reset_state(self): - self.slc.state = SpeedLimitControlState.disabled - self.slc.frame = -1 - self.slc.last_op_engaged_frame = 0 - self.slc.op_engaged = False - self.slc.op_engaged_prev = False - self.slc.initial_max_set = False - self.slc._speed_limit = 0. - self.slc.speed_limit_prev = 0. - self.slc.last_valid_speed_limit_offsetted = 0. - self.slc._distance = 0. - self.slc._source = SpeedLimitSource.none - self.slc.v_cruise_setpoint = 0. - self.slc.v_cruise_setpoint_prev = 0. + self.sla.state = SpeedLimitAssistState.disabled + self.sla.frame = -1 + self.sla.last_op_engaged_frame = 0 + self.sla.op_engaged = False + self.sla.op_engaged_prev = False + self.sla.initial_max_set = False + self.sla._speed_limit = 0. + self.sla.speed_limit_prev = 0. + self.sla.last_valid_speed_limit_offsetted = 0. + self.sla._distance = 0. + self.sla._source = SpeedLimitSource.none + self.sla.v_cruise_setpoint = 0. + self.sla.v_cruise_setpoint_prev = 0. self.events_sp.clear() def initialize_active_state(self, v_cruise_setpoint): - self.slc.state = SpeedLimitControlState.active - self.slc.v_cruise_setpoint = v_cruise_setpoint - self.slc.v_cruise_setpoint_prev = v_cruise_setpoint + self.sla.state = SpeedLimitAssistState.active + self.sla.v_cruise_setpoint = v_cruise_setpoint + self.sla.v_cruise_setpoint_prev = v_cruise_setpoint def test_initial_state(self): - assert self.slc.state == SpeedLimitControlState.disabled - assert not self.slc.is_enabled - assert not self.slc.is_active - assert V_CRUISE_UNSET == self.slc.get_v_target_from_control() + assert self.sla.state == SpeedLimitAssistState.disabled + assert not self.sla.is_enabled + assert not self.sla.is_active + assert V_CRUISE_UNSET == self.sla.get_v_target_from_control() def test_disabled(self): - self.params.put_bool("SpeedLimitControl", False) + self.params.put_bool("SpeedLimitAssist", False) for _ in range(int(10. / DT_MDL)): - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.disabled + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.disabled def test_transition_disabled_to_preactive(self): for _ in range(int(3. / DT_MDL)): - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.preActive - assert self.slc.is_enabled and not self.slc.is_active + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.preActive + assert self.sla.is_enabled and not self.sla.is_active def test_preactive_to_active_with_max_speed_confirmation(self): - self.slc.state = SpeedLimitControlState.preActive - v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.active - assert self.slc.is_enabled and self.slc.is_active - assert v_cruise_slc == SPEED_LIMITS['city'] + self.sla.state = SpeedLimitAssistState.preActive + v_cruise_sla = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.active + assert self.sla.is_enabled and self.sla.is_active + assert v_cruise_sla == SPEED_LIMITS['city'] def test_preactive_timeout_to_inactive(self): - self.slc.state = SpeedLimitControlState.preActive - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + self.sla.state = SpeedLimitAssistState.preActive + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) for _ in range(int(PRE_ACTIVE_GUARD_PERIOD / DT_MDL)): - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.inactive + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, SPEED_LIMITS['highway'], SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.inactive def test_preactive_to_pending_no_speed_limit(self): - self.slc.state = SpeedLimitControlState.preActive - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, SpeedLimitSource.none, self.events_sp) - assert self.slc.state == SpeedLimitControlState.pending - assert self.slc.is_enabled and not self.slc.is_active + self.sla.state = SpeedLimitAssistState.preActive + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, SpeedLimitSource.none, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.pending + assert self.sla.is_enabled and not self.sla.is_active def test_pending_to_active_when_speed_limit_available(self): - self.slc.state = SpeedLimitControlState.pending - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.active + self.sla.state = SpeedLimitAssistState.pending + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.active def test_pending_to_adapting_when_below_speed_limit(self): - self.slc.state = SpeedLimitControlState.pending - _ = self.slc.update(True, SPEED_LIMITS['city'] + 5, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.adapting - assert self.slc.is_enabled and self.slc.is_active + self.sla.state = SpeedLimitAssistState.pending + _ = self.sla.update(True, SPEED_LIMITS['city'] + 5, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.adapting + assert self.sla.is_enabled and self.sla.is_active def test_active_to_adapting_transition(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) - _ = self.slc.update(True, SPEED_LIMITS['city'] + 2, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.adapting + _ = self.sla.update(True, SPEED_LIMITS['city'] + 2, 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.adapting def test_adapting_to_active_transition(self): - self.slc.state = SpeedLimitControlState.adapting - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.sla.state = SpeedLimitAssistState.adapting + self.sla.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.active + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.active def test_manual_cruise_change_detection(self): - self.slc.state = SpeedLimitControlState.active + self.sla.state = SpeedLimitAssistState.active expected_cruise = SPEED_LIMITS['highway'] - self.slc.v_cruise_setpoint_prev = expected_cruise + self.sla.v_cruise_setpoint_prev = expected_cruise different_cruise = SPEED_LIMITS['highway'] + 5 - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, different_cruise, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.inactive + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, different_cruise, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.inactive @pytest.mark.parametrize("offset_type, offset_value, speed_limit, expected_offset", [ (OffsetType.fixed, 5, SPEED_LIMITS['city'], 5 * CV.MPH_TO_MS), # 5 MPH fixed offset @@ -161,8 +161,8 @@ class TestSpeedLimitController: (OffsetType.percentage, 5, SPEED_LIMITS['highway'], 0.05 * SPEED_LIMITS['highway']), # Different speed, percentage ]) def test_offset_calculations(self, offset_type, offset_value, speed_limit, expected_offset): - self.slc._speed_limit = speed_limit - actual_offset = self.slc.get_offset(offset_type, offset_value) + self.sla._speed_limit = speed_limit + actual_offset = self.sla.get_offset(offset_type, offset_value) assert actual_offset == pytest.approx(expected_offset, rel=0.01) def test_rapid_speed_limit_changes(self): @@ -170,78 +170,78 @@ class TestSpeedLimitController: speed_limits = [SPEED_LIMITS['city'], SPEED_LIMITS['highway'], SPEED_LIMITS['residential']] for _, speed_limit in enumerate(speed_limits): - _ = self.slc.update(True, speed_limit, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state in ACTIVE_STATES + _ = self.sla.update(True, speed_limit, 0, REQUIRED_INITIAL_MAX_SET_SPEED, speed_limit, 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state in ACTIVE_STATES def test_invalid_speed_limits_handling(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) - self.slc.last_valid_speed_limit_final = SPEED_LIMITS['city'] + self.sla.last_valid_speed_limit_final = SPEED_LIMITS['city'] invalid_limits = [-10, 0, 200 * CV.MPH_TO_MS] for invalid_limit in invalid_limits: - v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, invalid_limit, 0, SpeedLimitSource.car, self.events_sp) - assert isinstance(v_cruise_slc, (int, float)) - assert v_cruise_slc == V_CRUISE_UNSET or v_cruise_slc > 0 + v_cruise_sla = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, invalid_limit, 0, SpeedLimitSource.car, self.events_sp) + assert isinstance(v_cruise_sla, (int, float)) + assert v_cruise_sla == V_CRUISE_UNSET or v_cruise_sla > 0 def test_stale_data_handling(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) old_speed_limit = SPEED_LIMITS['city'] - self.slc.last_valid_speed_limit_final = old_speed_limit + self.sla.last_valid_speed_limit_final = old_speed_limit - v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state in ACTIVE_STATES - assert v_cruise_slc == old_speed_limit + v_cruise_sla = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, 0, 0, SpeedLimitSource.car, self.events_sp) + assert self.sla.state in ACTIVE_STATES + assert v_cruise_sla == old_speed_limit def test_different_speed_limit_sources(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) for source in (SpeedLimitSource.car, SpeedLimitSource.map): - v_cruise_slc = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, source, self.events_sp) - assert v_cruise_slc != V_CRUISE_UNSET + v_cruise_sla = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, source, self.events_sp) + assert v_cruise_sla != V_CRUISE_UNSET def test_distance_based_adapting(self): - self.slc.state = SpeedLimitControlState.adapting - self.slc.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED + self.sla.state = SpeedLimitAssistState.adapting + self.sla.v_cruise_setpoint_prev = REQUIRED_INITIAL_MAX_SET_SPEED distance = 100.0 current_speed = SPEED_LIMITS['highway'] target_speed = SPEED_LIMITS['city'] - v_cruise_slc = self.slc.update(True, current_speed, 0, REQUIRED_INITIAL_MAX_SET_SPEED, target_speed, distance, SpeedLimitSource.map, self.events_sp) - assert self.slc.state == SpeedLimitControlState.adapting - assert v_cruise_slc == target_speed # TODO-SP: assert expected accel, need to enable self.acceleration_solutions + v_cruise_sla = self.sla.update(True, current_speed, 0, REQUIRED_INITIAL_MAX_SET_SPEED, target_speed, distance, SpeedLimitSource.map, self.events_sp) + assert self.sla.state == SpeedLimitAssistState.adapting + assert v_cruise_sla == target_speed # TODO-SP: assert expected accel, need to enable self.acceleration_solutions def test_long_disengaged_to_disabled(self): self.initialize_active_state(REQUIRED_INITIAL_MAX_SET_SPEED) - v_cruise_slc = self.slc.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], + v_cruise_sla = self.sla.update(False, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED, SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state == SpeedLimitControlState.disabled - assert v_cruise_slc == V_CRUISE_UNSET + assert self.sla.state == SpeedLimitAssistState.disabled + assert v_cruise_sla == V_CRUISE_UNSET def test_maintain_states_with_no_changes(self): """Test that states are maintained when no significant changes occur""" test_states = [ - SpeedLimitControlState.preActive, - SpeedLimitControlState.pending, - SpeedLimitControlState.active, - SpeedLimitControlState.adapting + SpeedLimitAssistState.preActive, + SpeedLimitAssistState.pending, + SpeedLimitAssistState.active, + SpeedLimitAssistState.adapting ] for state in test_states: - self.slc.state = state - self.slc.op_engaged = True - if state in [SpeedLimitControlState.pending, SpeedLimitControlState.active, SpeedLimitControlState.adapting]: - self.slc.initial_max_set = True + self.sla.state = state + self.sla.op_engaged = True + if state in [SpeedLimitAssistState.pending, SpeedLimitAssistState.active, SpeedLimitAssistState.adapting]: + self.sla.initial_max_set = True initial_state = state - _ = self.slc.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED,SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) + _ = self.sla.update(True, SPEED_LIMITS['city'], 0, REQUIRED_INITIAL_MAX_SET_SPEED,SPEED_LIMITS['city'], 0, SpeedLimitSource.car, self.events_sp) - assert self.slc.state in ALL_STATES # Sanity check + assert self.sla.state in ALL_STATES # Sanity check - if initial_state == SpeedLimitControlState.preActive: - assert self.slc.state in [SpeedLimitControlState.preActive, SpeedLimitControlState.active] + if initial_state == SpeedLimitAssistState.preActive: + assert self.sla.state in [SpeedLimitAssistState.preActive, SpeedLimitAssistState.active] elif initial_state in ACTIVE_STATES: - assert self.slc.state in ACTIVE_STATES + assert self.sla.state in ACTIVE_STATES diff --git a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/test_speed_limit_resolver.py similarity index 93% rename from sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py rename to sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/test_speed_limit_resolver.py index e01a60164d..14433dc293 100644 --- a/sunnypilot/selfdrive/controls/lib/speed_limit_controller/tests/test_speed_limit_resolver.py +++ b/sunnypilot/selfdrive/controls/lib/speed_limit_assist/tests/test_speed_limit_resolver.py @@ -5,10 +5,10 @@ import pytest from pytest_mock import MockerFixture from cereal import custom -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller import LIMIT_MAX_MAP_DATA_AGE +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist import LIMIT_MAX_MAP_DATA_AGE -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.speed_limit_resolver import SpeedLimitResolver, ALL_SOURCES -from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_controller.common import Policy +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.speed_limit_resolver import SpeedLimitResolver, ALL_SOURCES +from openpilot.sunnypilot.selfdrive.controls.lib.speed_limit_assist.common import Policy SpeedLimitSource = custom.LongitudinalPlanSP.SpeedLimitSource diff --git a/sunnypilot/selfdrive/selfdrived/events.py b/sunnypilot/selfdrive/selfdrived/events.py index b40e86ad9f..035c5f13ee 100644 --- a/sunnypilot/selfdrive/selfdrived/events.py +++ b/sunnypilot/selfdrive/selfdrived/events.py @@ -17,7 +17,7 @@ EVENT_NAME_SP = {v: k for k, v in EventNameSP.schema.enumerants.items()} def speed_limit_adjust_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: - speedLimit = sm['longitudinalPlanSP'].slc.speedLimit + speedLimit = sm['longitudinalPlanSP'].sla.speedLimit speed = round(speedLimit * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH)) message = f'Adjusting to {speed} {"km/h" if metric else "mph"} speed limit' return Alert( From c895969de1268397913896fb8af5177247d10941 Mon Sep 17 00:00:00 2001 From: Jason Wen Date: Thu, 18 Sep 2025 00:20:00 -0400 Subject: [PATCH 188/188] in another PR --- .../sunnypilot/qt/offroad/settings/longitudinal_panel.cc | 8 -------- .../sunnypilot/qt/offroad/settings/longitudinal_panel.h | 1 - 2 files changed, 9 deletions(-) diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc index e248f2e112..df4a6077b5 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.cc @@ -35,13 +35,6 @@ LongitudinalPanel::LongitudinalPanel(QWidget *parent) : QWidget(parent) { ""); list->addItem(SmartCruiseControlVision); - speedLimitAssist = new ParamControl( - "SpeedLimitAssist", - tr("Speed Limit Assist (SLA)"), - tr("When you engage ACC, you will be prompted to set the cruising speed to the speed limit of the road adjusted by the Offset and Source Policy specified, or the current driving speed. The maximum cruising speed will always be the MAX set speed."), - ""); - list->addItem(speedLimitAssist); - customAccIncrement = new CustomAccIncrement("CustomAccIncrementsEnabled", tr("Custom ACC Speed Increments"), "", "", this); list->addItem(customAccIncrement); @@ -112,7 +105,6 @@ void LongitudinalPanel::refresh(bool _offroad) { customAccIncrement->refresh(); SmartCruiseControlVision->setEnabled(has_longitudinal_control || icbm_allowed); - speedLimitAssist->setEnabled(has_longitudinal_control || icbm_allowed); offroad = _offroad; } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h index 3d2c4ffbad..af89822595 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/longitudinal_panel.h @@ -32,5 +32,4 @@ private: CustomAccIncrement *customAccIncrement = nullptr; ParamControl *SmartCruiseControlVision; ParamControl *intelligentCruiseButtonManagement = nullptr; - ParamControl *speedLimitAssist = nullptr; };