From 155b105a44bd7316eacd6ba7b27f7d6da9507822 Mon Sep 17 00:00:00 2001 From: firestar5683 <168790843+firestar5683@users.noreply.github.com> Date: Thu, 22 Oct 2026 08:23:03 -0500 Subject: [PATCH] Restore mapped classic Hyundai MAIN and LKAS AOL controls --- .../modes/hyundai_classic_non_scc_aol.h | 16 +- .../tests/test_classic_scc_main_ingress.py | 71 ++++ .../tests/test_classic_scc_mapped_buttons.py | 38 ++ .../test_sonata_cruise_only_withdrawal.py | 4 +- .../test_sonata_mapped_button_callers.py | 380 ++++++++++++++++++ openpilot/starpilot/car/hyundai/aol.py | 1 + .../car/hyundai/classic_scc_intent.py | 15 + panda/board/obj/body_h7.bin.signed | Bin 227676 -> 227780 bytes panda/board/obj/bootstub.body_h7.bin | Bin 12040 -> 12040 bytes panda/board/obj/bootstub.panda.bin | Bin 11176 -> 11176 bytes .../obj/bootstub.panda_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/bootstub.panda_h7.bin | Bin 18824 -> 18824 bytes .../bootstub.panda_h7_can_ignition_only.bin | Bin 18824 -> 18824 bytes .../obj/bootstub.panda_h7_hkg_remote.bin | Bin 18824 -> 18824 bytes ....panda_h7_hkg_remote_can_ignition_only.bin | Bin 18824 -> 18824 bytes panda/board/obj/bootstub.panda_h7_remote.bin | Bin 18824 -> 18824 bytes ...stub.panda_h7_remote_can_ignition_only.bin | Bin 18824 -> 18824 bytes .../obj/bootstub.panda_h7_tesla_wake.bin | Bin 18824 -> 18824 bytes ....panda_h7_tesla_wake_can_ignition_only.bin | Bin 18824 -> 18824 bytes panda/board/obj/bootstub.panda_hkg_remote.bin | Bin 11176 -> 11176 bytes ...tub.panda_hkg_remote_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/bootstub.panda_remote.bin | Bin 11176 -> 11176 bytes ...ootstub.panda_remote_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/bootstub.panda_tesla_wake.bin | Bin 11176 -> 11176 bytes ...tub.panda_tesla_wake_can_ignition_only.bin | Bin 11176 -> 11176 bytes panda/board/obj/panda.bin.signed | Bin 219912 -> 220068 bytes .../obj/panda_can_ignition_only.bin.signed | Bin 219880 -> 220036 bytes panda/board/obj/panda_h7.bin.signed | Bin 228792 -> 228872 bytes .../obj/panda_h7_can_ignition_only.bin.signed | Bin 228768 -> 228848 bytes .../board/obj/panda_h7_hkg_remote.bin.signed | Bin 228864 -> 228980 bytes ...h7_hkg_remote_can_ignition_only.bin.signed | Bin 228848 -> 228964 bytes panda/board/obj/panda_h7_remote.bin.signed | Bin 228792 -> 228872 bytes ...nda_h7_remote_can_ignition_only.bin.signed | Bin 228768 -> 228848 bytes .../board/obj/panda_h7_tesla_wake.bin.signed | Bin 228924 -> 229032 bytes ...h7_tesla_wake_can_ignition_only.bin.signed | Bin 228896 -> 229008 bytes panda/board/obj/panda_hkg_remote.bin.signed | Bin 219980 -> 220128 bytes ...da_hkg_remote_can_ignition_only.bin.signed | Bin 219960 -> 220112 bytes panda/board/obj/panda_remote.bin.signed | Bin 219908 -> 220064 bytes .../panda_remote_can_ignition_only.bin.signed | Bin 219876 -> 220032 bytes panda/board/obj/panda_tesla_wake.bin.signed | Bin 220032 -> 220184 bytes ...da_tesla_wake_can_ignition_only.bin.signed | Bin 220000 -> 220160 bytes prebuilt.json | 2 +- tools/ci/run_vehicle_tests.py | 4 + tools/ci/tests/test_vehicle_ci.py | 10 + 44 files changed, 537 insertions(+), 4 deletions(-) create mode 100644 opendbc_repo/opendbc/safety/tests/test_classic_scc_main_ingress.py create mode 100644 openpilot/starpilot/aol/tests/test_classic_scc_mapped_buttons.py create mode 100644 openpilot/starpilot/aol/tests/test_sonata_mapped_button_callers.py diff --git a/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h b/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h index bb4e46c202..3fd28fe8d0 100644 --- a/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h +++ b/opendbc_repo/opendbc/safety/modes/hyundai_classic_non_scc_aol.h @@ -18,6 +18,8 @@ static bool classic_scc_aol_camera = false; static bool classic_long_aol_enabled = false; static bool classic_long_main_neutral = false; static bool classic_long_main_pressed = false; +static bool classic_scc_main_source_seen = false; +static uint32_t classic_scc_main_source_ts = 0U; static bool legacy_long_param(uint16_t param) { return (param == 0xE900U) || (param == 0xE901U) || (param == 0xE902U) || (param == 0xE903U) || @@ -74,6 +76,8 @@ static void classic_non_scc_aol_clear_authorization(void) { } classic_long_main_neutral = false; classic_long_main_pressed = false; + classic_scc_main_source_seen = false; + classic_scc_main_source_ts = 0U; aol_host_axis_mask = 0U; } @@ -169,7 +173,17 @@ static void classic_non_scc_aol_rx(const CANPacket_t *msg) { classic_non_scc_aol_claimed = false; } } - if (classic_long_aol_enabled && (msg->bus == 0U) && (msg->addr == 0x4F1U) && (GET_LEN(msg) == 4U)) { + if ((classic_long_aol_enabled || (classic_scc_aol_enabled && classic_non_scc_aol_lda)) && + (msg->bus == 0U) && (msg->addr == 0x4F1U) && (GET_LEN(msg) == 4U)) { + if (classic_scc_aol_enabled) { + if (!classic_scc_main_source_seen || + (safety_get_ts_elapsed(now, classic_scc_main_source_ts) > HYUNDAI_CLASSIC_SOURCE_MAX_AGE_US)) { + classic_long_main_neutral = false; + classic_long_main_pressed = false; + } + classic_scc_main_source_seen = true; + classic_scc_main_source_ts = now; + } const bool main_pressed = GET_BIT(msg, 3U); if (!main_pressed) { classic_long_main_neutral = true; diff --git a/opendbc_repo/opendbc/safety/tests/test_classic_scc_main_ingress.py b/opendbc_repo/opendbc/safety/tests/test_classic_scc_main_ingress.py new file mode 100644 index 0000000000..1b8b737597 --- /dev/null +++ b/opendbc_repo/opendbc/safety/tests/test_classic_scc_main_ingress.py @@ -0,0 +1,71 @@ +"""Physical MAIN may authorize only stock SCC LAT under existing profile guards.""" +import pytest + +from opendbc.safety.tests import test_hyundai_classic_scc_aol as fixture +from opendbc.safety.tests.libsafety.libsafety_py import make_CANPacket + + +@pytest.mark.parametrize('word', [0xC00, 0xC02, 0xC41, 0xE02, 0xC0A]) +def test_fresh_physical_main_authorizes_only_lateral(word): + case = fixture.TestHyundaiClassicSccAol(methodName='runTest') + case.setUp() + try: + case.reset(word) + for _ in range(6): + case.feed(word, main=False, source='BCM_PO_11') + assert case.request(1) == 0 + case.request(0) + # Raw CLU11 MAIN press captured in Sonata route09 at42.073543942s. + assert case.safety.safety_rx_hook(make_CANPacket(0x4F1, 0, bytes.fromhex('28557af0'))) + assert case.request(1) == 1 + assert not case.safety.get_controls_allowed() + assert case.request(3) == 1 + assert not case.safety.safety_tx_hook(case.packer.make_can_msg_safety('SCC12', 2 if word & 8 else 0, + {'ACCMode': 1, 'aReqRaw': 1, 'aReqValue': 1})) + case.safety.set_aol_test_heartbeat(False) + assert case.request(1) == 0 + finally: + case.tearDown() + + +@pytest.mark.parametrize('wrong_bus', [1, 2]) +def test_main_wrong_bus_does_not_authorize_lateral(wrong_bus): + case = fixture.TestHyundaiClassicSccAol(methodName='runTest') + case.setUp() + try: + case.reset(0xC02) + for _ in range(6): + case.feed(0xC02, source='BCM_PO_11') + case.safety.safety_rx_hook(make_CANPacket(0x4F1, wrong_bus, bytes.fromhex('28557af0'))) + assert case.request(1) == 0 + finally: + case.tearDown() + + +@pytest.mark.parametrize('word', [0xC00, 0xC02, 0xC0A]) +def test_held_at_boot_main_needs_release_before_authorization(word, monkeypatch): + case = fixture.TestHyundaiClassicSccAol(methodName='runTest') + case.setUp() + try: + case.reset(word) + original_rx = case.rx + held = [True] + + def physical_rx(name, values, **kwargs): + if name == 'CLU11': + values = dict(values, CF_Clu_CruiseSwMain=int(held[0])) + return original_rx(name, values, **kwargs) + monkeypatch.setattr(case, 'rx', physical_rx) + for _ in range(6): + case.feed(word, source='BCM_PO_11') + assert case.request(1) == 0 + case.request(0) + held[0] = False + case.feed(word, source='BCM_PO_11') + held[0] = True + case.feed(word, source='BCM_PO_11') + assert case.request(1) == 1 + case.safety.set_aol_test_heartbeat(False) + assert case.request(1) == 0 + finally: + case.tearDown() diff --git a/openpilot/starpilot/aol/tests/test_classic_scc_mapped_buttons.py b/openpilot/starpilot/aol/tests/test_classic_scc_mapped_buttons.py new file mode 100644 index 0000000000..7927212aae --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_classic_scc_mapped_buttons.py @@ -0,0 +1,38 @@ +"""Stock SCC buttons follow the configured mapping, independently of PCM MAIN.""" +from dataclasses import replace + +import pytest + +from opendbc.car.structs import car +from openpilot.starpilot.aol.tests.test_forte_intent import params, settings, state +from openpilot.starpilot.car.hyundai.classic_scc_intent import ClassicSccCardIntent +from opendbc.car.hyundai.values import CAR + +Button = car.CarState.ButtonEvent.Type + + +@pytest.mark.parametrize('button', [Button.mainCruise, Button.lkas]) +@pytest.mark.parametrize('available', [False, True]) +def test_mapped_buttons_toggle_once_without_requiring_stock_main(button, available): + owner = ClassicSccCardIntent(params(CAR.HYUNDAI_SONATA_HYBRID), settings(main=9)) + def update(events=(), **kwargs): + owner.update(state(main=available, events=events), fault_active=False, **kwargs) + update(now_ns=1) + assert not owner.allowed_latch + update(((button, True),), now_ns=2) + assert owner.allowed_latch and not owner.pause_lateral + update(((button, True),), now_ns=3) + assert owner.allowed_latch and not owner.pause_lateral + update(((button, False),), now_ns=4) + update(((button, True),), now_ns=5) + assert not owner.allowed_latch and owner.pause_lateral + + +@pytest.mark.parametrize('button', [Button.mainCruise, Button.lkas]) +def test_fault_and_disabled_settings_do_not_arm_mapped_buttons(button): + configured = settings(main=9) + for disabled in (False, True): + owner = ClassicSccCardIntent(params(CAR.HYUNDAI_SONATA_HYBRID), replace(configured, enabled=not disabled)) + owner.update(state(), fault_active=False, now_ns=1) + owner.update(state(events=((button, True),)), fault_active=not disabled, now_ns=2) + assert not owner.allowed_latch diff --git a/openpilot/starpilot/aol/tests/test_sonata_cruise_only_withdrawal.py b/openpilot/starpilot/aol/tests/test_sonata_cruise_only_withdrawal.py index 3ba4df514c..9470e1b7b7 100644 --- a/openpilot/starpilot/aol/tests/test_sonata_cruise_only_withdrawal.py +++ b/openpilot/starpilot/aol/tests/test_sonata_cruise_only_withdrawal.py @@ -30,7 +30,7 @@ from openpilot.cereal import log disable_ecu_module = importlib.import_module('opendbc.car.disable_ecu') -def classic_frames(ci, tick, *, main=0, lkas=0, button=0, engaged=False, brake=False): +def classic_frames(ci, tick, *, main=0, main_pressed=0, lkas=0, button=0, engaged=False, brake=False): assert ci.CP.carFingerprint == CAR.HYUNDAI_SONATA_HYBRID packer = CANPacker('hyundai_can_generated') @@ -53,7 +53,7 @@ def classic_frames(ci, tick, *, main=0, lkas=0, button=0, engaged=False, brake=F counter = tick // period values = {} if name == 'CLU11': - values.update(CF_Clu_AliveCnt1=counter%16, CF_Clu_CruiseSwState=button, CF_Clu_CruiseSwMain=main) + values.update(CF_Clu_AliveCnt1=counter%16, CF_Clu_CruiseSwState=button, CF_Clu_CruiseSwMain=main_pressed) elif name == 'E_EMS11': values.update(CR_Vcu_AccPedDep_Pos=0) elif name == 'WHL_SPD11': diff --git a/openpilot/starpilot/aol/tests/test_sonata_mapped_button_callers.py b/openpilot/starpilot/aol/tests/test_sonata_mapped_button_callers.py new file mode 100644 index 0000000000..1e2d5a981c --- /dev/null +++ b/openpilot/starpilot/aol/tests/test_sonata_mapped_button_callers.py @@ -0,0 +1,380 @@ +from typing import Any +import dataclasses +import json +import pytest +import importlib +import os +from types import SimpleNamespace +from unittest.mock import patch + +from opendbc.can import CANPacker +from opendbc.car import gen_empty_fingerprint, structs +from opendbc.car.hyundai.interface import CarInterface +from opendbc.car.hyundai.radar_interface import RadarInterface +from opendbc.car.hyundai.values import CAR +from opendbc.car.hyundai.classic_scc_aol import qualified as qualified_classic +from opendbc.safety.tests.libsafety import libsafety_py +from opendbc.safety.tests.test_hyundai import checksum +from openpilot.cereal import messaging +from openpilot.common.params import Params +from openpilot.common.prefix import OpenpilotPrefix +from openpilot.selfdrive.car import card as card_module +from openpilot.starpilot.aol.intent import AOL_TOGGLE, read_settings +from openpilot.starpilot.aol.runtime import current_native + +from openpilot.starpilot.aol.tests import test_ioniq_availability as caller_fixture +from openpilot.starpilot.aol.wire import PandaSlot, SafetyState, encode_safety +from openpilot.starpilot.car.hyundai.classic_scc_intent import ClassicSccCardIntent +from openpilot.cereal import log + +disable_ecu_module = importlib.import_module('opendbc.car.disable_ecu') + + +def classic_frames(ci, tick, *, main=0, main_pressed=0, lkas=0, button=0, engaged=False, brake=False): + + assert ci.CP.carFingerprint == CAR.HYUNDAI_SONATA_HYBRID + packer = CANPacker('hyundai_can_generated') + + + hz = {'E_EMS11':100, 'ELECT_GEAR':20, 'SCC11':50, 'SCC12':50, 'WHL_SPD11':100, 'TCS13':100, 'MDPS12':50, 'CLU11':50, + 'BCM_PO_11':50, 'LKAS11':100} + scheduled = {(packer.dbc.name_to_msg[name].address, 0): name for name in hz if name != 'LKAS11'} + for parser in ci.can_parsers.values(): + for address in parser.addresses: + scheduled[(address, parser.bus)] = parser.dbc.addr_to_msg[address].name + frames = [] + for (address,bus),name in sorted(scheduled.items()): + parser = next((p for p in ci.can_parsers.values() if p.bus == bus), None) + declared = parser.message_states[address].frequency if parser is not None and address in parser.addresses else 0 + frequency = hz.get(name, min(declared,100) if 0 < declared <= 100 else 100) + period = max(1,round(100/frequency)) + if tick % period: + continue + counter = tick // period + values = {} + if name == 'CLU11': + values.update(CF_Clu_AliveCnt1=counter%16, CF_Clu_CruiseSwState=button, CF_Clu_CruiseSwMain=main_pressed) + elif name == 'E_EMS11': + values.update(CR_Vcu_AccPedDep_Pos=0) + elif name == 'WHL_SPD11': + values.update(WHL_SPD_AliveCounter_LSB=counter%4, WHL_SPD_AliveCounter_MSB=(counter//4)%4, + WHL_SPD_FL=40,WHL_SPD_FR=40,WHL_SPD_RL=40,WHL_SPD_RR=40) + elif name == 'TCS13': + values.update(AliveCounterTCS=counter%8, ACCEnable=0, ACC_REQ=int(engaged), DriverOverride=2 if brake else 0) + elif name == 'MDPS12': + values.update(CR_Mdps_StrColTq=0, CR_Mdps_OutTq=0) + elif name == 'BCM_PO_11': + values['LDA_BTN']=lkas + elif name == 'CGW1': + values['CF_Gway_DrvSeatBeltSw']=1 + elif name == 'ELECT_GEAR': + values['Elect_Gear_Shifter']=5 + elif name == 'SCC11': + values['MainMode_ACC']=main + elif name == 'SCC12': + values.update(ACCMode=int(engaged), CR_VSM_Alive=counter%16) + elif name == 'SAS11': + values['SAS_Angle']=1. + elif name == 'CLU15': + values['CF_Clu_VehicleSpeed']=40 + frame = packer.make_can_msg(name,bus,values) + if name in ('SCC12','WHL_SPD11','TCS13'): + address, data, bus = checksum(frame) + frame = (address, bytes(data), bus) + frames.append(frame) + assert not ci.CP.openpilotLongitudinalControl and ci.CP.pcmCruise + assert all(address!=0x340 or bus==2 for address,_,bus in frames), 'Never echo outgoing LKAS on protected RX bus' + return frames + + +def _invalid_controls_emits_no_commands(received): + if 'sendcan' not in received: + return True + envelope = received['sendcan'] + return envelope.which() == 'sendcan' and not envelope.valid and len(envelope.sendcan) == 0 + + +@pytest.mark.parametrize('raw_fault', [0, 8]) +@pytest.mark.parametrize('withdrawal', ['brake', 'cruise-cancel']) +def test_main_then_lkas_toggle_runs_real_parser_card_native_and_commands(raw_fault, withdrawal, monkeypatch): + shared_long = False + native = libsafety_py.libsafety + case = caller_fixture.IoniqAvailabilityTests(methodName='runTest') + card = None + with OpenpilotPrefix(), patch.dict(os.environ, {'SIMULATION': '0', 'AOL_REPLAY_RUNTIME': '1'}): + saved = Params() + saved.put_bool('OpenpilotEnabledToggle', True, block=True) + saved.put_bool('AlwaysOnLateral', True, block=True) + saved.put('LKASButtonControl', AOL_TOGGLE, block=True) + saved.put('MainCruiseButtonControl', AOL_TOGGLE, block=True) + now = [10_000_000_000] + for key in ('CancelButtonControl', 'LongCancelButtonControl', 'VeryLongCancelButtonControl'): + saved.put(key, 0, block=True) + try: + saved.put_bool('AlphaLongitudinalEnabled', False, block=True) + saved.put_bool('IsReleaseBranch', False, block=True) + fp = gen_empty_fingerprint() + fp[0].update({0x391: 8, 0x38d: 8}) + fp[2][0x485] = 4 + fp[1][0x500] = 8 + cp = CarInterface.get_params(CAR.HYUNDAI_SONATA_HYBRID, fp, [], False, False, False) + assert int(cp.flags) == 0x1011580 + assert qualified_classic(cp) and cp.pcmCruise and not cp.openpilotLongitudinalControl + assert cp.lateralTuning.which() == 'torque' and not CarInterface.startup_required(cp) + card = card_module.Car(CI=CarInterface(cp), RI=RadarInterface(cp)) + assert qualified_classic(card.CP, marked_only=True) + assert card.CP.safetyConfigs[0].safetyParam == 0xC02 and card.CP.alternativeExperience == 32 + assert isinstance(card.aol_card_intent, ClassicSccCardIntent) + assert not card.ci_initialized and not saved.get_bool('ControlsReady') + card.slc_producer_session = 'authority-card' + card.CI.update([]) + sd, controls, drive = case._authority_callers(False, cp=card.CP.as_reader().as_builder(), genuine_events=True) + + sd.aol_settings = read_settings(saved) + assert sd.aol_settings.lkas_action == AOL_TOGGLE and sd.aol_settings.main_action == AOL_TOGGLE + assert sd.CP.to_dict() == card.CP.to_dict() == controls.CP.to_dict() + assert len(card.CP.safetyConfigs) == 1 and bool(card.CP.openpilotLongitudinalControl) == shared_long + config = card.CP.safetyConfigs[0] + model, word = int(config.safetyModel.raw), int(config.safetyParam) + native.set_alternative_experience(card.CP.alternativeExperience) + assert native.set_safety_hooks(model, word) == 0 + native.init_tests() + native.set_alternative_experience(card.CP.alternativeExperience) + queued, received, previous_sd = [], {}, {} + tick = 0 + native_counts = {'accepted': 0, 'blocked': 0, 'nonzero': 0, 'lateral_only_nonzero': 0} + freeze_native = [None] + last_native = [None] + telemetry_rate_bad = [False] + patch_scope = monkeypatch.context() + local = patch_scope.__enter__() + try: + diagnostic_requests = [] + def diagnostic_query(sender, receiver, bus, addresses, requests, responses): + assert bus == 0 and addresses == [(0x7d0, None)] + assert len(requests) == 1 + request = requests[0] + assert request in (b'\x10\x03', b'\x28\x83\x01') + diagnostic_requests.append(request) + def receive(timeout): + sender([(0x7d0, bytes([len(request)]) + request + bytes(7-len(request)), bus)]) + return {(0x7d0, None): b'\x50\x03'} if request == b'\x10\x03' else {} + return SimpleNamespace(get_data=receive) + local.setattr(disable_ecu_module, 'IsoTpParallelQuery', diagnostic_query) + local.setattr(card_module, 'REPLAY', False) + local.setattr(card_module.time, 'monotonic_ns', lambda: now[0]) + local.setattr(card_module.time, 'clock_gettime_ns', lambda clock: now[0]) + local.setattr(card_module.messaging, 'drain_sock_raw', lambda *args, **kwargs: list(queued)) + local.setattr(card.sm, 'update', lambda *args: None) + def panda_cadence(original): + due = [0.0] + def update(stamp, messages): + delivered = [] + for message in messages: + if message.which() == 'pandaStates': + if stamp + 1e-9 < due[0]: + continue + due[0] = stamp + (0.0 if telemetry_rate_bad[0] else 0.1) + delivered.append(message) + return original(stamp, delivered) + return update + local.setattr(sd.sm, 'update_msgs', panda_cadence(sd.sm.update_msgs)) + local.setattr(card.sm, 'update_msgs', panda_cadence(card.sm.update_msgs)) + create_message = messaging.new_message + def clocked_message(service=None, size=None, **kwargs): + kwargs.setdefault('logMonoTime', now[0]) + event = create_message(service, size, **kwargs) + if service == 'aolSafetyWire' and freeze_native[0] is not None: + event.logMonoTime = freeze_native[0].observedMonoTime + if service == 'pandaStates' and raw_fault: + for panda in event.pandaStates: + panda.faults = ['interruptRateCan2'] + return event + local.setattr(messaging, 'new_message', clocked_message) + original_encode = caller_fixture.encode_safety + def recorded_fault_wire(state): + state = dataclasses.replace(state, pandaInventory=tuple( + dataclasses.replace(slot, faults=slot.faults | raw_fault) for slot in state.pandaInventory)) + last_native[0] = state + return original_encode(freeze_native[0] or state) + local.setattr(caller_fixture, 'encode_safety', recorded_fault_wire) + send = card.pm.send + def capture(service, event): + raw = event if isinstance(event, bytes) else event.to_bytes() + received[service] = messaging.log_from_bytes(raw).as_builder() + return send(service, raw) + local.setattr(card.pm, 'send', capture) + + def step(*, main=1, main_pressed=0, lkas=0, button=0, engaged=False, withdraw_request=False, bad_checksum=False, + heartbeat_override=None, inject_relay=False, drop_wheel=False, brake=False): + nonlocal tick + tick += 1 + now[0] = source_ns = 10_000_000_000 + tick * 10_000_000 + prior_axis = previous_sd.get('aolAxisState') + prior_state = previous_sd.get('selfdriveState') + request = 0 if prior_axis is None else ( + int(prior_axis.aolAxisState.desiredLateral) | int(prior_axis.aolAxisState.desiredLongitudinal) << 1) + if withdraw_request: + request = 0 + heartbeat = bool(request or (prior_state is not None and prior_state.selfdriveState.enabled)) + native.set_timer(source_ns // 1000) + native.set_aol_test_heartbeat(heartbeat if heartbeat_override is None else heartbeat_override) + native.aol_set_host_request(request) + frames = classic_frames(card.CI, tick, main=main, main_pressed=main_pressed, lkas=lkas, button=button, engaged=engaged, brake=brake) + if drop_wheel: + frames = [frame for frame in frames if frame[0] != 0x386] + if bad_checksum: + frames = [(address, bytes([data[0] ^ 1]) + data[1:] if address == 0x386 else data, bus) + for address, data, bus in frames] + if inject_relay: + camera = next(frame for frame in frames if frame[0] == 0x340 and frame[2] == 2) + frames.append((camera[0], camera[1], 0)) + for address, data, bus in frames: + accepted = native.safety_rx_hook(libsafety_py.make_CANPacket(address, bus, data)) + assert bool(accepted) == (not bad_checksum or address != 0x386), (hex(address), bus) + if tick % 100 == 0: + native.safety_tick() + actual_request = native.aol_get_request_mask() + mask = native.aol_get_permission_mask() + + + + rx_invalid = bool(native.safety_test_toyota_rx_summary_invalid()) + relay_fault = bool(native.get_relay_malfunction()) + assert not relay_fault or inject_relay + batch = messaging.log_from_bytes(card_module.can_list_to_can_capnp(frames)).as_builder() + batch.logMonoTime = source_ns + queued[:] = [batch.to_bytes()] + device = messaging.new_message('deviceState', valid=True, logMonoTime=source_ns) + device.deviceState.started, device.deviceState.startedMonoTime = True, 9_000_000_000 + pandas = messaging.new_message('pandaStates', 1, valid=True, logMonoTime=source_ns) + panda = pandas.pandaStates[0] + panda.safetyModel, panda.safetyParam = config.safetyModel, word + panda.alternativeExperience = card.CP.alternativeExperience + panda.controlsAllowed = native.get_controls_allowed() + panda.safetyRxChecksInvalid = rx_invalid + panda.ignitionLine = True + panda.faults = (['interruptRateCan2'] if raw_fault else []) + (['relayMalfunction'] if relay_fault else []) + slot = PandaSlot(0, 'authority-panda', model, word, card.CP.alternativeExperience, + native.get_controls_allowed(), safetyRxChecksInvalid=rx_invalid, faults=raw_fault | int(relay_fault)) + wire = messaging.new_message('aolSafetyWire', 0, valid=True, logMonoTime=source_ns) + wire.aolSafetyWire = encode_safety(SafetyState(1, True, source_ns, source_ns + 30_000_000, + model, word, bool(mask & 1), bool(mask & 2), bool(actual_request & 1), bool(actual_request & 2), + 'authority-panda', card.aol_axis_session(), source_ns, (slot,))) + messages = [device.as_reader(), pandas.as_reader(), wire.as_reader()] + messages += [previous_sd[name] for name in ('selfdriveState', 'aolAxisState', 'onroadEvents') if name in previous_sd] + if 'onroadEvents' not in previous_sd: + messages.append(messaging.new_message('onroadEvents', 0, valid=True, logMonoTime=source_ns).as_reader()) + card.sm.update_msgs(source_ns / 1e9, messages) + state, _ = card.state_update() + if state.canValid and tick % 2 == 0: + assert state.cruiseState.available == bool(main) + for service in ('carState', 'carOutput', 'aolIntentWire', 'starpilotCarState'): + received.pop(service, None) + card.state_publish(state, None) + state_envelope = received['carState'] + output_envelope = received['carOutput'] + assert state_envelope.logMonoTime == source_ns and state_envelope.valid == state.canValid + assert output_envelope.logMonoTime == source_ns + + axis, host, command = drive(tick, intent_message=received['aolIntentWire'], + car_state_message=state_envelope, car_output_message=output_envelope, + companion=False, authority_message=received.get('starpilotCarState'), + allowed=bool(mask & 1), long_allowed=bool(mask & 2), native_request_lat=bool(actual_request & 1), + native_request_long=bool(actual_request & 2), panda_controls_allowed=native.get_controls_allowed(), + panda_fault=rx_invalid, relay_fault=relay_fault) + if sd.conditional_car_state_valid: + assert sd.aol_car_state_log_ns == state_envelope.logMonoTime + assert controls.sm.logMonoTime['carState'] == state_envelope.logMonoTime + assert controls.sm.valid['carState'] == state_envelope.valid + assert controls.sm['carState'].to_dict() == state_envelope.carState.to_dict() + assert controls.sm.logMonoTime['carOutput'] == output_envelope.logMonoTime + assert controls.sm.valid['carOutput'] == output_envelope.valid + assert controls.sm['carOutput'].to_dict() == output_envelope.carOutput.to_dict() + previous_sd.update(sd.pm.messages) + validated = current_native(sd.sm, sd.CP, now_ns=source_ns, axis_session_id=sd.aol_session_id) + if not rx_invalid and not relay_fault and freeze_native[0] is None and state.canValid and native.safety_config_valid(): + assert validated is not None, 'RECORDED_IRQ_ONLY_CURRENT_NATIVE_REJECTED' + assert validated is not None and validated.lateralAllowed == bool(mask & 1) + assert validated.requestedLateral == bool(actual_request & 1) + assert validated.pandaInventory[0].faults == raw_fault + assert list(sd.sm['pandaStates'][0].faults) == ([log.PandaState.FaultType.interruptRateCan2] if raw_fault else []) + else: + assert not axis.lateralActive and not axis.longitudinalActive + assert not command.latActive and not command.longActive + + now[0] += 1 + card.sm.update_msgs(now[0] / 1e9, [controls.pm.messages['carControl']]) + received.pop('sendcan', None) + card.controls_update(state, card.sm['carControl']) + steering = [] + if state.canValid: + assert 'sendcan' in received and received['sendcan'].valid + steering = [frame for frame in received['sendcan'].sendcan + if frame.address == 0x340 and frame.src == 0] + assert steering + for frame in received['sendcan'].sendcan: + accepted = bool(native.safety_tx_hook(libsafety_py.make_CANPacket(frame.address, frame.src, frame.dat))) + assert accepted == (not relay_fault) + native_counts['accepted' if accepted else 'blocked'] += 1 + if accepted and frame.address == 0x340 and (int.from_bytes(bytes(frame.dat)[2:4], 'little') & 0x7ff) != 1024: + native_counts['nonzero'] += 1 + native_counts['lateral_only_nonzero'] += int(not native.get_controls_allowed()) + return state, axis, host, command, steering + + for _ in range(120): + step(main=0) + assert native.safety_config_valid() + # Real Card constructed CI before applying the native AOL marker. + # A fresh physical MAIN must arm mapped intent without SET or LKAS. + for _ in range(14): + step(main=1, main_pressed=1) + nonzero = False + for _ in range(35): + state, axis, host, command, steering = step(main=1, engaged=False) + nonzero |= abs(command.actuators.torque) > 0 + assert axis.lateralActive and command.latActive, axis.to_dict() + assert not host.enabled and not native.get_controls_allowed() + assert not state.cruiseState.enabled and nonzero + assert not axis.longitudinalActive and not command.longActive + # First LKAS press turns the same mapped session off; its next fresh + # press restores it. Held repeats and stock MAIN do not overwrite it. + for _ in range(10): + state, axis, host, command, steering = step(main=1, lkas=1) + for _ in range(10): + state, axis, host, command, steering = step(main=1) + assert not axis.desiredLateral and not axis.lateralActive and not command.latActive + for _ in range(10): + state, axis, host, command, steering = step(main=1, lkas=1) + for _ in range(10): + state, axis, host, command, steering = step(main=1) + assert axis.lateralActive and command.latActive + assert not host.enabled and not native.get_controls_allowed() + assert card.aol_card_intent.allowed_latch + observations: list[dict[str, Any]] = [] + for index in range(20): + state, axis, host, command, steering = step(main=1, engaged=False, + brake=withdrawal == 'brake', button=4 if withdrawal == 'cruise-cancel' and index < 2 else 0) + observations.append({'cruise_enabled': state.cruiseState.enabled, 'main_available': state.cruiseState.available, + 'brake': state.brakePressed, 'buttons': [event.to_dict() for event in state.buttonEvents], + 'native_before_allowed': last_native[0].lateralAllowed, 'native_before_requested': last_native[0].requestedLateral, + 'native_controls': native.get_controls_allowed(), 'native_after_mask': native.aol_get_permission_mask(), + 'axis': axis.to_dict(), 'command_lat': command.latActive, 'command_long': command.longActive, + 'intent_allowed': card.aol_card_intent.allowed_latch, 'fault_rearm': card.aol_card_intent.fault_rearm, + 'pause_lateral': card.aol_card_intent.pause_lateral, 'events': [str(e) for e in sd.events.names]}) + proof = {'raw_fault': raw_fault, 'withdrawal': withdrawal, 'observations': observations} + print(json.dumps(proof)) + stopped = [row for row in observations if not row['cruise_enabled']] + assert stopped and all(row['main_available'] for row in stopped) + assert all(row['axis']['lateralActive'] and row['command_lat'] for row in stopped), ( + 'CLASSIC_CRUISE_ONLY_WITHDRAWAL_LOST_LAT ' + json.dumps(proof)) + assert all(not row['command_long'] and not row['axis']['longitudinalActive'] for row in observations) + finally: + patch_scope.__exit__(None, None, None) + finally: + native.init_tests() + native.set_alternative_experience(0) + native.set_safety_hooks(structs.CarParams.SafetyModel.noOutput, 0) + case.doCleanups() + if card is not None: + card.vehicle_startup.close() diff --git a/openpilot/starpilot/car/hyundai/aol.py b/openpilot/starpilot/car/hyundai/aol.py index 32bf412268..47acb07466 100644 --- a/openpilot/starpilot/car/hyundai/aol.py +++ b/openpilot/starpilot/car/hyundai/aol.py @@ -125,6 +125,7 @@ def _policy_for(CP) -> AolVehiclePolicy: ) if qualified_classic_scc(CP): return AolVehiclePolicy( + cruise_main_required=False, intent_supported=True, settings_supported=True, runtime_supported=True, diff --git a/openpilot/starpilot/car/hyundai/classic_scc_intent.py b/openpilot/starpilot/car/hyundai/classic_scc_intent.py index 537ee5d4a0..350dc26642 100644 --- a/openpilot/starpilot/car/hyundai/classic_scc_intent.py +++ b/openpilot/starpilot/car/hyundai/classic_scc_intent.py @@ -1,10 +1,25 @@ """Stock SCC engagement intent over the shared physical lateral owner.""" +from openpilot.starpilot.aol.intent import AolCardIntent, AOL_TOGGLE from openpilot.starpilot.car.hyundai.forte_intent import ForteCardIntent class ClassicSccCardIntent(ForteCardIntent): observe_stock_engagement = True + def __init__(self, cp, settings): + super().__init__(cp, settings) + self.independent_cruise_buttons = True + self.lkas_button_supported = self.has_lkas + + def update(self, CS, **kwargs): + if self.main_action == AOL_TOGGLE or self.lkas_action == AOL_TOGGLE: + # Stock SCC uses the configured physical toggles. Forte's MAIN-derived + # availability latch belongs to its NON_SCC owner. + AolCardIntent.update(self, CS, **kwargs) + self.physical_latch = self.allowed_latch + else: + super().update(CS, **kwargs) + def _arm_from_full_engagement(self, CS, *, source_ns): if not (CS.steerFaultTemporary or CS.gasPressed or CS.brakePressed): self.fault_rearm = False diff --git a/panda/board/obj/body_h7.bin.signed b/panda/board/obj/body_h7.bin.signed index 59bd42239ca10200af88dbe3aa981ca26fed92ec..fda388e52ceae81613f142ab55f528d80c49af05 100644 GIT binary patch delta 32840 zcmaHT30#y_-}ZfG1{pR1LD^x3fnXRw5Dm}~hCv1p4A;~&6_>)LM9nureOO(yQV)2$ zrlyr^I-)6=p@NlZPmD`x)+1Jmdjo=VSm(RWV9)!!-~0VO_+4|(ea^YhzWldy|Jv+Q z_sk`ESb
UxXi2$nFB7TbfO3C5Aq>y?YPF8sa^ZFN|{zCJ!;?2xEczG6= z|352+m{4}3aWjNS1&0G9e|FN%hoebS)whS|6PN8B2}X;htwUk#!fum#B;MUKc$=>s za*^KTr?|x;(D#lEfjQ5&M~0Fm)b8jQ$Y3TOo#Fm%hk$e5Tq1 K_=cg!ApQy^p;pTj=8c{lrv1bCTXmb|R;Lik zO;O%MSP0~{7&VxsV7CL47EYHR^Ggisf=tM+0pq5?c>ZR>yOV!u6hhl9$2uQjn(@#^ z#6BD-X#PN~1mkzx)Mk-V)3Gr3T<1zQE%Ju(7c6l 2Y<@e6$O^jOtONpn)9^mP6MNY!6xw)JmoGz&4}M*21q)aNkz=+BBs`9?OQz zFm_u;fv@q`CK!LQPr;Jll5*!PNSH?PIZ ooik^df&?c7LaxgmxT7ieC})6@CB2ps4LyAvD~;&U9OL z;|<2wh1PF)2i|DL8~&3k*eCPsFa|F}T)&V0bs{M0qsZY#A)2qHi;=!){|@Mz>g#*j zwgYAwTw(DM^e)?B@bC#8@`Vq~D?DH+HP7l+Ercix31=#8 Vym_indb?0BK}P5FPTedQRCce Zo$x{n+J UPWHb~n@{w96aQuHACC-ST45S #3f7Rc7(M?v%C96aB1PsRWp*=d!Ys@X0 z?_)CTK;&k-#n_HGm%nA9GC>laf1;Pc*$_P<3bTa`je||t>LM@b0;k7q(R a)iOn6R0c#@lK$-b22cE-ctWB45#{d43JNN0T+<`L;L2_^>b=|8e9cv~KH=a))s zl;^(m7?*}=$QP 2o<)7Li~I!q|`O|M6+cl2VtQ4!~0keeZON zi<;^Fo$EPz^K=T?ME$-}5gkqb>IBKAtzU&fY#MMzL&nm~Give 810JFD(aScRO)Oej$ z(9Exc$a*^a>*V-dKC!2-vb_$H?Ym$WLJe2hK8K*~3BKj}%{8w bJ$hMwyq0X>VZ`dqr2JIW(h HceDyjRq02mMUKj-IQ?y^Jf43=+W&J86&bi=uvUT*Oa zbjH(r=fXh}xt $3La@%JPHeHH& 7?2eyB0h@rn_rXgIl}pv(zZw`I6N#qmwWB zv&TG+f}B-!H0lx{Elr@k>IP$0q|~Xvkj$!6Ih0s;J(a)q6L_2rb(+x5_xo3?4LZX! z%RZ|t_0MBg%e@YHwHCf?V_e=2eV<>xLCQOIKSJqYyr3i*^y-EFF4NIFf_X==0+eJ# zmFqX(lH}>oSMbg*>0YIWS?No?%%s rlASnUK~O?sr37$ 7UO?Fg$3JY|tuYYQ(z7%S>i|nY+MRrL29qU{kjm9OQal1PesdZ@FZ6>dN zDYfO;OydpIWryDL7o?(1-_L;g+PF*cpb6i<K{JCIGcW0gu15vlWU0<{ZsAoZoBzu zTH!4=5cGQ|-T%**rLBFZ(ck|Wjs@%VV;G5~aX;p|bPj#FI^}uS0&moH2;Kc-tX=2O z?^$Qn&p(bM?#@GCvDaX=@}rZJhb?rdDVeCNmYYNpHV=_J!W%BP8Ywn>aJ0p;xm|`q zZLyqcx3jrlR}Hy(kvN7R(K=w-5Ze2C0vSfrug`Yag@Da~nO0wqCx6kV>$NW4TBv0J zo-?+)pVG+;>hSZ2Gu9()^^5Sya8Y8;&7*r?Ss!d?*poSMU%SPz>tl!jBcny;nkYo* z%y)}7F=aIOH579;?IM-JIY5X{TU*(`Gex4fWyO! AK~|OqGstT_EjQ3%O}nG zmYq!`s#WZZL`^|N-Xy!!hgZPGo=^)B&NU+;j9R^r!uH{-&qbsVG}$)Z=4U7veQqsB zg~eNGWCZ=~mY4bq^#t9>G%Sb^Ii^k0J _2c%xqsiieM7OVft!ik7&_qBIO0J^ z{p!)b&elEE#iv?}VRv%^_Ho18-KzM>(%JoJb(vmg-j9yDkf66pHK{N`IYgk0-R@ z8!G*6D7r1>H -Rw$ zGx8F2C}FdHs7YksCaP@te@7kuHOu~6TmIkC#s3OQzqn=s_Pf=vff3B>iuqgjyZBo! zc z)S38odL uA+bn!z zFT)l{-D2;mNgJJXHxx^` drKvnL{mYg@X`i$L1KdO!8usw`#iYl FO7c035^;~9|$fx qY)2Z#<`XI&`%kIq2FriWj-s_ zX*S{HeP-FqW-Kk!uQ&N;1;YTyk?wdnRl|pg@9QChdyW%s-!%$sLEZF~IJ0B$7C5Z% zc(6#9Y1pF`c4kz&g&H4ukW2K!qglD1gRx3(X*+CwI?QqB6Boyc5GXZU_H--s$t_P0 zf6vUbzh^BI)9#TFY Wt}w~<3fyVVcnM^Qc|3qL_{Dwpg(YJpZw zX4s579q&C3!o8e>k9{Cdx$rm?mu? <#gv?8cuNjp(}JEyWM*B_7*g#%?;@G}6@_c}7Mm3-#Dyq9VxvZi*OOKQgS pLhK zaS_K~X$@JiMn>g?!9rv{;B^?$UA$3!6}l5TmHnyyH^L>dauJrmreE#?`e zEW|hU;YjJ8*5VoGnPPCyk$Ikl58h wMVnYIRkc+6 z&4^sf85sRfY^KeMI~`0wzzt>LCiXV9w2YEiyXo*&-^A 3)8R dohUp%#Ibrv+Bo=&q#m{ET3R@ zEb*!qq6M?OL}4&n>aD#%C7Ufft7unWVwazIg}p-$cPs7I!&gDCb`R}4AsIJr^sYj- zJ5lKDV%+&OE2g2=H0;lFt%(wA7yZ=whTT_4cD&M^oFIoPza@jnp2{u~N8S@-33;C^ z5o-vEm2|a N{70Vk}h!$?Du#Z_D&BR-PnUE(?!d6j%8 z{vjh8QYgCGk<)!gV;@LvsXE+jIc)QN4&OTQwjJpk=O4j$rjHI&)+6TJa`($7;%aAV zw#2X)W3%ObwiO``uE{OpVd13{cbhHcVpuP7l?)I&dZ8{2Cj;z>3rXvZPWqs}$r1>6 zE!-(^SAtKD9F$sJfSa4c#a6 z9f(3gBE=0(WVVELik(hmI2kOaJCmivU#xW|uairn(uLHLx#C?HVkEDMVsA2$1c^<( z$y72z9OX)ekZ|#RR|HDLMpu$X!cRuIkunMK6^(t!Y!V~-c@RG`P>lB=H=Xzn-*6CD ziY@(4`gsyB3DJvj{Yb0iUtMB^7kQa9h`C;5u0w+F#54zVV4`^4izN3=@a?m;mc>Nm z7=F1|rcZwEbiizRUsQV&m}rW#y)nJUi95VWSmIldwbw(2TEb=z>{ H>@wO(Fgwk#7* zBC-AQ*fGXRwoG&hB2gZjW6KSEKdCsxFQ&^O7x+jV5k&m@euQfZ5_e9NTms3vQ~9y+ z`hz@ic@P 7DWZ2ME7`$2+|rZV#0%z+5O#p C7A9+b?znVQlloHx)#w+OmMZm5s6w JJeiCVmZjnc7C0ix_r6lQ2^|sf&4qkuoUt&ryQIVPAEI1xB-?UnyPgk;X zR6e=o;eqR{Slkv&M!54n9}JDYaHrMI+R$ouSZoU>LF6sbM@1g>9?$p%w`&i+?DNdh z)GhW2AsMsOY6uuZ^iZYw`?^DfNIYO^3=>U-8&qi#Sg~pzH~4?W4UVW_NvdGP4e@n= zj7{ODhQ3O0E@ML?GVLPruCbPl_Y5fyuPAYMMXU)yEp+1T5VDvI6UT*;M9Ht+;`&f> zIkpp%aQ!er;f-lx4{fh~W`CID8X|CY8OQ)Kf#DHW%QrTggHZAPmx!MvTqhoY-J!Q3 z;y-V Iz;Xg$Ayu_4&DW=mN^~G zmg(ZvFv7JTqx3=TW$mL 6i)oYrY);uhXx2L3iH`=e}1{7lue9X4Kf7W z5sXbIkS%HDY=t-iiDOsVu9mK5%Ea%O_h7AwsALLxSjq6!;L(e-APDuJZQKlIt&lpOHyr}?JM%-Lr%6xS(6AtJ{t6@u}0uQ2;C zWo84^T`po)1c}PLme!+64u_FxeYT)Do`OmCj-bf vIIfYVlkI33777V*-H2ijN{lqDPQ=GwYqWnf3J%BKZ9Z9uco0 z>Y@;2x@#2Vg!2W3ZUH_d09of`n2UxC>mg=^(83&g)EOLG JJfQ!IoWL7`%{DaBko>^WOeIiM$=a=oWmi4Ka zGV^uj-77hh@Ea4SMB?x_o;(pr@F9zAkwjy41c}OM(pPiML1r!%O4#)WCG5$AbL_?g z`T7!e;-cf`5_TM6CxKHBKFBO#kz#H%@ghpGAeyK~DFW@c6f>W%xuoGhDW5=}m1aEe zD{
2a+>nX!&T|RK@FO;a&so1D${yvI+qbfNVgw3gf*3 z(uX2^$77A4*p9G6V%H#&;^d0yE|`PBWw?l$FO#64;|R#cX8?lWW|$nBtg*>zq4~d% zz}1H`XK~fbWPx2 f>OYcIkAvVOc5ko5ytuA`_O-**!BLPK!@-!cK~ z#i_{6zrx}IaUmmQ@;bQn9|(#GJ1}!|dV1j$#iuZ`V4b=i=|n;CT_xgW8w5q2=#U5! z(?v{9#Q3_L9G^(+iL1Ys4E(CmLEU;&3I0zdzBQN(DjJcrf=!8A0p527%ZF`^IkAql zBzo7ZfvJ9rWmU((;6%fcj5?;peHLNS3MN@p&eR1f*u!_Attl*L;RWSvFyfLBJ_PPO zq#;21VwhhP;8O#@X5bKTwBQ0nye*co9W#;Rc7*KXN%jJ}3YsmI9n&(JEgL(g;`c+_ z@4Alh@UIcmh7gt7=N}R6z1mkW|79@G8ghZ Lu)$)GmW0|3TgyW-wb=X)iEnDj&``IroU0{@u@Hakg}WK<62Ke8 zd_yWG&^So$#fw_ 9kHxoi=NQWaFT2bGS-a&|B_x b z@1+!*-z(M>-g2mWkVned&e&$$gW4?a7>bLRZ`%^gzf$+$ZE~={&w+o_f#1$9XNO|> zUA%1@kgs@mC>aO3U`P`28M<8gn(-nojn4)1sq>JUt+?#8EY=Ap<~G=p?2D~nhm@ 6`AA#RX1S!59G>YRcPJDfUdyHF3 z3*ie;%^k6ejHq_(B>e7+O~rs6iq%w0)Z zHtGy(8i{(pt=3Y_Cphi- 9L>DKdH1+ ()wAgQuv@Mkkn(Cc68q?@3X|=d_x85Tv zXGo4+P=0cY;>g^k!Vt6`-7ksrZrq^hg*v(0;`$uXEHy(v>0RWBFkauH7CB_G{LP{k zDQ0~jKA3{kC*L22O<0xwn$%!t^nu1d 0 zwy@r!DV?O+A5Wx4o|j(+2?f29l0kg@W`QeRQm=bnFhloz$@H5nq*~}Nho+IITbuzt z(m~vvLHfpfC&?(TUqZ|+nC}$JwsCiUsRF`~sobC%%#P A (e%2(JGx)esrJ>+COIqyC*4mRGMs2UVgm8WrL{cLD`_h(yu-p% zEErBy@jhs98n{PaxGD9#x%u3}T+i1mOwyi?nzI^owyVmjy26hBlkdC1^&3HIT^&wB z9gg=t&yEJ-uoOWc2Dt>{ln(;@fB-Qn3)_32cCMZe#5gOGoT|w-4aO%d=^zF SJ91cr8~Wb_5dliah3Pw4f;J%t|T7T1?de-v)BS6!=QPoNSDF06zq zW-IPUDGYyEJjI-BqOrdL-OLJhT`b8a9!@=rcHQTd*tm9PgOGL+YqBv-JH@-% 3A}jq>JhaD14mw z{sd6$b4B9>G9L06hlyl^ `vK6o4{Ty-sYY=bxi}90)O7grzoIHsHOaAN-izkr@ 6CZCXMG3a%YJK`$#iPu0 XHyl} ze4oUZ84tqjBSsPv;aF2<)=33b?XzN!mF#K!-#MGbA6^GZ{HJ*LbqLUPf;9%O?LW zn~8~2aMTPGmro(F<7ZCmviMnf-hyUz{mv)-9AA1`?AUGb>>!3-&%K2%i?fx#FZuWT zo$>E9@*(3LJ{d#-zf{RDSN`X??-lK)5~ct5+Dd$tNzfFSxWKu#IHyIvKnPYC9$K2) zMEz6}V|TNSOLE?wN`m^X*YYi@7na2)t%q45mDj+yMm#u`sO&ef)mXe2#Op}sQ>T?g zzfmtlLzE&>7(5}(|6oCZ@*(8FF4KsnXjR)jqpEOoFWL1 5UxcLjO9k1IP(4uim;ys{8i#=Ar&{0+B=ODA0PkP{c zRUd1MTC$Gt^|d1sxd0#WoFa8t=_@2`r&v7=#I2L)noE?i?JU8#2V>?e^$i-MACm&c zVj$l!Wc5#R!%&Xu)j4xTpp6|uuhxmVx#T^vS$vX9ZuKk;@rUV C3}vH2BdNLOv2l?shfo&L+>(I%P?5l`{hETS#)MgLqu z23$Y+Pmpf6FkCX>qN#uNUzT4PUozLUbiAgT~`*{a==WZ3uBbjZYTrz}E^MSk|Ee z$MODE?U91N?IRD=KeNn4_@_w!5TgB$U*!LhE&oaOTd#S6DBfc7G7l8rdV|>eUDs|z z5!X?~2S}PeNYJdstKF;|o8E_S5S7bMpcwzMG(uxp&VCYW-XNLkAe(GF4 5;5Uq1a9BQx?Q*`Hx z7Vu+!iI_GQr 965aD*`ej5*rCeUyyKq|0dxearA}6QEZz_-h@skcOLl> zZI6A6Ty^1Ff@{|8CvJ1_^+eJAZ8Fs*J8|ce-3M@8Ztzd6TwMM(w*HZ#&wLy}1I5Jo zAg@P? WMNOk3U5EjESxasCpIDJ+oxf3Za*;;It{zcyw4F3 zvtt84efrtjGf%%;>;2R%Vk?w(lSP+&;^i_S@EbO{HxfjD!$yj+`6MoMTGB!NWt&Z^ zf9f}E+7xbWu(zsz17`=L&=o0-KS3 oBd1~< zz%zV50GDuTwlPNq|Bg*LdmL85j{kq+Paysaam*rYdne K#|GI1-CfIzRe?zjlA=Eb(0xPQAsJ&HSOouArT?n(z@K%J)^w|4Y?oQK@ zA7Hf}OD}&wv^L}0@77(*x`9D|^Z}l&i8?GMaeeASHPLm2&IO#<3FV`4o;exnQE&+C z>r22~ycn3LDP4@AKSdiClfJgiE~&Y7LhMa8mbxt={+j(z(|AZ@mVl~_GZ~iPZjRK@ zoTX&skQ2V^AQxc!z{>Vz%P@=O_OURv^v_&&%9ry-uN>inAJOnHqg5=7upL4gZCgq_ zY_4Gpg>lhi&QNU@akgpb9MXI`GK-9KK7l1Lt;`r_su!kxUT4g@j+W$Pkx;u-Ghdm8 zV$_Q0Pg!^gQCt>=p@wc;M*7%%yZBJS9;j2FqtHd@O^WH&WyHVtxiMJ8t=GZGfq%pm zR@|HK{jjuCyXBzbGwG1!WQ=EpFK~QI@IvyJk(2T>oh?kB=pIo;cP=NnxNHhoK@Pf{ z@r7_9-g8aF+Xbg_vuO*OaGqAL!0~kK@Kg2QWqmT1ziHC03djn%t;FkI8Vb`9q0(*` zjtH%QQ3Z%k&{-?dLJa>(5~-Q@%&NJSo?S^+_&&w`;8gUl!dmc~RL09F4!!RF`qN}@ zuI2G|_TYL4+X{A@rWo+nR#Ah2eCWLVwYzoh(`Ju&x&B}luel1ZlA96p?jDUX;`*WI zWIESKX4{?Z%@0o}j1{beRvJlmbZIDW|J}ORvs8_c4L>jV>e~-}1alXm`nVEyd-0_@ z;Uk;fYhWw_@2Lt3N1 jjf}nE zIvj5&ny#)x141pkThK;UBzSW(KaEamWZc`vw*|9e_&M?dI|YMjMXxEf9mXD{THZu` z*ONfcQ-k+6I$AG_S{7*p!e!R4ry{4Th1^9o7|`#pC&3n^WUI`m%t&5JG4q?KvUG+x zaimCkc|D1L?bP6TQS%~Kvm#K*L26#gXO=R$x9QQ4UNLXj@Mi4rjE;)76r;9mh|(>M zF`MbQ4J2sFsliGsf_k#Ee8O_}Ew>{oK?}b}ymuOOx8KWriED0iRcp7hKhpHxDQPgc z9sEePF{h?|0d4WzDBE%<33#O@1Mj`QIE_Wnn;VGMI&$T*mtdHHc5EP?ld4+#CzF1N zuJn9v9O1M^|42@NMmqGY`~L|sulmlFEE?B&5-c#-#z7JudXUj+JELr_leFkGW18zI z5*FcZd%8|m)69?XT<%fLMjXBG(cq0FAo$GSuiOixdfrmkW;>O$bm(}G4kM6!O7|;c zC0=4fxVYP$47mVkgS?}ygq@+EY$PKM!_ro tf6PYee~(zrB9{DY{oNBQ z@1*k|v)A<98YS>5!~N`QM@xNTU18p
jhuP#`V(~QYe;~1tOdUQUy*-ly$3HVRTw=)%xCh4hMzJb@%&;l|%NKO?CnVf@ zs%3Ph(v6=GH_V8GpODZAvpX}+cG%8_sTC#bJLg;E{b0S`D|d+Zf{~E4aBiI=Ej)FJ zPlHOuH_@r-9rElptxkf9^k3#mmK@GS-zT66m%)h(+=L-4q;G5@!#(DQ=P>)A4# * zbwmrQCwAa;#;M=un?aWeJ-eACxbSmXLy_c@o6nClYce>w$)(yY5Dr+&919{I$2c`$ zFCOvO) ?F`9oT;g}C@!@s55VI#)0&FYl)5Z$>v5Cq*^oy;&n~M)TF$G+hmj zZ1=YiACErRVaI}dQ3LA-bC%^rjVjk;Xn08zsc$YxaeCFv@x6f%YUcTH>6=SD+^%+L zbY%rz&`lrc)B#$|l`nX%7WxB{o$bvz3<7S|R%qtOXy#w?m$Wq(`^}4V#8%SZuB_F& zvmM{lceav|ZkIzGLCtofcMc0x>)j)|mrFjQwSURc^uks$p--{*X7Iv3HG0^rj9nRj zeY%x@L_-xfcL)YuJPqDP_LKK$z-*8a*fCFmz+scwv(v?OG4}lHd^_>QX7k|k>kDrJG)x& zeXm;ZSyUaZTL&Ly4?(%~kL_fbh3~>CGM8gi pNse8R+q<|8{dB+$ z>;SG(l0f!Tw;iOK1X8k-IOAyPvJ(@uCyhibAhP8hNNB3cx3yS7an0l3TphMXes-$s zazNRta4S>zt^& $vS=zp!f?aF@<~6rTyfdm2N2ib20(y2Q z8RJ~CWdkl!lkgxV93@{X 5i-h)B*8z1#mTm}$`^n>T&g+I$0ZdL=6Q6&vbo$ON z9D~bf&Mv$plj-qYB%bu6I8%5MSE}7j3@**B YdY_kaNiwLS#34=I^xPrh0S@~X85dzp)kH8<1RepGNw1*5|m?0@E0!a1i#H? z!k|yed*WqG>nFVYQJFGHVf}7Yy?l;Fe@X^Uat!H^_qA|=XvdH=(8K@Ru;ZN^r-j(u z`o01eDH;k!71yl|E9wcWLyd9@*dSSW=lly=
X}V=`Z{!JN}qsf4-*M99}v+8KevH+S~Kx3XbFeQ;o~ z!o_keng$(QobSV%LcXq+i|O4%WHn^hQu8ny2GO;7WC3o^oAWRcJb3YZ9`Y&5Ss?rg zUd2Z3Kl1DJ>#s-)=F7pa(e@>#@FTc>M~^a&qU{r@!#~L$CoXw+tGpTpQv|=tvTE7J zboQTQ0`Ip|0m&eJ>FNS<7u?d+Lb3tEZ*_&R!L!6k7#QC4`jLycWN|wVhQK|#;y76r zm>71G9Y`iat~}8UxXLWCpekZlso9MwTB{NV5B{0Og!w@&i43{OVyN#4@+5RxyK7#X zeD@jgp7m^FW1Bpx?G~F9#@VZr!mRS@70VyBnk`9S@<%P+I(w}~UndvQ3=;{R;+Qb@IL!0R8agT`O8wMzk*-Qv z2Oi4pSZE?4-TF`_Z8xwB0agNdni*J1uxoJ)d?+`Y_b}M(!5eY`hEg&1t%46y!mqHN zw8I41n(tA>R4hdg j#w% kt>AXrGHE~E*QMjPo;hj z<+sd&*F$-><$sFh|4j4TC8WQu;U47U$*_m=WV6@Ohw=pTy(JG}Tx2i)$D8c})Qu{2 zeYtHv@@fb2k1`)96b7+!ZsMF%Z^t6x!WdFkrp{ViRg9gK9sir5Gkt9jEMwt(tj zKqjGyv!^xM{V6=dMr6=fvcjx(XJ`*_BHrw7!S7~DIZg13HKl&T-?im*+qWdzusq?R zyaE*Np}gJfrhh0eHv6aXi%nIT*ldIf6Sp(pA#duq!Ip<7MZ?%jW`66F24kU^pMKT> zc-&k+>`HZ}M<5-i0Ix?DRR|cWj1W@AB$!@F+RLd)O{3>LP?|+>f)~<~Mbnfs ={r{sAm4uyk5}B$?WE4!LZ*L;4s_NC<8s_I8E+Jj$Kpj<>9* zIX{7Kwu4^%iTHR*!+Yr_6kKxmMf+gp6z5T4cnOo>)Wa(vk6~9v{6n{gLv=oIxQ^uF zN;q7|6?~uxEe~fWSYDu@J;A;T-%==S|D{j@bUNcxSj?-H57T$ch=<|8EEi~DD$|5t z%{e+c39I#1+JO5kdR0QZY||z*8`Gi4vZ`HHwT0;f@0G9D$~ERN9r3op4e56lm^DJb zT3|eM)&i3ds<6O#=y;pp9idB)chZr5wQ{K$MnSa^y4nQ!5E2Lv%I5%#&}kl@h;c$} zH{hZLUI@4n+T3RK^h~DJ( `|9oOBjdew5~w~O06**+ZP2klEFoKjezk<`w6&AvTb7`` zWd!_m2b+aC-nwpx{Q$ViGH#`o5Ff2emJlDKb7mE$J0FVE7&n!7cfd|qLJkNWqwzn3 zX)~7o_A_xdjNRNWJDH8Ti!g-L&1|i^0N(~5hA4)wF>7?^;S;Rk=is-sa<+(6UUGlp zhb8wX&ndZ|v{z+O)Tv RtFrDG3jzi6sU$J+TBy6>PS4wd)&5m>pbMjp7pbpu)?o2Wtg86qmSLANM4|qg_73 zAz<_*C+^h8K1pzImyfpwxTC6!(N7$lx9El3b|`rqsugTdampw9m?sHt2Ur?9`FZP? zwSt6t5 I4-u52{s2=Y4|26<~r)~o3jl3qXb!f(*=Sl?GS?@zus2VVtiE8 zvg#*M+x_u`l|?OUsDC9HJ$kcaB~!bF=s$`&z&;IH!!{-J4;vg{J2o9)yNrchWi7l| zD~9wS2iO*`b~y)yZSpK?*%C+|Psk{0*-DElNy5mmnGg?z)5DE}OM(OI+aiD`s5x~c z5rRM>(g6Enh6Z=FrBSZtj*e?ses<8v8zdoo)=ic4pBpME2R?T^e7Muk?({jV%b5VX zfW6bOI2h}}F8`++V7FbN)i+3=KGp4haelGtdw%){@^9_^<9uV?lYf`bwGBZ07S-M) zZch5Ph<=sTeY`zmf^;wSdmu;9IDk`zv|XsqZy{d2VAez!nBSuE;-;w#)%lEgP3Su< zYFeZtC(VJmY_BJQ)R-{P(3EkE<36Z%jU-;1LH5m_LfEgxM}J+4@S}4_e?82X0zs70 z(fLjE@=a_%GHKmSvQK07Ym>8SZWS4WV5>Zt-mSvPB7=5Rk?^?m9v*2I*`knx)n1Uz z7@r;FKdLN;xdvI~on#9`X2l$3Zb2uJew2B@{{Vg;kfKHH2=@@Yv!n9P6WwI}hQi bJ86!B9MVd}5s6`5xNn%-`if=1Mx}-z3m*>P>zv`-73- z|A1^~Jdj>(H|{AJ9rdGO#A-Co{xFJ%8~lqHI9EYRULU0hh(rhy|4Zfry(t=sK}NkJ z03^aFIdZ<>ufU)AyWpjiwnlG@(rmR~qmv6m)Ld9(go-- EAO(~##5ldbB^C)u3iaGi! |-vKmG{wYWE5M88J` z&cz{jn2T`)%+;2JM2}XLSAsv)afgfn+hxqJID~xNqSg6gu6eYmy~R!9;UE;TuLXwQ z?-zUs>K}s2wiK~*J^G@$V`hHMekR1mvJNKdJoL}x*7onK-EqPlq!$JqFB80{>Rj~r zEE`_mRI||UT(!M-&zOcj-9bk_z^Qsc@RLQ?e1^PVSUpjfUo8v@?+H!fpQ?*sRGahM zu0byW6;kWF&}+eg;x4wR{G*Ya+0g_#(_SqI0r#I^l0T5AwoHwGARp t zWlCIu_mqUoEGTU9>r>*UpcxAwh_K*UPe$xSOpEy~VHlUUo}GwlE>F}810KjJ&$#`- zgt6F5_kLsEjxq6ZciGr%Kd%z-0J*Ms3Ai5%O4yR{geIe^ge^jNIS*%+vNge+UlJ8J z2*|~Np9zn^bx8?Ar5s}dE_)0LRx=xclp{#V3%A8?G=Bk@RJsyuz)ILDL#@2I4Wzhb zNWueoI_zMS8EYZ!(I&qR=V!*E{kY=+?(qk4%Y{!Oz+-X^TlDre{Ld_idI~D*bCjvQ zq96yc7o1G1Agzw v-)4yZhYqC$+4}I3ZdIeqz=oln$05FZU-GBm)g6z+9$`*ttPtk*mN{P~XQL<` z#UjKH{5wr^ej~Y}X82)IYN2g^kOb*nqbSYz3f~0XJ4BpKa4|#FrZfK`TWu2$1KJE1 zV=GFHJ(RDE?)w@U;VKdDX-B+lK=b@&!^#DG{_BFV)aS4h(4tc#t<#{cDK)$x{JLQH9;E3fd-1-#LGSiF$5svOlmlzY3$WWN1y=I{ z%yYcb@@^+F3~)R?U+B#HLMvh7#LF7xoqAI(@s{vk7c31h)I~$x#coqwm52pl6zqcW zMwF&l5G*h=&YhNqy#VVcE3hQML<^Q6Bqm`P;qIeRdrbHaF5TlUdJ;y@x{Jqgd6?lL zI=@zm^_O&J*oueD$8iT>UE_A5T`5@qLQlG2uBoPr9%7Ux ma3@ zO)ot!WNIH}h_Zp#c1)B;?l--n6|;z&%eVM=E!<@fQ5qF2Sf2J?ExJlcfaq+x?JXYZ zK8YKN?;0>}B%X)4!FWEH{MQBJcHjSw#|_GN;Z^EI$tgyZ4(E%~3e%ln(V*&H9)}je z4Lc!9ZbkI%VPZX5Z<_jwxWLA6Dj8qRRH_hOeWdg%39zm@x?mDu{CV)-{09o&j# 34*{Cn>g&x2(`rDAO7QtPW%hJTWP^)(Rt*b(a-DUx=(zJCPICwt1VryMUl^8 zTqlYbd%#3=qKEzHx>=%>@r@`&CW}(QDN!1pB1&aT6dbO3(=~;6QS+X)!gCeYBt;kIH%K?GBZV~W~YKaJ5en;SPBq?F4)kq&tZFaD|lSDrduc8 zi$iyzbLev1L@9ovia#V_?RAmnP7vJ;Tq}gXp3`9;pTE~V;qyi{fh6*0_3B&vbGCvI z1~|{zFjslxdGPHJcZJUzmpq4Cz5zab-rFPao#FHHd&9d1Jl?o6z&*H+@-e6e!4(x~ z0Z0adE&K+=eR&*yKlp3lzXX3aG7NzKD)9PpIDAj|M*#PMzaD-s_)Vz5ji UriL>w2_8{1_uQNrG%M!&J=HJ3_Gv@@k0`SLw@sDN!l?m z=kpGp&VTT7 }3k7XMLQW^ri=F5L82|jWVXZIpINni8A9z#A zxO2KF-Gn<1$6F8soW-LR9L;LQPetKJr-{;H_#DRxI5XZ>(zG`e7{{rb`W&Zu0C46h zI4dq$aJ+@TBk?(vgG_C2DTvuLagNw~s0% ahm;RxG z^M0iQyNt9AcnoLZF9m)M{HrMn&eaPR9D~q55eppLK_v2muVNL>o~4o+Cm_Rl1=fY* z*12N4d=3H@sgew6;GKsG&iQ?K3&-KJ>Iq8xK1TdSoW$dz^miP4yeC2<3dx?Pw8~|n zN(u&nPQfJ1h8qcY7A-GFFJ^oO)uTNs*k4ph?;ceu;SKKk&0B=YN^!*8rjnv-FdBW( z>z6?6;ks~^Vw6U9l8S@nfR90n1`XKk3Pia3i!o%qv80Vx8r0{Cl9>QL#s$4bS^&a* z5DrLGa7O;9Jj>who*2qCDrw#sQS$j22&l~kupw}hoKYw$+UbB+y{?jCx?=&Sr{5Ai zY+A7R^9HV F!Nyh z`~Q#Wl?2t70}WDB-=;#{H;kqIQZm;Vw!9;9bKWuU+Ar^ieYRobKQCIfpKqE!w`lA2 zku!fR+?#(j+a)yfmG?`>&3Gg9mAz-^we@FzynFZBwp#zuyNZ27bF)r{kM%#Ze&u$@ zA78EV9s0_?n%H0J&z!urJ^GKz&(8m6==d?;zCFf&?dKDAd_C9DcWcF6GVFf=rc|j2 delta 32066 zcmaHT30#!L|Nk?4z#wQqswebbP**c}OJ3kxwah>D)%G+gz5EE~gFl>GrY5jANWH3iLn#MZE z$*q4#70c)x=UlRkRymKuub09PzY&W4 Oti>`$p;JOERx_Piri ~gmfqFi=Ms^A|aT_(*^xm=p;Kg_T^Z@5sijP7*bg-Yo?vw7Vz zj~kMJyAPy_I$+AMqKAi4#h%Af#Y(B5zWZ0@+r64ljjr;cA(Qbo4^Ya}BeD@rCYj0O zdyhKZ>;We2w7lxg*2;mt#iZ9hygvcB4&b9`x6*HVe?t1w1%5x1Z)v)}u17|rRH3#> z6|95i`@aqRa^+F7wz67jBa-c%m0zmv%Ow{&=|`G3B`uwmAt9#;@u|G6tt2F&a!aTy zA+st!?pNzY_R~2DD)MRN>V$C;-|H?iMQN8*@z7PK@OP6bh5_;_8;1;)_Tsp*SK4fQWX`a;jN!R$0$i1)O v}g>9%3n d6#AG# S8&c~l{B-dJ)HhFGLHbr00n<;t=IerQK=rTVF zc-vTBBZQC;%2EOo&6n6TI&VZUNv7|N2pT;hkYWTFnOiJcsXe{$+b3{zl~nC%xLeP$ z36Zz!3Faj!>Q)0~!GS`Awf+)Q2fB=y9 y7k#3ke0rK&MRLo?OQa^G~tM5vLpYOfRFWbBB|a^!r>D==5IhP_l}K zj*9nNnIkm~G9PY@D3DsJ?=pkkHl(_Yy-Amhsw4?CY;=F;UQe5`ip%Jf(Hd<5zS>al zS-FuFx-s)cR^)cme36yv`(*wR@QyL=>P5De?i{TlWwdtm^n!A%PX%BTKm=?CYyoTq zYy)fud;ogcfpj V(ECxE?xeRw_=)lEGf*FGC;6;2 Xrydw}-=8vtd1jR2~Ty%L>MYFf=s(*9$D$tjvU zCWxG-3&!Y_C-oi|_TBXmUSM^J=NcqhPt=pe@S5f^VX3F}=N$`kNGVf11k?ep0;2C( zQ+MUaP37z~Ci~_3a&~e)Co@CSeT@(z#7v#5sG*uR+Xxs#91vAuj81GKHJ$HhtGy#&PwW_%`V|>6L@@J^<_mYy&wL&>vq&8oSwz zHtcGYyIpS-q-hV_q@`@L{_TcdF5|PeO;$X^821dLt@ACTw4USIqu<#Ov0{jcs9jv_ zwk=(s=hrqDN)=UtuA;t_ZK2c0Y774NUbjbWZ4koS>XlV|MBDVO4drY*zP1sBybXl> zsGex**p8JLW8mWkl}l{q-2C&Cd%4JU;i&PoerRPmR*1w&tsc_N0cpmxRH7+mHBqHl zzf$&f;Cb_B%OzH4iC1uzF)Ou{6$M0T&42ISQoY^wNv9BA(6aDgDLX-fUJL?v9QvYi z#0fngPPIO#L7{VTOyB1ALdWz??kr4CX}Ncxlzlq?=^aeV{3mz1IUDE8|1WVspgUjW z5=_;LbZ~c_(5zF<25*^(SUrVeR;wrAghDv6s^!J3PVZAgw1T#nozb6aKIvV|zM)0q zf*~*L9G4yH6ChKB1N4AVfLVaNBd3}RiBq nBF|8sWSv%5OZj(WVY04@S9VTWA?Tm@VQ+ywjyxErOY=A4Aj z^HSY+f@E=p~kWazjFC3Gz zI_PAhU2WI)$Pro#g!Ta@<$Fe_U&j~%hn2HOZYR@D9?q-wWn|$JgLW8YkLc|QM|++{ zOTJWBk=_lUpG`bK9#u}5 Kmb#8WH!A$g)`sim$JzQy8NYe9*?6-*k93 >cH1R;A+zFH7+J^&Kmw2gWPra=E+2W4DMoramaEoT zxcpT^-f+bczGPPV{#2F6G|cVu0Zy538)a8*8aVV6Q}^tql!PNgGdZ G_5Sf7|7W(&^uMblLMgL- Wwozp<2+MX+TT05NX57U&i{rRK|L{6|tb04}eiUYBhd6URq!ltWvo& z+6ChXe`%J=WyC(?_8RmOQ^Y)yYn^|&!bdCUegeBJV!qn{los8fKIN>8}5md$!^ S3cE9GDl{9R( z>NLw3{Sjn|<1^F9FVsF$Nk&rTtfeklOl|V4Znv$Z;;iLPyFu8;RJ8ZYk)$_GeR=9X zC`ay~oP#g-_wA-87f_S?RhKN#lYS%1rOhvgLc~zM5=wGu$}3aIMEb!iZ;%G+`)Z0q znDv54IEqerHMC%KY?^~PUt}+MO`1C?K%V)IQF4tl!|}0s!^4BSRnr}B5ka~Pr{i#M zYfoZ!oRIz(T 6bb*dszoNsf{sjih-2Btc21)!) zA>#T!>K|@>xj?X*3k^+;hGOH7K)N1CWs%=mK3jge`J~XxS!g^_NJ2`P4-O|^H$H%X z`o%(Rvr)Ksc!SXgxaR%c`Qg^mg>FEx!Z9vqppaOJq?9ooNAOfv&Vu%R1q5%@3lU2# zDwjoBmvAcQSt^*UP&YgGe#ZEW` Cbu!0)2HLSl TN3fq=TQ>FLagkPPDn3G$AX*~9o z;zE~g n0bj~==my{YRF~4y z6GwErs+4u86p#^ou)--V#)IX|7rBC#B; scs?Z-5BcV`BrK)=EpiVVjr&V#tAdZq4`{Q`a_B z(0vL7y-~W$G5(Hi=whfM8(Pkyt>~f~q4Hqlil3h8I-CRCq%wgMZr+gr4nnP`x8`~m ztnS!jE@v0@pDj U=Jwl%k}ZUD8XfPEuKSp zI$EM$RGL+Ww%5^C?r5u9F-ai>KJT zw$LgUN{oIMeRfDbWz)QrU~AL8%8WhhOW1T+X=dcFW>J0kdY0hr88+ax5;jAV2iA;o z8a!XAjgE;lUtxn{!ZHs-!E?ICRo9syuvwbgdlN%ute|Wroismq>g?&1%_7UpO;xpQ z{&W{}Et`#6Grf3CqCLX~y|$V~Cx6x8cteCpYrXqTAp~V$#s7ovvj#6SSMO%Sz5_hu zx4##OUYkE0=d@ gI#P7 hv~*EC+ang!Vtlr9hC!uY}C zLx^=NZGLU6_o}uRGlh`nvy0fgzSvSBwakr9S{O0OGeo*|ct#PM+jry>sbpt>(C8}T zl4}X`!jAC5D0*R*ya7HTWT3!^Y8aELE*^G`v{_16POlO+0x%Mg3m63$4R{_fhTd2h z=lyz_=fI7JrIEtM^$vx)ge|3^d2d9l4}wWX-EDtJ(hxPbJzN0G&$TKP$FKz;l%jvk z0X>r!Td-{U#Wc>5Bpb15Tm+3|tDr7ntI7ywd?nWA&9V|!04M|$0g3@7fYP$sdou_Z z1SZWr&mInXHM{nnK-_9HpcmPmc3XM-vXD&Ej0|F(%9ixG!OBlbbz-K5?1h0u zXRvN4G_w~6YO%An9fsCxU5n@OA Ux8xl?Y4g&1|A%MAK-=2ICu$CnkqT ztaihpMtg2vy%U|fB$P~}tC!fPmkv~!zhL7BRvHyhnMP-oveJRYY)YIE+TRp$({78f z3*k+8BWv|tBx`yLtgLh(iHl_tSASgyewhVUsnE;pJuJqQF|}-#_A=WD@|msuf~^gy zW5?+2*L5i;2DaHgZ5R5Fum7gm;6gMpS%UVP=0c}pHYzv@eO7NQX4`v9pelVmqmG>% z_%>S+SI6oGo@SeS*Fl5GT^j9iY@nbKT7B{iEI}_=P4u0m(U2gjmg;)$36UwRv4VOn zX7EG^ZCd&~xloz@ufas(VXs{IMqlFoTbmbT9lxmiU S0SgXk1jg$TiVqfi|owWv* rEj&5i+1#2e-ovr6R7I+tb?voGSErj;N)68}xmX?7Cj zb-;E3X+7XFy|P>b1>LqhH}1M$F}sbjn@Ilz-0?fj?gAbHtbi7jF~Czmo8O=i!TMw6 z^89S D_^Xx-eFzC !h}W4MIP^%nsXs=)6^fL+jCiBfOj-s97Ymy?#W!HVhgc zI+`6LQ@G)|g`Qb8Tt2`bo1S{T=|^ld;>|Gf7ajd(Fu6n*y_td%^P54Ky&vBURSp{{ zQxGDMKY@9794!9Hu+kpsu&tr)1zO@yV+;Db2jmF4dG#loPl8&MbV0$m2tR!>3knp% z)9X{4Qqvv=%rwqSEoLexamgr=q}>b1HfEzFkUlC{K_=5Vg+XE6qM#h2phG-#gBqcR zENtqQWcjJ+d8Ezu0o`BdM`G!j!d$fTEea!pXi|}1<+P$VNmxS6--55ip_(SdEPf^k zOS3MrL3-_dL3$d3v%Ex0?p8*WRFTX?NXA1T8LvBB!gyXEv+;15QB^;{%=6xD#Z4Po za*QO8KM^$<$Yp0p%Q29oW5|$;j#dFs!xE@&b!b6*mwH*Jjde9Ocph;x4XBc4aMec* zY5uOgtHHCnVm5SaO{dMHCgF%Hjz}D4dViy<#Rc_#cGR=losqZL2@bskJ%6uF!-1tw zsOH>gH`>?Fktx`b#a~8`jLF%=68n_1#Hj4Rw^(BGjDjl8DK9g_$|-X%!$b%(-eldd zUsZL;lYywxrxcZyRhwA1lHjB5lPhf((yy=#SVYd?_X-=P-#lDoiiB*Ov>8!X*eLy0 zqx9Mp_JW(;_OX5Om*_HPjH5ertS3W%Jq(=snU0==(Y4!zxj!?v1auwfI!qPtE%N9% zCV+QZx!|pZQomF92zEqnTKr{LPFOCmB$GJcpv~)+_d~uLW3sBM{Mjg3b+=S;_ZU(C zfwn)+p&Q>0&wbV`nP@S T@KGJ0IAW&Yu=np)-)PFY~MA_E2t3G_hz zMBCp9^GZxs3?Ol1nL1DYpitn`6SqFX f2n#9d~x4Ij;3jRhGxyX3x$)Y}+XuU%1D zWx%#8ZcD)Lwzl5DwX!V(`HD6l zpWSYd2K|iU6R{UsJ7eIwN-F>1Oah; zs;UZ1@~RDfV>#_{%S{h#zCHlH3A-G0+&5b_ha0xFIYOv*q&637Kms-YK6~cut;U`g zdGGOU+=@D?EeL(ZwoOC7S#2uhb@bi_mE0}d!t~UoETX?&{|z<^0p|*s4w}a#&d2=l zi3&q+g8-QT&IaIi6%2HG*-&?FM{bM3n3my%9)jrJvfxa2loZAe2L2fx+SyyA9ar!? zIu_3p@w`>mab-}}piKUrwGYY~?8r5@4@T}els#?-2U!$~TpIP-=v{E5J-6lYjNF#B z)1ywLLGypwS_Bsogn5s) LLXAuIW~0pFqwcV^b@f&^H#j+-Kg4qrO{wTldQ>i~ zh$EWyold6NRlXR8PrYwe?+m|Z!|1Z5+I=&+Y{|5CQ*=z2y$AAr?F|@;4MX{R7ztJX zGnB2*hVtbc+DGi`kpW@3_8x`zNHEsktDvLB2+~4Vh^ORBz<)An>gFK$fv0ZvQw2QL z&c_s{Mr3Fq^Jr)7TENp?FWYVIv}|+Q@HGqzK8ElASkuf|oY>#wC8_Qiw^8RJ 6|DujF(4da !XF&gjaqv1bo`@S@3{+mdJGpQ6Ik13U@SbFGIk*c9fU!JpuO0{TN~ z$j=B@*Rr(^7S&-}dP1m)rb%0elSI0D>!)NAjo#*`+G~dn(Wn->Y`g8e!`zllqh*az zcsA0h+k7=;c2CWnhS{hSh_biQU*V$vG~~9_r*@YV(TZ(4;zdtw(~xlb2l88~$97dx zc!Cge=de;ahV>O(R@iMuc7=r}kdVJix@>WFUJ{_JU*WZ4vzgbEgml?{;idG|?OO62 zeP_EeIw3*O$$sgXeTmI>1^?(MXAj2<>Il +P4BHK7z`1cJDgTDAwf^OLy< ze2ZIaTWIHYts6ghr!ZM|hfQ&uMtu+%VQCA@2?UD?9Kq$CF57Qy+SvgkKQ}tpR~qH@ z$}DP>)Q3)cVtYy#eUL&bsQH6L-xF<`%n>6yQiKG>5lNOLI{@zwQ~L*ly;->5;|-Cu ztRp-ZCNpky7wGVRCl+u!Mo6}RGfe>VDPgjR)6Or{oP-sOtAz3h$ci}aoRA7oG@X$a zB31BbymvurK-vT8Nu;j4d|(N4i#YA-X_W~uHb+3sl=#<}+ls6(JL5^flfk8yc}76d zb;FM@pf|vex_-DiuGWq!ZiBPGG!4#$mV2s%jR%ZAwT7*_u!gO?u#WkkTf^Q&syw%j z`2n10?T1>TrVSsaIQu2KYDMNxqjwAq9rau{59X% V(I}Ct4;>M2&XQE9X@5J zpI2a_vxVp+7LTlAyhSbg>drav9awfI!g1^Rkr`Ht>W{SXy>kp$s%dVcp=^2eRG1~0 zRCOAs6UA`MS|HE;ZN*m{VYZ#jRn2TCso$;$vW}+0nst|Y@16wT%EH}MswMVt7?@5B z^fr^BaEc8)u>1xawc}qdzo7rLk0)TyeeiLLL}{o0`8Z!SQd7>N2M9XBIRw;cOqcGB zlW4o>XL|#^eY%A3pZNm0#hZoji}f7CwY|;0)1oHL-4@`Cz4Y*EIn%}oFu(Suw?3;> z&I0jp!ZEl@g*-?6La!Vys!yZ)Kle+iY6m%0mot4FpA(%qw(1EENciKun;Lio0@3om z@ErHR;r``1QFjyvF+H)JrqX@mAZ1S4_Xe2KrG4W@t6d6VXZWU}g7q3G=uXvFFmI&Q zNPUo2)!S^hVCTDO`J#af6?|OWki!Lz*D+3m(L;#7$I@=F7mDR3H`8XAvR3R*(Yl53 zuPeaU2~Ev~o0(_Gs%&mNZ!V;4w(M%dW|(|_+aDz9-AOz4#}HGc?!ZFg{zH3$*=Eyq zsLh?Wwe-`2@m_8r+XCdkBE81XV4H0q{p;XRa+T^1C6PQj=g=5TdG(>0UQaPAu=^GX zp*I@b%};HOG}f$wOJRgL4KDLC^9rJ&?uRGAojvDp($F0JqkypN4~#HYg_2Oa?Iq?9 z3e)TVF#lHd)aKGDgz|`scTm uENDCV zmj)s9sqH59s| 8oEvke#&b3tvBfy*GLb(tCp$1n6s#SF4eu)nEAOdg~L+zc{CYa=CrjEgSaX(R`(A zo#j``QG$(yHyuNHeft;Mpz)ZIglfC3)UM1XCdvYGqM(8LNA;-Wwv&`2FOT?C?^?y{ zlrs_5=co9Z%}863Zb8aj V{)Z`7CUT$}$TveCM2s3czy(%<%Q~-CtRO5Cd+G-vnoTIs6_U=xe#zm45uC z_k>NDJ-cnb-Lp#Qt2QN6S2)(_Ddr!e)3qW!h16=XJ~Py^Bau1XYkPP86hzv5xAK7M zsJUO(rf0E1<; gEc~aw&2}8+zo5LE2V8N?%cFn;#(FrRO4)agsDSgd?5H;TQXq+NvvkKB z!FVnxswq1M=R#XO45I?XM0KlUS9>1u$|+~{jurH*mV~!kV3pTy!MSX1UAwRaBl?lr zkNT27sqZmuP(aT%o2~u- o4cdEI;sfN{rx0Pe)Q9lOJJIs9meu>l{zfaw1$FYFK#v~mS zVT6=bwb=sNJ<(}sr$5NB&F0;1g9nChC>y2 yd$JkZWzW8*O+;Kd4grT9e|;n4AJXc&=u|Z83U- z#Wm9NRXQS2TUDY9@2L@GGMe~R(ipE~*Gh;Hk|6ZmD~4z$R&mdfcMMKvfG?moz%M3% zPnfh6bzCiYPDXjMr(-MjN%pMvHYrWNgDpP+mfrN-S7F|Lm6PBXf5-T5$3}MGpwi=- zIo-#B7Nds80z|Pm?uqRU5F6i~!WbdC1>4XUjM!Qq4mSQB2 % S@K!0j9Mb*NW);z-BlFReMlHe{Y*<#fu|vBBFTYT0H$4qyYId^rNN zmXEzjv;_@U*m}DE>tM2$e*bk!{AOh=LiM)0NVe}V9;>0IOKgWr@b-j&Qsd^DiEs*n z&zgAKTGUBRzgz>nN&ro(3H99`A=yHper$%RkiX%*Zl|kiLdi+Gt7bY5Kl#ZBBBT9J zs!)=BGBmhA-)`n!r0VrNVrzgY{N^Vbu>mTV#%E6l`~mNu3?nD#(UYOM3mvmxfq1PC zof Q`Buod2*^#O)! z4Nq-3z`{Up?@`^U#4+pjJC{qX!O|_p%a&WqD-aL11@7JuYk|bYYIQMM2T01&zqRbj z@1?5))-wI?2>Ck+QKs7BZ1r$fSczhI`cW9VTWld+a*T!9?$D#Bl5p&`p2~IFgQI%K zb2RI8c<}G`2-EJ?!dXAv3)6C&V@@YO|4$z7bqG$hno+dubRzkZo BIa)0)~;`FcE; zQh8k(ti_piDyOS>u8*g4>xO`eKCaV}GxS`Y)}_+E vXOy@Aj{XFd0nE zwqL03j1mfN`k5c0*7|;`|TgF-b#zR8tKzWJ5}&9(CdXt7mxtuZ1B zvY(TcIK3Ku%rPNz>A15&@|L6v=wSKTg z?!FLACTAS?Q`iE zmhQWu=EFkMAO)cSr02SG&gcL>$AGeLEm9t=L!$C)YW)M|qCzjyTI&R>fez z2^jDtns{D?)??4lL}XRf`FK)C8_z2t AZl+i5cMl}5ilAdIw# {A@=-?lgU{rg5h$dgtpMQvO-`i$=oSy$Hi-PFf zLfwCSPWmLnlbU`E$M#(KV;Fgfia$ Req21EwAyneQ5QV1q4z<|6Ym;F~T5R{* Wcf zbivQ>&kWZi02#o`Tksw|=3YCrd*p@?xoKDCpgnCix6Y4XF6$d3GUr4gk_TRFV#*kv zyIIIJgxiH8^GoEpEpLfz-@3Kf3f(0J>t(##Pp4iD^*xZNTX8=>_kOL?rNufY^ZxT> z6SUARS0{k5ny)626*TtROp-|}t`(^EMF^3jurWPv&Rv;%|KB|;n8${Z_lx_8@EG;~ zWnq7Hr(k{65&ATrj~)}q)x8iQRp1)!&ep;?!$Rg~&NkIH3qEbOBlP=UQVVQ0KgJ#0 z4;&8e&74O2!m%scZl0IFhM5yT$M5092l%bTNY}7061hw=Y3{e|SR&D`VHp4N$k~(R zY4 ybicTh``T$&aBJ)Gjd^ta~xf)rmTDWWLD0rnc*IobiNj`I^4^TM~}fkAKr5 z@c-OzDtFRHh%i|16zj0ED%$wlbk&Kc%*=5u?s~9xvoXmcvYJG#1Jg;m;6E=r*Elep zq}TovJp9zU@nCFUBGyB&t}o=R0vxT}(dx8glqH1~Oo7qVCLY733;!Cp*%f9SY%|I> z!nN;W_3Uwx&88_gG%oXFU91Y1jcgX3b0dso)3O^&z0OvxVW%V3p!FJd3TYknyQ%X! z#T9C`pq-y3Z}cU2L&x0IgtmhT-LC(colV?>p7=~}PXFyHpMko>t;VZ}?tEe!L$}-v zQtpcYttIz>Jo@6Dp;e$FtRAZeJ<}g=E`|P?e(P0oj_$vube4|HF>itZ^8Ky;BfiDD zz`85!N6JA4IM#ZlU=qIdg3H|eyvj|G$HwE=E)l{rQTjb*ApIEy)GN76RtZ5P;Ht=e zqM5&I$Xq(__ayJ164j `w<(-A)uhTi?^;zpGrZyL8hnd{^A~eXK;@L7)4h4
(E7h269TU@!6l+rTcARp$;KVK#idhX9KFA2OeJ#+!4@R}YWF1ojvgp%9d zjyp1sFl =oG|8hv}57yls6U~aJRjZUU5uufInjsUx;xgAOl(%ZM= z$cI#UM-x`u*1WdxzFc-IAjg=Kx`q9 m1l<=x@#_Ji1WDG{v{;3HcMp#-}=kwk%n3lMVH?Vb`5Le;{Sfw z;_B$GyMxIwdhKpDxkE$mX_a@@tz!%0*0H=LMC}3dsrCI|m{rMH`1E_;F4YnX+~ubb zxxMP%Ii(~Z%h21ope68N1rxM<)>a;z5hQ&gqf_q(^>T}x4Q2gx$j9gEiMrVq{gl3U z|GA{Pfim45ixx(`Q7Jr@sDdS<%7>1UaIEcCixUI}gKiVf#Ae%1Z03p*;|5D$b_fDu zuv`zO>Nxk}d^m7O_p!h&2cw~7oS5+zPSXKsbr>`s5ACInzVl$U@-Bw|5Yx2~{iL)E zI>1AxVh=RgI%&kim&r}~{zKpFD_v@1O3UN@-?6)MzGDw3q~9TZlM&g>Z3$M5p!P>! zf{;J#dCyTv&wJJ<@dHJX+ hitZ&J7usdyh;Ch7)+GZ|h$qZ_ zlu3N(mPc>kob-AejNjPDMfk0JoDGAP{qY3+PWUU-Zxu8?gY{6+EX+<1>^_6lTtu0I zR{Rx42Gi=l`ul}G%{2Ae{M$WWZHKK4TAP1Ox@i+zNu^dD7ZR;1`O1BmS( sv zx0rw83wH*a?l+ul?qT?e`M rHs! z{_$1(O(@> iiSWlbiXcnguUui*+VE_5o@4M@e_J7S24){D*X( zWT%;W$R;+P&TCPT33N?Mm1 #OhOG86+3eHN*|6>qK|Aeh|HC=8L3ClMau zSf-Sg*AI42WfV<(5~tzD7xhkOp&ThEaxQd;hh(O{koDd7Fx&U^-6x^A^HBL@mukRS zk8d~OjzwVd8vJg$+f$eZjxdI8VIwOS+1@2kRqSk$q%N5*erl9NchWDP&V{tAX?q8h zxxa0d#OEoEYA=Loaew=2*tz05J|#hmp{fbi9z}jtLX2Ql6sb*(HVJcr1Z%ZfDs7#< zL_Rz5BAZXW>>Bx6 Q5aGxwDdL1%|&EAl%q^S1Vj$)EUgc1w1szxRvCGbw28yzL`U|COXTGE zlv1#Vl$Of c@;(npuk=sqSFU5D9$R#pMoa&6xE<0M}Ox#Jj0TcONeWPs@ z(zlUrK>8MR;HbfARYmA0FIHRCVSB%0K$XyM*6l{ycD#RLS=zAT*%+F+sYSe`AQ2>0 z>{O8G^d@Znl$MPBjW&Y1DJ^OHIksI@lN{Jesv2#fj`H~Z<4~e@Y|2=CT>^CTl$NOd zI`lG3T T|>?j{_F!{fzA`f=hoE)e^5B6 i=VrZ6~spjb07p zE)g1S8Al6zh@XVKE{eTLtHiTQe9w t6>JtW=CUYAv{+@og#$QVL$MP(p4?+6a(!QP|9M}cILMAa^41(8US zespON>CbC@+=t|b@mcUhxZ)n{Ce9>2M2iZxXeSpw1nblydF$C_R@(tl6-;uSm&T4U z<4hJ81(Rs+4Y8#r&UDK6@hD#fG>J{3Ihd$?HzAB(;>9JF^AJ--%}3@d_g)ci1(T#S zN;pT@Qy2o>&|v)puPZbaY}YLw4#~sG1?#_2_7T~EZgx1vV6ax8WH;IwtQ(QrDNa{o zDPI%MtBFQiwuraNLEj%u|Ae bt3Hb5-%;>6kDq(N4*&Ua9`Q(a-SS!{-qv6xmWl2mWUyCJYy~UUs3g)B z!xHbb3RWPF2_eH>&pMj>eSYQM>*B{DL`!Cf=a6{Mc-3x;IbOk5V(=*~_dmPBmWlQd zGQx|``9-+Ag 41c^(7evnuv0C zzpv2@mJ;9vGa&IsI4 P1MPh?S@OK0MuXh6@%UQBE1n=SkTwpF!dqynh&c6~C z7nv!KoPC+KY`kMiy?;@II~1S6i5k+F-t#)2I|*)nfYHIrqE`2EGJ90vBCD7dN|qA6 zSQSbVB|Yt8ODMSz+XC|3FicP%0s%S0nd+R`AJMGIk+|pzFaS~k#z?#E8%Ll^kl5Id zs3a#-#GZ)y^G}Za=jCrMYF-ASyD6e63|`_#(f@qg9edP)_nu-=7}5Cs-;Mhkpz4WL zVPvU`=kiwD;`S!n1~ERIa5u$hV{ltZ+vwEFR$*!>n
}C#taVE9=<4fr6GIu5)D| zzbaA8M#rv&+yY$(F_xpqmh@7#N;IQ#?3<38j%%4R@htNkqPGyOOsxnj%32H6yfC|* z`3KH1Z-&d3i(V;k=_$p|a1s$6kTa~xCq#o)4TC9Xd57AhHHDbY+>;CtwBbmFFx~6$ zue~pXm9p{Tm P|yAsqAc;Q z5FYh%q~kMiseB?Xl_NhpOHlublzVm=(ha%r?24f}670GMkM#h2u$ZeOiQaw@o0(JM zX6CN!zFojyAP^v;;KVSP _ 1 z;;JYT>vN(_*0LcDWV6s<-T4;RD_oz7r=uVk+&HR?Cio8s!^D?kNbm%|y>BCmeOqP^ zm>RLode$|cL=3_O7n!wCC}Q8=D`L0ronrO(6dQ`z{&PJx7qNXPI|w**@4d_-*0-n2 zW?wMKy5hy@lFj}#o+gA1prum$Erw{F{rfm?DP&%vbAJ*P?HR*a=tspf `G5~Z^c r$%dw )Ho} zB$}3G)UiRhEFvsf%_K`onP&NFcK>y_Z}P$ImzT1Ec$bXwIHdDYhX95zML5KA+<*sc z0PF_rS$+l<-WJ=7?X!Tf93?yY;%7Tx^YSKJZTqy0CfmvOsrapS{C?Fw4*8?v-l0Sr z;q_)@n^W6r)_WzQo`#-b(^g^&^ynUyeQ}E|sJ#kN`&(G4*gBL%JL`K0>fLO+7@>!( zW)w^GBvh_{o0sg@6Wk&e&+AE2=pM}T>}05$u&HcEx&dh(z#n?S%{2UTix*&a7o(HN ztHFypPUVPjs%hb?-KFVaE@iu7W0vb?|4V)%3qDq{GKs{gUeOjC-$^aBzEh~n|J9}L z-YcY(iLp(%A-+K*2%Qa`@2IfwV%@#@ c;nxCNJ`o3v9362DPlWz#Fn$&nl qZi3|j zNOYq($3RpQb~r?+8wdEfXe=Mw)jNFKaccvYmOh3+QE2!$V1Yd!E43XAH+yecN6C zW$W(+&jH#0tk%xhl$NS)T;gvA;uk? Fs(^K;L#7sMY^i8jeiX{q88w4dduQDWh&Io2$-_LRzWJ^522pOjU&pg*;KcK*p` z;wdEX&?UszAYOWv*x!gv@TNGzNHkt sW+pBc`-fgF-@ zQx?L*f?l$pY)&1pt>LI~e|?i}Iy}N}>b2--_+UH*vmXlROv+mh}KL^9DUjrh9A zfd8NaZ~ruKRGGLWjU
K%0_}D_+TP|jXA%85#e@zipCQ?Rz7WbRTq|uzc zaGL7zvsTa9%t`?N8g4E*I%3_S8|=@G`~qJijI7Sf>zS})1CtdxEh7{ouiwICRYOe2 zvDR)id78nR2evG6!f>MM#gh27g|?)>RlklT@vY$`+4X5s1+xZuA(~nce;iJt6Q#+g z_(SsNW=XiW<$k1>CFD&!_wjZ)-7M+Fk91X01?%=G$V$Lxq^p@Mc?)wA2WOBp=Y5IP z%yAWDkkG-y4j7t#^d15W3#UH#1zN5wQJN(5flY)qx~$&tWcf_PlVvlmvA`;!zoNz) z-zA9GGKemSB!}eJHcF(pp4%%H&aYP6<#srSl4TTk7e!4b@eX>Ihb2Srar;4HnlLvG z2H+h%Ipgxgk-b|Sok>E6^h_?qXP4`q@* {-<}=>IqG+_o5uMXjKGi*wkF~9Bp5(F=8gx(b!z`ln4(|i4=cjN zV=nu8o@RUctY$vq%h@E@y*Ey2KY%yDS1ijW$^t_h_ptX)u3-Mju2p244l5L#{5h7{ zFPXy0vKm+MSK}H!8UcI&zJT5UKR|HLR@;L1)hzI}n|Js;`dPX^3?XZ9E1kr!eG!#U zbk$cmSN;l|i}%${XQx@;L~}vI7}MPbZT^u4qWuE1p^6EqDna =c(P}A6%l{hax0dHLmKg_Ot zZ)=N3Kg%Ea+nlwRE7)y#v_kSLnCqW#4XH4#f7wi8We&kq!w N~v}Nthja{};UDw#!#J~(P^c>P_Y<(h$JIB_G zwWCO;!+*m!KR3|-nebjRX9$7-c~1--P5$D>qvy#sa`346c}SM9Q2D+ ALCN;F`0?&Z;uVcV;oARa%R}1^dvSJACS|!|{EK9qL}nK^yhvUl^+# N9SwA#_ !+y~TSI(L<5wJ_$C|-r}}NL@NpJ5Km4bA(Gx5;-8bq zcydonnoQpHz8yc*%&pKYzMJ;GVGqUMCX lp_jzGQZK|nE0m~BKG*>7En2R* zZ~IzYG>zyAs-EsKYx6fd$*xqe`Pv>;s}UD?%}H|QkNgx}3<9NABX)5$s{oV(J^-?U zmgxt1)JkrPVv*W(-?kav;uKur-NpZDkoRiDHfr-#E;1ed8GzOs|5j mm?#EKhgh2=E}2d=u~APG%)1b$<0cIV9%CGn3YB6I=QXnWzie`* z!0=AEacP2!Xd5t%C&V+;$vb3&IC=*8wR>xbu`|KI>%=iLVLv-_bj3`9U?fh`SC-95 z`tME?AI*Z{{;a5dnY=rizr~@O`fm4C|M^{xu$ #`{(mZ8s9b> zocS&MZptfls5F-mgLK~UKaD~MXpjG8OBJ!xMi*pYW-g*Bk74`)Bz_4CB@@nB`d9sB z>+zItj0^fY1d&~U3*{s}aK=-VxE;s;0Qdm+|L@tp1{r^W_UH8nm#ZKZ@u YHt| zQ2q((`vd<+&+z}-fq#