diff --git a/.codespellignore b/.codespellignore index c7beb6f04f..9328517d4a 100644 --- a/.codespellignore +++ b/.codespellignore @@ -2,3 +2,4 @@ Wen REGIST PullRequest cancelled +FOF diff --git a/.github/labeler.yaml b/.github/labeler.yaml index e91764bb37..278ac58036 100644 --- a/.github/labeler.yaml +++ b/.github/labeler.yaml @@ -1,6 +1,6 @@ ci: - changed-files: - - any-glob-to-all-files: "{.github/**,**/test_*,Jenkinsfile}" + - any-glob-to-all-files: "{.github/**,**/test_*,**/test/**,Jenkinsfile}" chore: - changed-files: diff --git a/.github/workflows/auto_pr_review.yaml b/.github/workflows/auto_pr_review.yaml index d3cfa60d92..b9664b9066 100644 --- a/.github/workflows/auto_pr_review.yaml +++ b/.github/workflows/auto_pr_review.yaml @@ -1,7 +1,7 @@ name: "PR review" on: pull_request_target: - types: [opened, reopened, synchronize, edited] + types: [ opened, reopened, synchronize, edited ] jobs: labeler: @@ -29,17 +29,17 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - target: /^(?!master-new$).*/ + target: /^(?!master$).*/ exclude: /sunnypilot:.*/ change-to: ${{ github.base_ref }} already-exists-action: close_this - already-exists-comment: "Your PR should be made against the `master-new` branch" + already-exists-comment: "Your PR should be made against the `master` branch" update-pr-labels: name: Update fork's PR Labels runs-on: ubuntu-latest if: (github.event.pull_request.head.repo.fork && (contains(github.event_name, 'pull_request') && github.event.action == 'synchronize')) - env: + env: PR_LABEL: 'dev-c3' TRUST_FORK_PR_LABEL: 'trust-fork-pr' steps: @@ -64,7 +64,7 @@ jobs: core.setOutput('has-dev-c3', 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' uses: actions/github-script@v7 @@ -72,14 +72,14 @@ jobs: github-token: ${{ secrets.GITHUB_TOKEN }} script: | const prNumber = context.payload.pull_request.number; - + await github.rest.issues.removeLabel({ owner: context.repo.owner, repo: context.repo.repo, issue_number: prNumber, name: process.env.TRUST_FORK_PR_LABEL }); - + console.log(`Removed '${process.env.TRUST_FORK_PR_LABEL}' label from PR #${prNumber} as it received new commits`); // Add a comment to the PR diff --git a/.github/workflows/badges.yaml b/.github/workflows/badges.yaml index 505148beac..3019ba9cba 100644 --- a/.github/workflows/badges.yaml +++ b/.github/workflows/badges.yaml @@ -5,8 +5,8 @@ on: workflow_dispatch: env: - BASE_IMAGE: openpilot-base - DOCKER_REGISTRY: ghcr.io/commaai + BASE_IMAGE: sunnypilot-base + DOCKER_REGISTRY: ghcr.io/sunnypilot RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $DOCKER_REGISTRY/$BASE_IMAGE:latest /bin/bash -c jobs: diff --git a/.github/workflows/build-all-tinygrad-models.yaml b/.github/workflows/build-all-tinygrad-models.yaml new file mode 100644 index 0000000000..1c7812ee47 --- /dev/null +++ b/.github/workflows/build-all-tinygrad-models.yaml @@ -0,0 +1,303 @@ +name: Build and push all tinygrad models + +on: + workflow_dispatch: + inputs: + set_min_version: + description: 'Minimum selector version required for the models (see helpers.py or readme.md)' + required: true + type: string + +jobs: + setup: + runs-on: ubuntu-latest + outputs: + json_version: ${{ steps.get-json.outputs.json_version }} + recompiled_dir: ${{ steps.create-recompiled-dir.outputs.recompiled_dir }} + json_file: ${{ steps.get-json.outputs.json_file }} + model_matrix: ${{ steps.set-matrix.outputs.model_matrix }} + tinygrad_ref: ${{ steps.get-tinygrad-ref.outputs.tinygrad_ref }} + steps: + - name: Checkout sunnypilot repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot + path: sunnypilot + submodules: recursive + + - name: Get tinygrad_repo ref + id: get-tinygrad-ref + run: | + cd sunnypilot + export PYTHONPATH=$(pwd) + ref=$(python3 sunnypilot/models/tinygrad_ref.py) + echo "tinygrad_ref=$ref" >> $GITHUB_OUTPUT + echo "tinygrad_ref is $ref" + + - name: Checkout docs repo (sunnypilot-docs, gh-pages) + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot-docs + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Get next JSON version to use (from GitHub docs repo) + id: get-json + run: | + cd docs/docs + latest=$(ls driving_models_v*.json | sed -E 's/.*_v([0-9]+)\.json/\1/' | sort -n | tail -1) + next=$((latest+1)) + json_file="driving_models_v${next}.json" + cp "driving_models_v${latest}.json" "$json_file" + echo "json_file=docs/docs/$json_file" >> $GITHUB_OUTPUT + echo "json_version=$((next+0))" >> $GITHUB_OUTPUT + echo "SRC_JSON_FILE=docs/docs/driving_models_v${latest}.json" >> $GITHUB_ENV + + - name: Extract tinygrad models + id: set-matrix + working-directory: docs/docs + run: | + jq -c '[.bundles[] | select(.runner=="tinygrad") | {ref, display_name: (.display_name | gsub(" \\([^)]*\\)"; "")), is_20hz}]' "$(basename "${SRC_JSON_FILE}")" > matrix.json + echo "model_matrix=$(cat matrix.json)" >> $GITHUB_OUTPUT + + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + - run: | + mkdir -p ~/.ssh + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Clone GitLab docs repo and create new recompiled dir + id: create-recompiled-dir + env: + GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' + run: | + git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs + cd gitlab_docs + git checkout main + git sparse-checkout set --no-cone models/ + cd models + latest_dir=$(ls -d recompiled* 2>/dev/null | sed -E 's/recompiled([0-9]+)/\1/' | sort -n | tail -1) + if [[ -z "$latest_dir" ]]; then + next_dir=1 + else + next_dir=$((latest_dir+1)) + fi + recompiled_dir="${next_dir}" + mkdir -p "recompiled${recompiled_dir}" + touch "recompiled${recompiled_dir}/.gitkeep" + cd ../.. + echo "recompiled_dir=$recompiled_dir" >> $GITHUB_OUTPUT + + - name: Push empty recompiled dir to GitLab + run: | + cd gitlab_docs + git add models/recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }} + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Add recompiled${{ steps.create-recompiled-dir.outputs.recompiled_dir }} for build-all" || echo "No changes to commit" + git push origin main + + - name: Push new JSON to GitHub docs repo + run: | + cd docs + git pull origin gh-pages + git add docs/"$(basename ${{ steps.get-json.outputs.json_file }})" + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Add new ${{ steps.get-json.outputs.json_file }} for build-all" || echo "No changes to commit" + git push origin gh-pages + + get_and_build: + needs: [setup] + strategy: + matrix: + model: ${{ fromJson(needs.setup.outputs.model_matrix) }} + fail-fast: false + uses: ./.github/workflows/build-single-tinygrad-model.yaml + with: + upstream_branch: ${{ matrix.model.ref }} + custom_name: ${{ matrix.model.display_name }} + recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} + json_version: ${{ needs.setup.outputs.json_version }} + secrets: inherit + + retry_failed_models: + needs: [setup, get_and_build] + runs-on: ubuntu-latest + if: ${{ needs.setup.result != 'failure' && !cancelled() }} + outputs: + retry_matrix: ${{ steps.set-retry-matrix.outputs.retry_matrix }} + steps: + - uses: actions/download-artifact@v4 + with: + pattern: model-* + path: output + + - id: set-retry-matrix + run: | + echo '${{ needs.setup.outputs.model_matrix }}' > matrix.json + built=(); while IFS= read -r line; do built+=("$line"); done < <( + ls output | sed -E 's/^model-//' | sed -E 's/-[0-9]+$//' | sed -E 's/ \([^)]*\)//' | awk '{gsub(/^ +| +$/, ""); print}' + ) + jq -c --argjson built "$(printf '%s\n' "${built[@]}" | jq -R . | jq -s .)" \ + 'map(select(.display_name as $n | ($built | index($n | gsub("^ +| +$"; "")) | not)))' matrix.json > retry_matrix.json + echo "retry_matrix=$(cat retry_matrix.json)" >> $GITHUB_OUTPUT + + retry_get_and_build: + needs: [setup, get_and_build, retry_failed_models] + if: ${{ needs.get_and_build.result == 'failure' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '') }} + strategy: + matrix: + model: ${{ fromJson(needs.retry_failed_models.outputs.retry_matrix) }} + fail-fast: false + uses: ./.github/workflows/build-single-tinygrad-model.yaml + with: + upstream_branch: ${{ matrix.model.ref }} + custom_name: ${{ matrix.model.display_name }} + recompiled_dir: ${{ needs.setup.outputs.recompiled_dir }} + json_version: ${{ needs.setup.outputs.json_version }} + artifact_suffix: -retry + secrets: inherit + + publish_models: + name: Publish models sequentially + needs: [setup, get_and_build, retry_failed_models, retry_get_and_build] + if: ${{ !cancelled() && (needs.get_and_build.result != 'failure' || needs.retry_get_and_build.result == 'success' || (needs.retry_failed_models.outputs.retry_matrix != '[]' && needs.retry_failed_models.outputs.retry_matrix != '')) }} + runs-on: ubuntu-latest + strategy: + max-parallel: 1 + matrix: + model: ${{ fromJson(needs.setup.outputs.model_matrix) }} + env: + RECOMPILED_DIR: recompiled${{ needs.setup.outputs.recompiled_dir }} + JSON_FILE: ${{ needs.setup.outputs.json_file }} + ARTIFACT_NAME_INPUT: ${{ matrix.model.display_name }} + steps: + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + + - name: Add GitLab.com SSH key to known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Clone GitLab docs repo + env: + GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' + run: | + echo "Cloning GitLab" + git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs + cd gitlab_docs + echo "checkout models/${RECOMPILED_DIR}" + git sparse-checkout set --no-cone models/${RECOMPILED_DIR} + git checkout main + cd .. + + - name: Checkout docs repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot-docs + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Validate recompiled dir and JSON version + run: | + if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then + echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo" + exit 1 + fi + if [ ! -f "$JSON_FILE" ]; then + echo "JSON file $JSON_FILE does not exist!" + exit 1 + fi + + - name: Download artifact name file + uses: actions/download-artifact@v4 + with: + name: artifact-name-${{ env.ARTIFACT_NAME_INPUT }} + path: artifact_name + + - name: Read artifact name + id: read-artifact-name + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download model artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.read-artifact-name.outputs.artifact_name }} + path: output + + - name: Remove onnx files bc not needed for recompiled dir since they already exist from single build + run: | + find output -type f -name '*.onnx' -delete + find output -type f -name 'big_*.pkl' -delete + find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete + + - name: Copy model artifacts to gitlab + env: + ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} + run: | + ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" + mkdir -p "$ARTIFACT_DIR" + for path in output/*; do + if [ "$(basename "$path")" = "artifact_name.txt" ]; then + continue + fi + name="$(basename "$path")" + if [ -d "$path" ]; then + mkdir -p "$ARTIFACT_DIR/$name" + cp -r "$path"/* "$ARTIFACT_DIR/$name/" + echo "Copied dir $name -> $ARTIFACT_DIR/$name" + else + cp "$path" "$ARTIFACT_DIR/" + echo "Copied file $name -> $ARTIFACT_DIR/" + fi + done + + - name: Push recompiled dir to GitLab + env: + GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + run: | + cd gitlab_docs + git checkout main + git pull origin main + for d in models/"$RECOMPILED_DIR"/*/; do + git sparse-checkout add "$d" + done + git add models/"$RECOMPILED_DIR" + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Update $RECOMPILED_DIR with model from build-all-tinygrad-models" || echo "No changes to commit" + git push origin main + - run: | + cd docs + git pull origin gh-pages + + - name: update json + run: | + ARGS="" + [ -n "${{ inputs.set_min_version }}" ] && ARGS="$ARGS --set-min-version \"${{ inputs.set_min_version }}\"" + ARGS="$ARGS --sort-by-date" + ARGS="$ARGS --tinygrad-ref \"${{ needs.setup.outputs.tinygrad_ref }}\"" + eval python3 docs/json_parser.py \ + --json-path "$JSON_FILE" \ + --recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \ + $ARGS + + - name: Push updated json to GitHub + run: | + cd docs + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git checkout gh-pages + git add docs/"$(basename $JSON_FILE)" + git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit" + git push origin gh-pages diff --git a/.github/workflows/build-single-tinygrad-model.yaml b/.github/workflows/build-single-tinygrad-model.yaml new file mode 100644 index 0000000000..76f39fb6fe --- /dev/null +++ b/.github/workflows/build-single-tinygrad-model.yaml @@ -0,0 +1,228 @@ +name: Build Single Tinygrad Model and Push + +on: + workflow_call: + inputs: + upstream_branch: + description: 'Upstream commit to build from' + required: true + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + recompiled_dir: + description: 'Existing recompiled directory number (e.g. 3 for recompiled3)' + required: true + type: string + json_version: + description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' + required: true + type: string + artifact_suffix: + description: 'Suffix for artifact name' + required: false + type: string + default: '' + bypass_push: + description: 'Bypass pushing to GitLab for build-all' + required: false + default: true + type: boolean + workflow_dispatch: + inputs: + upstream_branch: + description: 'Upstream commit to build from' + required: true + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + recompiled_dir: + description: 'Existing recompiled directory number (e.g. 3 for recompiled3)' + required: true + type: string + json_version: + description: 'driving_models version number to update (e.g. 5 for driving_models_v5.json)' + required: true + type: string + model_folder: + description: 'Model folder' + type: choice + default: 'None' + options: + - None + - Simple Plan Models + - Space Lab Models + - TR Models + - DTR Models + - Custom Merge Models + - FOF series models + - Other + custom_model_folder: + description: 'Custom model folder name (if "Other" selected)' + required: false + type: string + generation: + description: 'Model generation' + required: false + type: string + version: + description: 'Minimum selector version' + required: false + type: string +env: + RECOMPILED_DIR: recompiled${{ inputs.recompiled_dir }} + JSON_FILE: docs/docs/driving_models_v${{ inputs.json_version }}.json + +jobs: + build_model: + uses: ./.github/workflows/sunnypilot-build-model.yaml + with: + upstream_branch: ${{ inputs.upstream_branch }} + custom_name: ${{ inputs.custom_name || inputs.upstream_branch }} + is_20hz: true + artifact_suffix: ${{ inputs.artifact_suffix }} + secrets: inherit + + publish_model: + if: ${{ !inputs.bypass_push && !cancelled() }} + concurrency: + group: gitlab-push-${{ inputs.recompiled_dir }} + cancel-in-progress: false + needs: build_model + runs-on: ubuntu-latest + steps: + - name: Set up SSH + uses: webfactory/ssh-agent@v0.9.0 + with: + ssh-private-key: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + + - name: Add GitLab.com SSH key to known_hosts + run: | + mkdir -p ~/.ssh + ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts + + - name: Clone GitLab docs repo + env: + GIT_SSH_COMMAND: 'ssh -o UserKnownHostsFile=~/.ssh/known_hosts' + run: | + echo "Cloning GitLab" + git clone --depth 1 --filter=tree:0 --sparse git@gitlab.com:sunnypilot/public/docs.sunnypilot.ai2.git gitlab_docs + cd gitlab_docs + echo "checkout models/${RECOMPILED_DIR}" + git sparse-checkout set --no-cone models/${RECOMPILED_DIR} + git checkout main + cd .. + + - name: Checkout docs repo + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot-docs + ref: gh-pages + path: docs + ssh-key: ${{ secrets.CI_SUNNYPILOT_DOCS_PRIVATE_KEY }} + + - name: Validate recompiled dir and JSON version + run: | + if [ ! -d "gitlab_docs/models/$RECOMPILED_DIR" ]; then + echo "Recompiled dir $RECOMPILED_DIR does not exist in GitLab repo" + exit 1 + fi + if [ ! -f "$JSON_FILE" ]; then + echo "JSON file $JSON_FILE does not exist!" + exit 1 + fi + + - name: Download artifact name file + uses: actions/download-artifact@v4 + with: + name: artifact-name-${{ inputs.custom_name || inputs.upstream_branch }} + path: artifact_name + + - name: Read artifact name + id: read-artifact-name + run: | + ARTIFACT_NAME=$(cat artifact_name/artifact_name.txt) + echo "artifact_name=$ARTIFACT_NAME" >> $GITHUB_OUTPUT + + - name: Download and extract model artifact + uses: actions/download-artifact@v4 + with: + name: ${{ steps.read-artifact-name.outputs.artifact_name }} + path: output + + - name: Remove unwanted files + run: | + find output -type f -name 'dmonitoring_model_tinygrad.pkl' -delete + find output -type f -name 'dmonitoring_model.onnx' -delete + + - name: Copy model artifact(s) to GitLab recompiled dir + env: + ARTIFACT_NAME: ${{ steps.read-artifact-name.outputs.artifact_name }} + run: | + ARTIFACT_DIR="gitlab_docs/models/${RECOMPILED_DIR}/${ARTIFACT_NAME}" + mkdir -p "$ARTIFACT_DIR" + for path in output/*; do + if [ "$(basename "$path")" = "artifact_name.txt" ]; then + continue + fi + name="$(basename "$path")" + if [ -d "$path" ]; then + mkdir -p "$ARTIFACT_DIR/$name" + cp -r "$path"/* "$ARTIFACT_DIR/$name/" + echo "Copied dir $name -> $ARTIFACT_DIR/$name" + else + cp "$path" "$ARTIFACT_DIR/" + echo "Copied file $name -> $ARTIFACT_DIR/" + fi + done + + - name: Push recompiled dir to GitLab + env: + GITLAB_SSH_PRIVATE_KEY: ${{ secrets.GITLAB_SSH_PRIVATE_KEY }} + run: | + cd gitlab_docs + git checkout main + git pull origin main + for d in models/"$RECOMPILED_DIR"/*/; do + git sparse-checkout add "$d" + done + git add models/"$RECOMPILED_DIR" + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git commit -m "Create/Update $RECOMPILED_DIR with new/updated model from build-single-tinygrad-model" || echo "No changes to commit" + git push origin main + + - run: | + cd docs + git pull origin gh-pages + + - name: Run json_parser.py to update JSON + run: | + FOLDER="${{ inputs.model_folder }}" + if [ "$FOLDER" = "Other" ]; then + FOLDER="${{ inputs.custom_model_folder }}" + fi + ARGS="" + if [ "$FOLDER" != "None" ] && [ -n "$FOLDER" ]; then + ARGS="$ARGS --model-folder \"$FOLDER\"" + fi + [ -n "${{ inputs.generation }}" ] && ARGS="$ARGS --generation \"${{ inputs.generation }}\"" + [ -n "${{ inputs.version }}" ] && ARGS="$ARGS --version \"${{ inputs.version }}\"" + eval python3 docs/json_parser.py \ + --json-path "$JSON_FILE" \ + --recompiled-dir "gitlab_docs/models/$RECOMPILED_DIR" \ + --sort-by-date \ + $ARGS + + - name: Push updated JSON to GitHub docs repo + run: | + cd docs + git config --global user.name "GitHub Action" + git config --global user.email "action@github.com" + git checkout gh-pages + git add docs/"$(basename $JSON_FILE)" + git commit -m "Update $(basename $JSON_FILE) after recompiling model" || echo "No changes to commit" + git push origin gh-pages diff --git a/.github/workflows/cereal_validation.yaml b/.github/workflows/cereal_validation.yaml index 5d75b6bc13..2290481c56 100644 --- a/.github/workflows/cereal_validation.yaml +++ b/.github/workflows/cereal_validation.yaml @@ -4,7 +4,6 @@ on: push: branches: - master - - master-new pull_request: paths: - 'cereal/**' @@ -17,7 +16,7 @@ on: type: string concurrency: - group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + group: cereal-validation-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: diff --git a/.github/workflows/compile-openpilot/action.yaml b/.github/workflows/compile-openpilot/action.yaml index 87590b41cb..4015746c0e 100644 --- a/.github/workflows/compile-openpilot/action.yaml +++ b/.github/workflows/compile-openpilot/action.yaml @@ -15,7 +15,7 @@ runs: scons -j$(nproc) --cache-populate" - name: Save scons cache uses: actions/cache/save@v4 - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + if: github.ref == 'refs/heads/master' with: path: .ci_cache/scons_cache key: scons-${{ runner.arch }}-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} diff --git a/.github/workflows/jenkins-pr-trigger.yaml b/.github/workflows/jenkins-pr-trigger.yaml index db1d524018..14e2fdf49b 100644 --- a/.github/workflows/jenkins-pr-trigger.yaml +++ b/.github/workflows/jenkins-pr-trigger.yaml @@ -9,6 +9,9 @@ jobs: scan-comments: runs-on: ubuntu-latest if: ${{ github.event.issue.pull_request }} + permissions: + contents: write + issues: write steps: - name: Check for trigger phrase id: check_comment @@ -43,3 +46,14 @@ jobs: git config --global user.email "github-actions[bot]@users.noreply.github.com" git checkout -b tmp-jenkins-${{ github.event.issue.number }} GIT_LFS_SKIP_PUSH=1 git push -f origin tmp-jenkins-${{ github.event.issue.number }} + + - name: Delete trigger comment + if: steps.check_comment.outputs.result == 'true' && always() + uses: actions/github-script@v7 + with: + script: | + await github.rest.issues.deleteComment({ + owner: context.repo.owner, + repo: context.repo.repo, + comment_id: context.payload.comment.id, + }); diff --git a/.github/workflows/lfs-maintenance.yaml b/.github/workflows/lfs-maintenance.yaml index 954dc9128d..8780abfbb3 100644 --- a/.github/workflows/lfs-maintenance.yaml +++ b/.github/workflows/lfs-maintenance.yaml @@ -9,11 +9,11 @@ on: - cron: '0 0 * * *' # Runs at 00:00 UTC every day push: branches: - - 'master-new' + - 'master' pull_request: branches: - - 'master-new' - workflow_dispatch: # enables manual triggering + - 'master' + workflow_dispatch: # enables manual triggering inputs: upstream_branch: default: 'master' @@ -30,7 +30,7 @@ jobs: with: repository: 'commaai/openpilot' ref: ${{ inputs.upstream_branch }} - + - name: LFS Fetch run: | git lfs fetch @@ -48,7 +48,7 @@ jobs: - name: Add GitLab public keys run: | ssh-keyscan -H gitlab.com >> ~/.ssh/known_hosts - + - name: Ensure branch run: | if git symbolic-ref -q HEAD >/dev/null; then diff --git a/.github/workflows/prebuilt.yaml b/.github/workflows/prebuilt.yaml index 61e3f3bc4f..dde290d5c5 100644 --- a/.github/workflows/prebuilt.yaml +++ b/.github/workflows/prebuilt.yaml @@ -6,7 +6,7 @@ on: env: DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: selfdrive/test/docker_build.sh prebuilt + BUILD: release/ci/docker_build_sp.sh prebuilt jobs: build_prebuilt: diff --git a/.github/workflows/release-drafter.yml b/.github/workflows/release-drafter.yml index 62712a770c..c072e98e24 100644 --- a/.github/workflows/release-drafter.yml +++ b/.github/workflows/release-drafter.yml @@ -3,7 +3,6 @@ name: Release Drafter on: push: branches: - - master-new - master tags: - 'v*' diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index d2228a2079..b9303045d0 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -5,12 +5,12 @@ on: workflow_dispatch: jobs: - build_masterci: - name: build master-ci + build___nightly: + name: build __nightly env: ImageOS: ubuntu24 container: - image: ghcr.io/commaai/openpilot-base:latest + image: ghcr.io/sunnypilot/sunnypilot-base:latest runs-on: ubuntu-latest if: github.repository == 'sunnypilot/sunnypilot' permissions: @@ -27,7 +27,7 @@ jobs: with: ref: master wait-interval: 30 - running-workflow-name: 'build master-ci' + running-workflow-name: 'build __nightly' repo-token: ${{ secrets.GITHUB_TOKEN }} check-regexp: ^((?!.*(build prebuilt).*).)*$ - uses: actions/checkout@v4 @@ -38,5 +38,5 @@ jobs: run: | git config --global --add safe.directory '*' git lfs pull - - name: Push master-ci - run: BRANCH=__nightly release/build_devel.sh + - name: Push __nightly + run: BRANCH=__nightly release/build_stripped.sh diff --git a/.github/workflows/repo-maintenance.yaml b/.github/workflows/repo-maintenance.yaml index 6bd86ce7e9..49dae93747 100644 --- a/.github/workflows/repo-maintenance.yaml +++ b/.github/workflows/repo-maintenance.yaml @@ -6,14 +6,14 @@ on: workflow_dispatch: env: - BASE_IMAGE: openpilot-base - BUILD: selfdrive/test/docker_build.sh base + BASE_IMAGE: sunnypilot-base + BUILD: release/ci/docker_build_sp.sh base RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c jobs: update_translations: runs-on: ubuntu-latest - if: github.repository == 'commaai/openpilot' + if: github.repository == 'sunnypilot/sunnypilot' steps: - uses: actions/checkout@v4 - uses: ./.github/workflows/setup-with-retry @@ -23,7 +23,7 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 with: - author: Vehicle Researcher + author: github-actions[bot] commit-message: "Update translations" title: "[bot] Update translations" body: "Automatic PR from repo-maintenance -> update_translations" @@ -36,13 +36,14 @@ jobs: name: package_updates runs-on: ubuntu-latest container: - image: ghcr.io/commaai/openpilot-base:latest + image: ghcr.io/sunnypilot/sunnypilot-base:latest if: github.repository == 'sunnypilot/sunnypilot' steps: - uses: actions/checkout@v4 with: submodules: true - name: uv lock + if: github.repository == 'commaai/openpilot' run: | python3 -m ensurepip --upgrade pip3 install uv @@ -50,6 +51,7 @@ jobs: - name: bump submodules run: | git config --global --add safe.directory '*' + git config submodule.tinygrad.update none git submodule update --remote git add . - name: update car docs @@ -61,8 +63,8 @@ jobs: - name: Create Pull Request uses: peter-evans/create-pull-request@9153d834b60caba6d51c9b9510b087acf9f33f83 with: - author: Vehicle Researcher - token: ${{ secrets.ACTIONS_CREATE_PR_PAT }} + author: github-actions[bot] + token: ${{ github.repository == 'commaai/openpilot' && secrets.ACTIONS_CREATE_PR_PAT || secrets.GITHUB_TOKEN }} commit-message: Update Python packages title: '[bot] Update Python packages' branch: auto-package-updates diff --git a/.github/workflows/selfdrive_tests.yaml b/.github/workflows/selfdrive_tests.yaml index f8897258c6..640ef3d01a 100644 --- a/.github/workflows/selfdrive_tests.yaml +++ b/.github/workflows/selfdrive_tests.yaml @@ -4,7 +4,6 @@ on: push: branches: - master - - master-new pull_request: workflow_dispatch: workflow_call: @@ -15,28 +14,30 @@ on: type: string concurrency: - group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} + group: selfdrive-tests-ci-run-${{ inputs.run_number }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.run_id || github.head_ref || github.ref }}-${{ github.workflow }}-${{ github.event_name }} cancel-in-progress: true env: - REPORT_NAME: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && 'master' || github.event.number }} PYTHONWARNINGS: error - BASE_IMAGE: openpilot-base + BASE_IMAGE: sunnypilot-base AZURE_TOKEN: ${{ secrets.AZURE_COMMADATACI_OPENPILOTCI_TOKEN }} DOCKER_LOGIN: docker login ghcr.io -u ${{ github.actor }} -p ${{ secrets.GITHUB_TOKEN }} - BUILD: selfdrive/test/docker_build.sh base + BUILD: release/ci/docker_build_sp.sh base RUN: docker run --shm-size 2G -v $PWD:/tmp/openpilot -w /tmp/openpilot -e CI=1 -e PYTHONWARNINGS=error -e FILEREADER_CACHE=1 -e PYTHONPATH=/tmp/openpilot -e NUM_JOBS -e JOB_ID -e GITHUB_ACTION -e GITHUB_REF -e GITHUB_HEAD_REF -e GITHUB_SHA -e GITHUB_REPOSITORY -e GITHUB_RUN_ID -v $GITHUB_WORKSPACE/.ci_cache/scons_cache:/tmp/scons_cache -v $GITHUB_WORKSPACE/.ci_cache/comma_download_cache:/tmp/comma_download_cache -v $GITHUB_WORKSPACE/.ci_cache/openpilot_cache:/tmp/openpilot_cache $BASE_IMAGE /bin/bash -c - PYTEST: pytest --continue-on-collection-errors --cov --cov-report=xml --cov-append --durations=0 --durations-min=5 --hypothesis-seed 0 -n logical + PYTEST: pytest --continue-on-collection-errors --durations=0 -n logical jobs: build_release: - if: github.repository == 'commaai/openpilot' # build_release blocked for the time being to only comma as we may have a different process. name: build release - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} env: STRIPPED_DIR: /tmp/releasepilot steps: @@ -51,7 +52,7 @@ jobs: command: git lfs pull - name: Build devel timeout-minutes: 1 - run: TARGET_DIR=$STRIPPED_DIR release/build_devel.sh + run: TARGET_DIR=$STRIPPED_DIR release/build_stripped.sh - uses: ./.github/workflows/setup-with-retry - name: Build openpilot and run checks timeout-minutes: ${{ ((steps.restore-scons-cache.outputs.cache-hit == 'true') && 10 || 30) }} # allow more time when we missed the scons cache @@ -66,17 +67,37 @@ jobs: - name: Check submodules if: github.repository == 'sunnypilot/sunnypilot' timeout-minutes: 3 - run: release/check-submodules.sh + run: | + if [ "${{ github.ref }}" != "refs/heads/master" ]; then + git fetch origin master:refs/remotes/origin/master + + SUBMODULE_PATHS=$(git diff origin/master HEAD --name-only | grep -E '^[^/]+$' | while read path; do + if git ls-files --stage "$path" | grep -q "^160000"; then + echo "$path" + fi + done | tr '\n' ' ') + + if [ -n "$SUBMODULE_PATHS" ]; then + echo "Changed submodule paths: $SUBMODULE_PATHS" + export SUBMODULE_PATHS="$SUBMODULE_PATHS" + export CHECK_PR_REFS=true + fi + fi + release/check-submodules.sh build: - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v4 with: submodules: true - name: Setup docker push - if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'commaai/openpilot' + if: github.ref == 'refs/heads/master' && github.event_name != 'pull_request' && github.repository == 'sunnypilot/sunnypilot' run: | echo "PUSH_IMAGE=true" >> "$GITHUB_ENV" $DOCKER_LOGIN @@ -86,6 +107,7 @@ 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 @@ -107,7 +129,7 @@ jobs: PYTHONWARNINGS: default - name: Save Homebrew cache uses: actions/cache/save@v4 - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + if: github.ref == 'refs/heads/master' with: path: ~/Library/Caches/Homebrew key: brew-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} @@ -124,15 +146,19 @@ jobs: run: . .venv/bin/activate && scons -j$(nproc) - name: Save scons cache uses: actions/cache/save@v4 - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') + if: github.ref == 'refs/heads/master' with: path: /tmp/scons_cache key: scons-${{ runner.arch }}-macos-${{ env.CACHE_COMMIT_DATE }}-${{ github.sha }} static_analysis: name: static analysis - runs-on: - - 'ubuntu-latest' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} env: PYTHONWARNINGS: default steps: @@ -146,40 +172,46 @@ jobs: unit_tests: name: unit tests - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v4 with: submodules: true - uses: ./.github/workflows/setup-with-retry + id: setup-step - name: Build openpilot run: ${{ env.RUN }} "scons -j$(nproc)" - name: Run unit tests - timeout-minutes: ${{ contains(runner.name, 'nsc') && 1 || 20 }} + timeout-minutes: ${{ contains(runner.name, 'nsc') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 999 }} run: | - ${{ env.RUN }} "$PYTEST --collect-only -m 'not slow' &> /dev/null && \ + ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ + # Pre-compile Python bytecode so each pytest worker doesn't need to + $PYTEST --collect-only -m 'not slow' -qq && \ MAX_EXAMPLES=1 $PYTEST -m 'not slow' && \ ./selfdrive/ui/tests/create_test_translations.sh && \ QT_QPA_PLATFORM=offscreen ./selfdrive/ui/tests/test_translations && \ chmod -R 777 /tmp/comma_download_cache" - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v4 - with: - name: ${{ github.job }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} process_replay: name: process replay - if: github.repository == 'commaai/openpilot' # disable process_replay for forks - runs-on: - - 'ubuntu-24.04' + if: false # disable process_replay for forks + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} steps: - uses: actions/checkout@v4 with: submodules: true - uses: ./.github/workflows/setup-with-retry + id: setup-step - name: Cache test routes id: dependency-cache uses: actions/cache@v4 @@ -190,12 +222,10 @@ jobs: run: | ${{ env.RUN }} "scons -j$(nproc)" - name: Run replay - timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && 1 || 20 }} + timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.dependency-cache.outputs.cache-hit == 'true') && ((steps.setup-step.outputs.duration < 18) && 1 || 2) || 20 }} run: | - ${{ env.RUN }} "coverage run selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ - chmod -R 777 /tmp/comma_download_cache && \ - coverage combine && \ - coverage xml" + ${{ env.RUN }} "selfdrive/test/process_replay/test_processes.py -j$(nproc) && \ + chmod -R 777 /tmp/comma_download_cache" - name: Print diff id: print-diff if: always() @@ -207,7 +237,7 @@ jobs: name: process_replay_diff.txt path: selfdrive/test/process_replay/diff.txt - name: Upload reference logs - if: ${{ failure() && steps.print-diff.outcome == 'success' && github.repository == 'commaai/openpilot' && env.AZURE_TOKEN != '' }} + if: false # TODO: move this to github instead of azure run: | ${{ env.RUN }} "unset PYTHONWARNINGS && AZURE_TOKEN='$AZURE_TOKEN' python3 selfdrive/test/process_replay/test_processes.py -j$(nproc) --upload-only" - name: Run regen @@ -216,119 +246,27 @@ jobs: run: | ${{ env.RUN }} "ONNXCPU=1 $PYTEST selfdrive/test/process_replay/test_regen.py && \ chmod -R 777 /tmp/comma_download_cache" - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v4 - with: - name: ${{ github.job }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - test_cars: - name: cars - runs-on: - - 'ubuntu-24.04' - strategy: - fail-fast: false - matrix: - job: [0, 1, 2, 3] - steps: - - uses: actions/checkout@v4 - with: - submodules: true - - uses: ./.github/workflows/setup-with-retry - - name: Cache test routes - id: routes-cache - uses: actions/cache@v4 - with: - path: .ci_cache/comma_download_cache - key: car_models-${{ hashFiles('selfdrive/car/tests/test_models.py', 'opendbc/car/tests/routes.py') }}-${{ matrix.job }} - - name: Build openpilot - run: ${{ env.RUN }} "scons -j$(nproc)" - - name: Test car models - timeout-minutes: ${{ contains(runner.name, 'nsc') && (steps.routes-cache.outputs.cache-hit == 'true') && 1 || 6 }} - run: | - ${{ env.RUN }} "MAX_EXAMPLES=1 $PYTEST selfdrive/car/tests/test_models.py && \ - chmod -R 777 /tmp/comma_download_cache" - env: - NUM_JOBS: 4 - JOB_ID: ${{ matrix.job }} - - name: "Upload coverage to Codecov" - uses: codecov/codecov-action@v4 - with: - name: ${{ github.job }}-${{ matrix.job }} - env: - CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - car_docs_diff: - name: PR comments - runs-on: ubuntu-latest - #if: github.event_name == 'pull_request' - if: false # TODO: run this in opendbc? - steps: - - uses: actions/checkout@v4 - with: - submodules: true - ref: ${{ github.event.pull_request.base.ref }} - - run: git lfs pull - - uses: ./.github/workflows/setup-with-retry - - name: Get base car info - run: | - ${{ env.RUN }} "scons -j$(nproc) && python3 selfdrive/debug/dump_car_docs.py --path /tmp/openpilot_cache/base_car_docs" - sudo chown -R $USER:$USER ${{ github.workspace }} - - uses: actions/checkout@v4 - with: - submodules: true - path: current - - run: cd current && git lfs pull - - name: Save car docs diff - id: save_diff - run: | - cd current - ${{ env.RUN }} "scons -j$(nproc)" - output=$(${{ env.RUN }} "python3 selfdrive/debug/print_docs_diff.py --path /tmp/openpilot_cache/base_car_docs") - output="${output//$'\n'/'%0A'}" - echo "::set-output name=diff::$output" - - name: Find comment - if: ${{ env.AZURE_TOKEN != '' }} - uses: peter-evans/find-comment@3eae4d37986fb5a8592848f6a574fdf654e61f9e - id: fc - with: - issue-number: ${{ github.event.pull_request.number }} - body-includes: This PR makes changes to - - name: Update comment - if: ${{ steps.save_diff.outputs.diff != '' && env.AZURE_TOKEN != '' }} - uses: peter-evans/create-or-update-comment@71345be0265236311c031f5c7866368bd1eff043 - with: - comment-id: ${{ steps.fc.outputs.comment-id }} - issue-number: ${{ github.event.pull_request.number }} - body: "${{ steps.save_diff.outputs.diff }}" - edit-mode: replace - - name: Delete comment - if: ${{ steps.fc.outputs.comment-id != '' && steps.save_diff.outputs.diff == '' && env.AZURE_TOKEN != '' }} - uses: actions/github-script@v7 - with: - script: | - github.rest.issues.deleteComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: ${{ steps.fc.outputs.comment-id }} - }) simulator_driving: name: simulator driving - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} if: (github.repository == 'commaai/openpilot') && ((github.event_name != 'pull_request') || (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) steps: - uses: actions/checkout@v4 with: submodules: true - uses: ./.github/workflows/setup-with-retry + id: setup-step - name: Build openpilot run: | ${{ env.RUN }} "scons -j$(nproc)" - name: Driving test - timeout-minutes: 1 + timeout-minutes: ${{ (steps.setup-step.outputs.duration < 18) && 1 || 2 }} run: | ${{ env.RUN }} "source selfdrive/test/setup_xvfb.sh && \ source selfdrive/test/setup_vsound.sh && \ @@ -337,8 +275,13 @@ jobs: create_ui_report: # This job name needs to be the same as UI_JOB_NAME in ui_preview.yaml name: Create UI Report - runs-on: - - 'ubuntu-24.04' + runs-on: ${{ + (github.repository == 'commaai/openpilot') && + ((github.event_name != 'pull_request') || + (github.event.pull_request.head.repo.full_name == 'commaai/openpilot')) + && fromJSON('["namespace-profile-amd64-8x16", "namespace-experiments:docker.builds.local-cache=separate"]') + || fromJSON('["ubuntu-24.04"]') }} + if: false # FIXME: FrameReader is broken on CI runners steps: - uses: actions/checkout@v4 with: @@ -362,5 +305,5 @@ jobs: - name: Upload Test Report uses: actions/upload-artifact@v4 with: - name: ${{ env.REPORT_NAME }} + name: report-${{ inputs.run_number || '1' }}-${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} path: selfdrive/ui/tests/test_ui/report_1/screenshots diff --git a/.github/workflows/setup-with-retry/action.yaml b/.github/workflows/setup-with-retry/action.yaml index ad297403cf..98a3913600 100644 --- a/.github/workflows/setup-with-retry/action.yaml +++ b/.github/workflows/setup-with-retry/action.yaml @@ -10,9 +10,17 @@ inputs: required: false default: 30 +outputs: + duration: + description: 'Duration of the setup process in seconds' + value: ${{ steps.get_duration.outputs.duration }} + runs: using: "composite" steps: + - id: start_time + shell: bash + run: echo "START_TIME=$(date +%s)" >> $GITHUB_ENV - id: setup1 uses: ./.github/workflows/setup continue-on-error: true @@ -35,3 +43,10 @@ runs: uses: ./.github/workflows/setup with: is_retried: true + - id: get_duration + shell: bash + run: | + END_TIME=$(date +%s) + DURATION=$((END_TIME - START_TIME)) + echo "Total duration: $DURATION seconds" + echo "duration=$DURATION" >> $GITHUB_OUTPUT diff --git a/.github/workflows/stale.yaml b/.github/workflows/stale.yaml index cc29813952..075b617245 100644 --- a/.github/workflows/stale.yaml +++ b/.github/workflows/stale.yaml @@ -7,6 +7,7 @@ on: env: DAYS_BEFORE_PR_CLOSE: 2 DAYS_BEFORE_PR_STALE: 9 + DAYS_BEFORE_PR_STALE_DRAFT: 30 jobs: stale: @@ -24,6 +25,28 @@ jobs: exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE }} days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} + exempt-draft-pr: false + + # issue config + days-before-issue-stale: -1 # ignore issues for now + + # same as above, but give draft PRs more time + stale_drafts: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v9 + with: + exempt-all-milestones: true + + # pull request config + stale-pr-message: 'This PR has had no activity for ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} days. It will be automatically closed in ${{ env.DAYS_BEFORE_PR_CLOSE }} days if there is no activity.' + close-pr-message: 'This PR has been automatically closed due to inactivity. Feel free to re-open once activity resumes.' + stale-pr-label: stale + delete-branch: ${{ github.event.pull_request.head.repo.full_name == 'commaai/openpilot' }} # only delete branches on the main repo + exempt-pr-labels: "ignore stale,needs testing" # if wip or it needs testing from the community, don't mark as stale + days-before-pr-stale: ${{ env.DAYS_BEFORE_PR_STALE_DRAFT }} + days-before-pr-close: ${{ env.DAYS_BEFORE_PR_CLOSE }} + exempt-draft-pr: true # issue config days-before-issue-stale: -1 # ignore issues for now diff --git a/.github/workflows/sunnypilot-build-model.yaml b/.github/workflows/sunnypilot-build-model.yaml index dc02d1d8d2..dfe84e7853 100644 --- a/.github/workflows/sunnypilot-build-model.yaml +++ b/.github/workflows/sunnypilot-build-model.yaml @@ -9,6 +9,27 @@ env: MODELS_DIR: ${{ github.workspace }}/selfdrive/modeld/models on: + workflow_call: + inputs: + upstream_branch: + description: 'Upstream branch to build from' + required: true + default: 'master' + type: string + custom_name: + description: 'Custom name for the model (no date, only name)' + required: false + type: string + is_20hz: + description: 'Is this a 20Hz model' + required: false + type: boolean + default: true + artifact_suffix: + description: 'Suffix for artifact name' + required: false + type: string + default: '' workflow_dispatch: inputs: upstream_branch: @@ -32,34 +53,53 @@ run-name: Build model [${{ inputs.custom_name || inputs.upstream_branch }}] from jobs: get_model: runs-on: ubuntu-latest + env: + REF: ${{ inputs.upstream_branch }} outputs: model_date: ${{ steps.commit-date.outputs.model_date }} steps: - - uses: actions/checkout@v4 + # Note: To allow dynamic models from both openpilot and sunnypilot (merges/mashups), we try commaai as default, + # and fallback to sunnypilot if the ref checkout fails. + - name: Checkout commaai/openpilot + id: checkout_upstream + continue-on-error: true + uses: actions/checkout@v4 with: - repository: ${{ env.UPSTREAM_REPO }} - ref: ${{ github.event.inputs.upstream_branch }} + repository: commaai/openpilot + ref: ${{ inputs.upstream_branch }} submodules: recursive + path: openpilot + + - name: Fallback to sunnypilot/sunnypilot + if: steps.checkout_upstream.outcome == 'failure' + uses: actions/checkout@v4 + with: + repository: sunnypilot/sunnypilot + ref: ${{ inputs.upstream_branch }} + submodules: recursive + path: openpilot - name: Get commit date id: commit-date run: | - # Get the commit date in YYYY-MM-DD format + cd ${{ github.workspace }}/openpilot commit_date=$(git log -1 --format=%cd --date=format:'%B %d, %Y') echo "model_date=${commit_date}" >> $GITHUB_OUTPUT cat $GITHUB_OUTPUT - - run: git lfs pull + - run: | + cd ${{ github.workspace }}/openpilot + git lfs pull - name: 'Upload Artifact' uses: actions/upload-artifact@v4 with: - name: models - path: ${{ github.workspace }}/selfdrive/modeld/models/*.onnx + name: models-${{ env.REF }}${{ inputs.artifact_suffix }} + path: ${{ github.workspace }}/openpilot/selfdrive/modeld/models/*.onnx build_model: - runs-on: self-hosted + runs-on: [self-hosted, tici] needs: get_model env: MODEL_NAME: ${{ inputs.custom_name || inputs.upstream_branch }} (${{ needs.get_model.outputs.model_date }}) - + REF: ${{ inputs.upstream_branch }} steps: - uses: actions/checkout@v4 with: @@ -71,7 +111,7 @@ jobs: with: path: ${{env.SCONS_CACHE_DIR}} key: scons-${{ runner.os }}-${{ runner.arch }}-${{ github.head_ref || github.ref_name }}-model-${{ github.sha }} - # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) + # Note: GitHub Actions enforces cache isolation between different build sources (PR builds, workflow dispatches, etc.) # 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 }}-${{ github.head_ref || github.ref_name }}-model @@ -114,7 +154,7 @@ jobs: - name: Download model artifacts uses: actions/download-artifact@v4 with: - name: models + name: models-${{ env.REF }}${{ inputs.artifact_suffix }} path: ${{ github.workspace }}/selfdrive/modeld/models - name: Build Model @@ -157,12 +197,22 @@ jobs: --upstream-branch "${{ inputs.upstream_branch }}" \ ${{ inputs.is_20hz && '--is-20hz' || '' }} + - name: Write artifact name to file + run: echo "model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }}" > ${{ env.OUTPUT_DIR }}/artifact_name.txt + - name: Upload Build Artifacts + id: upload-artifact uses: actions/upload-artifact@v4 with: - name: model-${{ env.MODEL_NAME }}-${{ github.run_number }} + name: model-${{ env.MODEL_NAME }}${{ inputs.artifact_suffix }}-${{ github.run_number }} path: ${{ env.OUTPUT_DIR }} + - name: Upload artifact name file + uses: actions/upload-artifact@v4 + with: + name: artifact-name-${{ inputs.custom_name || inputs.upstream_branch }} + path: ${{ env.OUTPUT_DIR }}/artifact_name.txt + - name: Re-enable powersave if: always() run: | diff --git a/.github/workflows/sunnypilot-build-prebuilt.yaml b/.github/workflows/sunnypilot-build-prebuilt.yaml index 6cd5023a17..d654f5ab46 100644 --- a/.github/workflows/sunnypilot-build-prebuilt.yaml +++ b/.github/workflows/sunnypilot-build-prebuilt.yaml @@ -8,21 +8,15 @@ env: PUBLIC_REPO_URL: "https://github.com/sunnypilot/sunnypilot" # Branch configurations - STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master-new' }} # 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. + STAGING_C3_SOURCE_BRANCH: ${{ vars.STAGING_C3_SOURCE_BRANCH || 'master' }} # vars are set on repo settings. # Runtime configuration SOURCE_BRANCH: "${{ github.head_ref || github.ref_name }}" on: push: - branches: [ master, master-new, master-dev-c3-new ] - tags: [ '*' ] + branches: [ master, master-dev-c3-new ] + tags: [ 'release/*' ] pull_request_target: types: [ labeled ] workflow_dispatch: @@ -34,9 +28,72 @@ 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 }} + is_stable_branch: ${{ steps.strategy.outputs.is_stable_branch }} + build: ${{ steps.strategy.outputs.build }} + 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) + ') + + 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" + + echo "new_branch=$NEW_BRANCH" >> $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') + 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 + + 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'); + 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 @@ -44,19 +101,26 @@ 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 ] + needs: [ validate_tests, prepare_strategy ] concurrency: group: build-${{ github.head_ref || github.ref_name }} cancel-in-progress: false - runs-on: self-hosted + 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 }} - if: ${{ (always() && !failure() && !cancelled()) && (!contains(github.event_name, 'pull_request') || (github.event.action == 'labeled' && github.event.label.name == 'prebuilt')) }} + 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() && !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: @@ -77,61 +141,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,16 +241,19 @@ jobs: if: always() run: | PYTHONPATH=$PYTHONPATH:${{ github.workspace }}/ ${{ github.workspace }}/scripts/manage-powersave.py --enable - - + + 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 ] + # 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: ${{ (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 @@ -267,19 +285,26 @@ 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 -----" echo "" 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 ] 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 @@ -299,7 +324,7 @@ jobs: export extra_version_identifier=${{ needs.build.outputs.extra_version_identifier || github.run_number}} echo ${TEMPLATE} | envsubst | jq -c '.' | tee payload.json curl -X POST -H "Content-Type: application/json" -d @payload.json $DISCORD_WEBHOOK - + echo "" echo "---- â„¹ï¸ To update the list of branches that notify to dev-feedback -----" echo "" @@ -307,7 +332,7 @@ jobs: echo "2. Current value: ${{ vars.DEV_FEEDBACK_NOTIFICATION_BRANCHES }}" echo "3. Update as needed (JSON array with no spaces)" shell: alpine.sh {0} - + manage-pr-labels: name: Remove prebuilt label runs-on: ubuntu-latest diff --git a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml index 031be6a50d..d4c201824e 100644 --- a/.github/workflows/sunnypilot-master-dev-c3-prep.yaml +++ b/.github/workflows/sunnypilot-master-dev-c3-prep.yaml @@ -1,7 +1,7 @@ name: Build dev-c3-new env: - DEFAULT_SOURCE_BRANCH: "master-new" + DEFAULT_SOURCE_BRANCH: "master" DEFAULT_TARGET_BRANCH: "master-dev-c3-new" PR_LABEL: "dev-c3" LFS_URL: 'https://gitlab.com/sunnypilot/public/sunnypilot-new-lfs.git/info/lfs' @@ -11,18 +11,16 @@ on: push: branches: - master - - master-new pull_request_target: - types: [ labeled ] - branches: - - 'master' - - 'master-new' + types: [ labeled ] + branches: + - 'master' workflow_dispatch: inputs: source_branch: description: 'Source branch to reset from' required: true - default: 'master-new' + default: 'master' type: string target_branch: description: 'Target branch to reset and squash into' @@ -43,10 +41,10 @@ jobs: reset-and-squash: runs-on: ubuntu-latest 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')))) - ) + (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')))) + ) steps: - uses: actions/checkout@v4 with: @@ -56,9 +54,9 @@ jobs: - name: Wait for Tests 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')))) - ) + (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')))) + ) with: workflow: selfdrive_tests.yaml # The workflow file to monitor github-token: ${{ secrets.GITHUB_TOKEN }} @@ -153,6 +151,7 @@ jobs: } }' -F label="is:pr is:open label:${PR_LABEL} draft:false sort:created-asc") + PR_LIST=${PR_LIST//\'/} echo "PR_LIST=${PR_LIST}" >> $GITHUB_OUTPUT env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ui_preview.yaml b/.github/workflows/ui_preview.yaml index f9b24e0f2c..334b289632 100644 --- a/.github/workflows/ui_preview.yaml +++ b/.github/workflows/ui_preview.yaml @@ -3,20 +3,18 @@ on: push: branches: - master - - master-new pull_request_target: types: [assigned, opened, synchronize, reopened, edited] branches: - 'master' - - 'master-new' paths: - 'selfdrive/ui/**' workflow_dispatch: env: UI_JOB_NAME: "Create UI Report" - REPORT_NAME: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && 'master' || github.event.number }} - SHA: ${{ github.event_name == 'push' && (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.sha || github.event.pull_request.head.sha }} + REPORT_NAME: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && 'master' || github.event.number }} + SHA: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.sha || github.event.pull_request.head.sha }} BRANCH_NAME: "openpilot/pr-${{ github.event.number }}" jobs: @@ -66,7 +64,7 @@ jobs: ref: openpilot_master_ui - name: Saving new master ui - if: (github.ref == 'refs/heads/master' || github.ref == 'refs/heads/master-new') && github.event_name == 'push' + if: github.ref == 'refs/heads/master' && github.event_name == 'push' working-directory: ${{ github.workspace }}/master_ui run: | git checkout --orphan=new_master_ui diff --git a/.gitignore b/.gitignore index b556885632..3c0dad521d 100644 --- a/.gitignore +++ b/.gitignore @@ -13,9 +13,11 @@ venv/ model2.png a.out .hypothesis +.cache/ /docs_site/ +*.mp4 *.dylib *.DSYM *.d @@ -70,8 +72,6 @@ flycheck_* cppcheck_report.txt comma*.sh -selfdrive/modeld/thneed/compile -selfdrive/modeld/models/*.thneed selfdrive/modeld/models/*.pkl sunnypilot/modeld*/thneed/compile sunnypilot/modeld*/models/*.thneed diff --git a/.idea/tools/External Tools.xml b/.idea/tools/External Tools.xml index fa2375e2ee..d5d03136ea 100644 --- a/.idea/tools/External Tools.xml +++ b/.idea/tools/External Tools.xml @@ -2,7 +2,7 @@ @@ -16,7 +16,7 @@ diff --git a/Dockerfile.sunnypilot b/Dockerfile.sunnypilot new file mode 100644 index 0000000000..88a226ad08 --- /dev/null +++ b/Dockerfile.sunnypilot @@ -0,0 +1,12 @@ +FROM ghcr.io/sunnypilot/sunnypilot-base:latest + +ENV PYTHONUNBUFFERED=1 + +ENV OPENPILOT_PATH=/home/batman/openpilot + +RUN mkdir -p ${OPENPILOT_PATH} +WORKDIR ${OPENPILOT_PATH} + +COPY . ${OPENPILOT_PATH}/ + +RUN scons --cache-readonly -j$(nproc) diff --git a/Dockerfile.sunnypilot_base b/Dockerfile.sunnypilot_base new file mode 100644 index 0000000000..ec1e3b6c06 --- /dev/null +++ b/Dockerfile.sunnypilot_base @@ -0,0 +1,83 @@ +FROM ubuntu:24.04 + +ENV PYTHONUNBUFFERED=1 + +ENV DEBIAN_FRONTEND=noninteractive +RUN apt-get update && \ + apt-get install -y --no-install-recommends sudo tzdata locales ssh pulseaudio xvfb x11-xserver-utils gnome-screenshot python3-tk python3-dev && \ + rm -rf /var/lib/apt/lists/* + +RUN sed -i -e 's/# en_US.UTF-8 UTF-8/en_US.UTF-8 UTF-8/' /etc/locale.gen && locale-gen +ENV LANG=en_US.UTF-8 +ENV LANGUAGE=en_US:en +ENV LC_ALL=en_US.UTF-8 + +COPY tools/install_ubuntu_dependencies.sh /tmp/tools/ +RUN /tmp/tools/install_ubuntu_dependencies.sh && \ + rm -rf /var/lib/apt/lists/* /tmp/* && \ + cd /usr/lib/gcc/arm-none-eabi/* && \ + rm -rf arm/ thumb/nofp thumb/v6* thumb/v8* thumb/v7+fp thumb/v7-r+fp.sp + +# Add OpenCL +RUN apt-get update && apt-get install -y --no-install-recommends \ + apt-utils \ + alien \ + unzip \ + tar \ + curl \ + xz-utils \ + dbus \ + gcc-arm-none-eabi \ + tmux \ + vim \ + libx11-6 \ + wget \ + rsync \ + && rm -rf /var/lib/apt/lists/* + +RUN mkdir -p /tmp/opencl-driver-intel && \ + cd /tmp/opencl-driver-intel && \ + wget https://github.com/intel/llvm/releases/download/2024-WW14/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \ + wget https://github.com/oneapi-src/oneTBB/releases/download/v2021.12.0/oneapi-tbb-2021.12.0-lin.tgz && \ + mkdir -p /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \ + cd /opt/intel/oclcpuexp_2024.17.3.0.09_rel && \ + tar -zxvf /tmp/opencl-driver-intel/oclcpuexp-2024.17.3.0.09_rel.tar.gz && \ + mkdir -p /etc/OpenCL/vendors && \ + echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64/libintelocl.so > /etc/OpenCL/vendors/intel_expcpu.icd && \ + cd /opt/intel && \ + tar -zxvf /tmp/opencl-driver-intel/oneapi-tbb-2021.12.0-lin.tgz && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbb.so.12 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + ln -s /opt/intel/oneapi-tbb-2021.12.0/lib/intel64/gcc4.8/libtbbmalloc.so.2 /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 && \ + mkdir -p /etc/ld.so.conf.d && \ + echo /opt/intel/oclcpuexp_2024.17.3.0.09_rel/x64 > /etc/ld.so.conf.d/libintelopenclexp.conf && \ + ldconfig -f /etc/ld.so.conf.d/libintelopenclexp.conf && \ + cd / && \ + rm -rf /tmp/opencl-driver-intel + +ENV NVIDIA_VISIBLE_DEVICES=all +ENV NVIDIA_DRIVER_CAPABILITIES=graphics,utility,compute +ENV QTWEBENGINE_DISABLE_SANDBOX=1 + +RUN dbus-uuidgen > /etc/machine-id +RUN apt-get update && apt-get install -y fonts-noto-cjk fonts-noto-color-emoji + +ARG USER=batman +ARG USER_UID=1001 +RUN useradd -m -s /bin/bash -u $USER_UID $USER +RUN usermod -aG sudo $USER +RUN echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers +USER $USER + +COPY --chown=$USER pyproject.toml uv.lock /home/$USER +COPY --chown=$USER tools/install_python_dependencies.sh /home/$USER/tools/ + +ENV VIRTUAL_ENV=/home/$USER/.venv +ENV PATH="$VIRTUAL_ENV/bin:$PATH" +RUN cd /home/$USER && \ + tools/install_python_dependencies.sh && \ + rm -rf tools/ pyproject.toml uv.lock .cache + +USER root +RUN sudo git config --global --add safe.directory /tmp/openpilot diff --git a/Jenkinsfile b/Jenkinsfile index a14bf59299..0905abd6da 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -167,7 +167,7 @@ node { env.GIT_COMMIT = checkout(scm).GIT_COMMIT def excludeBranches = ['__nightly', 'devel', 'devel-staging', 'release3', 'release3-staging', - 'testing-closet*', 'hotfix-*'] + 'release-tici', 'testing-closet*', 'hotfix-*'] def excludeRegex = excludeBranches.join('|').replaceAll('\\*', '.*') if (env.BRANCH_NAME != 'master' && !env.BRANCH_NAME.contains('__jenkins_loop_')) { diff --git a/README.md b/README.md index 897852b03f..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,12 +49,35 @@ 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. -Pull requests should be against the most current `master-new` branch. +Pull requests should be against the most current `master` branch. ## 📊 User Data @@ -73,7 +86,7 @@ By default, sunnypilot uploads the driving data to comma servers. You can also a sunnypilot is open source software. The user is free to disable data collection if they wish to do so. sunnypilot logs the road-facing camera, CAN, GPS, IMU, magnetometer, thermal sensors, crashes, and operating system logs. -The driver-facing camera is only logged if you explicitly opt-in in settings. The microphone is not recorded. +The driver-facing camera and microphone are only logged if you explicitly opt-in in settings. By using this software, you understand that use of this software or its related services will generate certain types of user data, which may be logged and stored at the sole discretion of comma. By accepting this agreement, you grant an irrevocable, perpetual, worldwide right to comma for the use of this data. diff --git a/RELEASES.md b/RELEASES.md index 91652d6dc7..dacf0eaa17 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -1,5 +1,22 @@ -Version 0.9.10 (2025-06-30) +Version 0.10.1 (2025-09-08) ======================== +* Record driving feedback using LKAS button +* Honda City 2023 support thanks to drFritz! + +Version 0.10.0 (2025-08-05) +======================== +* New driving model + * New training architecture + * Described in our CVPR paper: "Learning to Drive from a World Model" + * Longitudinal MPC replaced by E2E planning from World Model in Experimental Mode + * Action from lateral MPC as training objective replaced by E2E planning from World Model + * Low-speed lead car ground-truth fixes +* Enable live-learned steering actuation delay +* Opt-in audio recording for dashcam video +* Acura MDX 2025 support thanks to vanillagorillaa and MVL! +* Honda Accord 2023-25 support thanks to vanillagorillaa and MVL! +* Honda CR-V 2023-25 support thanks to vanillagorillaa and MVL! +* Honda Pilot 2023-25 support thanks to vanillagorillaa and MVL! Version 0.9.9 (2025-05-23) ======================== diff --git a/SConstruct b/SConstruct index 77c8525896..192f83a0f6 100644 --- a/SConstruct +++ b/SConstruct @@ -17,7 +17,7 @@ AGNOS = TICI Decider('MD5-timestamp') -SetOption('num_jobs', int(os.cpu_count()/2)) +SetOption('num_jobs', max(1, int(os.cpu_count()/2))) AddOption('--kaitai', action='store_true', @@ -39,10 +39,6 @@ AddOption('--clazy', action='store_true', help='build with clazy') -AddOption('--compile_db', - action='store_true', - help='build clang compilation database') - AddOption('--ccflags', action='store', type='string', @@ -86,7 +82,6 @@ assert arch in ["larch64", "aarch64", "x86_64", "Darwin"] lenv = { "PATH": os.environ['PATH'], - "LD_LIBRARY_PATH": [Dir(f"#third_party/acados/{arch}/lib").abspath], "PYTHONPATH": Dir("#").abspath + ':' + Dir(f"#third_party/acados").abspath, "ACADOS_SOURCE_DIR": Dir("#third_party/acados").abspath, @@ -94,7 +89,7 @@ lenv = { "TERA_PATH": Dir("#").abspath + f"/third_party/acados/{arch}/t_renderer" } -rpath = lenv["LD_LIBRARY_PATH"].copy() +rpath = [] if arch == "larch64": cpppath = [ @@ -137,7 +132,6 @@ else: f"{brew_prefix}/include", f"{brew_prefix}/opt/openssl@3.0/include", ] - lenv["DYLD_LIBRARY_PATH"] = lenv["LD_LIBRARY_PATH"] # Linux else: libpath = [ @@ -234,8 +228,7 @@ if arch == "Darwin": darwin_rpath_link_flags = [f"-Wl,-rpath,{path}" for path in env["RPATH"]] env["LINKFLAGS"] += darwin_rpath_link_flags -if GetOption('compile_db'): - env.CompilationDatabase('compile_commands.json') +env.CompilationDatabase('compile_commands.json') # Setup cache dir default_cache_dir = '/data/scons_cache' if AGNOS else '/tmp/scons_cache' diff --git a/cereal/custom.capnp b/cereal/custom.capnp index c9e4f3e16d..5468e0f39d 100644 --- a/cereal/custom.capnp +++ b/cereal/custom.capnp @@ -25,6 +25,27 @@ struct ModularAssistiveDrivingSystem { } } +# Same struct as Log.RadarState.LeadData +struct LeadData { + dRel @0 :Float32; + yRel @1 :Float32; + vRel @2 :Float32; + aRel @3 :Float32; + vLead @4 :Float32; + dPath @6 :Float32; + vLat @7 :Float32; + vLeadK @8 :Float32; + aLeadK @9 :Float32; + fcw @10 :Bool; + status @11 :Bool; + aLeadTau @12 :Float32; + modelProb @13 :Float32; + radar @14 :Bool; + radarTrackId @15 :Int32 = -1; + + aLeadDEPRECATED @5 :Float32; +} + struct SelfdriveStateSP @0x81c2f05a394cf4af { mads @0 :ModularAssistiveDrivingSystem; } @@ -151,6 +172,8 @@ struct OnroadEventSP @0xda96579883444c35 { experimentalModeSwitched @14; wrongCarModeAlertOnly @15; pedalPressedAlertOnly @16; + laneTurnLeft @17; + laneTurnRight @18; } } @@ -174,6 +197,8 @@ struct CarParamsSP @0x80ae746ee2596b11 { struct CarControlSP @0xa5cd762cd951a455 { mads @0 :ModularAssistiveDrivingSystem; params @1 :List(Param); + leadOne @2 :LeadData; + leadTwo @3 :LeadData; struct Param { key @0 :Text; @@ -235,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 7c7983d147..474b4777cb 100644 --- a/cereal/log.capnp +++ b/cereal/log.capnp @@ -127,7 +127,9 @@ struct OnroadEvent @0xc4fa6047f024e718 { espActive @90; personalityChanged @91; aeb @92; - userFlag @95; + userBookmark @95; + excessiveActuation @96; + audioFeedback @97; soundsUnavailableDEPRECATED @47; } @@ -492,7 +494,6 @@ struct DeviceState @0xa4d8b5af2aa492eb { gpuTempC @27 :List(Float32); dspTempC @49 :Float32; memoryTempC @28 :Float32; - nvmeTempC @35 :List(Float32); modemTempC @36 :List(Float32); pmicTempC @39 :List(Float32); intakeTempC @46 :Float32; @@ -568,6 +569,7 @@ struct DeviceState @0xa4d8b5af2aa492eb { chargingDisabledDEPRECATED @18 :Bool; usbOnlineDEPRECATED @12 :Bool; ambientTempCDEPRECATED @30 :Float32; + nvmeTempCDEPRECATED @35 :List(Float32); } struct PandaState @0xa7649e2575e4591e { @@ -585,7 +587,7 @@ struct PandaState @0xa7649e2575e4591e { fanPower @28 :UInt8; fanStallCount @34 :UInt8; - spiChecksumErrorCount @33 :UInt16; + spiErrorCount @33 :UInt16; harnessStatus @21 :HarnessStatus; sbu1Voltage @35 :Float32; @@ -1083,7 +1085,7 @@ struct ModelDataV2 { confidence @23: ConfidenceClass; # Model perceived motion - temporalPose @21 :Pose; + temporalPoseDEPRECATED @21 :Pose; # e2e lateral planner action @26: Action; @@ -2467,16 +2469,27 @@ struct DebugAlert { alertText2 @1 :Text; } -struct UserFlag { +struct UserBookmark @0xfe346a9de48d9b50 { } -struct Microphone { +struct SoundPressure @0xdc24138990726023 { soundPressure @0 :Float32; # uncalibrated, A-weighted soundPressureWeighted @3 :Float32; soundPressureWeightedDb @1 :Float32; - filteredSoundPressureWeightedDb @2 :Float32; + + filteredSoundPressureWeightedDbDEPRECATED @2 :Float32; +} + +struct AudioData { + data @0 :Data; + sampleRate @1 :UInt32; +} + +struct AudioFeedback { + audio @0 :AudioData; + blockNum @1 :UInt16; } struct Touch { @@ -2556,7 +2569,8 @@ struct Event { livestreamDriverEncodeIdx @119 :EncodeIndex; # microphone data - microphone @103 :Microphone; + soundPressure @103 :SoundPressure; + rawAudioData @147 :AudioData; # systems stuff androidLog @20 :AndroidLogEntry; @@ -2578,9 +2592,13 @@ struct Event { mapRenderState @105: MapRenderState; # UI services - userFlag @93 :UserFlag; uiDebug @102 :UIDebug; + # driving feedback + userBookmark @93 :UserBookmark; + bookmarkButton @148 :UserBookmark; + audioFeedback @149 :AudioFeedback; + # *********** debug *********** testJoystick @52 :Joystick; roadEncodeData @86 :EncodeData; @@ -2613,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/messaging/tests/test_messaging.py b/cereal/messaging/tests/test_messaging.py index 6234a11c08..583eb8b0d8 100644 --- a/cereal/messaging/tests/test_messaging.py +++ b/cereal/messaging/tests/test_messaging.py @@ -30,7 +30,7 @@ def zmq_sleep(t=1): # TODO: this should take any capnp struct and returrn a msg with random populated data def random_carstate(): - fields = ["vEgo", "aEgo", "gas", "steeringAngleDeg"] + fields = ["vEgo", "aEgo", "brake", "steeringAngleDeg"] msg = messaging.new_message("carState") cs = msg.carState for f in fields: @@ -177,8 +177,8 @@ class TestMessaging: # wait 5 socket timeouts before sending msg = random_carstate() - delayed_send(sock_timeout*5, pub_sock, msg.to_bytes()) start_time = time.monotonic() + delayed_send(sock_timeout*5, pub_sock, msg.to_bytes()) recvd = messaging.recv_one_retry(sub_sock) assert (time.monotonic() - start_time) >= sock_timeout*5 assert isinstance(recvd, capnp._DynamicStructReader) diff --git a/cereal/messaging/tests/test_pub_sub_master.py b/cereal/messaging/tests/test_pub_sub_master.py index e47e713393..5e26b49701 100644 --- a/cereal/messaging/tests/test_pub_sub_master.py +++ b/cereal/messaging/tests/test_pub_sub_master.py @@ -86,7 +86,7 @@ class TestSubMaster: "cameraOdometry": (20, 10), "liveCalibration": (4, 4), "carParams": (None, None), - "userFlag": (None, None), + "userBookmark": (None, None), } for service, (max_freq, min_freq) in checks.items(): diff --git a/cereal/services.py b/cereal/services.py index 3e72041b72..b48290bc77 100755 --- a/cereal/services.py +++ b/cereal/services.py @@ -72,8 +72,11 @@ _services: dict[str, tuple] = { "navRoute": (True, 0.), "navThumbnail": (True, 0.), "qRoadEncodeIdx": (False, 20.), - "userFlag": (True, 0., 1), - "microphone": (True, 10., 10), + "userBookmark": (True, 0., 1), + "soundPressure": (True, 10., 10), + "rawAudioData": (False, 20.), + "bookmarkButton": (True, 0., 1), + "audioFeedback": (True, 0., 1), # sunnypilot "modelManagerSP": (False, 1., 1), @@ -85,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/codecov.yml b/codecov.yml deleted file mode 100644 index 519e7d1a38..0000000000 --- a/codecov.yml +++ /dev/null @@ -1,13 +0,0 @@ -comment: false -coverage: - status: - project: - default: - informational: true - patch: off - -ignore: - - "**/test_*.py" - - "selfdrive/test/**" - - "system/version.py" # codecov changes depending on if we are in a branch or not - - "tools" diff --git a/common/conversions.py b/common/constants.py similarity index 83% rename from common/conversions.py rename to common/constants.py index b02b33c625..7ca425c4b2 100644 --- a/common/conversions.py +++ b/common/constants.py @@ -1,6 +1,7 @@ import numpy as np -class Conversions: +# conversions +class CV: # Speed MPH_TO_KPH = 1.609344 KPH_TO_MPH = 1. / MPH_TO_KPH @@ -17,3 +18,6 @@ class Conversions: # Mass LB_TO_KG = 0.453592 + + +ACCELERATION_DUE_TO_GRAVITY = 9.81 # m/s^2 diff --git a/common/model.h b/common/model.h index 596819256a..a984f55e8d 100644 --- a/common/model.h +++ b/common/model.h @@ -1 +1 @@ -#define DEFAULT_MODEL "Vegetarian Filet o Fish (Default)" +#define DEFAULT_MODEL "Steam Powered (Default)" diff --git a/common/params.cc b/common/params.cc index 0a8821c37a..39592cb905 100644 --- a/common/params.cc +++ b/common/params.cc @@ -103,10 +103,10 @@ Params::~Params() { assert(queue.empty()); } -std::vector Params::allKeys(ParamKeyType type) const { +std::vector Params::allKeys(ParamKeyFlag flag) const { std::vector ret; for (auto &p : keys) { - if (type == ALL || (p.second & type)) { + if (flag == ALL || (p.second.flags & flag)) { ret.push_back(p.first); } } @@ -117,8 +117,16 @@ bool Params::checkKey(const std::string &key) { return keys.find(key) != keys.end(); } +ParamKeyFlag Params::getKeyFlag(const std::string &key) { + return static_cast(keys[key].flags); +} + ParamKeyType Params::getKeyType(const std::string &key) { - return static_cast(keys[key]); + return keys[key].type; +} + +std::optional Params::getKeyDefaultValue(const std::string &key) { + return keys[key].default_value; } int Params::put(const char* key, const char* value, size_t value_size) { @@ -197,17 +205,17 @@ std::map Params::readAll() { return util::read_files_in_dir(getParamPath()); } -void Params::clearAll(ParamKeyType key_type) { +void Params::clearAll(ParamKeyFlag key_flag) { FileLock file_lock(params_path + "/.lock"); - // 1) delete params of key_type + // 1) delete params of key_flag // 2) delete files that are not defined in the keys. if (DIR *d = opendir(getParamPath().c_str())) { struct dirent *de = NULL; while ((de = readdir(d))) { if (de->d_type != DT_DIR) { auto it = keys.find(de->d_name); - if (it == keys.end() || (it->second & key_type)) { + if (it == keys.end() || (it->second.flags & key_flag)) { unlink(getParamPath(de->d_name).c_str()); } } diff --git a/common/params.h b/common/params.h index 73e21a3fee..de4f9b435f 100644 --- a/common/params.h +++ b/common/params.h @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -9,17 +10,34 @@ #include "common/queue.h" -enum ParamKeyType { +enum ParamKeyFlag { PERSISTENT = 0x02, CLEAR_ON_MANAGER_START = 0x04, CLEAR_ON_ONROAD_TRANSITION = 0x08, CLEAR_ON_OFFROAD_TRANSITION = 0x10, DONT_LOG = 0x20, DEVELOPMENT_ONLY = 0x40, - BACKUP = 0x80, + CLEAR_ON_IGNITION_ON = 0x80, + BACKUP = 0x100, ALL = 0xFFFFFFFF }; +enum ParamKeyType { + STRING = 0, // must be utf-8 decodable + BOOL = 1, + INT = 2, + FLOAT = 3, + TIME = 4, // ISO 8601 + JSON = 5, + BYTES = 6 +}; + +struct ParamKeyAttributes { + uint32_t flags; + ParamKeyType type; + std::optional default_value = std::nullopt; +}; + class Params { public: explicit Params(const std::string &path = {}); @@ -28,16 +46,18 @@ public: Params(const Params&) = delete; Params& operator=(const Params&) = delete; - std::vector allKeys(ParamKeyType type = ALL) const; + std::vector allKeys(ParamKeyFlag flag = ALL) const; bool checkKey(const std::string &key); + ParamKeyFlag getKeyFlag(const std::string &key); ParamKeyType getKeyType(const std::string &key); + std::optional getKeyDefaultValue(const std::string &key); inline std::string getParamPath(const std::string &key = {}) { return params_path + params_prefix + (key.empty() ? "" : "/" + key); } // Delete a value int remove(const std::string &key); - void clearAll(ParamKeyType type); + void clearAll(ParamKeyFlag flag); // helpers for reading values std::string get(const std::string &key, bool block = false); diff --git a/common/params.py b/common/params.py index 66808083dc..494617200f 100644 --- a/common/params.py +++ b/common/params.py @@ -1,5 +1,6 @@ -from openpilot.common.params_pyx import Params, ParamKeyType, UnknownKeyName +from openpilot.common.params_pyx import Params, ParamKeyFlag, ParamKeyType, UnknownKeyName assert Params +assert ParamKeyFlag assert ParamKeyType assert UnknownKeyName diff --git a/common/params_keys.h b/common/params_keys.h index 0f37a0a4a6..445ecd2d75 100644 --- a/common/params_keys.h +++ b/common/params_keys.h @@ -3,208 +3,229 @@ #include #include -inline static std::unordered_map keys = { - {"AccessToken", CLEAR_ON_MANAGER_START | DONT_LOG}, - {"AdbEnabled", PERSISTENT}, - {"AlwaysOnDM", PERSISTENT}, - {"ApiCache_Device", PERSISTENT}, - {"ApiCache_FirehoseStats", PERSISTENT}, - {"AssistNowToken", PERSISTENT}, - {"AthenadPid", PERSISTENT}, - {"AthenadUploadQueue", PERSISTENT}, - {"AthenadRecentlyViewedRoutes", PERSISTENT}, - {"BootCount", PERSISTENT}, - {"CalibrationParams", PERSISTENT}, - {"CameraDebugExpGain", CLEAR_ON_MANAGER_START}, - {"CameraDebugExpTime", CLEAR_ON_MANAGER_START}, - {"CarBatteryCapacity", PERSISTENT}, - {"CarParams", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"CarParamsCache", CLEAR_ON_MANAGER_START}, - {"CarParamsPersistent", PERSISTENT}, - {"CarParamsPrevRoute", PERSISTENT}, - {"CompletedTrainingVersion", PERSISTENT}, - {"ControlsReady", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"CurrentBootlog", PERSISTENT}, - {"CurrentRoute", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"DisableLogging", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"DisablePowerDown", PERSISTENT | BACKUP}, - {"DisableUpdates", PERSISTENT | BACKUP}, - {"DisengageOnAccelerator", PERSISTENT | BACKUP}, - {"DongleId", PERSISTENT}, - {"DoReboot", CLEAR_ON_MANAGER_START}, - {"DoShutdown", CLEAR_ON_MANAGER_START}, - {"DoUninstall", CLEAR_ON_MANAGER_START}, - {"AlphaLongitudinalEnabled", PERSISTENT | DEVELOPMENT_ONLY | BACKUP}, - {"ExperimentalMode", PERSISTENT | BACKUP}, - {"ExperimentalModeConfirmed", PERSISTENT | BACKUP}, - {"FirmwareQueryDone", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"ForcePowerDown", PERSISTENT}, - {"GitBranch", PERSISTENT}, - {"GitCommit", PERSISTENT}, - {"GitCommitDate", PERSISTENT}, - {"GitDiff", PERSISTENT}, - {"GithubSshKeys", PERSISTENT | BACKUP}, - {"GithubUsername", PERSISTENT | BACKUP}, - {"GitRemote", PERSISTENT}, - {"GsmApn", PERSISTENT | BACKUP}, - {"GsmMetered", PERSISTENT | BACKUP}, - {"GsmRoaming", PERSISTENT | BACKUP}, - {"HardwareSerial", PERSISTENT}, - {"HasAcceptedTerms", PERSISTENT}, - {"InstallDate", PERSISTENT}, - {"IsDriverViewEnabled", CLEAR_ON_MANAGER_START}, - {"IsEngaged", PERSISTENT}, - {"IsLdwEnabled", PERSISTENT | BACKUP}, - {"IsMetric", PERSISTENT | BACKUP}, - {"IsOffroad", CLEAR_ON_MANAGER_START}, - {"IsOnroad", PERSISTENT}, - {"IsRhdDetected", PERSISTENT}, - {"IsReleaseBranch", CLEAR_ON_MANAGER_START}, - {"IsTakingSnapshot", CLEAR_ON_MANAGER_START}, - {"IsTestedBranch", CLEAR_ON_MANAGER_START}, - {"JoystickDebugMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LanguageSetting", PERSISTENT | BACKUP}, - {"LastAthenaPingTime", CLEAR_ON_MANAGER_START}, - {"LastGPSPosition", PERSISTENT}, - {"LastManagerExitReason", CLEAR_ON_MANAGER_START}, - {"LastOffroadStatusPacket", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LastPowerDropDetected", CLEAR_ON_MANAGER_START}, - {"LastUpdateException", CLEAR_ON_MANAGER_START}, - {"LastUpdateTime", PERSISTENT}, - {"LiveDelay", PERSISTENT | BACKUP}, - {"LiveParameters", PERSISTENT}, - {"LiveParametersV2", PERSISTENT}, - {"LiveTorqueParameters", PERSISTENT | DONT_LOG}, - {"LocationFilterInitialState", PERSISTENT}, - {"LongitudinalManeuverMode", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"LongitudinalPersonality", PERSISTENT | BACKUP}, - {"NetworkMetered", PERSISTENT}, - {"ObdMultiplexingChanged", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"ObdMultiplexingEnabled", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_BadNvme", CLEAR_ON_MANAGER_START}, - {"Offroad_CarUnrecognized", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_ConnectivityNeeded", CLEAR_ON_MANAGER_START}, - {"Offroad_ConnectivityNeededPrompt", CLEAR_ON_MANAGER_START}, - {"Offroad_IsTakingSnapshot", CLEAR_ON_MANAGER_START}, - {"Offroad_NeosUpdate", CLEAR_ON_MANAGER_START}, - {"Offroad_NoFirmware", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_Recalibration", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_StorageMissing", CLEAR_ON_MANAGER_START}, - {"Offroad_TemperatureTooHigh", CLEAR_ON_MANAGER_START}, - {"Offroad_UnofficialHardware", CLEAR_ON_MANAGER_START}, - {"Offroad_UpdateFailed", CLEAR_ON_MANAGER_START}, - {"OnroadCycleRequested", CLEAR_ON_MANAGER_START}, - {"OpenpilotEnabledToggle", PERSISTENT | BACKUP}, - {"PandaHeartbeatLost", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"PandaSomResetTriggered", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"PandaSignatures", CLEAR_ON_MANAGER_START}, - {"PrimeType", PERSISTENT}, - {"RecordFront", PERSISTENT | BACKUP}, - {"RecordFrontLock", PERSISTENT}, // for the internal fleet - {"SecOCKey", PERSISTENT | DONT_LOG}, // Candidate for | BACKUP - {"RouteCount", PERSISTENT}, - {"SnoozeUpdate", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"SshEnabled", PERSISTENT | BACKUP}, - {"TermsVersion", PERSISTENT}, - {"TrainingVersion", PERSISTENT}, - {"UbloxAvailable", PERSISTENT}, - {"UpdateAvailable", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"UpdateFailedCount", CLEAR_ON_MANAGER_START}, - {"UpdaterAvailableBranches", PERSISTENT}, - {"UpdaterCurrentDescription", CLEAR_ON_MANAGER_START}, - {"UpdaterCurrentReleaseNotes", CLEAR_ON_MANAGER_START}, - {"UpdaterFetchAvailable", CLEAR_ON_MANAGER_START}, - {"UpdaterNewDescription", CLEAR_ON_MANAGER_START}, - {"UpdaterNewReleaseNotes", CLEAR_ON_MANAGER_START}, - {"UpdaterState", CLEAR_ON_MANAGER_START}, - {"UpdaterTargetBranch", CLEAR_ON_MANAGER_START}, - {"UpdaterLastFetchTime", PERSISTENT}, - {"Version", PERSISTENT}, +#include "cereal/gen/cpp/log.capnp.h" + +inline static std::unordered_map keys = { + {"AccessToken", {CLEAR_ON_MANAGER_START | DONT_LOG, STRING}}, + {"AdbEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"AlwaysOnDM", {PERSISTENT | BACKUP, BOOL}}, + {"ApiCache_Device", {PERSISTENT, STRING}}, + {"ApiCache_FirehoseStats", {PERSISTENT, JSON}}, + {"AssistNowToken", {PERSISTENT, STRING}}, + {"AthenadPid", {PERSISTENT, INT}}, + {"AthenadUploadQueue", {PERSISTENT, JSON}}, + {"AthenadRecentlyViewedRoutes", {PERSISTENT, STRING}}, + {"BootCount", {PERSISTENT, INT}}, + {"CalibrationParams", {PERSISTENT, BYTES}}, + {"CameraDebugExpGain", {CLEAR_ON_MANAGER_START, STRING}}, + {"CameraDebugExpTime", {CLEAR_ON_MANAGER_START, STRING}}, + {"CarBatteryCapacity", {PERSISTENT, INT}}, + {"CarParams", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}}, + {"CarParamsCache", {CLEAR_ON_MANAGER_START, BYTES}}, + {"CarParamsPersistent", {PERSISTENT, BYTES}}, + {"CarParamsPrevRoute", {PERSISTENT, BYTES}}, + {"CompletedTrainingVersion", {PERSISTENT, STRING, "0"}}, + {"ControlsReady", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"CurrentBootlog", {PERSISTENT, STRING}}, + {"CurrentRoute", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, STRING}}, + {"DisableLogging", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"DisablePowerDown", {PERSISTENT | BACKUP, BOOL}}, + {"DisableUpdates", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"DisengageOnAccelerator", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"DongleId", {PERSISTENT, STRING}}, + {"DoReboot", {CLEAR_ON_MANAGER_START, BOOL}}, + {"DoShutdown", {CLEAR_ON_MANAGER_START, BOOL}}, + {"DoUninstall", {CLEAR_ON_MANAGER_START, BOOL}}, + {"DriverTooDistracted", {CLEAR_ON_MANAGER_START | CLEAR_ON_IGNITION_ON, BOOL}}, + {"AlphaLongitudinalEnabled", {PERSISTENT | DEVELOPMENT_ONLY | BACKUP, BOOL}}, + {"ExperimentalMode", {PERSISTENT | BACKUP, BOOL}}, + {"ExperimentalModeConfirmed", {PERSISTENT | BACKUP, BOOL}}, + {"FirmwareQueryDone", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"ForcePowerDown", {PERSISTENT, BOOL}}, + {"GitBranch", {PERSISTENT, STRING}}, + {"GitCommit", {PERSISTENT, STRING}}, + {"GitCommitDate", {PERSISTENT, STRING}}, + {"GitDiff", {PERSISTENT, STRING}}, + {"GithubSshKeys", {PERSISTENT | BACKUP, STRING}}, + {"GithubUsername", {PERSISTENT | BACKUP, STRING}}, + {"GitRemote", {PERSISTENT, STRING}}, + {"GsmApn", {PERSISTENT | BACKUP, STRING}}, + {"GsmMetered", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"GsmRoaming", {PERSISTENT | BACKUP, BOOL}}, + {"HardwareSerial", {PERSISTENT, STRING}}, + {"HasAcceptedTerms", {PERSISTENT, STRING, "0"}}, + {"InstallDate", {PERSISTENT, TIME}}, + {"IsDriverViewEnabled", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsEngaged", {PERSISTENT, BOOL}}, + {"IsLdwEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"IsMetric", {PERSISTENT | BACKUP, BOOL}}, + {"IsOffroad", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsOnroad", {PERSISTENT, BOOL}}, + {"IsRhdDetected", {PERSISTENT, BOOL}}, + {"IsReleaseBranch", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsTakingSnapshot", {CLEAR_ON_MANAGER_START, BOOL}}, + {"IsTestedBranch", {CLEAR_ON_MANAGER_START, BOOL}}, + {"JoystickDebugMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"LanguageSetting", {PERSISTENT | BACKUP, STRING, "main_en"}}, + {"LastAthenaPingTime", {CLEAR_ON_MANAGER_START, INT}}, + {"LastGPSPosition", {PERSISTENT, STRING}}, + {"LastManagerExitReason", {CLEAR_ON_MANAGER_START, STRING}}, + {"LastOffroadStatusPacket", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, JSON}}, + {"LastPowerDropDetected", {CLEAR_ON_MANAGER_START, STRING}}, + {"LastUpdateException", {CLEAR_ON_MANAGER_START, STRING}}, + {"LastUpdateRouteCount", {PERSISTENT, INT, "0"}}, + {"LastUpdateTime", {PERSISTENT, TIME}}, + {"LastUpdateUptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, + {"LiveDelay", {PERSISTENT | BACKUP, BYTES}}, + {"LiveParameters", {PERSISTENT, JSON}}, + {"LiveParametersV2", {PERSISTENT, BYTES}}, + {"LiveTorqueParameters", {PERSISTENT | DONT_LOG, BYTES}}, + {"LocationFilterInitialState", {PERSISTENT, BYTES}}, + {"LongitudinalManeuverMode", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"LongitudinalPersonality", {PERSISTENT | BACKUP, INT, std::to_string(static_cast(cereal::LongitudinalPersonality::STANDARD))}}, + {"NetworkMetered", {PERSISTENT | BACKUP, BOOL}}, + {"ObdMultiplexingChanged", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"ObdMultiplexingEnabled", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"Offroad_CarUnrecognized", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_ConnectivityNeeded", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ConnectivityNeededPrompt", {CLEAR_ON_MANAGER_START, JSON}}, + {"Offroad_ExcessiveActuation", {PERSISTENT, JSON}}, + {"Offroad_IsTakingSnapshot", {CLEAR_ON_MANAGER_START, JSON}}, + {"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}}, + {"OnroadCycleRequested", {CLEAR_ON_MANAGER_START, BOOL}}, + {"OpenpilotEnabledToggle", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"PandaHeartbeatLost", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"PandaSomResetTriggered", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"PandaSignatures", {CLEAR_ON_MANAGER_START, BYTES}}, + {"PrimeType", {PERSISTENT, INT}}, + {"RecordAudio", {PERSISTENT | BACKUP, BOOL}}, + {"RecordAudioFeedback", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"RecordFront", {PERSISTENT | BACKUP, BOOL}}, + {"RecordFrontLock", {PERSISTENT, BOOL}}, // for the internal fleet + {"SecOCKey", {PERSISTENT | DONT_LOG | BACKUP, STRING}}, + {"RouteCount", {PERSISTENT, INT, "0"}}, + {"SnoozeUpdate", {CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION, BOOL}}, + {"SshEnabled", {PERSISTENT | BACKUP, BOOL}}, + {"TermsVersion", {PERSISTENT, STRING}}, + {"TrainingVersion", {PERSISTENT, STRING}}, + {"UbloxAvailable", {PERSISTENT, BOOL}}, + {"UpdateAvailable", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BOOL}}, + {"UpdateFailedCount", {CLEAR_ON_MANAGER_START, INT}}, + {"UpdaterAvailableBranches", {PERSISTENT, STRING}}, + {"UpdaterCurrentDescription", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterCurrentReleaseNotes", {CLEAR_ON_MANAGER_START, BYTES}}, + {"UpdaterFetchAvailable", {CLEAR_ON_MANAGER_START, BOOL}}, + {"UpdaterNewDescription", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterNewReleaseNotes", {CLEAR_ON_MANAGER_START, BYTES}}, + {"UpdaterState", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterTargetBranch", {CLEAR_ON_MANAGER_START, STRING}}, + {"UpdaterLastFetchTime", {PERSISTENT, TIME}}, + {"UptimeOffroad", {PERSISTENT, FLOAT, "0.0"}}, + {"UptimeOnroad", {PERSISTENT, FLOAT, "0.0"}}, + {"Version", {PERSISTENT, STRING}}, // --- sunnypilot params --- // - {"ApiCache_DriveStats", PERSISTENT}, - {"AutoLaneChangeBsmDelay", PERSISTENT}, - {"AutoLaneChangeTimer", PERSISTENT}, - {"BlinkerMinLateralControlSpeed", PERSISTENT | BACKUP}, - {"BlinkerPauseLateralControl", PERSISTENT | BACKUP}, - {"CarParamsSP", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"CarParamsSPCache", CLEAR_ON_MANAGER_START}, - {"CarParamsSPPersistent", PERSISTENT}, - {"CarPlatformBundle", PERSISTENT}, - {"CustomAccIncrementsEnabled", PERSISTENT | BACKUP}, - {"CustomAccLongPressIncrement", PERSISTENT | BACKUP}, - {"CustomAccShortPressIncrement", PERSISTENT | BACKUP}, - {"DeviceBootMode", PERSISTENT | BACKUP}, - {"EnableGithubRunner", PERSISTENT | BACKUP}, - {"MaxTimeOffroad", PERSISTENT | BACKUP}, - {"Brightness", PERSISTENT | BACKUP}, - {"ModelRunnerTypeCache", CLEAR_ON_ONROAD_TRANSITION}, - {"OffroadMode", CLEAR_ON_MANAGER_START}, - {"QuietMode", PERSISTENT | BACKUP}, + {"ApiCache_DriveStats", {PERSISTENT, JSON}}, + {"AutoLaneChangeBsmDelay", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"AutoLaneChangeTimer", {PERSISTENT | BACKUP, INT, "0"}}, + {"BlinkerMinLateralControlSpeed", {PERSISTENT | BACKUP, INT, "20"}}, // MPH or km/h + {"BlinkerPauseLateralControl", {PERSISTENT | BACKUP, INT, "0"}}, + {"Brightness", {PERSISTENT | BACKUP, INT, "0"}}, + {"CarParamsSP", {CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION, BYTES}}, + {"CarParamsSPCache", {CLEAR_ON_MANAGER_START, BYTES}}, + {"CarParamsSPPersistent", {PERSISTENT, BYTES}}, + {"CarPlatformBundle", {PERSISTENT | BACKUP, JSON}}, + {"ChevronInfo", {PERSISTENT | BACKUP, INT, "4"}}, + {"CustomAccIncrementsEnabled", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"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"}}, + {"IsDevelopmentBranch", {CLEAR_ON_MANAGER_START, BOOL}}, + {"MaxTimeOffroad", {PERSISTENT | BACKUP, INT, "1800"}}, + {"ModelRunnerTypeCache", {CLEAR_ON_ONROAD_TRANSITION, INT}}, + {"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 - {"Mads", PERSISTENT | BACKUP}, - {"MadsMainCruiseAllowed", PERSISTENT | BACKUP}, - {"MadsSteeringMode", PERSISTENT | BACKUP}, - {"MadsUnifiedEngagementMode", PERSISTENT | BACKUP}, + {"Mads", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"MadsMainCruiseAllowed", {PERSISTENT | BACKUP, BOOL, "1"}}, + {"MadsSteeringMode", {PERSISTENT | BACKUP, INT, "0"}}, + {"MadsUnifiedEngagementMode", {PERSISTENT | BACKUP, BOOL, "1"}}, // Model Manager params - {"ModelManager_ActiveBundle", PERSISTENT}, - {"ModelManager_DownloadIndex", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION}, - {"ModelManager_LastSyncTime", CLEAR_ON_MANAGER_START | CLEAR_ON_OFFROAD_TRANSITION}, - {"ModelManager_ModelsCache", PERSISTENT | BACKUP}, + {"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}}, // Neural Network Lateral Control - {"NeuralNetworkLateralControl", PERSISTENT | BACKUP}, + {"NeuralNetworkLateralControl", {PERSISTENT | BACKUP, BOOL, "0"}}, // sunnylink params - {"EnableSunnylinkUploader", PERSISTENT | BACKUP}, - {"LastSunnylinkPingTime", CLEAR_ON_MANAGER_START}, - {"SunnylinkCache_Roles", PERSISTENT}, - {"SunnylinkCache_Users", PERSISTENT}, - {"SunnylinkDongleId", PERSISTENT}, - {"SunnylinkdPid", PERSISTENT}, - {"SunnylinkEnabled", PERSISTENT}, + {"EnableSunnylinkUploader", {PERSISTENT | BACKUP, BOOL}}, + {"LastSunnylinkPingTime", {CLEAR_ON_MANAGER_START, INT}}, + {"SunnylinkCache_Roles", {PERSISTENT, STRING}}, + {"SunnylinkCache_Users", {PERSISTENT, STRING}}, + {"SunnylinkDongleId", {PERSISTENT, STRING}}, + {"SunnylinkdPid", {PERSISTENT, INT}}, + {"SunnylinkEnabled", {PERSISTENT, BOOL}}, // Backup Manager params - {"BackupManager_CreateBackup", PERSISTENT}, - {"BackupManager_RestoreVersion", PERSISTENT}, + {"BackupManager_CreateBackup", {PERSISTENT, BOOL}}, + {"BackupManager_RestoreVersion", {PERSISTENT, STRING}}, // sunnypilot car specific params - {"HyundaiLongitudinalTuning", PERSISTENT}, + {"HyundaiLongitudinalTuning", {PERSISTENT | BACKUP, INT, "0"}}, // Hyundai Longitudinal Tuning Live Parameters {"LongTuningCustomToggle", {PERSISTENT | BACKUP, INT, "0"}}, {"LongTuningAccelMax", {PERSISTENT | BACKUP, FLOAT, "2.0"}}, - {"LongTuningVEgoStopping", {PERSISTENT | BACKUP, FLOAT, ".25"}}, - {"LongTuningStoppingDecelRate", {PERSISTENT | BACKUP, FLOAT, ".40"}}, {"LongTuningJerkLimits", {PERSISTENT | BACKUP, FLOAT, "4.0"}}, {"LongTuningMinUpperJerk", {PERSISTENT | BACKUP, FLOAT, ".5"}}, {"LongTuningMinLowerJerk", {PERSISTENT | BACKUP, FLOAT, ".5"}}, - {"DynamicExperimentalControl", PERSISTENT}, - {"BlindSpot", PERSISTENT | BACKUP}, + {"DynamicExperimentalControl", {PERSISTENT | BACKUP, BOOL, "0"}}, + {"BlindSpot", {PERSISTENT | BACKUP, BOOL, "0"}}, - // model panel params - {"LagdToggle", PERSISTENT | BACKUP}, + // 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}, - {"MapdVersion", PERSISTENT}, - {"MapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, - {"NextMapSpeedLimit", CLEAR_ON_ONROAD_TRANSITION}, - {"Offroad_OSMUpdateRequired", CLEAR_ON_MANAGER_START}, - {"OsmDbUpdatesCheck", CLEAR_ON_MANAGER_START}, // mapd database update happens with device ON, reset on boot - {"OSMDownloadBounds", PERSISTENT}, - {"OsmDownloadedDate", PERSISTENT}, - {"OSMDownloadLocations", PERSISTENT}, - {"OSMDownloadProgress", CLEAR_ON_MANAGER_START}, - {"OsmLocal", PERSISTENT}, - {"OsmLocationName", PERSISTENT}, - {"OsmLocationTitle", PERSISTENT}, - {"OsmLocationUrl", PERSISTENT}, - {"OsmStateName", PERSISTENT}, - {"OsmStateTitle", PERSISTENT}, - {"OsmWayTest", PERSISTENT}, - {"RoadName", CLEAR_ON_ONROAD_TRANSITION}, + {"MapAdvisorySpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT}}, + {"MapdVersion", {PERSISTENT, STRING}}, + {"MapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, FLOAT, "0.0"}}, + {"NextMapSpeedLimit", {CLEAR_ON_ONROAD_TRANSITION, JSON}}, + {"Offroad_OSMUpdateRequired", {CLEAR_ON_MANAGER_START, JSON}}, + {"OsmDbUpdatesCheck", {CLEAR_ON_MANAGER_START, BOOL}}, // mapd database update happens with device ON, reset on boot + {"OSMDownloadBounds", {PERSISTENT, STRING}}, + {"OsmDownloadedDate", {PERSISTENT, STRING, "0.0"}}, + {"OSMDownloadLocations", {PERSISTENT, JSON}}, + {"OSMDownloadProgress", {CLEAR_ON_MANAGER_START, JSON}}, + {"OsmLocal", {PERSISTENT, BOOL}}, + {"OsmLocationName", {PERSISTENT, STRING}}, + {"OsmLocationTitle", {PERSISTENT, STRING}}, + {"OsmLocationUrl", {PERSISTENT, STRING}}, + {"OsmStateName", {PERSISTENT, STRING, "All"}}, + {"OsmStateTitle", {PERSISTENT, STRING}}, + {"OsmWayTest", {PERSISTENT, STRING}}, + {"RoadName", {CLEAR_ON_ONROAD_TRANSITION, STRING}}, }; diff --git a/common/params_pyx.pyx b/common/params_pyx.pyx index e851020d83..bffa89b5d3 100644 --- a/common/params_pyx.pyx +++ b/common/params_pyx.pyx @@ -1,19 +1,35 @@ # distutils: language = c++ # cython: language_level = 3 +import builtins +import datetime +import json from libcpp cimport bool from libcpp.string cimport string from libcpp.vector cimport vector +from libcpp.optional cimport optional + +from openpilot.common.swaglog import cloudlog cdef extern from "common/params.h": - cpdef enum ParamKeyType: + cpdef enum ParamKeyFlag: PERSISTENT CLEAR_ON_MANAGER_START CLEAR_ON_ONROAD_TRANSITION CLEAR_ON_OFFROAD_TRANSITION DEVELOPMENT_ONLY + CLEAR_ON_IGNITION_ON BACKUP ALL + cpdef enum ParamKeyType: + STRING + BOOL + INT + FLOAT + TIME + JSON + BYTES + cdef cppclass c_Params "Params": c_Params(string) except + nogil string get(string, bool) nogil @@ -24,10 +40,31 @@ cdef extern from "common/params.h": void putBoolNonBlocking(string, bool) nogil int putBool(string, bool) nogil bool checkKey(string) nogil + ParamKeyType getKeyType(string) nogil + optional[string] getKeyDefaultValue(string) nogil string getParamPath(string) nogil - void clearAll(ParamKeyType) - vector[string] allKeys(ParamKeyType) + void clearAll(ParamKeyFlag) + vector[string] allKeys(ParamKeyFlag) +PYTHON_2_CPP = { + (str, STRING): lambda v: v, + (builtins.bool, BOOL): lambda v: "1" if v else "0", + (int, INT): str, + (float, FLOAT): str, + (datetime.datetime, TIME): lambda v: v.isoformat(), + (dict, JSON): json.dumps, + (list, JSON): json.dumps, + (bytes, BYTES): lambda v: v, +} +CPP_2_PYTHON = { + STRING: lambda v: v.decode("utf-8"), + BOOL: lambda v: v == b"1", + INT: int, + FLOAT: float, + TIME: lambda v: datetime.datetime.fromisoformat(v.decode("utf-8")), + JSON: json.loads, + BYTES: lambda v: v, +} def ensure_bytes(v): return v.encode() if isinstance(v, str) else v @@ -51,8 +88,8 @@ cdef class Params: def __dealloc__(self): del self.p - def clear_all(self, tx_type=ParamKeyType.ALL): - self.p.clearAll(tx_type) + def clear_all(self, tx_flag=ParamKeyFlag.ALL): + self.p.clearAll(tx_flag) def check_key(self, key): key = ensure_bytes(key) @@ -60,21 +97,38 @@ cdef class Params: raise UnknownKeyName(key) return key - def get(self, key, bool block=False, encoding=None): + def python2cpp(self, proposed_type, expected_type, value, key): + cast = PYTHON_2_CPP.get((proposed_type, expected_type)) + if cast: + return cast(value) + raise TypeError(f"Type mismatch while writing param {key}: {proposed_type=} {expected_type=} {value=}") + + def _cpp2python(self, t, value, default, key): + if value is None: + return None + try: + return CPP_2_PYTHON[t](value) + except (KeyError, TypeError, ValueError): + cloudlog.warning(f"Failed to cast param {key} with {value=} from type {t=}") + return self._cpp2python(t, default, None, key) + + def get(self, key, bool block=False, bool return_default=False): cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + cdef optional[string] default = self.p.getKeyDefaultValue(k) cdef string val with nogil: val = self.p.get(k, block) + default_val = (default.value() if default.has_value() else None) if return_default else None if val == b"": if block: # If we got no value while running in blocked mode # it means we got an interrupt while waiting raise KeyboardInterrupt else: - return None - - return val if encoding is None else val.decode(encoding) + return self._cpp2python(t, default_val, None, key) + return self._cpp2python(t, val, default_val, key) def get_bool(self, key, bool block=False): cdef string k = self.check_key(key) @@ -83,6 +137,11 @@ cdef class Params: r = self.p.getBool(k, block) return r + def _put_cast(self, key, dat): + cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + return ensure_bytes(self.python2cpp(type(dat), t, dat, key)) + def put(self, key, dat): """ Warning: This function blocks until the param is written to disk! @@ -91,7 +150,7 @@ cdef class Params: in general try to avoid writing params as much as possible. """ cdef string k = self.check_key(key) - cdef string dat_bytes = ensure_bytes(dat) + cdef string dat_bytes = self._put_cast(key, dat) with nogil: self.p.put(k, dat_bytes) @@ -102,7 +161,7 @@ cdef class Params: def put_nonblocking(self, key, dat): cdef string k = self.check_key(key) - cdef string dat_bytes = ensure_bytes(dat) + cdef string dat_bytes = self._put_cast(key, dat) with nogil: self.p.putNonBlocking(k, dat_bytes) @@ -120,5 +179,19 @@ cdef class Params: cdef string key_bytes = ensure_bytes(key) return self.p.getParamPath(key_bytes).decode("utf-8") - def all_keys(self, type=ParamKeyType.ALL): - return self.p.allKeys(type) + def get_type(self, key): + return self.p.getKeyType(self.check_key(key)) + + def all_keys(self, flag=ParamKeyFlag.ALL): + return self.p.allKeys(flag) + + def get_default_value(self, key): + cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + cdef optional[string] default = self.p.getKeyDefaultValue(k) + return self._cpp2python(t, default.value(), None, key) if default.has_value() else None + + def cpp2python(self, key, value): + cdef string k = self.check_key(key) + cdef ParamKeyType t = self.p.getKeyType(k) + return self._cpp2python(t, value, None, key) diff --git a/common/pid.py b/common/pid.py index f2ab935f45..99142280ca 100644 --- a/common/pid.py +++ b/common/pid.py @@ -14,10 +14,8 @@ class PIDController: if isinstance(self._k_d, Number): self._k_d = [[0], [self._k_d]] - self.pos_limit = pos_limit - self.neg_limit = neg_limit + self.set_limits(pos_limit, neg_limit) - self.i_unwind_rate = 0.3 / rate self.i_rate = 1.0 / rate self.speed = 0.0 @@ -35,10 +33,6 @@ class PIDController: def k_d(self): return np.interp(self.speed, self._k_d[0], self._k_d[1]) - @property - def error_integral(self): - return self.i/self.k_i - def reset(self): self.p = 0.0 self.i = 0.0 @@ -46,25 +40,25 @@ class PIDController: self.f = 0.0 self.control = 0 - def update(self, error, error_rate=0.0, speed=0.0, override=False, feedforward=0., freeze_integrator=False): - self.speed = speed + def set_limits(self, pos_limit, neg_limit): + self.pos_limit = pos_limit + self.neg_limit = neg_limit + def update(self, error, error_rate=0.0, speed=0.0, feedforward=0., freeze_integrator=False): + self.speed = speed self.p = float(error) * self.k_p self.f = feedforward * self.k_f self.d = error_rate * self.k_d - if override: - self.i -= self.i_unwind_rate * float(np.sign(self.i)) - else: - if not freeze_integrator: - self.i = self.i + error * self.k_i * self.i_rate + if not freeze_integrator: + i = self.i + error * self.k_i * self.i_rate - # Clip i to prevent exceeding control limits - control_no_i = self.p + self.d + self.f - control_no_i = np.clip(control_no_i, self.neg_limit, self.pos_limit) - self.i = np.clip(self.i, self.neg_limit - control_no_i, self.pos_limit - control_no_i) + # Don't allow windup if already clipping + test_control = self.p + i + self.d + self.f + i_upperbound = self.i if test_control > self.pos_limit else self.pos_limit + i_lowerbound = self.i if test_control < self.neg_limit else self.neg_limit + self.i = np.clip(i, i_lowerbound, i_upperbound) control = self.p + self.i + self.d + self.f - self.control = np.clip(control, self.neg_limit, self.pos_limit) return self.control diff --git a/common/prefix.py b/common/prefix.py index 762ae70fb4..207f8477d7 100644 --- a/common/prefix.py +++ b/common/prefix.py @@ -9,20 +9,19 @@ from openpilot.system.hardware.hw import Paths from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT class OpenpilotPrefix: - def __init__(self, prefix: str = None, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): + def __init__(self, prefix: str = None, create_dirs_on_enter: bool = True, clean_dirs_on_exit: bool = True, shared_download_cache: bool = False): self.prefix = prefix if prefix else str(uuid.uuid4().hex[0:15]) self.msgq_path = os.path.join(Paths.shm_path(), self.prefix) + self.create_dirs_on_enter = create_dirs_on_enter self.clean_dirs_on_exit = clean_dirs_on_exit self.shared_download_cache = shared_download_cache def __enter__(self): self.original_prefix = os.environ.get('OPENPILOT_PREFIX', None) os.environ['OPENPILOT_PREFIX'] = self.prefix - try: - os.mkdir(self.msgq_path) - except FileExistsError: - pass - os.makedirs(Paths.log_root(), exist_ok=True) + + if self.create_dirs_on_enter: + self.create_dirs() if self.shared_download_cache: os.environ["COMMA_CACHE"] = DEFAULT_DOWNLOAD_CACHE_ROOT @@ -40,6 +39,13 @@ class OpenpilotPrefix: pass return False + def create_dirs(self): + try: + os.mkdir(self.msgq_path) + except FileExistsError: + pass + os.makedirs(Paths.log_root(), exist_ok=True) + def clean_dirs(self): symlink_path = Params().get_param_path() if os.path.exists(symlink_path): diff --git a/common/run.py b/common/run.py index 06deb6388d..75395ead1f 100644 --- a/common/run.py +++ b/common/run.py @@ -1,4 +1,6 @@ import subprocess +from contextlib import contextmanager +from subprocess import Popen, PIPE, TimeoutExpired def run_cmd(cmd: list[str], cwd=None, env=None) -> str: @@ -11,3 +13,16 @@ def run_cmd_default(cmd: list[str], default: str = "", cwd=None, env=None) -> st except subprocess.CalledProcessError: return default + +@contextmanager +def managed_proc(cmd: list[str], env: dict[str, str]): + proc = Popen(cmd, env=env, stdout=PIPE, stderr=PIPE) + try: + yield proc + finally: + if proc.poll() is None: + proc.terminate() + try: + proc.wait(timeout=5) + except TimeoutExpired: + proc.kill() diff --git a/common/tests/test_params.py b/common/tests/test_params.py index 16cbc45295..592bf2c4b2 100644 --- a/common/tests/test_params.py +++ b/common/tests/test_params.py @@ -1,10 +1,11 @@ import pytest +import datetime import os import threading import time import uuid -from openpilot.common.params import Params, ParamKeyType, UnknownKeyName +from openpilot.common.params import Params, ParamKeyFlag, UnknownKeyName class TestParams: def setup_method(self): @@ -12,7 +13,7 @@ class TestParams: def test_params_put_and_get(self): self.params.put("DongleId", "cb38263377b873ee") - assert self.params.get("DongleId") == b"cb38263377b873ee" + assert self.params.get("DongleId") == "cb38263377b873ee" def test_params_non_ascii(self): st = b"\xe1\x90\xff" @@ -20,7 +21,7 @@ class TestParams: assert self.params.get("CarParams") == st def test_params_get_cleared_manager_start(self): - self.params.put("CarParams", "test") + self.params.put("CarParams", b"test") self.params.put("DongleId", "cb38263377b873ee") assert self.params.get("CarParams") == b"test" @@ -29,24 +30,24 @@ class TestParams: f.write("test") assert os.path.isfile(undefined_param) - self.params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) + self.params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START) assert self.params.get("CarParams") is None assert self.params.get("DongleId") is not None assert not os.path.isfile(undefined_param) def test_params_two_things(self): self.params.put("DongleId", "bob") - self.params.put("AthenadPid", "123") - assert self.params.get("DongleId") == b"bob" - assert self.params.get("AthenadPid") == b"123" + self.params.put("AthenadPid", 123) + assert self.params.get("DongleId") == "bob" + assert self.params.get("AthenadPid") == 123 def test_params_get_block(self): def _delayed_writer(): time.sleep(0.1) - self.params.put("CarParams", "test") + self.params.put("CarParams", b"test") threading.Thread(target=_delayed_writer).start() assert self.params.get("CarParams") is None - assert self.params.get("CarParams", True) == b"test" + assert self.params.get("CarParams", block=True) == b"test" def test_params_unknown_key_fails(self): with pytest.raises(UnknownKeyName): @@ -76,17 +77,17 @@ class TestParams: self.params.put_bool("IsMetric", False) assert not self.params.get_bool("IsMetric") - self.params.put("IsMetric", "1") + self.params.put("IsMetric", True) assert self.params.get_bool("IsMetric") - self.params.put("IsMetric", "0") + self.params.put("IsMetric", False) assert not self.params.get_bool("IsMetric") def test_put_non_blocking_with_get_block(self): q = Params() def _delayed_writer(): time.sleep(0.1) - Params().put_nonblocking("CarParams", "test") + Params().put_nonblocking("CarParams", b"test") threading.Thread(target=_delayed_writer).start() assert q.get("CarParams") is None assert q.get("CarParams", True) == b"test" @@ -107,3 +108,34 @@ class TestParams: assert len(keys) > 20 assert len(keys) == len(set(keys)) assert b"CarParams" in keys + + def test_params_default_value(self): + self.params.remove("LanguageSetting") + self.params.remove("LongitudinalPersonality") + self.params.remove("LiveParameters") + + assert self.params.get("LanguageSetting") is None + assert self.params.get("LanguageSetting", return_default=False) is None + assert isinstance(self.params.get("LanguageSetting", return_default=True), str) + assert isinstance(self.params.get("LongitudinalPersonality", return_default=True), int) + assert self.params.get("LiveParameters") is None + assert self.params.get("LiveParameters", return_default=True) is None + + def test_params_get_type(self): + # json + self.params.put("ApiCache_FirehoseStats", {"a": 0}) + assert self.params.get("ApiCache_FirehoseStats") == {"a": 0} + + # int + self.params.put("BootCount", 1441) + assert self.params.get("BootCount") == 1441 + + # bool + self.params.put("AdbEnabled", True) + assert self.params.get("AdbEnabled") + assert isinstance(self.params.get("AdbEnabled"), bool) + + # time + now = datetime.datetime.now(datetime.UTC) + self.params.put("InstallDate", now) + assert self.params.get("InstallDate") == now diff --git a/common/util.cc b/common/util.cc index a853faed04..26a2bd60bc 100644 --- a/common/util.cc +++ b/common/util.cc @@ -1,4 +1,5 @@ #include "common/util.h" +#include "common/swaglog.h" #include #include @@ -12,6 +13,7 @@ #include #include #include +#include #ifdef __linux__ #include @@ -78,8 +80,9 @@ std::string read_file(const std::string& fn) { std::ifstream f(fn, std::ios::binary | std::ios::in); if (f.is_open()) { f.seekg(0, std::ios::end); - int size = f.tellg(); - if (f.good() && size > 0) { + std::streamsize size = f.tellg(); + // seekg and tellg on a directory doesn't return pos_type(-1) but max(streamsize) + if (f.good() && size > 0 && size < std::numeric_limits::max()) { std::string result(size, '\0'); f.seekg(0, std::ios::beg); f.read(result.data(), size); @@ -149,11 +152,16 @@ int safe_fflush(FILE *stream) { return ret; } -int safe_ioctl(int fd, unsigned long request, void *argp) { +int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_msg) { int ret; do { ret = ioctl(fd, request, argp); } while ((ret == -1) && (errno == EINTR)); + + if (ret == -1 && exception_msg) { + LOGE("safe_ioctl error: %s %s(%d) (fd: %d request: %lx argp: %p)", exception_msg, strerror(errno), errno, fd, request, argp); + throw std::runtime_error(exception_msg); + } return ret; } diff --git a/common/util.h b/common/util.h index 4fb4b13fae..f46db4d9fa 100644 --- a/common/util.h +++ b/common/util.h @@ -88,7 +88,7 @@ int write_file(const char* path, const void* data, size_t size, int flags = O_WR FILE* safe_fopen(const char* filename, const char* mode); size_t safe_fwrite(const void * ptr, size_t size, size_t count, FILE * stream); int safe_fflush(FILE *stream); -int safe_ioctl(int fd, unsigned long request, void *argp); +int safe_ioctl(int fd, unsigned long request, void *argp, const char* exception_msg = nullptr); std::string readlink(const std::string& path); bool file_exists(const std::string& fn); diff --git a/common/version.h b/common/version.h index 67b69da7c7..669f303211 100644 --- a/common/version.h +++ b/common/version.h @@ -1 +1 @@ -#define COMMA_VERSION "0.9.10" +#define COMMA_VERSION "0.10.1" diff --git a/conftest.py b/conftest.py index b62a88c188..0bd638b87d 100644 --- a/conftest.py +++ b/conftest.py @@ -2,7 +2,6 @@ import contextlib import gc import os import pytest -import random from openpilot.common.prefix import OpenpilotPrefix from openpilot.system.manager import manager @@ -49,8 +48,6 @@ def clean_env(): @pytest.fixture(scope="function", autouse=True) def openpilot_function_fixture(request): - random.seed(0) - with clean_env(): # setup a clean environment for each test with OpenpilotPrefix(shared_download_cache=request.node.get_closest_marker("shared_download_cache") is not None) as prefix: diff --git a/docs/CARS.md b/docs/CARS.md index c53a30caed..dbc1dbcd74 100644 --- a/docs/CARS.md +++ b/docs/CARS.md @@ -4,322 +4,344 @@ 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. -# 312 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|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
||| -|Audi|A3 Sportback e-tron 2017-18|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
||| -|Audi|Q2 2018|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
||| -|Audi|Q3 2019-24|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
||| -|Audi|RS3 2018|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
||| -|Audi|S3 2015-17|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
||| -|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 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
||| -|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 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
||| -|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 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
||| -|Chevrolet|Silverado 1500 2020-21|Safety 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
||| -|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 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
||| -|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Chrysler|Pacifica Hybrid 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| -|CUPRA|Ateca 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
||| -|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| -|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=MewJc9LYp9M| -|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=MewJc9LYp9M| -|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://www.youtube.com/watch?v=MewJc9LYp9M| -|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| -|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| -|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| -|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| -|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||https://www.youtube.com/watch?v=uUGkH6C_EQU| -|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G80 2018-19|All|Stock|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
||| -|Genesis|G80 (2.5T Advanced Trim, with HDA II) 2024[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|G90 2017-20|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
||| -|Genesis|GV60 (Advanced Trim) 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 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
||| -|Genesis|GV60 (Performance Trim) 2022-23[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 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
||| -|Genesis|GV70 (2.5T Trim, without HDA II) 2022-24[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 L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 (3.5T Trim, without HDA II) 2022-23[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
||| -|Genesis|GV70 Electrified (Australia Only) 2022[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 Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Genesis|GV70 Electrified (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|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 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|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 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
||| -|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|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 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|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
||| -|Honda|HR-V 2019-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|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|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|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
||| -|Hyundai|Azera Hybrid 2020|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|Custin 2023|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|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 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|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.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|Elantra 2021-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 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|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E 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|Elantra Hybrid 2021-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 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|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J 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|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E 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 5 (Southeast Asia and Europe only) 2022-24[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 Q 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 5 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q 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 5 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|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|Ioniq 6 (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P 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 Electric 2019|Smart Cruise Control (SCC)|Stock|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 Electric 2020|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|Ioniq Hybrid 2017-19|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 Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|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|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 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
||| -|Hyundai|Kona Hybrid 2020|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 I 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|Nexo 2021|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|Palisade 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|Santa Cruz 2022-24[6](#footnotes)|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 N 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|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D 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|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L 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|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L 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|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L 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|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E 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|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai 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
||| -|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai 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
||| -|Hyundai|Staria 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 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|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai L 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|Tucson 2022[6](#footnotes)|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 N 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|Tucson 2023-24[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 N 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|Tucson Diesel 2019|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 L 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|Tucson Hybrid 2022-24[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 N 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|Tucson Plug-in Hybrid 2024[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 N 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|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Carnival 2022-24[6](#footnotes)|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 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
||| -|Kia|Carnival (China only) 2023[6](#footnotes)|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 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
||| -|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (Southeast Asia only) 2022-24[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 P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|EV6 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Forte 2019-21|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 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
||| -|Kia|Forte 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 E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|K5 2021-24|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 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
||| -|Kia|K5 Hybrid 2020-22|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 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
||| -|Kia|K8 Hybrid (with HDA II) 2023[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 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 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
||| -|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro EV 2021|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
||| -|Kia|Niro EV 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 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
||| -|Kia|Niro EV (with HDA II) 2025[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|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
||| -|Kia|Niro EV (without HDA II) 2023-25[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 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
||| -|Kia|Niro Hybrid 2018|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.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
||| -|Kia|Niro Hybrid 2021|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 D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 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 F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Hybrid 2023[6](#footnotes)|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 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
||| -|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.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
||| -|Kia|Niro Plug-in Hybrid 2020|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 2021|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 D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Niro Plug-in Hybrid 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 F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.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
||| -|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|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
||| -|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|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
||| -|Kia|Seltos 2021|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 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
||| -|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sorento 2021-23[6](#footnotes)|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 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
||| -|Kia|Sorento Hybrid 2021-23[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 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
||| -|Kia|Sorento Plug-in Hybrid 2022-23[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 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
||| -|Kia|Sportage 2023-24[6](#footnotes)|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 N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Sportage Hybrid 2023[6](#footnotes)|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 N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Kia|Stinger 2018-20|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 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
||| -|Kia|Stinger 2022-23|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
||| -|Kia|Telluride 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
||| -|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 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
||| -|Lexus|ES 2017-18|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
||| -|Lexus|ES 2019-25|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
||| -|Lexus|ES Hybrid 2017-18|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
||| -|Lexus|ES Hybrid 2019-25|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
||| -|Lexus|GS F 2016|All|Stock|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
||| -|Lexus|IS 2017-19|All|Stock|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
||| -|Lexus|IS 2022-24|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
||| -|Lexus|LC 2024|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
||| -|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 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
||| -|Lexus|NX 2020-21|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
||| -|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 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
||| -|Lexus|NX Hybrid 2020-21|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
||| -|Lexus|RC 2018-20|All|Stock|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
||| -|Lexus|RC 2023|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
||| -|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 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
||| -|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 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
||| -|Lexus|RX 2020-22|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
||| -|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 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
||| -|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 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
||| -|Lexus|RX Hybrid 2020-22|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
||| -|Lexus|UX Hybrid 2019-24|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
||| -|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Nissan[7](#footnotes)|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| -|Nissan[7](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| -|Nissan[7](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| -|Nissan[7](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| -|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://youtu.be/uaISd1j7Z4U| -|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||https://youtu.be/uaISd1j7Z4U| -|SEAT|Ateca 2016-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
||| -|SEAT|Leon 2014-20|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
||| -|Subaru|Ascent 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Forester 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2017-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Impreza 2020-22|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Legacy 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|Outback 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Subaru|XV 2020-21|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| -|Å koda|Fabia 2022-23[15](#footnotes)|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
[17](#footnotes)||| -|Å koda|Kamiq 2021-23[13,15](#footnotes)|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
[17](#footnotes)||| -|Å koda|Karoq 2019-23[15](#footnotes)|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
||| -|Å koda|Kodiaq 2017-23[15](#footnotes)|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
||| -|Å koda|Octavia 2015-19[15](#footnotes)|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
||| -|Å koda|Octavia RS 2016[15](#footnotes)|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
||| -|Å koda|Octavia Scout 2017-19[15](#footnotes)|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
||| -|Å koda|Scala 2020-23[15](#footnotes)|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
[17](#footnotes)||| -|Å koda|Superb 2015-22[15](#footnotes)|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
||| -|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
||| -|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
||| -|Toyota|Avalon 2017-18|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|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 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|Avalon 2022|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 Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 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|Avalon Hybrid 2022|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|C-HR 2017-20|All|Stock|0 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|C-HR 2021|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|C-HR Hybrid 2017-20|All|Stock|0 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|C-HR Hybrid 2021-22|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|Camry 2018-20|All|Stock|0 mph[12](#footnotes)|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|Camry 2021-24|All|openpilot|0 mph[12](#footnotes)|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|Camry Hybrid 2018-20|All|Stock|0 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|Camry Hybrid 2021-24|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|Corolla 2017-19|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|Corolla 2020-22|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|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 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|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 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|Corolla Hatchback 2019-22|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|Corolla Hybrid 2020-22|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|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 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|Highlander 2017-19|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|Highlander 2020-23|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|Highlander Hybrid 2017-19|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|Highlander Hybrid 2020-23|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|Mirai 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|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 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|Prius 2017-20|All|openpilot available[2](#footnotes)|0 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|Prius 2021-22|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|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 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|Prius Prime 2021-22|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|Prius v 2017|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
||| -|Toyota|RAV4 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
||| -|Toyota|RAV4 2017-18|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|RAV4 2019-21|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|RAV4 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 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|RAV4 Hybrid 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
||| -|Toyota|RAV4 Hybrid 2017-18|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|RAV4 Hybrid 2019-21|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|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
||| -|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
||| -|Volkswagen|Arteon Shooting Brake 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|Atlas 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|Atlas Cross Sport 2020-22|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|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|CC 2018-22|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|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|e-Golf 2014-20|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|Golf 2015-20|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-empty.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|Golf Alltrack 2015-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-empty.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|Golf GTD 2015-20|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|Golf GTE 2015-20|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|Golf GTI 2015-21|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-empty.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|Golf R 2015-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
||| -|Volkswagen|Golf SportsVan 2015-20|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|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| -|Volkswagen|Jetta 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|Jetta GLI 2021-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|Passat 2015-22[14](#footnotes)|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|Passat Alltrack 2015-22|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|Passat GTE 2015-22|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|Polo 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
[17](#footnotes)||| -|Volkswagen|Polo GTI 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
[17](#footnotes)||| -|Volkswagen|T-Cross 2021|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
[17](#footnotes)||| -|Volkswagen|T-Roc 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|Taos 2022-24|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|Teramont 2018-22|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|Teramont Cross Sport 2021-22|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|Teramont X 2021-22|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|Tiguan 2018-24|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|Tiguan eHybrid 2021-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|Touran 2016-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
||| +|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
||| +|Audi|A3 Sportback e-tron 2017-18|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
||| +|Audi|Q2 2018|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
||| +|Audi|Q3 2019-24|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
||| +|Audi|RS3 2018|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
||| +|Audi|S3 2015-17|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
||| +|Chevrolet|Bolt EUV 2022-23|Premier or Premier Redline Trim without Super Cruise Package|openpilot available[1](#footnotes)|3 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
||| +|Chevrolet|Bolt EV 2022-23|2LT Trim with Adaptive Cruise Control Package|openpilot available[1](#footnotes)|3 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
||| +|Chevrolet|Equinox 2019-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 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
||| +|Chevrolet|Silverado 1500 2020-21|Safety 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
||| +|Chevrolet|Trailblazer 2021-22|Adaptive Cruise Control (ACC)|openpilot available[1](#footnotes)|3 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
||| +|Chrysler|Pacifica 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica 2019-20|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica 2021-23|All|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica Hybrid 2017-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Chrysler|Pacifica Hybrid 2019-25|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|comma|body|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|None||| +|CUPRA|Ateca 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
||| +|Dodge|Durango 2020-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Bronco Sport 2021-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| +|Ford|Escape 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Ford|Escape Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Ford|Escape Plug-in Hybrid 2020-22|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Escape Plug-in Hybrid 2023-24|Co-Pilot360 Assist+|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Ford|Expedition 2022-24|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Ford|Explorer 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Explorer Hybrid 2020-24|Co-Pilot360 Assist+|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|F-150 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|F-150 Hybrid 2021-23|Co-Pilot360 Assist 2.0|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Focus 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Focus Hybrid 2018[3](#footnotes)|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Ford|Kuga Plug-in Hybrid 2020-23|Adaptive Cruise Control with Lane Centering|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ford|Kuga Plug-in Hybrid 2024|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Ford|Maverick 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| +|Ford|Maverick 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| +|Ford|Maverick Hybrid 2022|LARIAT Luxury|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| +|Ford|Maverick Hybrid 2023-24|Co-Pilot360 Assist|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 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
||| +|Ford|Mustang Mach-E 2021-24|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Ford|Ranger 2024|Adaptive Cruise Control with Lane Centering|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q4 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
||| +|Genesis|G70 2018|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G70 2019-21|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G70 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G80 2017|All|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G80 2018-19|All|Stock|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
||| +|Genesis|G80 (2.5T Advanced Trim, with HDA II) 2024[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|G90 2017-20|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
||| +|Genesis|GV60 (Advanced Trim) 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 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
||| +|Genesis|GV60 (Performance Trim) 2022-23[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 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
||| +|Genesis|GV70 (2.5T Trim, without HDA II) 2022-24[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 L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV70 (3.5T Trim, without HDA II) 2022-23[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
||| +|Genesis|GV70 Electrified (Australia Only) 2022[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 Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Genesis|GV70 Electrified (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|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-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-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
||| +|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|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
||| +|Honda|HR-V 2019-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|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|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
||| +|Hyundai|Azera Hybrid 2020|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|Custin 2023|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|Elantra 2017-18|Smart Cruise Control (SCC)|Stock|19 mph|32 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|Elantra 2019|Smart Cruise Control (SCC)|Stock|19 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.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|Elantra 2021-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 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|Elantra GT 2017-20|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E 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|Elantra Hybrid 2021-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 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|Elantra Non-SCC 2022|No Smart Cruise Control (Non-SCC)|Stock|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|Genesis 2015-16|Smart Cruise Control (SCC)|Stock|19 mph|37 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai J 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|i30 2017-19|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E 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 5 (Southeast Asia and Europe only) 2022-24[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 Q 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 5 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q 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 5 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|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|Ioniq 6 (with HDA II) 2023-24[6](#footnotes)|Highway Driving Assist II|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P 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 Electric 2019|Smart Cruise Control (SCC)|Stock|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 Electric 2020|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|Ioniq Hybrid 2017-19|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 Hybrid 2020-22|Smart Cruise Control (SCC)|Stock|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|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-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
||| +|Hyundai|Kona Electric Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|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 Hybrid 2020|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 I 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 Non-SCC 2019|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.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|Nexo 2021|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|Palisade 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|Santa Cruz 2022-24[6](#footnotes)|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 N 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|Santa Fe 2019-20|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D 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|Santa Fe 2021-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L 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|Santa Fe Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L 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|Santa Fe Plug-in Hybrid 2022-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L 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|Sonata 2018-19|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E 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|Sonata 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai 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
||| +|Hyundai|Sonata Hybrid 2020-23|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai 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
||| +|Hyundai|Staria 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 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|Tucson 2021|Smart Cruise Control (SCC)|openpilot available[1](#footnotes)|19 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai L 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|Tucson 2022[6](#footnotes)|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 N 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|Tucson 2023-24[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 N 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|Tucson Diesel 2019|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 L 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|Tucson Hybrid 2022-24[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 N 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|Tucson Plug-in Hybrid 2024[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 N 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|Veloster 2019-20|Smart Cruise Control (SCC)|Stock|5 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Jeep|Grand Cherokee 2016-18|Adaptive Cruise Control (ACC)|Stock|0 mph|9 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Jeep|Grand Cherokee 2019-21|Adaptive Cruise Control (ACC)|Stock|0 mph|39 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 FCA connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Carnival 2022-24[6](#footnotes)|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 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
||| +|Kia|Carnival (China only) 2023[6](#footnotes)|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 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
||| +|Kia|Ceed 2019-21|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Ceed Plug-in Hybrid Non-SCC 2022|No Smart Cruise Control (Non-SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai I connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|EV6 (Southeast Asia only) 2022-24[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 P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|EV6 (with HDA II) 2022-24[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai P connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|EV6 (without HDA II) 2022-24[6](#footnotes)|Highway Driving Assist|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai L connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Forte 2019-21|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 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
||| +|Kia|Forte 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 E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|K5 2021-24|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 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
||| +|Kia|K5 Hybrid 2020-22|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 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
||| +|Kia|K8 Hybrid (with HDA II) 2023[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai Q connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV 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 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
||| +|Kia|Niro EV 2020|All|openpilot available[1](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro EV 2021|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
||| +|Kia|Niro EV 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 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
||| +|Kia|Niro EV (with HDA II) 2025[6](#footnotes)|Highway Driving Assist II|openpilot available[1](#footnotes)|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
||| +|Kia|Niro EV (without HDA II) 2023-25[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 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
||| +|Kia|Niro Hybrid 2018|Smart Cruise Control (SCC)|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.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
||| +|Kia|Niro Hybrid 2021|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 D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Hybrid 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 F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Hybrid 2023[6](#footnotes)|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 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
||| +|Kia|Niro Plug-in Hybrid 2018-19|All|Stock|10 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.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
||| +|Kia|Niro Plug-in Hybrid 2020|Smart Cruise Control (SCC)|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Plug-in Hybrid 2021|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 D connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Niro Plug-in Hybrid 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 F connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Optima 2017|Advanced Smart Cruise Control|Stock|0 mph|32 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.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
||| +|Kia|Optima 2019-20|Smart Cruise Control (SCC)|Stock|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
||| +|Kia|Optima Hybrid 2019|Smart Cruise Control (SCC)|Stock|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
||| +|Kia|Seltos 2021|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 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
||| +|Kia|Sorento 2018|Advanced Smart Cruise Control & LKAS|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sorento 2019|Smart Cruise Control (SCC)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Hyundai E connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sorento 2021-23[6](#footnotes)|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 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
||| +|Kia|Sorento Hybrid 2021-23[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 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
||| +|Kia|Sorento Plug-in Hybrid 2022-23[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 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
||| +|Kia|Sportage 2023-24[6](#footnotes)|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 N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Sportage Hybrid 2023[6](#footnotes)|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 N connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Kia|Stinger 2018-20|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 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
||| +|Kia|Stinger 2022-23|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
||| +|Kia|Telluride 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 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
||| +|Lexus|CT Hybrid 2017-18|Lexus Safety System+|openpilot available[2](#footnotes)|0 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
||| +|Lexus|ES 2017-18|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
||| +|Lexus|ES 2019-25|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
||| +|Lexus|ES Hybrid 2017-18|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
||| +|Lexus|ES Hybrid 2019-25|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
||| +|Lexus|GS F 2016|All|Stock|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
||| +|Lexus|IS 2017-19|All|Stock|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
||| +|Lexus|IS 2022-24|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
||| +|Lexus|LC 2024-25|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
||| +|Lexus|NX 2018-19|All|openpilot available[2](#footnotes)|0 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
||| +|Lexus|NX 2020-21|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
||| +|Lexus|NX Hybrid 2018-19|All|openpilot available[2](#footnotes)|0 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
||| +|Lexus|NX Hybrid 2020-21|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
||| +|Lexus|RC 2018-20|All|Stock|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
||| +|Lexus|RC 2023|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
||| +|Lexus|RX 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 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
||| +|Lexus|RX 2017-19|All|openpilot available[2](#footnotes)|0 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
||| +|Lexus|RX 2020-22|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
||| +|Lexus|RX Hybrid 2016|Lexus Safety System+|openpilot available[2](#footnotes)|0 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
||| +|Lexus|RX Hybrid 2017-19|All|openpilot available[2](#footnotes)|0 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
||| +|Lexus|RX Hybrid 2020-22|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
||| +|Lexus|UX Hybrid 2019-24|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
||| +|Lincoln|Aviator 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Lincoln|Aviator Plug-in Hybrid 2020-24|Co-Pilot360 Plus|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ford Q3 connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|MAN|eTGE 2020-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|MAN|TGE 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Mazda|CX-5 2022-25|All|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Mazda|CX-9 2021-23|All|Stock|0 mph|28 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Mazda connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Nissan[7](#footnotes)|Altima 2019-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| +|Nissan[7](#footnotes)|Leaf 2018-23|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| +|Nissan[7](#footnotes)|Rogue 2018-20|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| +|Nissan[7](#footnotes)|X-Trail 2017|ProPILOT Assist|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Nissan 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
||| +|Ram|1500 2019-24|Adaptive Cruise Control (ACC)|Stock|32 mph|1 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ram|2500 2020-24|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Ram|3500 2019-22|Adaptive Cruise Control (ACC)|Stock|0 mph|36 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Ram connector
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 mount
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Rivian|R1S 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Rivian|R1T 2022-24|All|openpilot|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 Rivian A connector
- 1 USB-C coupler
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 comma power v3
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|SEAT|Ateca 2016-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
||| +|SEAT|Leon 2014-20|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
||| +|Subaru|Ascent 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Crosstrek 2020-23|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Forester 2017-18|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Forester 2019-21|All[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2017-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Impreza 2020-22|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Legacy 2015-18|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Legacy 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2015-17|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2018-19|EyeSight Driver Assistance[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|Outback 2020-22|All[8](#footnotes)|Stock|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2018-19|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-empty.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Subaru|XV 2020-21|EyeSight Driver Assistance[8](#footnotes)|openpilot available[1,9](#footnotes)|0 mph|0 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-empty.svg)](##)|
Parts- 1 Subaru 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
Tools- 1 Pry Tool
- 1 Socket Wrench 8mm or 5/16" (deep)
||| +|Å koda|Fabia 2022-23[15](#footnotes)|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
[17](#footnotes)||| +|Å koda|Kamiq 2021-23[13,15](#footnotes)|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
[17](#footnotes)||| +|Å koda|Karoq 2019-23[15](#footnotes)|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
||| +|Å koda|Kodiaq 2017-23[15](#footnotes)|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
||| +|Å koda|Octavia 2015-19[15](#footnotes)|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
||| +|Å koda|Octavia RS 2016[15](#footnotes)|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
||| +|Å koda|Octavia Scout 2017-19[15](#footnotes)|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
||| +|Å koda|Scala 2020-23[15](#footnotes)|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
[17](#footnotes)||| +|Å koda|Superb 2015-22[15](#footnotes)|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
||| +|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
||| +|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
||| +|Toyota|Avalon 2017-18|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|Avalon 2019-21|All|openpilot available[2](#footnotes)|0 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|Avalon 2022|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 Hybrid 2019-21|All|openpilot available[2](#footnotes)|0 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|Avalon Hybrid 2022|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|C-HR 2017-20|All|Stock|0 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|C-HR 2021|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|C-HR Hybrid 2017-20|All|Stock|0 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|C-HR Hybrid 2021-22|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|Camry 2018-20|All|Stock|0 mph[12](#footnotes)|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|Camry 2021-24|All|openpilot|0 mph[12](#footnotes)|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|Camry Hybrid 2018-20|All|Stock|0 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|Camry Hybrid 2021-24|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|Corolla 2017-19|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|Corolla 2020-22|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|Corolla Cross (Non-US only) 2020-23|All|openpilot|17 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|Corolla Cross Hybrid (Non-US only) 2020-22|All|openpilot|17 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|Corolla Hatchback 2019-22|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|Corolla Hybrid 2020-22|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|Corolla Hybrid (South America only) 2020-23|All|openpilot|17 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|Highlander 2017-19|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|Highlander 2020-23|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|Highlander Hybrid 2017-19|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|Highlander Hybrid 2020-23|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|Mirai 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|Prius 2016|Toyota Safety Sense P|openpilot available[2](#footnotes)|0 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|Prius 2017-20|All|openpilot available[2](#footnotes)|0 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|Prius 2021-22|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|Prius Prime 2017-20|All|openpilot available[2](#footnotes)|0 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|Prius Prime 2021-22|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|Prius v 2017|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
||| +|Toyota|RAV4 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
||| +|Toyota|RAV4 2017-18|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|RAV4 2019-21|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|RAV4 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 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|RAV4 Hybrid 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
||| +|Toyota|RAV4 Hybrid 2017-18|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|RAV4 Hybrid 2019-21|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|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
||| +|Volkswagen|Arteon Shooting Brake 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|Atlas 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|Atlas Cross Sport 2020-22|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|California 2021-23|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Caravelle 2020|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|CC 2018-22|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|Crafter 2017-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|e-Crafter 2018-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|e-Golf 2014-20|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|Golf 2015-20|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-empty.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|Golf Alltrack 2015-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-empty.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|Golf GTD 2015-20|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|Golf GTE 2015-20|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|Golf GTI 2015-21|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-empty.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|Golf R 2015-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
||| +|Volkswagen|Golf SportsVan 2015-20|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|Grand California 2019-24|Adaptive Cruise Control (ACC) & Lane Assist|openpilot available[1,16](#footnotes)|0 mph|31 mph|[![star](assets/icon-star-full.svg)](##)|[![star](assets/icon-star-full.svg)](##)|
Parts- 1 USB-C coupler
- 1 VW J533 connector
- 1 angled mount (8 degrees)
- 1 comma 3X
- 1 harness box
- 1 long OBD-C cable (9.5 ft)
- 1 right angle OBD-C cable (1.5 ft)
Buy Here
||| +|Volkswagen|Jetta 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|Jetta GLI 2021-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|Passat 2015-22[14](#footnotes)|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|Passat Alltrack 2015-22|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|Passat GTE 2015-22|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|Polo 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
[17](#footnotes)||| +|Volkswagen|Polo GTI 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
[17](#footnotes)||| +|Volkswagen|T-Cross 2021|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
[17](#footnotes)||| +|Volkswagen|T-Roc 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|Taos 2022-24|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|Teramont 2018-22|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|Teramont Cross Sport 2021-22|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|Teramont X 2021-22|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|Tiguan 2018-24|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|Tiguan eHybrid 2021-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|Touran 2016-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
||| ### Footnotes 1openpilot Longitudinal Control (Alpha) is available behind a toggle; the toggle is only available in non-release branches such as `devel` or `nightly-dev`.
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 1950db8a05..154734b7fc 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -6,7 +6,7 @@ Development is coordinated through [Discord](https://discord.comma.ai) and GitHu ### Getting Started -* Setup your [development environment](../tools/) +* Set up your [development environment](/tools/) * Join our [Discord](https://discord.comma.ai) * Docs are at https://docs.comma.ai and https://blog.comma.ai diff --git a/docs/how-to/turn-the-speed-blue.md b/docs/how-to/turn-the-speed-blue.md index 64f4475dfa..13b3b03e80 100644 --- a/docs/how-to/turn-the-speed-blue.md +++ b/docs/how-to/turn-the-speed-blue.md @@ -1,11 +1,11 @@ # Turn the speed blue *A getting started guide for openpilot development* -In 30 minutes, we'll get an openpilot development environment setup on your computer and make some changes to openpilot's UI. +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. -## 1. Setup your development environment +## 1. Set up your development environment Run this to clone openpilot and install all the dependencies: ```bash diff --git a/launch_env.sh b/launch_env.sh index b0bff4e57f..4c011c6ac0 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.4" + export AGNOS_VERSION="12.8" fi export STAGING_ROOT="/data/safe_staging" 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/opendbc_repo b/opendbc_repo index 57187bcb60..d2a39cbd82 160000 --- a/opendbc_repo +++ b/opendbc_repo @@ -1 +1 @@ -Subproject commit 57187bcb605272165d20b3261d1ee50073bfc2a3 +Subproject commit d2a39cbd826f4727cc2da57fa53eb54695a54f3e diff --git a/panda b/panda index c33cfa0803..7eab6fd61b 160000 --- a/panda +++ b/panda @@ -1 +1 @@ -Subproject commit c33cfa0803deef5dc53d103ae1a2513e2ab94196 +Subproject commit 7eab6fd61bae085e0fd44cccb79dc6451163029e diff --git a/pyproject.toml b/pyproject.toml index 68e5e35296..0968cd4ed3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,7 +26,7 @@ dependencies = [ "pycapnp", "Cython", "setuptools", - "numpy >=2.0, <2.2", # Linting issues with mypy in 2.2 + "numpy >=2.0", # body / webrtcd "aiohttp", @@ -42,7 +42,6 @@ dependencies = [ # modeld "onnx >= 1.14.0", - "llvmlite", # logging "pyzmq", @@ -81,15 +80,13 @@ docs = [ ] testing = [ - "coverage", "hypothesis ==6.47.*", "mypy", "pytest", - "pytest-cov", "pytest-cpp", "pytest-subtests", - # https://github.com/pytest-dev/pytest-xdist/issues/1215 - "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@909e97b49d12401c10608f9d777bfc9dab8a4413", + # https://github.com/pytest-dev/pytest-xdist/pull/1229 + "pytest-xdist @ git+https://github.com/sshane/pytest-xdist@2b4372bd62699fb412c4fe2f95bf9f01bd2018da", "pytest-timeout", "pytest-randomly", "pytest-asyncio", @@ -123,7 +120,6 @@ 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')", - "rerun-sdk >= 0.18", ] [project.urls] @@ -155,6 +151,7 @@ markers = [ testpaths = [ "common", "selfdrive", + "system/manager", "system/updated", "system/athena", "system/camerad", @@ -180,9 +177,6 @@ skip = "./third_party/*, ./tinygrad/*, ./tinygrad_repo/*, ./msgq/*, ./panda/*, . [tool.mypy] python_version = "3.11" -plugins = [ - "numpy.typing.mypy_plugin", -] exclude = [ "cereal/", "msgq/", @@ -265,9 +259,7 @@ lint.flake8-implicit-str-concat.allow-multiline = false "pytest.main".msg = "pytest.main requires special handling that is easy to mess up!" "unittest".msg = "Use pytest" "pyray.measure_text_ex".msg = "Use openpilot.system.ui.lib.text_measure" - -[tool.coverage.run] -concurrency = ["multiprocessing", "thread"] +"time.time".msg = "Use time.monotonic" [tool.ruff.format] quote-style = "preserve" diff --git a/release/README.md b/release/README.md index 06404be6ab..fb651fa05a 100644 --- a/release/README.md +++ b/release/README.md @@ -3,29 +3,34 @@ ``` ## release checklist -**Go to `devel-staging`** +### Go to staging +- [ ] make a GitHub issue to track release +- [ ] create release master branch - [ ] update RELEASES.md -- [ ] update `devel-staging`: `git reset --hard origin/master-ci` -- [ ] open a pull request from `devel-staging` to `devel` -- [ ] post on Discord - -**Go to `devel`** - [ ] bump version on master: `common/version.h` and `RELEASES.md` -- [ ] before merging the pull request +- [ ] build new userdata partition from `release3-staging` +- [ ] post on Discord, tag `@release crew` + +Updating staging: +1. either rebase on master or cherry-pick changes +2. run this to update: `BRANCH=devel-staging release/build_devel.sh` +3. build new userdata partition from `release3-staging` + +### Go to release +- [ ] before going to release, test the following: - [ ] update from previous release -> new release - [ ] update from new release -> previous release - [ ] fresh install with `openpilot-test.comma.ai` - [ ] drive on fresh install - [ ] no submodules or LFS - [ ] check sentry, MTBF, etc. - -**Go to `release3`** + - [ ] stress test passes in production - [ ] publish the blog post - [ ] `git reset --hard origin/release3-staging` - [ ] tag the release: `git tag v0.X.X && git push origin v0.X.X` - [ ] create GitHub release - [ ] final test install on `openpilot.comma.ai` - [ ] update factory provisioning -- [ ] close out milestone +- [ ] close out milestone and issue - [ ] post on Discord, X, etc. ``` diff --git a/release/build_devel.sh b/release/build_stripped.sh similarity index 79% rename from release/build_devel.sh rename to release/build_stripped.sh index 05dcaafcaa..88c0101a45 100755 --- a/release/build_devel.sh +++ b/release/build_stripped.sh @@ -17,28 +17,23 @@ rm -rf $TARGET_DIR mkdir -p $TARGET_DIR cd $TARGET_DIR cp -r $SOURCE_DIR/.git $TARGET_DIR -pre-commit uninstall || true -echo "[-] bringing __nightly and devel in sync T=$SECONDS" +echo "[-] setting up stripped branch sync T=$SECONDS" cd $TARGET_DIR -git fetch --depth 1 origin __nightly -git fetch --depth 1 origin devel - -git checkout -f --track origin/__nightly -git reset --hard __nightly -git checkout __nightly -git reset --hard origin/devel -git clean -xdff -git lfs uninstall +# tmp branch +git checkout --orphan tmp # remove everything except .git -echo "[-] erasing old openpilot T=$SECONDS" +echo "[-] erasing old sunnypilot T=$SECONDS" +git submodule deinit -f --all +git rm -rf --cached . find . -maxdepth 1 -not -path './.git' -not -name '.' -not -name '..' -exec rm -rf '{}' \; -# reset source tree +# cleanup before the copy cd $SOURCE_DIR git clean -xdff +git submodule foreach --recursive git clean -xdff # do the files copy echo "[-] copying files T=$SECONDS" @@ -47,6 +42,7 @@ cp -pR --parents $(./release/release_files.py) $TARGET_DIR/ # in the directory cd $TARGET_DIR +rm -rf .git/modules/ rm -f panda/board/obj/panda.bin.signed # include source commit hash and build date in commit @@ -61,7 +57,7 @@ echo -n "$GIT_COMMIT_DATE" > git_src_commit_date echo "[-] committing version $VERSION T=$SECONDS" git add -f . git status -git commit -a -m "openpilot v$VERSION release +git commit -a -m "sunnypilot v$VERSION release date: $DATETIME master commit: $GIT_HASH @@ -85,7 +81,7 @@ fi if [ ! -z "$BRANCH" ]; then echo "[-] Pushing to $BRANCH T=$SECONDS" - git push -f origin __nightly:$BRANCH + git push -f origin tmp:$BRANCH fi echo "[-] done T=$SECONDS, ready at $TARGET_DIR" diff --git a/release/check-submodules.sh b/release/check-submodules.sh index 93869a7403..7120fa15d8 100755 --- a/release/check-submodules.sh +++ b/release/check-submodules.sh @@ -1,17 +1,43 @@ #!/usr/bin/env bash +has_submodule_changes() { + local submodule_path="$1" + if [ -n "$SUBMODULE_PATHS" ]; then + echo "$SUBMODULE_PATHS" | grep -q "$submodule_path" + return $? + fi + return 1 +} + while read hash submodule ref; do + if [ -z "$hash" ] || [ -z "$submodule" ]; then + continue + fi + + hash=$(echo "$hash" | sed 's/^[+-]//') + if [ "$submodule" = "tinygrad_repo" ]; then echo "Skipping $submodule" continue fi - git -C $submodule fetch --depth 100 origin master - git -C $submodule branch -r --contains $hash | grep "origin/master" - if [ "$?" -eq 0 ]; then - echo "$submodule ok" + if [ "$CHECK_PR_REFS" = "true" ] && has_submodule_changes "$submodule"; then + echo "Checking $submodule (non-master): verifying hash $hash exists" + git -C $submodule fetch --depth 100 origin + if git -C $submodule cat-file -e $hash 2>/dev/null; then + echo "$submodule ok (hash exists)" + else + echo "$submodule: $hash does not exist in the repository" + exit 1 + fi else - echo "$submodule: $hash is not on master" - exit 1 + git -C $submodule fetch --depth 100 origin master + git -C $submodule branch -r --contains $hash | grep "origin/master" + if [ "$?" -eq 0 ]; then + echo "$submodule ok" + else + echo "$submodule: $hash is not on master" + exit 1 + fi fi done <<< $(git submodule status --recursive) diff --git a/release/ci/docker_build_sp.sh b/release/ci/docker_build_sp.sh new file mode 100755 index 0000000000..9a8d390c61 --- /dev/null +++ b/release/ci/docker_build_sp.sh @@ -0,0 +1,26 @@ +#!/usr/bin/env bash +set -e + +# To build sim and docs, you can run the following to mount the scons cache to the same place as in CI: +# mkdir -p .ci_cache/scons_cache +# sudo mount --bind /tmp/scons_cache/ .ci_cache/scons_cache + +SCRIPT_DIR=$(dirname "$0") +OPENPILOT_DIR=$SCRIPT_DIR/../../ +if [ -n "$TARGET_ARCHITECTURE" ]; then + PLATFORM="linux/$TARGET_ARCHITECTURE" + TAG_SUFFIX="-$TARGET_ARCHITECTURE" +else + PLATFORM="linux/$(uname -m)" + TAG_SUFFIX="" +fi + +source $SCRIPT_DIR/docker_common_sp.sh $1 "$TAG_SUFFIX" + +DOCKER_BUILDKIT=1 docker buildx build --provenance false --pull --platform $PLATFORM --load --cache-to type=inline --cache-from type=registry,ref=$REMOTE_TAG -t $DOCKER_IMAGE:latest -t $REMOTE_TAG -t $LOCAL_TAG -f $OPENPILOT_DIR/$DOCKER_FILE $OPENPILOT_DIR + +if [ -n "$PUSH_IMAGE" ]; then + docker push $REMOTE_TAG + docker tag $REMOTE_TAG $REMOTE_SHA_TAG + docker push $REMOTE_SHA_TAG +fi diff --git a/release/ci/docker_common_sp.sh b/release/ci/docker_common_sp.sh new file mode 100755 index 0000000000..e8f515762a --- /dev/null +++ b/release/ci/docker_common_sp.sh @@ -0,0 +1,18 @@ +if [ "$1" = "base" ]; then + export DOCKER_IMAGE=sunnypilot-base + export DOCKER_FILE=Dockerfile.sunnypilot_base +elif [ "$1" = "prebuilt" ]; then + export DOCKER_IMAGE=sunnypilot-prebuilt + export DOCKER_FILE=Dockerfile.sunnypilot +else + echo "Invalid docker build image: '$1'" + exit 1 +fi + +export DOCKER_REGISTRY=ghcr.io/sunnypilot +export COMMIT_SHA=$(git rev-parse HEAD) + +TAG_SUFFIX=$2 +LOCAL_TAG=$DOCKER_IMAGE$TAG_SUFFIX +REMOTE_TAG=$DOCKER_REGISTRY/$LOCAL_TAG +REMOTE_SHA_TAG=$DOCKER_REGISTRY/$LOCAL_TAG:$COMMIT_SHA diff --git a/release/ci/install_github_runner.sh b/release/ci/install_github_runner.sh index f6b1e217c5..9f11e4841c 100755 --- a/release/ci/install_github_runner.sh +++ b/release/ci/install_github_runner.sh @@ -5,6 +5,7 @@ set -e DEFAULT_REPO_URL="https://github.com/sunnypilot" START_AT_BOOT=false RESTORE_MODE=false +RUNNER_VERSION="2.325.0" # Parse command line arguments while [[ $# -gt 0 ]]; do @@ -108,9 +109,9 @@ setup_system_configs() { install_runner() { echo "Downloading and setting up runner..." cd "$RUNNER_DIR" - curl -o actions-runner-linux-arm64-2.322.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.322.0/actions-runner-linux-arm64-2.322.0.tar.gz - sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-2.322.0.tar.gz - sudo rm ./actions-runner-linux-arm64-2.322.0.tar.gz + curl -o actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz -L https://github.com/actions/runner/releases/download/v${RUNNER_VERSION}/actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz + sudo -u ${RUNNER_USER} tar -xzf ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz + sudo rm ./actions-runner-linux-arm64-${RUNNER_VERSION}.tar.gz sudo chmod +x ./config.sh } @@ -149,25 +150,32 @@ EOL } install_service() { + local service_name + if [ -f "${RUNNER_DIR}/.service" ]; then + service_name=$(cat "${RUNNER_DIR}/.service") + else + service_name="actions.runner.sunnypilot.$(uname -n)" + fi + + create_service_template remount_rw + local service_path="/etc/systemd/system/${service_name}" echo "Installing systemd service..." + if [ -f "${service_path}" ]; then + echo "Service ${service_path} found in systemd, we will delete it" + sudo rm -f "${service_path}" + fi + cd "$RUNNER_DIR" sudo ./svc.sh install $RUNNER_USER if [ "$START_AT_BOOT" = false ]; then - local service_name - if [ -f "${RUNNER_DIR}/.service" ]; then - service_name=$(cat "${RUNNER_DIR}/.service") - else - service_name="actions.runner.sunnypilot.$(uname -n)" - fi sudo systemctl disable "${service_name}" fi remount_ro } check_restore_prerequisites() { - local needs_restore=false local can_restore=false local service_name="" @@ -189,25 +197,16 @@ check_restore_prerequisites() { exit 1 fi - # Then check if restoration is needed (if either service or user is missing) - if ! systemctl list-unit-files "${service_name}" &>/dev/null; then - echo "Service ${service_name} not found in systemd" - needs_restore=true - fi - if ! id "${RUNNER_USER}" &>/dev/null; then echo "User ${RUNNER_USER} does not exist" - needs_restore=true fi # Only proceed if we can restore AND need to restore - if [ "$can_restore" = true ] && [ "$needs_restore" = true ]; then - echo "Restoration is needed and possible" + if [ "$can_restore" = true ]; then + echo "Restoration is possible" return 0 else - if [ "$needs_restore" = false ]; then - echo "System is already properly configured (user and service exist)" - fi + echo "No restoration possible" exit 0 fi } @@ -226,7 +225,6 @@ perform_install() { setup_system_configs install_runner set_directory_permissions - create_service_template configure_runner install_service echo "Installation completed successfully" diff --git a/release/ci/model_generator.py b/release/ci/model_generator.py index 069b8e41af..ee41343be8 100755 --- a/release/ci/model_generator.py +++ b/release/ci/model_generator.py @@ -13,17 +13,32 @@ def create_short_name(full_name): words = [re.sub(r'[^a-zA-Z0-9]', '', word) for word in clean_name.split() if re.sub(r'[^a-zA-Z0-9]', '', word)] if len(words) == 1: - # If there's only one word, return it as is, lowercased, truncated to 8 characters - truncated = words[0][:8] - return truncated.lower() if truncated.isupper() else truncated + return words[0][:8].upper() # Handle special case: Name + Version (e.g., "Word A1" -> "WordA1") if len(words) == 2 and re.match(r'^[A-Za-z]\d+$', words[1]): - first_word = words[0].lower() if words[0].isupper() else words[0] - return (first_word + words[1])[:8] + return (words[0] + words[1])[:8].upper() - # Normal case: first letter and trailing numbers from each word - result = ''.join(word if word.isdigit() else word[0] + (re.search(r'\d+$', word) or [''])[0] for word in words) + result = "" + for word in words: + # Version or number patterns + if (re.match(r'^\d+[a-zA-Z]+$', word) or + re.match(r'^\d+[vVbB]\d+$', word) or + re.match(r'^[vVbB]\d+$', word) or + re.match(r'^\d{4}$', word)): + result += word.upper() + # All uppercase abbreviations (2-3 letters) + elif re.match(r'^[A-Z]{2,3}$', word): + result += word + # Letters+digits (for example tr15 rev2) + elif re.match(r'^[a-zA-Z]+[0-9]+$', word): + result += word[0].upper() + ''.join(re.findall(r'\d+', word)) + elif word.isalpha(): + result += word[0].upper() + elif word.isdigit(): + result += word + else: + result += word[0].upper() return result[:8] @@ -58,14 +73,14 @@ def generate_metadata(model_path: Path, output_dir: Path, short_name: str): "artifact": { "file_name": tinygrad_file.name, "download_uri": { - "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", + "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", "sha256": tinygrad_hash } }, "metadata": { "file_name": metadata_file.name, "download_uri": { - "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", + "url": "https://gitlab.com/sunnypilot/public/docs.sunnypilot.ai/-/raw/main/", "sha256": metadata_hash } } @@ -83,12 +98,12 @@ def create_metadata_json(models: list, output_dir: Path, custom_name=None, short "ref": upstream_branch, "environment": "development", "runner": "tinygrad", - "build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), - "models": models, - "overrides": {}, "index": -1, "minimum_selector_version": "-1", "generation": "-1", + "build_time": datetime.now(UTC).strftime("%Y-%m-%dT%H:%M:%SZ"), + "overrides": {}, + "models": models, } # Write metadata to output_dir 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! diff --git a/release/ci/squash_and_merge_prs.py b/release/ci/squash_and_merge_prs.py index b26b08d500..0f20f4f900 100755 --- a/release/ci/squash_and_merge_prs.py +++ b/release/ci/squash_and_merge_prs.py @@ -12,7 +12,7 @@ TRUST_FORK_LABEL = "trust-fork-pr" def setup_argument_parser(): parser = argparse.ArgumentParser(description='Process and squash GitHub PRs') parser.add_argument('--pr-data', type=str, help='PR data in JSON format') - parser.add_argument('--source-branch', type=str, default='master-new', + 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', help='Target branch for merging') diff --git a/release/identity.sh b/release/identity.sh index c699c94650..1913df3c5b 100644 --- a/release/identity.sh +++ b/release/identity.sh @@ -1,4 +1,4 @@ -export GIT_COMMITTER_NAME="Vehicle Researcher" -export GIT_COMMITTER_EMAIL="user@comma.ai" -export GIT_AUTHOR_NAME="Vehicle Researcher" -export GIT_AUTHOR_EMAIL="user@comma.ai" +export GIT_COMMITTER_NAME="github-actions[bot]" +export GIT_COMMITTER_EMAIL="github-actions[bot]@users.noreply.github.com" +export GIT_AUTHOR_NAME="github-actions[bot]" +export GIT_AUTHOR_EMAIL="github-actions[bot]@users.noreply.github.com" diff --git a/release/release_files.py b/release/release_files.py index 98b88293b9..0b9e034bf1 100755 --- a/release/release_files.py +++ b/release/release_files.py @@ -17,6 +17,8 @@ blacklist = [ ".gitattributes", ".git$", ".gitmodules", + ".run/", + ".idea/", ] # gets you through the blacklist diff --git a/selfdrive/assets/icons/link.png b/selfdrive/assets/icons/link.png new file mode 100644 index 0000000000..8a795c05b8 --- /dev/null +++ b/selfdrive/assets/icons/link.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a69514fe68dd1b4c73f28771640dcba10fc40989d1c7e771cb48bfd830fef206 +size 5713 diff --git a/selfdrive/assets/icons/microphone.png b/selfdrive/assets/icons/microphone.png new file mode 100644 index 0000000000..6cb9cc0254 --- /dev/null +++ b/selfdrive/assets/icons/microphone.png @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:9fc1f7f31d41f26ea7d6f52b3096f7a91844a3b897bc233a8489253c46f0403b +size 6324 diff --git a/selfdrive/car/car_specific.py b/selfdrive/car/car_specific.py index 151a8479a7..270111524e 100644 --- a/selfdrive/car/car_specific.py +++ b/selfdrive/car/car_specific.py @@ -57,7 +57,7 @@ class CarSpecificEvents: events.add(EventName.belowSteerSpeed) elif self.CP.brand == 'honda': - events = self.create_common_events(CS, CS_prev, pcm_enable=False) + events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport], pcm_enable=False) if self.CP.pcmCruise and CS.vEgo < self.CP.minEnableSpeed: events.add(EventName.belowEngageSpeed) @@ -78,7 +78,8 @@ class CarSpecificEvents: events.add(EventName.manualRestart) elif self.CP.brand == 'toyota': - events = self.create_common_events(CS, CS_prev) + # TODO: when we check for unexpected disengagement, check gear not S1, S2, S3 + events = self.create_common_events(CS, CS_prev, extra_gears=[GearShifter.sport]) if self.CP.openpilotLongitudinalControl: if CS.cruiseState.standstill and not CS.brakePressed: diff --git a/selfdrive/car/card.py b/selfdrive/car/card.py index fc4c931334..b067bae75e 100755 --- a/selfdrive/car/card.py +++ b/selfdrive/car/card.py @@ -1,5 +1,4 @@ #!/usr/bin/env python3 -import json import os import time import threading @@ -90,7 +89,6 @@ class Car: is_release = self.params.get_bool("IsReleaseBranch") - init_params_list_sp = sunnypilot_interfaces.get_init_params(self.params) if CI is None: # wait for one pandaState and one CAN packet @@ -109,7 +107,8 @@ class Car: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - fixed_fingerprint = json.loads(self.params.get("CarPlatformBundle", encoding='utf-8') or "{}").get("platform", None) + fixed_fingerprint = (self.params.get("CarPlatformBundle") or {}).get("platform", None) + init_params_list_sp = sunnypilot_interfaces.initialize_params(self.params) self.CI = get_car(*self.can_callbacks, obd_callback(self.params), alpha_long_allowed, is_release, num_pandas, cached_params, fixed_fingerprint, init_params_list_sp) @@ -150,7 +149,7 @@ class Car: except Exception: pass - secoc_key = self.params.get("SecOCKey", encoding='utf8') + secoc_key = self.params.get("SecOCKey") if secoc_key is not None: saved_secoc_key = bytes.fromhex(secoc_key.strip()) if len(saved_secoc_key) == 16: diff --git a/selfdrive/car/cruise.py b/selfdrive/car/cruise.py index 01ef2d7c99..5ffb43f9e5 100644 --- a/selfdrive/car/cruise.py +++ b/selfdrive/car/cruise.py @@ -2,7 +2,7 @@ import math import numpy as np from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.car.cruise_ext import VCruiseHelperSP diff --git a/selfdrive/car/helpers.py b/selfdrive/car/helpers.py index 6187be1474..275c84479f 100644 --- a/selfdrive/car/helpers.py +++ b/selfdrive/car/helpers.py @@ -58,5 +58,7 @@ def convert_carControlSP(struct: capnp.lib.capnp._DynamicStructReader) -> struct struct_dataclass.mads = structs.ModularAssistiveDrivingSystem(**remove_deprecated(struct_dict.get('mads', {}))) 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', {}))) return struct_dataclass diff --git a/selfdrive/car/tests/test_cruise_speed.py b/selfdrive/car/tests/test_cruise_speed.py index dd78b77680..4f9444d4bb 100644 --- a/selfdrive/car/tests/test_cruise_speed.py +++ b/selfdrive/car/tests/test_cruise_speed.py @@ -6,7 +6,7 @@ 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 openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.selfdrive.test.longitudinal_maneuvers.maneuver import Maneuver ButtonEvent = car.CarState.ButtonEvent diff --git a/selfdrive/car/tests/test_models.py b/selfdrive/car/tests/test_models.py index 0fc6716dd3..4f61ad3c35 100644 --- a/selfdrive/car/tests/test_models.py +++ b/selfdrive/car/tests/test_models.py @@ -5,7 +5,6 @@ import pytest import random import unittest # noqa: TID251 from collections import defaultdict, Counter -from functools import partial import hypothesis.strategies as st from hypothesis import Phase, given, settings from parameterized import parameterized_class @@ -23,8 +22,7 @@ from openpilot.common.basedir import BASEDIR from openpilot.selfdrive.pandad import can_capnp_to_list from openpilot.selfdrive.test.helpers import read_segment_list from openpilot.system.hardware.hw import DEFAULT_DOWNLOAD_CACHE_ROOT -from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source_zst, openpilotci_source, internal_source, \ - internal_source_zst, comma_api_source, auto_source +from openpilot.tools.lib.logreader import LogReader, LogsUnavailable, openpilotci_source, internal_source, comma_api_source from openpilot.tools.lib.route import SegmentName SafetyModel = car.CarParams.SafetyModel @@ -126,9 +124,8 @@ class TestCarModelBase(unittest.TestCase): segment_range = f"{cls.test_route.route}/{seg}" try: - source = partial(auto_source, sources=[internal_source, internal_source_zst] if len(INTERNAL_SEG_LIST) else \ - [openpilotci_source_zst, openpilotci_source, comma_api_source]) - lr = LogReader(segment_range, source=source, sort_by_time=True) + sources = [internal_source] if len(INTERNAL_SEG_LIST) else [openpilotci_source, comma_api_source] + lr = LogReader(segment_range, sources=sources, sort_by_time=True) return cls.get_testing_data_from_logreader(lr) except (LogsUnavailable, AssertionError): pass @@ -199,7 +196,6 @@ class TestCarModelBase(unittest.TestCase): def test_car_interface(self): # TODO: also check for checksum violations from can parser can_invalid_cnt = 0 - can_valid = False CC = structs.CarControl().as_reader() CC_SP = structs.CarControlSP() @@ -207,11 +203,8 @@ class TestCarModelBase(unittest.TestCase): CS, _ = self.CI.update(msg) self.CI.apply(CC, CC_SP, msg[0]) - if CS.canValid: - can_valid = True - # wait max of 2s for low frequency msgs to be seen - if i > 200 or can_valid: + if i > 250: can_invalid_cnt += not CS.canValid self.assertEqual(can_invalid_cnt, 0) @@ -331,7 +324,7 @@ class TestCarModelBase(unittest.TestCase): vehicle_speed_seen = self.CP.steerControlType == SteerControlType.angle and not self.CP.notCar - for dat in msgs: + for n, dat in enumerate(msgs): # due to panda updating state selectively, only edges are expected to match # TODO: warm up CarState with real CAN messages to check edge of both sources # (eg. toyota's gasPressed is the inverse of a signal being set) @@ -350,6 +343,8 @@ class TestCarModelBase(unittest.TestCase): can = [(int(time.monotonic() * 1e9), [CanData(address=address, dat=dat, src=bus)])] CS, _ = self.CI.update(can) + if n < 5: # CANParser warmup time + continue if self.safety.get_gas_pressed_prev() != prev_panda_gas: self.assertEqual(CS.gasPressed, self.safety.get_gas_pressed_prev()) @@ -369,7 +364,7 @@ class TestCarModelBase(unittest.TestCase): if self.safety.get_steering_disengage_prev() != prev_panda_steering_disengage: self.assertEqual(CS.steeringDisengage, self.safety.get_steering_disengage_prev()) - if self.safety.get_vehicle_moving() != prev_panda_vehicle_moving: + if self.safety.get_vehicle_moving() != prev_panda_vehicle_moving and not self.CP.notCar: self.assertEqual(not CS.standstill, self.safety.get_vehicle_moving()) # check vehicle speed if angle control car or available @@ -427,7 +422,7 @@ class TestCarModelBase(unittest.TestCase): # TODO: check rest of panda's carstate (steering, ACC main on, etc.) checks['gasPressed'] += CS.gasPressed != self.safety.get_gas_pressed_prev() - checks['standstill'] += CS.standstill == self.safety.get_vehicle_moving() + checks['standstill'] += (CS.standstill == self.safety.get_vehicle_moving()) and not self.CP.notCar # check vehicle speed if angle control car or available if self.safety.get_vehicle_speed_min() > 0 or self.safety.get_vehicle_speed_max() > 0: diff --git a/selfdrive/controls/controlsd.py b/selfdrive/controls/controlsd.py index 04d79d3330..9e88a37690 100755 --- a/selfdrive/controls/controlsd.py +++ b/selfdrive/controls/controlsd.py @@ -2,11 +2,11 @@ import math import threading import time -from typing import SupportsFloat +from numbers import Number from cereal import car, log import cereal.messaging as messaging -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, Priority, Ratekeeper from openpilot.common.swaglog import cloudlog @@ -21,6 +21,8 @@ from openpilot.selfdrive.controls.lib.latcontrol_torque import LatControlTorque from openpilot.selfdrive.controls.lib.longcontrol import LongControl from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase from openpilot.sunnypilot.selfdrive.controls.controlsd_ext import ControlsExt State = log.SelfdriveState.OpenpilotState @@ -30,15 +32,16 @@ LaneChangeDirection = log.LaneChangeDirection ACTUATOR_FIELDS = tuple(car.CarControl.Actuators.schema.fields.keys()) -class Controls(ControlsExt): +class Controls(ControlsExt, ModelStateBase): def __init__(self) -> None: self.params = Params() cloudlog.info("controlsd is waiting for CarParams") self.CP = messaging.log_from_bytes(self.params.get("CarParams", block=True), car.CarParams) cloudlog.info("controlsd got CarParams") - # Initialize sunnypilot controlsd extension + # Initialize sunnypilot controlsd extension and base model state ControlsExt.__init__(self, self.CP, self.params) + ModelStateBase.__init__(self) self.CI = interfaces[self.CP.carFingerprint](self.CP, self.CP_SP) @@ -48,7 +51,7 @@ class Controls(ControlsExt): poll='selfdriveState') self.pm = messaging.PubMaster(['carControl', 'controlsState'] + self.pm_services_ext) - self.steer_limited_by_controls = False + self.steer_limited_by_safety = False self.curvature = 0.0 self.desired_curvature = 0.0 @@ -92,8 +95,12 @@ class Controls(ControlsExt): 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.LaC.extension.update_lateral_lag(self.sm['liveDelay'].lateralDelay) + + self.lat_delay = get_lat_delay(self.params, self.sm["liveDelay"].lateralDelay) + self.LaC.extension.update_lateral_lag(self.lat_delay) long_plan = self.sm['longitudinalPlan'] model_v2 = self.sm['modelV2'] @@ -135,14 +142,14 @@ class Controls(ControlsExt): actuators.curvature = self.desired_curvature steer, steeringAngleDeg, lac_log = self.LaC.update(CC.latActive, CS, self.VM, lp, - self.steer_limited_by_controls, self.desired_curvature, + self.steer_limited_by_safety, self.desired_curvature, self.calibrated_pose, curvature_limited) # TODO what if not available actuators.torque = float(steer) actuators.steeringAngleDeg = float(steeringAngleDeg) # Ensure no NaNs/Infs for p in ACTUATOR_FIELDS: attr = getattr(actuators, p) - if not isinstance(attr, SupportsFloat): + if not isinstance(attr, Number): continue if not math.isfinite(attr): @@ -163,10 +170,7 @@ class Controls(ControlsExt): CC.cruiseControl.override = CC.enabled and not CC.longActive and self.CP.openpilotLongitudinalControl CC.cruiseControl.cancel = CS.cruiseState.enabled and (not CC.enabled or not self.CP.pcmCruise) - - speeds = self.sm['longitudinalPlan'].speeds - if len(speeds): - CC.cruiseControl.resume = CC.enabled and CS.cruiseState.standstill and speeds[-1] > 0.1 + CC.cruiseControl.resume = CC.enabled and CS.cruiseState.standstill and not self.sm['longitudinalPlan'].shouldStop hudControl = CC.hudControl hudControl.setSpeed = float(CS.vCruiseCluster * CV.KPH_TO_MS) @@ -185,10 +189,10 @@ class Controls(ControlsExt): if self.sm['selfdriveState'].active: CO = self.sm['carOutput'] if self.CP.steerControlType == car.CarParams.SteerControlType.angle: - self.steer_limited_by_controls = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \ + self.steer_limited_by_safety = abs(CC.actuators.steeringAngleDeg - CO.actuatorsOutput.steeringAngleDeg) > \ STEER_ANGLE_SATURATION_THRESHOLD else: - self.steer_limited_by_controls = abs(CC.actuators.torque - CO.actuatorsOutput.torque) > 1e-2 + self.steer_limited_by_safety = abs(CC.actuators.torque - CO.actuatorsOutput.torque) > 1e-2 # TODO: both controlsState and carControl valids should be set by # sm.all_checks(), but this creates a circular dependency diff --git a/selfdrive/controls/lib/desire_helper.py b/selfdrive/controls/lib/desire_helper.py index 23386e2ad7..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 openpilot.common.conversions import Conversions as CV +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/controls/lib/drive_helpers.py b/selfdrive/controls/lib/drive_helpers.py index 56eeac3bdf..e28fa3021c 100644 --- a/selfdrive/controls/lib/drive_helpers.py +++ b/selfdrive/controls/lib/drive_helpers.py @@ -1,6 +1,5 @@ import numpy as np -from cereal import log -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.realtime import DT_CTRL, DT_MDL MIN_SPEED = 1.0 @@ -40,14 +39,6 @@ def clip_curvature(v_ego, prev_curvature, new_curvature, roll): return float(new_curvature), limited_accel or limited_max_curv -def get_speed_error(modelV2: log.ModelDataV2, v_ego: float) -> float: - # ToDo: Try relative error, and absolute speed - if len(modelV2.temporalPose.trans): - vel_err = np.clip(modelV2.temporalPose.trans[0] - v_ego, -MAX_VEL_ERR, MAX_VEL_ERR) - return float(vel_err) - return 0.0 - - def get_accel_from_plan(speeds, accels, t_idxs, action_t=DT_MDL, vEgoStopping=0.05): if len(speeds) == len(t_idxs): v_now = speeds[0] diff --git a/selfdrive/controls/lib/latcontrol.py b/selfdrive/controls/lib/latcontrol.py index a2c28ed8ce..615168a262 100644 --- a/selfdrive/controls/lib/latcontrol.py +++ b/selfdrive/controls/lib/latcontrol.py @@ -15,15 +15,15 @@ class LatControl(ABC): self.steer_max = 1.0 @abstractmethod - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): pass def reset(self): self.sat_count = 0. - def _check_saturation(self, saturated, CS, steer_limited_by_controls, curvature_limited): + def _check_saturation(self, saturated, CS, steer_limited_by_safety, curvature_limited): # Saturated only if control output is not being limited by car torque/angle rate limits - if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_controls and not CS.steeringPressed: + if (saturated or curvature_limited) and CS.vEgo > self.sat_check_min_speed and not steer_limited_by_safety and not CS.steeringPressed: self.sat_count += self.sat_count_rate else: self.sat_count -= self.sat_count_rate diff --git a/selfdrive/controls/lib/latcontrol_angle.py b/selfdrive/controls/lib/latcontrol_angle.py index fe6275d9d0..787bd2dbe1 100644 --- a/selfdrive/controls/lib/latcontrol_angle.py +++ b/selfdrive/controls/lib/latcontrol_angle.py @@ -3,6 +3,7 @@ import math from cereal import log from openpilot.selfdrive.controls.lib.latcontrol import LatControl +# TODO This is speed dependent STEER_ANGLE_SATURATION_THRESHOLD = 2.5 # Degrees @@ -10,9 +11,9 @@ class LatControlAngle(LatControl): def __init__(self, CP, CP_SP, CI): super().__init__(CP, CP_SP, CI) self.sat_check_min_speed = 5. - self.use_steer_limited_by_controls = CP.brand == "tesla" + self.use_steer_limited_by_safety = CP.brand == "tesla" - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): angle_log = log.ControlsState.LateralAngleState.new_message() if not active: @@ -23,9 +24,9 @@ class LatControlAngle(LatControl): angle_steers_des = math.degrees(VM.get_steer_from_curvature(-desired_curvature, CS.vEgo, params.roll)) angle_steers_des += params.angleOffsetDeg - if self.use_steer_limited_by_controls: + if self.use_steer_limited_by_safety: # these cars' carcontrollers calculate max lateral accel and jerk, so we can rely on carOutput for saturation - angle_control_saturated = steer_limited_by_controls + angle_control_saturated = steer_limited_by_safety else: # for cars which use a method of limiting torque such as a torque signal (Nissan and Toyota) # or relying on EPS (Ford Q3), carOutput does not capture maxing out torque # TODO: this can be improved diff --git a/selfdrive/controls/lib/latcontrol_pid.py b/selfdrive/controls/lib/latcontrol_pid.py index eb8a1ed5f9..fd79c29bd1 100644 --- a/selfdrive/controls/lib/latcontrol_pid.py +++ b/selfdrive/controls/lib/latcontrol_pid.py @@ -13,11 +13,7 @@ class LatControlPID(LatControl): k_f=CP.lateralTuning.pid.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max) self.get_steer_feedforward = CI.get_steer_feedforward_function() - def reset(self): - super().reset() - self.pid.reset() - - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): pid_log = log.ControlsState.LateralPIDState.new_message() pid_log.steeringAngleDeg = float(CS.steeringAngleDeg) pid_log.steeringRateDeg = float(CS.steeringRateDeg) @@ -29,20 +25,24 @@ class LatControlPID(LatControl): pid_log.steeringAngleDesiredDeg = angle_steers_des pid_log.angleError = error if not active: - output_steer = 0.0 + output_torque = 0.0 pid_log.active = False - self.pid.reset() + else: # offset does not contribute to resistive torque - steer_feedforward = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo) + ff = self.get_steer_feedforward(angle_steers_des_no_offset, CS.vEgo) + freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + + output_torque = self.pid.update(error, + feedforward=ff, + speed=CS.vEgo, + freeze_integrator=freeze_integrator) - output_steer = self.pid.update(error, override=CS.steeringPressed, - feedforward=steer_feedforward, speed=CS.vEgo) pid_log.active = True pid_log.p = float(self.pid.p) pid_log.i = float(self.pid.i) pid_log.f = float(self.pid.f) - pid_log.output = float(output_steer) - pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_steer) < 1e-3, CS, steer_limited_by_controls, curvature_limited)) + pid_log.output = float(output_torque) + pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited)) - return output_steer, angle_steers_des, pid_log + return output_torque, angle_steers_des, pid_log diff --git a/selfdrive/controls/lib/latcontrol_torque.py b/selfdrive/controls/lib/latcontrol_torque.py index 5edf4cf91b..e4554ae464 100644 --- a/selfdrive/controls/lib/latcontrol_torque.py +++ b/selfdrive/controls/lib/latcontrol_torque.py @@ -2,8 +2,8 @@ import math import numpy as np from cereal import log -from opendbc.car.interfaces import LatControlInputs -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from opendbc.car.lateral import FRICTION_THRESHOLD, get_friction +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.selfdrive.controls.lib.latcontrol import LatControl from openpilot.common.pid import PIDController @@ -28,35 +28,34 @@ class LatControlTorque(LatControl): def __init__(self, CP, CP_SP, CI): super().__init__(CP, CP_SP, CI) self.torque_params = CP.lateralTuning.torque.as_builder() - self.pid = PIDController(self.torque_params.kp, self.torque_params.ki, - k_f=self.torque_params.kf, pos_limit=self.steer_max, neg_limit=-self.steer_max) self.torque_from_lateral_accel = CI.torque_from_lateral_accel() - self.use_steering_angle = self.torque_params.useSteeringAngle + self.lateral_accel_from_torque = CI.lateral_accel_from_torque() + self.pid = PIDController(self.torque_params.kp, self.torque_params.ki, + k_f=self.torque_params.kf) + 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 self.torque_params.latAccelOffset = latAccelOffset self.torque_params.friction = friction + self.update_limits() - def update(self, active, CS, VM, params, steer_limited_by_controls, desired_curvature, calibrated_pose, curvature_limited): + def update_limits(self): + self.pid.set_limits(self.lateral_accel_from_torque(self.steer_max, self.torque_params), + self.lateral_accel_from_torque(-self.steer_max, self.torque_params)) + + def update(self, active, CS, VM, params, steer_limited_by_safety, desired_curvature, calibrated_pose, curvature_limited): pid_log = log.ControlsState.LateralTorqueState.new_message() if not active: output_torque = 0.0 pid_log.active = False else: - actual_curvature_vm = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) + actual_curvature = -VM.calc_curvature(math.radians(CS.steeringAngleDeg - params.angleOffsetDeg), CS.vEgo, params.roll) roll_compensation = params.roll * ACCELERATION_DUE_TO_GRAVITY - if self.use_steering_angle: - actual_curvature = actual_curvature_vm - curvature_deadzone = abs(VM.calc_curvature(math.radians(self.steering_angle_deadzone_deg), CS.vEgo, 0.0)) - else: - assert calibrated_pose is not None - actual_curvature_pose = calibrated_pose.angular_velocity.yaw / CS.vEgo - actual_curvature = np.interp(CS.vEgo, [2.0, 5.0], [actual_curvature_vm, actual_curvature_pose]) - curvature_deadzone = 0.0 + 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 @@ -68,36 +67,34 @@ class LatControlTorque(LatControl): setpoint = desired_lateral_accel + low_speed_factor * desired_curvature measurement = actual_lateral_accel + low_speed_factor * actual_curvature gravity_adjusted_lateral_accel = desired_lateral_accel - roll_compensation - torque_from_setpoint = self.torque_from_lateral_accel(LatControlInputs(setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, - setpoint, lateral_accel_deadzone, friction_compensation=False, gravity_adjusted=False) - torque_from_measurement = self.torque_from_lateral_accel(LatControlInputs(measurement, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, - measurement, lateral_accel_deadzone, friction_compensation=False, gravity_adjusted=False) - pid_log.error = float(torque_from_setpoint - torque_from_measurement) - ff = self.torque_from_lateral_accel(LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.torque_params, - desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, friction_compensation=True, - gravity_adjusted=True) - # 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) + # 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 + ff += get_friction(desired_lateral_accel - actual_lateral_accel, lateral_accel_deadzone, FRICTION_THRESHOLD, self.torque_params) - freeze_integrator = steer_limited_by_controls or CS.steeringPressed or CS.vEgo < 5 - output_torque = self.pid.update(pid_log.error, + freeze_integrator = steer_limited_by_safety or CS.steeringPressed or CS.vEgo < 5 + output_lataccel = self.pid.update(pid_log.error, feedforward=ff, speed=CS.vEgo, 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) pid_log.d = float(self.pid.d) pid_log.f = float(self.pid.f) - pid_log.output = float(-output_torque) + pid_log.output = float(-output_torque) # TODO: log lat accel? pid_log.actualLateralAccel = float(actual_lateral_accel) pid_log.desiredLateralAccel = float(desired_lateral_accel) - pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_controls, curvature_limited)) + pid_log.saturated = bool(self._check_saturation(self.steer_max - abs(output_torque) < 1e-3, CS, steer_limited_by_safety, curvature_limited)) # TODO left is positive in this convention return -output_torque, 0.0, pid_log diff --git a/selfdrive/controls/lib/lateral_mpc_lib/SConscript b/selfdrive/controls/lib/lateral_mpc_lib/SConscript index 2c03da06a6..c9ebf89207 100644 --- a/selfdrive/controls/lib/lateral_mpc_lib/SConscript +++ b/selfdrive/controls/lib/lateral_mpc_lib/SConscript @@ -1,4 +1,4 @@ -Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'opendbc_python', 'np_version') +Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'np_version') gen = "c_generated_code" @@ -55,18 +55,23 @@ source_list = ['lat_mpc.py', ] lenv = env.Clone() +acados_rel_path = Dir(gen).rel_path(Dir(f"#third_party/acados/{arch}/lib")) +lenv["RPATH"] += [lenv.Literal(f'\\$$ORIGIN/{acados_rel_path}')] lenv.Clean(generated_files, Dir(gen)) generated_lat = lenv.Command(generated_files, source_list, f"cd {Dir('.').abspath} && python3 lat_mpc.py") -lenv.Depends(generated_lat, [msgq_python, common_python, opendbc_python]) +lenv.Depends(generated_lat, [msgq_python, common_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CCFLAGS"].append("-Wno-unused") if arch != "Darwin": lenv["LINKFLAGS"].append("-Wl,--disable-new-dtags") +else: + lenv["LINKFLAGS"].append("-Wl,-install_name,@loader_path/libacados_ocp_solver_lat.dylib") + lenv["LINKFLAGS"].append(f"-Wl,-rpath,@loader_path/{acados_rel_path}") lib_solver = lenv.SharedLibrary(f"{gen}/acados_ocp_solver_lat", build_files, LIBS=['m', 'acados', 'hpipm', 'blasfeo', 'qpOASES_e']) @@ -78,7 +83,8 @@ libacados_ocp_solver_pxd = File(f'{gen}/acados_solver.pxd') libacados_ocp_solver_c = File(f'{gen}/acados_ocp_solver_pyx.c') lenv2 = envCython.Clone() -lenv2["LINKFLAGS"] += [lib_solver[0].get_labspath()] +lenv2["LIBPATH"] += [lib_solver[0].dir.abspath] +lenv2["RPATH"] += [lenv2.Literal('\\$$ORIGIN')] lenv2.Command(libacados_ocp_solver_c, [acados_ocp_solver_pyx, acados_ocp_solver_common, libacados_ocp_solver_pxd], f'cython' + \ @@ -86,6 +92,6 @@ lenv2.Command(libacados_ocp_solver_c, f' -I {libacados_ocp_solver_pxd.get_dir().get_labspath()}' + \ f' -I {acados_ocp_solver_common.get_dir().get_labspath()}' + \ f' {acados_ocp_solver_pyx.get_labspath()}') -lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c]) +lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=['acados_ocp_solver_lat']) lenv2.Depends(lib_cython, lib_solver) lenv2.Depends(libacados_ocp_solver_c, np_version) diff --git a/selfdrive/controls/lib/ldw.py b/selfdrive/controls/lib/ldw.py index caf03fec73..78a6d6cf6e 100644 --- a/selfdrive/controls/lib/ldw.py +++ b/selfdrive/controls/lib/ldw.py @@ -1,6 +1,6 @@ from cereal import log from openpilot.common.realtime import DT_CTRL -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV CAMERA_OFFSET = 0.04 diff --git a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript b/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript index 2a155717c0..164b965142 100644 --- a/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript +++ b/selfdrive/controls/lib/longitudinal_mpc_lib/SConscript @@ -1,4 +1,4 @@ -Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'opendbc_python', 'pandad_python', 'np_version') +Import('env', 'envCython', 'arch', 'msgq_python', 'common_python', 'pandad_python', 'np_version') gen = "c_generated_code" @@ -61,12 +61,13 @@ source_list = ['long_mpc.py', ] lenv = env.Clone() +acados_rel_path = Dir(gen).rel_path(Dir(f"#third_party/acados/{arch}/lib")) +lenv["RPATH"] += [lenv.Literal(f'\\$$ORIGIN/{acados_rel_path}')] lenv.Clean(generated_files, Dir(gen)) - generated_long = lenv.Command(generated_files, source_list, f"cd {Dir('.').abspath} && python3 long_mpc.py") -lenv.Depends(generated_long, [msgq_python, common_python, opendbc_python, pandad_python]) +lenv.Depends(generated_long, [msgq_python, common_python, pandad_python]) lenv["CFLAGS"].append("-DACADOS_WITH_QPOASES") lenv["CXXFLAGS"].append("-DACADOS_WITH_QPOASES") @@ -75,6 +76,7 @@ if arch != "Darwin": lenv["LINKFLAGS"].append("-Wl,--disable-new-dtags") else: lenv["LINKFLAGS"].append("-Wl,-install_name,@loader_path/libacados_ocp_solver_long.dylib") + lenv["LINKFLAGS"].append(f"-Wl,-rpath,@loader_path/{acados_rel_path}") lib_solver = lenv.SharedLibrary(f"{gen}/acados_ocp_solver_long", build_files, LIBS=['m', 'acados', 'hpipm', 'blasfeo', 'qpOASES_e']) @@ -86,7 +88,8 @@ libacados_ocp_solver_pxd = File(f'{gen}/acados_solver.pxd') libacados_ocp_solver_c = File(f'{gen}/acados_ocp_solver_pyx.c') lenv2 = envCython.Clone() -lenv2["LINKFLAGS"] += [lib_solver[0].get_labspath()] +lenv2["LIBPATH"] += [lib_solver[0].dir.abspath] +lenv2["RPATH"] += [lenv2.Literal('\\$$ORIGIN')] lenv2.Command(libacados_ocp_solver_c, [acados_ocp_solver_pyx, acados_ocp_solver_common, libacados_ocp_solver_pxd], f'cython' + \ @@ -94,6 +97,6 @@ lenv2.Command(libacados_ocp_solver_c, f' -I {libacados_ocp_solver_pxd.get_dir().get_labspath()}' + \ f' -I {acados_ocp_solver_common.get_dir().get_labspath()}' + \ f' {acados_ocp_solver_pyx.get_labspath()}') -lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c]) +lib_cython = lenv2.Program(f'{gen}/acados_ocp_solver_pyx.so', [libacados_ocp_solver_c], LIBS=['acados_ocp_solver_long']) lenv2.Depends(lib_cython, lib_solver) lenv2.Depends(libacados_ocp_solver_c, np_version) diff --git a/selfdrive/controls/lib/longitudinal_planner.py b/selfdrive/controls/lib/longitudinal_planner.py index 4e842f8d1b..96248b7132 100755 --- a/selfdrive/controls/lib/longitudinal_planner.py +++ b/selfdrive/controls/lib/longitudinal_planner.py @@ -4,14 +4,14 @@ import numpy as np import cereal.messaging as messaging from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import DT_MDL from openpilot.selfdrive.modeld.constants import ModelConstants from openpilot.selfdrive.controls.lib.longcontrol import LongCtrlState from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import LongitudinalMpc from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import T_IDXS as T_IDXS_MPC -from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_speed_error, get_accel_from_plan +from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N, get_accel_from_plan from openpilot.selfdrive.car.cruise import V_CRUISE_MAX, V_CRUISE_UNSET from openpilot.common.swaglog import cloudlog @@ -21,7 +21,7 @@ LON_MPC_STEP = 0.2 # first step is 0.2s A_CRUISE_MAX_VALS = [1.6, 1.2, 0.8, 0.6] A_CRUISE_MAX_BP = [0., 10.0, 25., 40.] CONTROL_N_T_IDX = ModelConstants.T_IDXS[:CONTROL_N] -ALLOW_THROTTLE_THRESHOLD = 0.5 +ALLOW_THROTTLE_THRESHOLD = 0.4 MIN_ALLOW_THROTTLE_SPEED = 2.5 # Lookup table for turns @@ -54,6 +54,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def __init__(self, CP, init_v=0.0, init_a=0.0, dt=DT_MDL): self.CP = CP self.mpc = LongitudinalMpc(dt=dt) + # TODO remove mpc modes when TR released self.mpc.mode = 'acc' LongitudinalPlannerSP.__init__(self, self.CP, self.mpc) self.fcw = False @@ -63,7 +64,6 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.a_desired = init_a self.v_desired_filter = FirstOrderFilter(init_v, 2.0, self.dt) self.prev_accel_clip = [ACCEL_MIN, ACCEL_MAX] - self.v_model_error = 0.0 self.output_a_target = 0.0 self.output_should_stop = False @@ -73,12 +73,12 @@ class LongitudinalPlanner(LongitudinalPlannerSP): self.solverExecutionTime = 0.0 @staticmethod - def parse_model(model_msg, model_error): + def parse_model(model_msg): if (len(model_msg.position.x) == ModelConstants.IDX_N and len(model_msg.velocity.x) == ModelConstants.IDX_N and len(model_msg.acceleration.x) == ModelConstants.IDX_N): - x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x) - model_error * T_IDXS_MPC - v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x) - model_error + x = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.position.x) + v = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.velocity.x) a = np.interp(T_IDXS_MPC, ModelConstants.T_IDXS, model_msg.acceleration.x) j = np.zeros(len(T_IDXS_MPC)) else: @@ -93,10 +93,14 @@ class LongitudinalPlanner(LongitudinalPlannerSP): return x, v, a, j, throttle_prob def update(self, sm): - self.mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' + mode = 'blended' if sm['selfdriveState'].experimentalMode else 'acc' + if not self.mlsim: + self.mpc.mode = mode LongitudinalPlannerSP.update(self, sm) if dec_mpc_mode := self.get_mpc_mode(): - self.mode = dec_mpc_mode + mode = dec_mpc_mode + if not self.mlsim: + self.mpc.mode = dec_mpc_mode if len(sm['carControl'].orientationNED) == 3: accel_coast = get_coast_accel(sm['carControl'].orientationNED[1]) @@ -119,7 +123,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): # No change cost when user is controlling the speed, or when standstill prev_accel_constraint = not (reset_state or sm['carState'].standstill) - if self.mode == 'acc': + if mode == 'acc': accel_clip = [ACCEL_MIN, get_max_accel(v_ego)] steer_angle_without_offset = sm['carState'].steeringAngleDeg - sm['liveParameters'].angleOffsetDeg accel_clip = limit_accel_in_turns(v_ego, steer_angle_without_offset, accel_clip, self.CP) @@ -133,9 +137,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): # Prevent divergence, smooth in current v_ego self.v_desired_filter.x = max(0.0, self.v_desired_filter.update(v_ego)) - # Compute model v_ego error - self.v_model_error = get_speed_error(sm['modelV2'], v_ego) - x, v, a, j, throttle_prob = self.parse_model(sm['modelV2'], self.v_model_error) + x, v, a, j, throttle_prob = self.parse_model(sm['modelV2']) # Don't clip at low speeds since throttle_prob doesn't account for creep self.allow_throttle = throttle_prob > ALLOW_THROTTLE_THRESHOLD or v_ego <= MIN_ALLOW_THROTTLE_SPEED @@ -171,7 +173,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): output_a_target_e2e = sm['modelV2'].action.desiredAcceleration output_should_stop_e2e = sm['modelV2'].action.shouldStop - if self.mode == 'acc': + if mode == 'acc' or not self.mlsim: output_a_target = output_a_target_mpc self.output_should_stop = output_should_stop_mpc else: @@ -186,7 +188,7 @@ class LongitudinalPlanner(LongitudinalPlannerSP): def publish(self, sm, pm): plan_send = messaging.new_message('longitudinalPlan') - plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState']) + plan_send.valid = sm.all_checks(service_list=['carState', 'controlsState', 'selfdriveState', 'radarState']) longitudinalPlan = plan_send.longitudinalPlan longitudinalPlan.modelMonoTime = sm.logMonoTime['modelV2'] diff --git a/selfdrive/controls/lib/tests/test_latcontrol.py b/selfdrive/controls/tests/test_latcontrol.py similarity index 91% rename from selfdrive/controls/lib/tests/test_latcontrol.py rename to selfdrive/controls/tests/test_latcontrol.py index 727f68195f..7906a31528 100644 --- a/selfdrive/controls/lib/tests/test_latcontrol.py +++ b/selfdrive/controls/tests/test_latcontrol.py @@ -5,6 +5,7 @@ from opendbc.car.car_helpers import interfaces from opendbc.car.honda.values import CAR as HONDA from opendbc.car.toyota.values import CAR as TOYOTA from opendbc.car.nissan.values import CAR as NISSAN +from opendbc.car.gm.values import CAR as GM from opendbc.car.vehicle_model import VehicleModel from openpilot.selfdrive.car.helpers import convert_to_capnp from openpilot.selfdrive.controls.lib.latcontrol_pid import LatControlPID @@ -17,7 +18,8 @@ from openpilot.sunnypilot.selfdrive.car import interfaces as sunnypilot_interfac class TestLatControl: - @parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), (NISSAN.NISSAN_LEAF, LatControlAngle)]) + @parameterized.expand([(HONDA.HONDA_CIVIC, LatControlPID), (TOYOTA.TOYOTA_RAV4, LatControlTorque), + (NISSAN.NISSAN_LEAF, LatControlAngle), (GM.CHEVROLET_BOLT_EUV, LatControlTorque)]) def test_saturation(self, car_name, controller): CarInterface = interfaces[car_name] CP = CarInterface.get_non_essential_params(car_name) diff --git a/selfdrive/debug/car/fw_versions.py b/selfdrive/debug/car/fw_versions.py index 03d066cdcb..6ae10d2fb2 100755 --- a/selfdrive/debug/car/fw_versions.py +++ b/selfdrive/debug/car/fw_versions.py @@ -47,15 +47,15 @@ if __name__ == "__main__": num_pandas = len(messaging.recv_one_retry(pandaStates_sock).pandaStates) - t = time.time() + t = time.monotonic() print("Getting vin...") set_obd_multiplexing(True) vin_rx_addr, vin_rx_bus, vin = get_vin(*can_callbacks, (0, 1)) print(f'RX: {hex(vin_rx_addr)}, BUS: {vin_rx_bus}, VIN: {vin}') - print(f"Getting VIN took {time.time() - t:.3f} s") + print(f"Getting VIN took {time.monotonic() - t:.3f} s") print() - t = time.time() + t = time.monotonic() fw_vers = get_fw_versions(*can_callbacks, set_obd_multiplexing, query_brand=args.brand, extra=extra, num_pandas=num_pandas, progress=True) _, candidates = match_fw_to_car(fw_vers, vin) @@ -71,4 +71,4 @@ if __name__ == "__main__": print() print("Possible matches:", candidates) - print(f"Getting fw took {time.time() - t:.3f} s") + print(f"Getting fw took {time.monotonic() - t:.3f} s") diff --git a/selfdrive/debug/car/vw_mqb_config.py b/selfdrive/debug/car/vw_mqb_config.py index 7ff5774a71..3c55642e40 100755 --- a/selfdrive/debug/car/vw_mqb_config.py +++ b/selfdrive/debug/car/vw_mqb_config.py @@ -40,8 +40,7 @@ if __name__ == "__main__": panda = Panda() panda.set_safety_mode(CarParams.SafetyModel.elm327) - bus = 1 if panda.has_obd() else 0 - uds_client = UdsClient(panda, MQB_EPS_CAN_ADDR, MQB_EPS_CAN_ADDR + RX_OFFSET, bus, timeout=0.2) + uds_client = UdsClient(panda, MQB_EPS_CAN_ADDR, MQB_EPS_CAN_ADDR + RX_OFFSET, 1, timeout=0.2) try: uds_client.diagnostic_session_control(SESSION_TYPE.EXTENDED_DIAGNOSTIC) diff --git a/selfdrive/debug/cpu_usage_stat.py b/selfdrive/debug/cpu_usage_stat.py index 234dfea3cc..397e9f35f5 100755 --- a/selfdrive/debug/cpu_usage_stat.py +++ b/selfdrive/debug/cpu_usage_stat.py @@ -37,8 +37,6 @@ monitored_proc_names = [ cpu_time_names = ['user', 'system', 'children_user', 'children_system'] -timer = getattr(time, 'monotonic', time.time) - def get_arg_parser(): parser = argparse.ArgumentParser(formatter_class=argparse.ArgumentDefaultsHelpFormatter) @@ -72,7 +70,7 @@ if __name__ == "__main__": print('Add monitored proc:', k) stats[k] = {'cpu_samples': defaultdict(list), 'min': defaultdict(lambda: None), 'max': defaultdict(lambda: None), 'avg': defaultdict(float), 'last_cpu_times': None, 'last_sys_time': None} - stats[k]['last_sys_time'] = timer() + stats[k]['last_sys_time'] = time.monotonic() stats[k]['last_cpu_times'] = p.cpu_times() monitored_procs.append(p) i = 0 @@ -80,7 +78,7 @@ if __name__ == "__main__": while True: for p in monitored_procs: k = ' '.join(p.cmdline()) - cur_sys_time = timer() + cur_sys_time = time.monotonic() cur_cpu_times = p.cpu_times() cpu_times = np.subtract(cur_cpu_times, stats[k]['last_cpu_times']) / (cur_sys_time - stats[k]['last_sys_time']) stats[k]['last_sys_time'] = cur_sys_time diff --git a/selfdrive/debug/run_process_on_route.py b/selfdrive/debug/run_process_on_route.py index 2ccb0fb3e7..d6743fedcb 100755 --- a/selfdrive/debug/run_process_on_route.py +++ b/selfdrive/debug/run_process_on_route.py @@ -1,23 +1,25 @@ #!/usr/bin/env python3 - import argparse from openpilot.selfdrive.test.process_replay.process_replay import CONFIGS, replay_process +from openpilot.selfdrive.test.process_replay.test_processes import EXCLUDED_PROCS from openpilot.tools.lib.logreader import LogReader, save_log +ALLOW_PROCS = {c.proc_name for c in CONFIGS} + if __name__ == "__main__": parser = argparse.ArgumentParser(description="Run process on route and create new logs", formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--fingerprint", help="The fingerprint to use") parser.add_argument("route", help="The route name to use") - parser.add_argument("process", nargs='+', help="The process(s) to run") + parser.add_argument("--fingerprint", help="The fingerprint to use") + parser.add_argument("--whitelist-procs", nargs='*', default=ALLOW_PROCS, help="Whitelist given processes (e.g. controlsd)") + parser.add_argument("--blacklist-procs", nargs='*', default=EXCLUDED_PROCS, help="Blacklist given processes (e.g. controlsd)") args = parser.parse_args() - cfgs = [c for c in CONFIGS if c.proc_name in args.process] - - lr = LogReader(args.route) - inputs = list(lr) + allowed_procs = set(args.whitelist_procs) - set(args.blacklist_procs) + cfgs = [c for c in CONFIGS if c.proc_name in allowed_procs] + inputs = list(LogReader(args.route)) outputs = replay_process(cfgs, inputs, fingerprint=args.fingerprint) # Remove message generated by the process under test and merge in the new messages @@ -25,6 +27,6 @@ if __name__ == "__main__": inputs = [i for i in inputs if i.which() not in produces] outputs = sorted(inputs + outputs, key=lambda x: x.logMonoTime) - fn = f"{args.route.replace('/', '_')}_{'_'.join(args.process)}.zst" + fn = f"{args.route.replace('/', '_')}_{'_'.join(allowed_procs)}.zst" print(f"Saving log to {fn}") save_log(fn, outputs) diff --git a/selfdrive/locationd/calibrationd.py b/selfdrive/locationd/calibrationd.py index 59f30dbf77..03c044982e 100755 --- a/selfdrive/locationd/calibrationd.py +++ b/selfdrive/locationd/calibrationd.py @@ -14,7 +14,7 @@ from typing import NoReturn from cereal import log, car import cereal.messaging as messaging from openpilot.system.hardware import HARDWARE -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.common.transformations.orientation import rot_from_euler, euler_from_rot diff --git a/selfdrive/locationd/lagd.py b/selfdrive/locationd/lagd.py index e6767db473..7f1d5fd032 100755 --- a/selfdrive/locationd/lagd.py +++ b/selfdrive/locationd/lagd.py @@ -12,6 +12,7 @@ from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose, fft_next_good_size, parabolic_peak_interp +from openpilot.sunnypilot.livedelay.lagd_toggle import LagdToggle BLOCK_SIZE = 100 BLOCK_NUM = 50 @@ -157,8 +158,7 @@ class LateralLagEstimator: block_count: int = BLOCK_NUM, min_valid_block_count: int = BLOCK_NUM_NEEDED, block_size: int = BLOCK_SIZE, window_sec: float = MOVING_WINDOW_SEC, okay_window_sec: float = MIN_OKAY_WINDOW_SEC, min_recovery_buffer_sec: float = MIN_RECOVERY_BUFFER_SEC, min_vego: float = MIN_VEGO, min_yr: float = MIN_ABS_YAW_RATE, min_ncc: float = MIN_NCC, - max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE, - enabled: bool = True): + max_lat_accel: float = MAX_LAT_ACCEL, max_lat_accel_diff: float = MAX_LAT_ACCEL_DIFF, min_confidence: float = MIN_CONFIDENCE): self.dt = dt self.window_sec = window_sec self.okay_window_sec = okay_window_sec @@ -173,7 +173,6 @@ class LateralLagEstimator: self.min_confidence = min_confidence self.max_lat_accel = max_lat_accel self.max_lat_accel_diff = max_lat_accel_diff - self.enabled = enabled self.t = 0.0 self.lat_active = False @@ -208,7 +207,7 @@ class LateralLagEstimator: liveDelay = msg.liveDelay valid_mean_lag, valid_std, current_mean_lag, current_std = self.block_avg.get() - if self.enabled and self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): + if self.block_avg.valid_blocks >= self.min_valid_block_count and not np.isnan(valid_mean_lag) and not np.isnan(valid_std): if valid_std > MAX_LAG_STD: liveDelay.status = log.LiveDelayData.Status.invalid else: @@ -371,14 +370,13 @@ def main(): params = Params() CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) - # TODO: remove me, lagd is in shadow mode on release - is_release = params.get_bool("IsReleaseBranch") - - lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency, enabled=not is_release) + lag_learner = LateralLagEstimator(CP, 1. / SERVICE_LIST['livePose'].frequency) if (initial_lag_params := retrieve_initial_lag(params, CP)) is not None: lag, valid_blocks = initial_lag_params lag_learner.reset(lag, valid_blocks) + lagd_toggle = LagdToggle(CP) + while True: sm.update() if sm.all_checks(): @@ -397,3 +395,6 @@ def main(): if sm.frame % 1200 == 0: # cache every 60 seconds params.put_nonblocking("LiveDelay", lag_msg_dat) + + if sm.frame % 60 == 0: # read from and write to params every 3 seconds + lagd_toggle.update(lag_msg) diff --git a/selfdrive/locationd/models/car_kf.py b/selfdrive/locationd/models/car_kf.py index 349897f371..27cc4ef9c9 100755 --- a/selfdrive/locationd/models/car_kf.py +++ b/selfdrive/locationd/models/car_kf.py @@ -5,7 +5,7 @@ from typing import Any import numpy as np -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.selfdrive.locationd.models.constants import ObservationKind from openpilot.common.swaglog import cloudlog @@ -93,8 +93,9 @@ class CarKalman(KalmanFilter): dim_state = CarKalman.initial_x.shape[0] name = CarKalman.name - # vehicle models comes from The Science of Vehicle Dynamics: Handling, Braking, and Ride of Road and Race Cars - # Model used is in 6.15 with formula from 6.198 + # Linearized single-track lateral dynamics, equations 7.211-7.213 + # Massimo Guiggiani, The Science of Vehicle Dynamics: Handling, Braking, and Ride of Road and Race Cars + # Springer Cham, 2023. doi: https://doi.org/10.1007/978-3-031-06461-6 # globals global_vars = [sp.Symbol(name) for name in CarKalman.global_vars] diff --git a/selfdrive/locationd/paramsd.py b/selfdrive/locationd/paramsd.py index ec15f501ae..b4084fe5bc 100755 --- a/selfdrive/locationd/paramsd.py +++ b/selfdrive/locationd/paramsd.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import os -import json import numpy as np import capnp @@ -207,12 +206,11 @@ def migrate_cached_vehicle_params_if_needed(params: Params): return try: - last_parameters_dict = json.loads(last_parameters_data_old) last_parameters_msg = messaging.new_message('liveParameters') last_parameters_msg.liveParameters.valid = True - last_parameters_msg.liveParameters.steerRatio = last_parameters_dict['steerRatio'] - last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_dict['stiffnessFactor'] - last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_dict['angleOffsetAverageDeg'] + last_parameters_msg.liveParameters.steerRatio = last_parameters_data_old['steerRatio'] + last_parameters_msg.liveParameters.stiffnessFactor = last_parameters_data_old['stiffnessFactor'] + last_parameters_msg.liveParameters.angleOffsetAverageDeg = last_parameters_data_old['angleOffsetAverageDeg'] params.put("LiveParametersV2", last_parameters_msg.to_bytes()) except Exception as e: cloudlog.error(f"Failed to perform parameter migration: {e}") diff --git a/selfdrive/locationd/test/test_lagd.py b/selfdrive/locationd/test/test_lagd.py index 8fb829edd8..a3dfce9c29 100644 --- a/selfdrive/locationd/test/test_lagd.py +++ b/selfdrive/locationd/test/test_lagd.py @@ -110,19 +110,6 @@ class TestLagd: assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED assert msg.liveDelay.calPerc == 100 - def test_disabled_estimator(self): - mocked_CP = car.CarParams(steerActuatorDelay=0.8) - estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, enabled=False) - lag_frames = 5 - process_messages(estimator, lag_frames, int(MIN_OKAY_WINDOW_SEC / DT) + BLOCK_NUM_NEEDED * BLOCK_SIZE) - msg = estimator.get_msg(True) - assert msg.liveDelay.status == 'unestimated' - assert np.allclose(msg.liveDelay.lateralDelay, 1.0, atol=0.01) - assert np.allclose(msg.liveDelay.lateralDelayEstimate, lag_frames * DT, atol=0.01) - assert np.allclose(msg.liveDelay.lateralDelayEstimateStd, 0.0, atol=0.01) - assert msg.liveDelay.validBlocks == BLOCK_NUM_NEEDED - assert msg.liveDelay.calPerc == 100 - def test_estimator_masking(self): mocked_CP, lag_frames = car.CarParams(steerActuatorDelay=0.8), random.randint(1, 19) estimator = LateralLagEstimator(mocked_CP, DT, min_recovery_buffer_sec=0.0, min_yr=0.0, min_valid_block_count=1) diff --git a/selfdrive/locationd/test/test_paramsd.py b/selfdrive/locationd/test/test_paramsd.py index 2129bf4386..dd496b7675 100644 --- a/selfdrive/locationd/test/test_paramsd.py +++ b/selfdrive/locationd/test/test_paramsd.py @@ -1,6 +1,5 @@ import random import numpy as np -import json from cereal import messaging from openpilot.selfdrive.locationd.paramsd import retrieve_initial_vehicle_params, migrate_cached_vehicle_params_if_needed @@ -47,7 +46,7 @@ class TestParamsd: CP = next(m for m in lr if m.which() == "carParams").carParams msg = get_random_live_parameters(CP) - params.put("LiveParameters", json.dumps(msg.liveParameters.to_dict())) + params.put("LiveParameters", msg.liveParameters.to_dict()) params.put("CarParamsPrevRoute", CP.as_builder().to_bytes()) params.remove("LiveParametersV2") @@ -60,7 +59,7 @@ class TestParamsd: def test_read_saved_params_corrupted_old_format(self): params = Params() - params.put("LiveParameters", b'\x00\x00\x02\x00\x01\x00:F\xde\xed\xae;') + params.put("LiveParameters", {}) params.remove("LiveParametersV2") migrate_cached_vehicle_params_if_needed(params) diff --git a/selfdrive/locationd/torqued.py b/selfdrive/locationd/torqued.py index 23bd99931b..e0409957f5 100755 --- a/selfdrive/locationd/torqued.py +++ b/selfdrive/locationd/torqued.py @@ -4,12 +4,13 @@ from collections import deque, defaultdict import cereal.messaging as messaging from cereal import car, log -from opendbc.car.vehicle_model import ACCELERATION_DUE_TO_GRAVITY +from openpilot.common.constants import ACCELERATION_DUE_TO_GRAVITY from openpilot.common.params import Params from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.locationd.helpers import PointBuckets, ParameterEstimator, PoseCalibrator, Pose +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay HISTORY = 5 # secs POINTS_PER_BUCKET = 1500 @@ -32,7 +33,7 @@ MIN_BUCKET_POINTS = np.array([100, 300, 500, 500, 500, 500, 300, 100]) MIN_ENGAGE_BUFFER = 2 # secs VERSION = 1 # bump this to invalidate old parameter caches -ALLOWED_CARS = ['toyota', 'hyundai', 'rivian'] +ALLOWED_CARS = ['toyota', 'hyundai', 'rivian', 'honda'] def slope2rot(slope): @@ -51,6 +52,8 @@ class TorqueBuckets(PointBuckets): class TorqueEstimator(ParameterEstimator): def __init__(self, CP, decimated=False, track_all_points=False): + super().__init__() + self.CP = CP self.hist_len = int(HISTORY / DT_MDL) self.lag = 0.0 self.track_all_points = track_all_points # for offline analysis, without max lateral accel or max steer torque filters @@ -95,6 +98,7 @@ class TorqueEstimator(ParameterEstimator): # try to restore cached params params = Params() + self.params = params params_cache = params.get("CarParamsPrevRoute") torque_cache = params.get("LiveTorqueParameters") if params_cache is not None and torque_cache is not None: @@ -176,7 +180,7 @@ class TorqueEstimator(ParameterEstimator): elif which == "liveCalibration": self.calibrator.feed_live_calib(msg) elif which == "liveDelay": - self.lag = msg.lateralDelay + self.lag = get_lat_delay(self.params, msg.lateralDelay) # calculate lateral accel from past steering torque elif which == "livePose": if len(self.raw_points['steer_torque']) == self.hist_len: diff --git a/selfdrive/modeld/SConscript b/selfdrive/modeld/SConscript index 545fcdcff7..6802b45e6e 100644 --- a/selfdrive/modeld/SConscript +++ b/selfdrive/modeld/SConscript @@ -50,23 +50,21 @@ def tg_compile(flags, model_name): # Compile small models for model_name in ['driving_vision', 'driving_policy', 'dmonitoring_model']: flags = { - 'larch64': 'QCOM=1', - 'Darwin': 'CPU=1 IMAGE=0 JIT=2', - }.get(arch, 'LLVM=1 LLVMOPT=1 BEAM=0 IMAGE=0 JIT=2') + 'larch64': 'DEV=QCOM', + 'Darwin': 'DEV=CPU IMAGE=0', + }.get(arch, 'DEV=LLVM IMAGE=0') tg_compile(flags, model_name) # Compile BIG model if USB GPU is available -import subprocess -from tinygrad import Device - -# because tg doesn't support multi-process -devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath) -if b"AMD" in devs: - del Device - print("USB GPU detected... building") - flags = "AMD=1 AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0" - bp = tg_compile(flags, "big_driving_policy") - bv = tg_compile(flags, "big_driving_vision") - lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially -else: - print("USB GPU not detected... skipping") +if "USBGPU" in os.environ: + import subprocess + # because tg doesn't support multi-process + devs = subprocess.check_output('python3 -c "from tinygrad import Device; print(list(Device.get_available_devices()))"', shell=True, cwd=env.Dir('#').abspath) + if b"AMD" in devs: + print("USB GPU detected... building") + flags = "DEV=AMD AMD_IFACE=USB AMD_LLVM=1 NOLOCALS=0 IMAGE=0" + bp = tg_compile(flags, "big_driving_policy") + bv = tg_compile(flags, "big_driving_vision") + lenv.SideEffect('lock', [bp, bv]) # tg doesn't support multi-process so build serially + else: + print("USB GPU not detected... skipping") diff --git a/selfdrive/modeld/dmonitoringmodeld.py b/selfdrive/modeld/dmonitoringmodeld.py index 2a3df3f652..215056e6e6 100755 --- a/selfdrive/modeld/dmonitoringmodeld.py +++ b/selfdrive/modeld/dmonitoringmodeld.py @@ -1,20 +1,15 @@ #!/usr/bin/env python3 import os from openpilot.system.hardware import TICI +os.environ['DEV'] = 'QCOM' if TICI else 'LLVM' from tinygrad.tensor import Tensor from tinygrad.dtype import dtypes -if TICI: - from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address - os.environ['QCOM'] = '1' -else: - os.environ['LLVM'] = '1' import math import time import pickle import ctypes import numpy as np from pathlib import Path -from setproctitle import setproctitle from cereal import messaging from cereal.messaging import PubMaster, SubMaster @@ -25,7 +20,7 @@ from openpilot.common.transformations.model import dmonitoringmodel_intrinsics, from openpilot.common.transformations.camera import _ar_ox_fisheye, _os_fisheye from openpilot.selfdrive.modeld.models.commonmodel_pyx import CLContext, MonitoringModelFrame from openpilot.selfdrive.modeld.parse_model_outputs import sigmoid -from openpilot.system import sentry +from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address MODEL_WIDTH, MODEL_HEIGHT = DM_INPUT_SIZE CALIB_LEN = 3 @@ -95,7 +90,7 @@ class ModelState: self.tensor_inputs['input_img'] = Tensor(self.frame.buffer_from_cl(input_img_cl).reshape((1, MODEL_WIDTH*MODEL_HEIGHT)), dtype=dtypes.uint8).realize() - output = self.model_run(**self.tensor_inputs).numpy().flatten() + output = self.model_run(**self.tensor_inputs).contiguous().realize().uop.base.buffer.numpy() t2 = time.perf_counter() return output, t2 - t1 @@ -133,12 +128,8 @@ def get_driverstate_packet(model_output: np.ndarray, frame_id: int, location_ts: def main(): - setproctitle(PROCESS_NAME) config_realtime_process(7, 5) - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - cl_context = CLContext() model = ModelState(cl_context) cloudlog.warning("models loaded, dmonitoringmodeld starting") @@ -180,7 +171,4 @@ if __name__ == "__main__": try: main() except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise + cloudlog.warning("got SIGINT") diff --git a/selfdrive/modeld/fill_model_msg.py b/selfdrive/modeld/fill_model_msg.py index a91c6395c7..a2b54b420e 100644 --- a/selfdrive/modeld/fill_model_msg.py +++ b/selfdrive/modeld/fill_model_msg.py @@ -89,13 +89,6 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(modelV2.orientation, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.T_FROM_CURRENT_EULER].T) fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) - # temporal pose - temporal_pose = modelV2.temporalPose - temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() - temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() - temporal_pose.rot = net_output_data['sim_pose'][0,ModelConstants.POSE_WIDTH//2:].tolist() - temporal_pose.rotStd = net_output_data['sim_pose_stds'][0,ModelConstants.POSE_WIDTH//2:].tolist() - # poly path fill_xyz_poly(driving_model_data.path, ModelConstants.POLY_PATH_DEGREE, *net_output_data['plan'][0,:,Plan.POSITION].T) diff --git a/selfdrive/modeld/modeld.py b/selfdrive/modeld/modeld.py index ac88dfbb5f..5f9ccf5072 100755 --- a/selfdrive/modeld/modeld.py +++ b/selfdrive/modeld/modeld.py @@ -1,16 +1,11 @@ #!/usr/bin/env python3 import os from openpilot.system.hardware import TICI +os.environ['DEV'] = 'QCOM' if TICI else 'LLVM' USBGPU = "USBGPU" in os.environ if USBGPU: - os.environ['AMD'] = '1' + os.environ['DEV'] = 'AMD' os.environ['AMD_IFACE'] = 'USB' -elif TICI: - from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address - os.environ['QCOM'] = '1' -else: - os.environ['LLVM'] = '1' - os.environ['JIT'] = '2' from tinygrad.tensor import Tensor from tinygrad.dtype import dtypes import time @@ -19,7 +14,6 @@ import numpy as np import cereal.messaging as messaging from cereal import car, log from pathlib import Path -from setproctitle import setproctitle from cereal.messaging import PubMaster, SubMaster from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from opendbc.car.car_helpers import get_demo_car_params @@ -29,13 +23,16 @@ from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.realtime import config_realtime_process, DT_MDL from openpilot.common.transformations.camera import DEVICE_CAMERAS from openpilot.common.transformations.model import get_warp_matrix -from openpilot.system import sentry from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper -from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value +from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value, get_curvature_from_plan from openpilot.selfdrive.modeld.parse_model_outputs import Parser from openpilot.selfdrive.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.selfdrive.modeld.constants import ModelConstants, Plan from openpilot.selfdrive.modeld.models.commonmodel_pyx import DrivingModelFrame, CLContext +from openpilot.selfdrive.modeld.runners.tinygrad_helpers import qcom_tensor_from_opencl_address + +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase PROCESS_NAME = "selfdrive.modeld.modeld" @@ -46,8 +43,8 @@ POLICY_PKL_PATH = Path(__file__).parent / 'models/driving_policy_tinygrad.pkl' VISION_METADATA_PATH = Path(__file__).parent / 'models/driving_vision_metadata.pkl' POLICY_METADATA_PATH = Path(__file__).parent / 'models/driving_policy_metadata.pkl' -LAT_SMOOTH_SECONDS = 0.0 -LONG_SMOOTH_SECONDS = 0.0 +LAT_SMOOTH_SECONDS = 0.1 +LONG_SMOOTH_SECONDS = 0.3 MIN_LAT_CONTROL_SPEED = 0.3 @@ -60,7 +57,11 @@ def get_action_from_model(model_output: dict[str, np.ndarray], prev_action: log. action_t=long_action_t) desired_accel = smooth_value(desired_accel, prev_action.desiredAcceleration, LONG_SMOOTH_SECONDS) - desired_curvature = model_output['desired_curvature'][0, 0] + desired_curvature = get_curvature_from_plan(plan[:,Plan.T_FROM_CURRENT_EULER][:,2], + plan[:,Plan.ORIENTATION_RATE][:,2], + ModelConstants.T_IDXS, + v_ego, + lat_action_t) if v_ego > MIN_LAT_CONTROL_SPEED: desired_curvature = smooth_value(desired_curvature, prev_action.desiredCurvature, LAT_SMOOTH_SECONDS) else: @@ -79,13 +80,15 @@ 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 ModelState: +class ModelState(ModelStateBase): frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] output: np.ndarray prev_desire: np.ndarray # for tracking the rising edge of the pulse def __init__(self, context: CLContext): + ModelStateBase.__init__(self) + self.LAT_SMOOTH_SECONDS = LAT_SMOOTH_SECONDS with open(VISION_METADATA_PATH, 'rb') as f: vision_metadata = pickle.load(f) self.vision_input_shapes = vision_metadata['input_shapes'] @@ -104,15 +107,12 @@ class ModelState: 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.full_prev_desired_curv = np.zeros((1, ModelConstants.FULL_HISTORY_BUFFER_LEN, ModelConstants.PREV_DESIRED_CURV_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': 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), - 'lateral_control_params': np.zeros((1, ModelConstants.LATERAL_CONTROL_PARAMS_LEN), dtype=np.float32), - 'prev_desired_curv': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.PREV_DESIRED_CURV_LEN), dtype=np.float32), 'features_buffer': np.zeros((1, ModelConstants.INPUT_HISTORY_BUFFER_LEN, ModelConstants.FEATURE_LEN), dtype=np.float32), } @@ -145,7 +145,6 @@ class ModelState: self.numpy_inputs['desire'][:] = 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'] - self.numpy_inputs['lateral_control_params'][:] = inputs['lateral_control_params'] imgs_cl = {name: self.frames[name].prepare(bufs[name], transforms[name].flatten()) for name in self.vision_input_names} if TICI and not USBGPU: @@ -161,21 +160,16 @@ class ModelState: if prepare_only: return None - self.vision_output = self.vision_run(**self.vision_inputs).numpy().flatten() + 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.policy_output = self.policy_run(**self.policy_inputs).numpy().flatten() + 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)) - # TODO model only uses last value now - self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] - self.full_prev_desired_curv[0,-1,:] = policy_outputs_dict['desired_curvature'][0, :] - self.numpy_inputs['prev_desired_curv'][:] = self.full_prev_desired_curv[0, self.temporal_idxs] - combined_outputs_dict = {**vision_outputs_dict, **policy_outputs_dict} if SEND_RAW_PRED: combined_outputs_dict['raw_pred'] = np.concatenate([self.vision_output.copy(), self.policy_output.copy()]) @@ -186,9 +180,6 @@ class ModelState: def main(demo=False): cloudlog.warning("modeld init") - sentry.set_tag("daemon", PROCESS_NAME) - cloudlog.bind(daemon=PROCESS_NAME) - setproctitle(PROCESS_NAME) if not USBGPU: # USB GPU currently saturates a core so can't do this yet, # also need to move the aux USB interrupts for good timings @@ -225,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() @@ -296,8 +287,9 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - lat_delay = sm["liveDelay"].lateralDelay + LAT_SMOOTH_SECONDS - lateral_control_params = np.array([v_ego, lat_delay], dtype=np.float32) + if sm.frame % 60 == 0: + model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) + lat_delay = model.lat_delay + LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -330,7 +322,6 @@ def main(demo=False): inputs:dict[str, np.ndarray] = { 'desire': vec_desire, 'traffic_convention': traffic_convention, - 'lateral_control_params': lateral_control_params, } mt1 = time.perf_counter() @@ -342,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 @@ -356,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 @@ -363,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 @@ -374,7 +368,4 @@ if __name__ == "__main__": args = parser.parse_args() main(demo=args.demo) except KeyboardInterrupt: - cloudlog.warning(f"child {PROCESS_NAME} got SIGINT") - except Exception: - sentry.capture_exception() - raise + cloudlog.warning("got SIGINT") diff --git a/selfdrive/modeld/models/driving_policy.onnx b/selfdrive/modeld/models/driving_policy.onnx index 343c3a0aa8..867a0d3b9b 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:5f714fb38bcd44b5d9f44e3a8e1595e1dfdf7558f0eec3485cf3f2dbb6dc7d8d -size 15971805 +oid sha256:04b763fb71efe57a8a4c4168a8043ecd58939015026ded0dc755ded6905ac251 +size 12343523 diff --git a/selfdrive/modeld/models/driving_vision.onnx b/selfdrive/modeld/models/driving_vision.onnx index 924d11304d..ce0dc927e7 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:3ac4867fbc618037e8d03143edbfeeae960f2025644b5dcf36c6665271b4f874 -size 34883375 +oid sha256:e66bb8d53eced3786ed71a59b55ffc6810944cb217f0518621cc76303260a1ef +size 46271991 diff --git a/selfdrive/modeld/parse_model_outputs.py b/selfdrive/modeld/parse_model_outputs.py index 810c44ccb9..038f51ca9c 100644 --- a/selfdrive/modeld/parse_model_outputs.py +++ b/selfdrive/modeld/parse_model_outputs.py @@ -22,9 +22,10 @@ class Parser: self.ignore_missing = ignore_missing def check_missing(self, outs, name): - if name not in outs and not self.ignore_missing: + missing = name not in outs + if missing and not self.ignore_missing: raise ValueError(f"Missing output {name}") - return name not in outs + return missing def parse_categorical_crossentropy(self, name, outs, out_shape=None): if self.check_missing(outs, name): @@ -84,28 +85,34 @@ class Parser: outs[name] = pred_mu_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape) + def is_mhp(self, outs, name, shape): + if self.check_missing(outs, name): + return False + if outs[name].shape[1] == 2 * shape: + return False + return True + def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(ModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) + self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) + self.parse_binary_crossentropy('lane_lines_prob', outs) self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(ModelConstants.DESIRE_PRED_LEN,ModelConstants.DESIRE_PRED_WIDTH)) self.parse_binary_crossentropy('meta', outs) + self.parse_binary_crossentropy('lead_prob', outs) + lead_mhp = self.is_mhp(outs, 'lead', ModelConstants.LEAD_MHP_SELECTION * ModelConstants.LEAD_TRAJ_LEN * ModelConstants.LEAD_WIDTH) + lead_in_N, lead_out_N = (ModelConstants.LEAD_MHP_N, ModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0) + lead_out_shape = (ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH) if lead_mhp else \ + (ModelConstants.LEAD_MHP_SELECTION, ModelConstants.LEAD_TRAJ_LEN, ModelConstants.LEAD_WIDTH) + self.parse_mdn('lead', outs, in_N=lead_in_N, out_N=lead_out_N, out_shape=lead_out_shape) return outs def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_mdn('plan', outs, in_N=ModelConstants.PLAN_MHP_N, out_N=ModelConstants.PLAN_MHP_SELECTION, - out_shape=(ModelConstants.IDX_N,ModelConstants.PLAN_WIDTH)) - self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_LANE_LINES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('road_edges', outs, in_N=0, out_N=0, out_shape=(ModelConstants.NUM_ROAD_EDGES,ModelConstants.IDX_N,ModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(ModelConstants.POSE_WIDTH,)) - self.parse_mdn('lead', outs, in_N=ModelConstants.LEAD_MHP_N, out_N=ModelConstants.LEAD_MHP_SELECTION, - out_shape=(ModelConstants.LEAD_TRAJ_LEN,ModelConstants.LEAD_WIDTH)) - if 'lat_planner_solution' in outs: - self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(ModelConstants.IDX_N,ModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - if 'desired_curvature' in outs: - self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(ModelConstants.DESIRED_CURV_WIDTH,)) - for k in ['lead_prob', 'lane_lines_prob']: - self.parse_binary_crossentropy(k, outs) + plan_mhp = self.is_mhp(outs, 'plan', ModelConstants.IDX_N * ModelConstants.PLAN_WIDTH) + plan_in_N, plan_out_N = (ModelConstants.PLAN_MHP_N, ModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0) + self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, out_shape=(ModelConstants.IDX_N, ModelConstants.PLAN_WIDTH)) self.parse_categorical_crossentropy('desire_state', outs, out_shape=(ModelConstants.DESIRE_PRED_WIDTH,)) return outs diff --git a/selfdrive/monitoring/helpers.py b/selfdrive/monitoring/helpers.py index 67776994f0..9649df9a2b 100644 --- a/selfdrive/monitoring/helpers.py +++ b/selfdrive/monitoring/helpers.py @@ -6,6 +6,7 @@ import cereal.messaging as messaging from openpilot.selfdrive.selfdrived.events import Events from openpilot.common.realtime import DT_DMON from openpilot.common.filter_simple import FirstOrderFilter +from openpilot.common.params import Params from openpilot.common.stat_live import RunningStatFilter from openpilot.common.transformations.camera import DEVICE_CAMERAS @@ -159,6 +160,9 @@ class DriverMonitoring: self.threshold_pre = self.settings._DISTRACTED_PRE_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME self.threshold_prompt = self.settings._DISTRACTED_PROMPT_TIME_TILL_TERMINAL / self.settings._DISTRACTED_TIME + self.params = Params() + self.too_distracted = self.params.get_bool("DriverTooDistracted") + self._reset_awareness() self._set_timers(active_monitoring=True) self._reset_events() @@ -305,10 +309,15 @@ class DriverMonitoring: def _update_events(self, driver_engaged, op_engaged, standstill, wrong_gear, car_speed): self._reset_events() - # Block engaging after max number of distrations or when alert active + # Block engaging until ignition cycle after max number or time of distractions if self.terminal_alert_cnt >= self.settings._MAX_TERMINAL_ALERTS or \ - self.terminal_time >= self.settings._MAX_TERMINAL_DURATION or \ - self.always_on and self.awareness <= self.threshold_prompt: + self.terminal_time >= self.settings._MAX_TERMINAL_DURATION: + if not self.too_distracted: + self.params.put_bool_nonblocking("DriverTooDistracted", True) + self.too_distracted = True + + # Always-on distraction lockout is temporary + if self.too_distracted or (self.always_on and self.awareness <= self.threshold_prompt): self.current_events.add(EventName.tooDistracted) always_on_valid = self.always_on and not wrong_gear diff --git a/selfdrive/pandad/can_list_to_can_capnp.cc b/selfdrive/pandad/can_list_to_can_capnp.cc index 6ad999da91..f2cf153453 100644 --- a/selfdrive/pandad/can_list_to_can_capnp.cc +++ b/selfdrive/pandad/can_list_to_can_capnp.cc @@ -1,5 +1,5 @@ #include "cereal/messaging/messaging.h" -#include "opendbc/can/common.h" +#include "selfdrive/pandad/can_types.h" void can_list_to_can_capnp_cpp(const std::vector &can_list, std::string &out, bool sendcan, bool valid) { MessageBuilder msg; diff --git a/selfdrive/pandad/can_types.h b/selfdrive/pandad/can_types.h new file mode 100644 index 0000000000..5fae581cfa --- /dev/null +++ b/selfdrive/pandad/can_types.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +struct CanFrame { + long src; + uint32_t address; + std::vector dat; +}; + +struct CanData { + uint64_t nanos; + std::vector frames; +}; \ No newline at end of file diff --git a/selfdrive/pandad/panda_safety.cc b/selfdrive/pandad/panda_safety.cc index 48589c4146..29e96032bf 100644 --- a/selfdrive/pandad/panda_safety.cc +++ b/selfdrive/pandad/panda_safety.cc @@ -2,9 +2,7 @@ #include "cereal/messaging/messaging.h" #include "common/swaglog.h" -void PandaSafety::configureSafetyMode() { - bool is_onroad = params_.getBool("IsOnroad"); - +void PandaSafety::configureSafetyMode(bool is_onroad) { if (is_onroad && !safety_configured_) { updateMultiplexingMode(); diff --git a/selfdrive/pandad/pandad.cc b/selfdrive/pandad/pandad.cc index af27276510..26f42add6c 100644 --- a/selfdrive/pandad/pandad.cc +++ b/selfdrive/pandad/pandad.cc @@ -36,8 +36,7 @@ // Ignition: // - If any of the ignition sources in any panda is high, ignition is high -#define MAX_IR_POWER 0.5f -#define MIN_IR_POWER 0.0f +#define MAX_IR_PANDA_VAL 50 #define CUTOFF_IL 400 #define SATURATE_IL 1000 @@ -85,6 +84,15 @@ Panda *connect(std::string serial="", uint32_t index=0) { panda->set_can_fd_auto(i, true); } + bool is_deprecated_panda = std::find(DEPRECATED_PANDA_TYPES.begin(), + DEPRECATED_PANDA_TYPES.end(), + panda->hw_type) != DEPRECATED_PANDA_TYPES.end(); + + if (is_deprecated_panda) { + LOGW("panda %s is deprecated (hw_type: %i), skipping firmware check...", panda->hw_serial().c_str(), static_cast(panda->hw_type)); + return panda.release(); + } + if (!panda->up_to_date() && !getenv("BOARDD_SKIP_FW_CHECK")) { throw std::runtime_error("Panda firmware out of date. Run pandad.py to update."); } @@ -169,7 +177,7 @@ void fill_panda_state(cereal::PandaState::Builder &ps, cereal::PandaState::Panda ps.setFanPower(health.fan_power); ps.setFanStallCount(health.fan_stall_count); ps.setSafetyRxChecksInvalid((bool)(health.safety_rx_checks_invalid_pkt)); - ps.setSpiChecksumErrorCount(health.spi_checksum_error_count_pkt); + ps.setSpiErrorCount(health.spi_error_count_pkt); ps.setSbu1Voltage(health.sbu1_voltage_mV / 1000.0f); ps.setSbu2Voltage(health.sbu2_voltage_mV / 1000.0f); } @@ -202,7 +210,7 @@ void fill_panda_can_state(cereal::PandaState::PandaCanState::Builder &cs, const cs.setCanCoreResetCnt(can_health.can_core_reset_cnt); } -std::optional send_panda_states(PubMaster *pm, const std::vector &pandas, bool spoofing_started, bool always_offroad) { +std::optional send_panda_states(PubMaster *pm, const std::vector &pandas, bool is_onroad, bool spoofing_started, bool always_offroad) { bool ignition_local = false; const uint32_t pandas_cnt = pandas.size(); @@ -269,8 +277,9 @@ std::optional send_panda_states(PubMaster *pm, const std::vector panda->set_power_saving(power_save_desired); } - // set safety mode to NO_OUTPUT when car is off. ELM327 is an alternative if we want to leverage athenad/connect - if (!ignition_local && (health.safety_mode_pkt != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) { + // set safety mode to NO_OUTPUT when car is off or we're not onroad. ELM327 is an alternative if we want to leverage athenad/connect + bool should_close_relay = !ignition_local || !is_onroad; + if (should_close_relay && (health.safety_mode_pkt != (uint8_t)(cereal::CarParams::SafetyModel::NO_OUTPUT))) { panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); } @@ -337,14 +346,14 @@ void send_peripheral_state(Panda *panda, PubMaster *pm) { pm->send("peripheralState", msg); } -void process_panda_state(std::vector &pandas, PubMaster *pm, bool engaged, bool engaged_mads, bool spoofing_started, bool always_offroad) { +void process_panda_state(std::vector &pandas, PubMaster *pm, bool engaged, bool engaged_mads, bool is_onroad, bool spoofing_started, bool always_offroad) { std::vector connected_serials; for (Panda *p : pandas) { connected_serials.push_back(p->hw_serial()); } { - auto ignition_opt = send_panda_states(pm, pandas, spoofing_started, always_offroad); + auto ignition_opt = send_panda_states(pm, pandas, is_onroad, spoofing_started, always_offroad); if (!ignition_opt) { LOGE("Failed to get ignition_opt"); return; @@ -384,8 +393,8 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) static uint64_t last_driver_camera_t = 0; static uint16_t prev_fan_speed = 999; - static uint16_t ir_pwr = 0; - static uint16_t prev_ir_pwr = 999; + static int ir_pwr = 0; + static int prev_ir_pwr = 999; static FirstOrderFilter integ_lines_filter(0, 30.0, 0.05); @@ -408,11 +417,11 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) last_driver_camera_t = event.getLogMonoTime(); if (cur_integ_lines <= CUTOFF_IL) { - ir_pwr = 100.0 * MIN_IR_POWER; + ir_pwr = 0; } else if (cur_integ_lines > SATURATE_IL) { - ir_pwr = 100.0 * MAX_IR_POWER; + ir_pwr = 100; } else { - ir_pwr = 100.0 * (MIN_IR_POWER + ((cur_integ_lines - CUTOFF_IL) * (MAX_IR_POWER - MIN_IR_POWER) / (SATURATE_IL - CUTOFF_IL))); + ir_pwr = 100 * (cur_integ_lines - CUTOFF_IL) / (SATURATE_IL - CUTOFF_IL); } } @@ -421,8 +430,9 @@ void process_peripheral_state(Panda *panda, PubMaster *pm, bool no_fan_control) ir_pwr = 0; } - if (ir_pwr != prev_ir_pwr || sm.frame % 100 == 0 || ir_pwr >= 50.0) { - panda->set_ir_pwr(ir_pwr); + if (ir_pwr != prev_ir_pwr || sm.frame % 100 == 0) { + int16_t ir_panda = util::map_val(ir_pwr, 0, 100, 0, MAX_IR_PANDA_VAL); + panda->set_ir_pwr(ir_panda); Hardware::set_ir_power(ir_pwr); prev_ir_pwr = ir_pwr; } @@ -437,6 +447,7 @@ void pandad_run(std::vector &pandas) { // Start the CAN send thread std::thread send_thread(can_send_thread, pandas, fake_send); + Params params; RateKeeper rk("pandad", 100); SubMaster sm({"selfdriveState", "selfdriveStateSP", "carParams"}); PubMaster pm({"can", "pandaStates", "peripheralState"}); @@ -444,6 +455,7 @@ void pandad_run(std::vector &pandas) { Panda *peripheral_panda = pandas[0]; bool engaged = false; bool engaged_mads = false; + bool is_onroad = false; bool always_offroad = false; // Main loop: receive CAN data and process states @@ -460,9 +472,10 @@ void pandad_run(std::vector &pandas) { sm.update(0); engaged = sm.allAliveAndValid({"selfdriveState"}) && sm["selfdriveState"].getSelfdriveState().getEnabled(); engaged_mads = process_mads_heartbeat(&sm); + is_onroad = params.getBool("IsOnroad"); always_offroad = panda_safety.getOffroadMode(); - process_panda_state(pandas, &pm, engaged, engaged_mads, spoofing_started, always_offroad); - panda_safety.configureSafetyMode(); + process_panda_state(pandas, &pm, engaged, engaged_mads, is_onroad, spoofing_started, always_offroad); + panda_safety.configureSafetyMode(is_onroad); } // Send out peripheralState at 2Hz @@ -487,7 +500,6 @@ void pandad_run(std::vector &pandas) { } // Close relay on exit to prevent a fault - const bool is_onroad = Params().getBool("IsOnroad"); if (is_onroad && !engaged) { for (auto &p : pandas) { if (p->connected()) { diff --git a/selfdrive/pandad/pandad.h b/selfdrive/pandad/pandad.h index 7b37fbe59a..cdacdc894b 100644 --- a/selfdrive/pandad/pandad.h +++ b/selfdrive/pandad/pandad.h @@ -8,10 +8,21 @@ void pandad_main_thread(std::vector serials); +// deprecated devices +static const std::vector DEPRECATED_PANDA_TYPES = { + cereal::PandaState::PandaType::WHITE_PANDA, + cereal::PandaState::PandaType::GREY_PANDA, + cereal::PandaState::PandaType::BLACK_PANDA, + cereal::PandaState::PandaType::PEDAL, + cereal::PandaState::PandaType::UNO, + cereal::PandaState::PandaType::RED_PANDA_V2 +}; + + class PandaSafety { public: PandaSafety(const std::vector &pandas) : pandas_(pandas) {} - void configureSafetyMode(); + void configureSafetyMode(bool is_onroad); bool getOffroadMode(); private: diff --git a/selfdrive/pandad/pandad.py b/selfdrive/pandad/pandad.py index b33ffb6473..265f673628 100755 --- a/selfdrive/pandad/pandad.py +++ b/selfdrive/pandad/pandad.py @@ -36,6 +36,12 @@ 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 deprecated from upstream + hw_type = panda.get_type() + if hw_type in Panda.DEPRECATED_DEVICES: + cloudlog.warning(f"Panda {panda_serial} is deprecated (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() @@ -87,7 +93,7 @@ def main() -> None: # TODO: remove this in the next AGNOS # wait until USB is up before counting - if time.monotonic() < 35.: + if time.monotonic() < 60.: no_internal_panda_count = 0 # Handle missing internal panda diff --git a/selfdrive/pandad/pandad_api_impl.pyx b/selfdrive/pandad/pandad_api_impl.pyx index 6683b843ae..aaecb8a594 100644 --- a/selfdrive/pandad/pandad_api_impl.pyx +++ b/selfdrive/pandad/pandad_api_impl.pyx @@ -6,7 +6,7 @@ from libcpp.string cimport string from libcpp cimport bool from libc.stdint cimport uint8_t, uint32_t, uint64_t -cdef extern from "opendbc/can/common.h": +cdef extern from "selfdrive/pandad/can_types.h": cdef struct CanFrame: long src uint32_t address diff --git a/selfdrive/pandad/spi.cc b/selfdrive/pandad/spi.cc index 108b11b9dc..b6ee57801a 100644 --- a/selfdrive/pandad/spi.cc +++ b/selfdrive/pandad/spi.cc @@ -66,58 +66,44 @@ PandaSpiHandle::PandaSpiHandle(std::string serial) : PandaCommsHandle(serial) { // 50MHz is the max of the 845. note that some older // revs of the comma three may not support this speed uint32_t spi_speed = 50000000; - - if (!util::file_exists(SPI_DEVICE)) { - goto fail; - } - - spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); - if (spi_fd < 0) { - LOGE("failed opening SPI device %d", spi_fd); - goto fail; - } - - // SPI settings - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_MODE, &spi_mode); - if (ret < 0) { - LOGE("failed setting SPI mode %d", ret); - goto fail; - } - - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed); - if (ret < 0) { - LOGE("failed setting SPI speed"); - goto fail; - } - - ret = util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word); - if (ret < 0) { - LOGE("failed setting SPI bits per word"); - goto fail; - } - - // get hw UID/serial - ret = control_read(0xc3, 0, 0, uid, uid_len, 100); - if (ret == uid_len) { - std::stringstream stream; - for (int i = 0; i < uid_len; i++) { - stream << std::hex << std::setw(2) << std::setfill('0') << int(uid[i]); + try { + if (!util::file_exists(SPI_DEVICE)) { + throw std::runtime_error("Error connecting to panda: SPI device not found"); } - hw_serial = stream.str(); - } else { - LOGD("failed to get serial %d", ret); - goto fail; - } - if (!serial.empty() && (serial != hw_serial)) { - goto fail; - } + spi_fd = open(SPI_DEVICE.c_str(), O_RDWR); + if (spi_fd < 0) { + LOGE("failed opening SPI device %d", spi_fd); + throw std::runtime_error("Error connecting to panda: failed to open SPI device"); + } + // SPI settings + util::safe_ioctl(spi_fd, SPI_IOC_WR_MODE, &spi_mode, "failed setting SPI mode"); + util::safe_ioctl(spi_fd, SPI_IOC_WR_MAX_SPEED_HZ, &spi_speed, "failed setting SPI speed"); + util::safe_ioctl(spi_fd, SPI_IOC_WR_BITS_PER_WORD, &spi_bits_per_word, "failed setting SPI bits per word"); + + // get hw UID/serial + ret = control_read(0xc3, 0, 0, uid, uid_len, 100); + if (ret == uid_len) { + std::stringstream stream; + for (int i = 0; i < uid_len; i++) { + stream << std::hex << std::setw(2) << std::setfill('0') << int(uid[i]); + } + hw_serial = stream.str(); + } else { + LOGD("failed to get serial %d", ret); + throw std::runtime_error("Error connecting to panda: failed to get serial"); + } + + if (!serial.empty() && (serial != hw_serial)) { + throw std::runtime_error("Error connecting to panda: serial mismatch"); + } + + } catch (...) { + cleanup(); + throw; + } return; - -fail: - cleanup(); - throw std::runtime_error("Error connecting to panda"); } PandaSpiHandle::~PandaSpiHandle() { diff --git a/selfdrive/selfdrived/alertmanager.py b/selfdrive/selfdrived/alertmanager.py index 79897404e4..86ea68809b 100644 --- a/selfdrive/selfdrived/alertmanager.py +++ b/selfdrive/selfdrived/alertmanager.py @@ -18,7 +18,7 @@ def set_offroad_alert(alert: str, show_alert: bool, extra_text: str = None) -> N if show_alert: a = copy.copy(OFFROAD_ALERTS[alert]) a['extra'] = extra_text or '' - Params().put(alert, json.dumps(a)) + Params().put(alert, a) else: Params().remove(alert) diff --git a/selfdrive/selfdrived/alerts_offroad.json b/selfdrive/selfdrived/alerts_offroad.json index 1668bf17fd..183c1f8547 100644 --- a/selfdrive/selfdrived/alerts_offroad.json +++ b/selfdrive/selfdrived/alerts_offroad.json @@ -25,18 +25,14 @@ "text": "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.", "severity": 0 }, - "Offroad_UnofficialHardware": { - "text": "Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support.", + "Offroad_UnregisteredHardware": { + "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_BadNvme": { - "text": "Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe.", - "severity": 1 - }, "Offroad_CarUnrecognized": { "text": "sunnypilot 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 @@ -48,5 +44,10 @@ "Offroad_OSMUpdateRequired": { "text": "OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display.\n\n%1", "severity": 0 + }, + "Offroad_ExcessiveActuation": { + "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." } } diff --git a/selfdrive/selfdrived/events.py b/selfdrive/selfdrived/events.py index 9bfc638db3..591e54918e 100755 --- a/selfdrive/selfdrived/events.py +++ b/selfdrive/selfdrived/events.py @@ -4,10 +4,12 @@ import os from cereal import log, car import cereal.messaging as messaging -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.git import get_short_branch from openpilot.common.realtime import DT_CTRL from openpilot.selfdrive.locationd.calibrationd import MIN_SPEED_FILTER +from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER +from openpilot.selfdrive.ui.feedback.feedbackd import FEEDBACK_MAX_DURATION from openpilot.sunnypilot.selfdrive.selfdrived.events_base import EventsBase, Priority, ET, Alert, \ NoEntryAlert, SoftDisableAlert, UserSoftDisableAlert, ImmediateDisableAlert, EngagementAlert, NormalPermanentAlert, \ @@ -40,6 +42,7 @@ class Events(EventsBase): return log.OnroadEvent + # ********** helper functions ********** def get_display_speed(speed_ms: float, metric: bool) -> str: speed = int(round(speed_ms * (CV.MS_TO_KPH if metric else CV.MS_TO_MPH))) @@ -92,6 +95,14 @@ def calibration_incomplete_alert(CP: car.CarParams, CS: car.CarState, sm: messag Priority.LOWEST, VisualAlert.none, AudibleAlert.none, .2) +def audio_feedback_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: + duration = FEEDBACK_MAX_DURATION - ((sm['audioFeedback'].blockNum + 1) * SAMPLE_BUFFER / SAMPLE_RATE) + return NormalPermanentAlert( + "Recording Audio Feedback", + f"{round(duration)} second{'s' if round(duration) != 1 else ''} remaining. Press again to save early.", + priority=Priority.LOW) + + # *** debug alerts *** def out_of_space_alert(CP: car.CarParams, CS: car.CarState, sm: messaging.SubMaster, metric: bool, soft_disable_time: int, personality) -> Alert: @@ -203,6 +214,7 @@ def invalid_lkas_setting_alert(CP: car.CarParams, CS: car.CarState, sm: messagin return NormalPermanentAlert("Invalid LKAS setting", text) + EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { # ********** events with no alerts ********** @@ -591,6 +603,11 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.NO_ENTRY: NoEntryAlert("Distraction Level Too High"), }, + EventName.excessiveActuation: { + ET.SOFT_DISABLE: soft_disable_alert("Excessive Actuation"), + ET.NO_ENTRY: NoEntryAlert("Excessive Actuation"), + }, + EventName.overheat: { ET.PERMANENT: overheat_alert, ET.SOFT_DISABLE: soft_disable_alert("System Overheated"), @@ -819,9 +836,13 @@ EVENTS: dict[int, dict[str, Alert | AlertCallbackType]] = { ET.WARNING: personality_changed_alert, }, - EventName.userFlag: { + EventName.userBookmark: { ET.PERMANENT: NormalPermanentAlert("Bookmark Saved", duration=1.5), }, + + EventName.audioFeedback: { + ET.PERMANENT: audio_feedback_alert, + }, } diff --git a/selfdrive/selfdrived/helpers.py b/selfdrive/selfdrived/helpers.py new file mode 100644 index 0000000000..f7468cbe43 --- /dev/null +++ b/selfdrive/selfdrived/helpers.py @@ -0,0 +1,54 @@ +import math +from enum import StrEnum, auto + +from cereal import car, messaging +from openpilot.common.realtime import DT_CTRL +from openpilot.selfdrive.locationd.helpers import Pose +from opendbc.car import ACCELERATION_DUE_TO_GRAVITY +from opendbc.car.lateral import ISO_LATERAL_ACCEL +from opendbc.car.interfaces import ACCEL_MIN, ACCEL_MAX + +MIN_EXCESSIVE_ACTUATION_COUNT = int(0.25 / DT_CTRL) +MIN_LATERAL_ENGAGE_BUFFER = int(1 / DT_CTRL) + + +class ExcessiveActuationType(StrEnum): + LONGITUDINAL = auto() + LATERAL = auto() + + +class ExcessiveActuationCheck: + def __init__(self): + self._excessive_counter = 0 + self._engaged_counter = 0 + + def update(self, sm: messaging.SubMaster, CS: car.CarState, calibrated_pose: Pose) -> ExcessiveActuationType | None: + # CS.aEgo can be noisy to bumps in the road, transitioning from standstill, losing traction, etc. + # longitudinal + accel_calibrated = calibrated_pose.acceleration.x + excessive_long_actuation = sm['carControl'].longActive and (accel_calibrated > ACCEL_MAX * 2 or accel_calibrated < ACCEL_MIN * 2) + + # lateral + yaw_rate = calibrated_pose.angular_velocity.yaw + roll = sm['liveParameters'].roll + roll_compensated_lateral_accel = (CS.vEgo * yaw_rate) - (math.sin(roll) * ACCELERATION_DUE_TO_GRAVITY) + + # Prevent false positives after overriding + excessive_lat_actuation = False + self._engaged_counter = self._engaged_counter + 1 if sm['carControl'].latActive and not CS.steeringPressed else 0 + if self._engaged_counter > MIN_LATERAL_ENGAGE_BUFFER: + if abs(roll_compensated_lateral_accel) > ISO_LATERAL_ACCEL * 2: + excessive_lat_actuation = True + + # livePose acceleration can be noisy due to bad mounting or aliased livePose measurements + livepose_valid = abs(CS.aEgo - accel_calibrated) < 2 + self._excessive_counter = self._excessive_counter + 1 if livepose_valid and (excessive_long_actuation or excessive_lat_actuation) else 0 + + excessive_type = None + if self._excessive_counter > MIN_EXCESSIVE_ACTUATION_COUNT: + if excessive_long_actuation: + excessive_type = ExcessiveActuationType.LONGITUDINAL + else: + excessive_type = ExcessiveActuationType.LATERAL + + return excessive_type diff --git a/selfdrive/selfdrived/selfdrived.py b/selfdrive/selfdrived/selfdrived.py index dfda2c31f7..c56c7fe65f 100755 --- a/selfdrive/selfdrived/selfdrived.py +++ b/selfdrive/selfdrived/selfdrived.py @@ -15,7 +15,9 @@ from openpilot.common.swaglog import cloudlog from openpilot.common.gps import get_gps_location_service from openpilot.selfdrive.car.car_specific import CarSpecificEvents +from openpilot.selfdrive.locationd.helpers import PoseCalibrator, Pose from openpilot.selfdrive.selfdrived.events import Events, ET +from openpilot.selfdrive.selfdrived.helpers import ExcessiveActuationCheck from openpilot.selfdrive.selfdrived.state import StateMachine from openpilot.selfdrive.selfdrived.alertmanager import AlertManager, set_offroad_alert @@ -30,6 +32,7 @@ from openpilot.sunnypilot.selfdrive.selfdrived.events import EventsSP REPLAY = "REPLAY" in os.environ SIMULATION = "SIMULATION" in os.environ TESTING_CLOSET = "TESTING_CLOSET" in os.environ + LONGITUDINAL_PERSONALITY_MAP = {v: k for k, v in log.LongitudinalPersonality.schema.enumerants.items()} ThermalStatus = log.DeviceState.ThermalStatus @@ -67,6 +70,11 @@ class SelfdriveD(CruiseHelper): self.car_events = CarSpecificEvents(self.CP) + self.pose_calibrator = PoseCalibrator() + self.calibrated_pose: Pose | None = None + self.excessive_actuation_check = ExcessiveActuationCheck() + self.excessive_actuation = self.params.get("Offroad_ExcessiveActuation") is not None + # Setup sockets self.pm = messaging.PubMaster(['selfdriveState', 'onroadEvents'] + ['selfdriveStateSP', 'onroadEventsSP']) @@ -78,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: @@ -87,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', 'userFlag'] + \ + '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)) @@ -121,7 +130,7 @@ class SelfdriveD(CruiseHelper): self.logged_comm_issue = None self.not_running_prev = None self.experimental_mode = False - self.personality = self.read_personality_param() + self.personality = self.params.get("LongitudinalPersonality", return_default=True) self.recalibrating_seen = False self.state_machine = StateMachine() self.rk = Ratekeeper(100, print_delay_threshold=None) @@ -135,7 +144,8 @@ class SelfdriveD(CruiseHelper): self.ignored_processes.update({'mapd'}) # Determine startup event - self.startup_event = EventName.startup if build_metadata.openpilot.comma_remote and build_metadata.tested_channel else EventName.startupMaster + is_remote = build_metadata.openpilot.comma_remote or build_metadata.openpilot.sunnypilot_remote + self.startup_event = EventName.startup if is_remote and build_metadata.tested_channel else EventName.startupMaster if not car_recognized: self.startup_event = EventName.startupNoCar elif car_recognized and self.CP.passive: @@ -182,9 +192,12 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.selfdriveInitializing) return - # Check for user flag (bookmark) press - if self.sm.updated['userFlag']: - self.events.add(EventName.userFlag) + # Check for user bookmark press (bookmark button or end of LKAS button feedback) + if self.sm.updated['userBookmark']: + self.events.add(EventName.userBookmark) + + if self.sm.updated['audioFeedback']: + self.events.add(EventName.audioFeedback) # Don't add any more events while in dashcam mode if self.CP.passive: @@ -253,6 +266,26 @@ class SelfdriveD(CruiseHelper): if self.sm['driverAssistance'].leftLaneDeparture or self.sm['driverAssistance'].rightLaneDeparture: self.events.add(EventName.ldw) + # ****************************************************************************************** + # NOTE: To fork maintainers. + # Disabling or nerfing safety features will get you and your users banned from our servers. + # We recommend that you do not change these numbers from the defaults. + if self.sm.updated['liveCalibration']: + self.pose_calibrator.feed_live_calib(self.sm['liveCalibration']) + if self.sm.updated['livePose']: + device_pose = Pose.from_live_pose(self.sm['livePose']) + self.calibrated_pose = self.pose_calibrator.build_calibrated_pose(device_pose) + + if self.calibrated_pose is not None: + excessive_actuation = self.excessive_actuation_check.update(self.sm, CS, self.calibrated_pose) + if not self.excessive_actuation and excessive_actuation is not None: + set_offroad_alert("Offroad_ExcessiveActuation", True, extra_text=str(excessive_actuation)) + self.excessive_actuation = True + + if self.excessive_actuation: + self.events.add(EventName.excessiveActuation) + # ****************************************************************************************** + # Handle lane change if self.sm['modelV2'].meta.laneChangeState == LaneChangeState.preLaneChange: direction = self.sm['modelV2'].meta.laneChangeDirection @@ -268,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): @@ -304,13 +344,12 @@ class SelfdriveD(CruiseHelper): self.events.add(EventName.cameraFrameRate) if not REPLAY and self.rk.lagging: self.events.add(EventName.selfdrivedLagging) - if not self.sm.valid['radarState']: - if self.sm['radarState'].radarErrors.canError: - self.events.add(EventName.canError) - elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: - self.events.add(EventName.radarTempUnavailable) - else: - self.events.add(EventName.radarFault) + if self.sm['radarState'].radarErrors.canError: + self.events.add(EventName.canError) + elif self.sm['radarState'].radarErrors.radarUnavailableTemporary: + self.events.add(EventName.radarTempUnavailable) + elif any(self.sm['radarState'].radarErrors.to_dict().values()): + self.events.add(EventName.radarFault) if not self.sm.valid['pandaStates']: self.events.add(EventName.usbError) if CS.canTimeout: @@ -406,13 +445,13 @@ class SelfdriveD(CruiseHelper): if any(not be.pressed and be.type == ButtonType.gapAdjustCruise for be in CS.buttonEvents): if not self.experimental_mode_switched: self.personality = (self.personality - 1) % 3 - self.params.put_nonblocking('LongitudinalPersonality', str(self.personality)) + self.params.put_nonblocking('LongitudinalPersonality', self.personality) self.events.add(EventName.personalityChanged) self.experimental_mode_switched = False def data_sample(self): - car_state = messaging.recv_one(self.car_state_sock) - CS = car_state.carState if car_state else self.CS_prev + _car_state = messaging.recv_one(self.car_state_sock) + CS = _car_state.carState if _car_state else self.CS_prev self.sm.update(0) @@ -537,19 +576,13 @@ class SelfdriveD(CruiseHelper): self.CS_prev = CS - def read_personality_param(self): - try: - return int(self.params.get('LongitudinalPersonality')) - except (ValueError, TypeError): - return log.LongitudinalPersonality.standard - def params_thread(self, evt): while not evt.is_set(): self.is_metric = self.params.get_bool("IsMetric") self.is_ldw_enabled = self.params.get_bool("IsLdwEnabled") self.disengage_on_accelerator = self.params.get_bool("DisengageOnAccelerator") self.experimental_mode = self.params.get_bool("ExperimentalMode") and self.CP.openpilotLongitudinalControl - self.personality = self.read_personality_param() + self.personality = self.params.get("LongitudinalPersonality", return_default=True) self.mads.read_params() time.sleep(0.1) diff --git a/selfdrive/selfdrived/tests/test_alerts.py b/selfdrive/selfdrived/tests/test_alerts.py index 75365b7a52..a1867f2289 100644 --- a/selfdrive/selfdrived/tests/test_alerts.py +++ b/selfdrive/selfdrived/tests/test_alerts.py @@ -111,7 +111,7 @@ class TestAlerts: alert = copy.copy(self.offroad_alerts[a]) set_offroad_alert(a, True) alert['extra'] = '' - assert json.dumps(alert) == params.get(a, encoding='utf8') + assert alert == params.get(a) # then delete it set_offroad_alert(a, False) @@ -125,6 +125,6 @@ class TestAlerts: alert = self.offroad_alerts[a] set_offroad_alert(a, True, extra_text="a"*i) - written_alert = json.loads(params.get(a, encoding='utf8')) + written_alert = params.get(a) assert "a"*i == written_alert['extra'] assert alert["text"] == written_alert['text'] diff --git a/selfdrive/test/process_replay/process_replay.py b/selfdrive/test/process_replay/process_replay.py index bc5ccf7845..b1190c25e9 100755 --- a/selfdrive/test/process_replay/process_replay.py +++ b/selfdrive/test/process_replay/process_replay.py @@ -4,8 +4,9 @@ import time import copy import heapq import signal -from collections import Counter, OrderedDict +from collections import Counter from dataclasses import dataclass, field +from itertools import islice from typing import Any from collections.abc import Callable, Iterable from tqdm import tqdm @@ -16,12 +17,13 @@ import cereal.messaging as messaging from cereal import car from cereal.services import SERVICE_LIST from msgq.visionipc import VisionIpcServer, get_endpoint_name as vipc_get_endpoint_name +from opendbc.car.can_definitions import CanData from opendbc.car.car_helpers import get_car, interfaces from openpilot.common.params import Params from openpilot.common.prefix import OpenpilotPrefix from openpilot.common.timeout import Timeout from openpilot.common.realtime import DT_CTRL -from openpilot.selfdrive.car.card import can_comm_callbacks, convert_to_capnp +from openpilot.selfdrive.car.card import convert_to_capnp from openpilot.system.manager.process_config import managed_processes from openpilot.selfdrive.test.process_replay.vision_meta import meta_from_camera_state, available_streams from openpilot.selfdrive.test.process_replay.migration import migrate_all @@ -30,22 +32,10 @@ from openpilot.tools.lib.logreader import LogIterable from openpilot.tools.lib.framereader import FrameReader # Numpy gives different results based on CPU features after version 19 -NUMPY_TOLERANCE = 1e-7 +NUMPY_TOLERANCE = 1e-2 PROC_REPLAY_DIR = os.path.dirname(os.path.abspath(__file__)) FAKEDATA = os.path.join(PROC_REPLAY_DIR, "fakedata/") -class DummySocket: - def __init__(self): - self.data: list[bytes] = [] - - def receive(self, non_blocking: bool = False) -> bytes | None: - if non_blocking: - return None - - return self.data.pop() - - def send(self, data: bytes): - self.data.append(data) class LauncherWithCapture: def __init__(self, capture: ProcessOutputCapture, launcher: Callable): @@ -63,8 +53,7 @@ class ReplayContext: self.pubs = cfg.pubs self.main_pub = cfg.main_pub self.main_pub_drained = cfg.main_pub_drained - self.unlocked_pubs = cfg.unlocked_pubs - assert(len(self.pubs) != 0 or self.main_pub is not None) + assert len(self.pubs) != 0 or self.main_pub is not None def __enter__(self): self.open_context() @@ -79,9 +68,8 @@ class ReplayContext: messaging.set_fake_prefix(self.proc_name) if self.main_pub is None: - self.events = OrderedDict() - pubs_with_events = [pub for pub in self.pubs if pub not in self.unlocked_pubs] - for pub in pubs_with_events: + self.events = {} + for pub in self.pubs: self.events[pub] = messaging.fake_event_handle(pub, enable=True) else: self.events = {self.main_pub: messaging.fake_event_handle(self.main_pub, enable=True)} @@ -138,16 +126,21 @@ class ProcessConfig: processing_time: float = 0.001 timeout: int = 30 simulation: bool = True + # Set to service process receives on first main_pub: str | None = None - main_pub_drained: bool = True + main_pub_drained: bool = False vision_pubs: list[str] = field(default_factory=list) ignore_alive_pubs: list[str] = field(default_factory=list) - unlocked_pubs: list[str] = field(default_factory=list) + + def __post_init__(self): + # If the process is polling a service, we can just lock that one to speed up replay + if self.main_pub is None and isinstance(self.should_recv_callback, MessageBasedRcvCallback): + self.main_pub = self.should_recv_callback.trigger_msg_type class ProcessContainer: def __init__(self, cfg: ProcessConfig): - self.prefix = OpenpilotPrefix(clean_dirs_on_exit=False) + self.prefix = OpenpilotPrefix(create_dirs_on_enter=False, clean_dirs_on_exit=False) self.cfg = copy.deepcopy(cfg) self.process = copy.deepcopy(managed_processes[cfg.proc_name]) self.msg_queue: list[capnp._DynamicStructReader] = [] @@ -229,6 +222,7 @@ class ProcessContainer: fingerprint: str | None, capture_output: bool ): with self.prefix as p: + self.prefix.create_dirs() self._setup_env(params_config, environ_config) if self.cfg.config_callback is not None: @@ -254,11 +248,6 @@ class ProcessContainer: if self.cfg.init_callback is not None: self.cfg.init_callback(self.rc, self.pm, all_msgs, fingerprint) - # wait for process to startup - with Timeout(10, error_msg=f"timed out waiting for process to start: {repr(self.cfg.proc_name)}"): - while not all(self.pm.all_readers_updated(s) for s in self.cfg.pubs if s not in self.cfg.ignore_alive_pubs): - time.sleep(0) - def stop(self): with self.prefix: self.process.signal(signal.SIGKILL) @@ -267,28 +256,42 @@ class ProcessContainer: self.prefix.clean_dirs() self._clean_env() + def get_output_msgs(self, start_time: int): + assert self.rc and self.sockets + + output_msgs = [] + self.rc.wait_for_recv_called() + for socket in self.sockets: + ms = messaging.drain_sock(socket) + for m in ms: + m = m.as_builder() + m.logMonoTime = start_time + int(self.cfg.processing_time * 1e9) + output_msgs.append(m.as_reader()) + return output_msgs + def run_step(self, msg: capnp._DynamicStructReader, frs: dict[str, FrameReader] | None) -> list[capnp._DynamicStructReader]: assert self.rc and self.pm and self.sockets and self.process.proc output_msgs = [] - with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"): - end_of_cycle = True - if self.cfg.should_recv_callback is not None: - end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt) - - self.msg_queue.append(msg) - if end_of_cycle: - self.rc.wait_for_recv_called() + end_of_cycle = True + if self.cfg.should_recv_callback is not None: + end_of_cycle = self.cfg.should_recv_callback(msg, self.cfg, self.cnt) + self.msg_queue.append(msg) + if end_of_cycle: + with self.prefix, Timeout(self.cfg.timeout, error_msg=f"timed out testing process {repr(self.cfg.proc_name)}"): # call recv to let sub-sockets reconnect, after we know the process is ready if self.cnt == 0: for s in self.sockets: messaging.recv_one_or_none(s) - # empty recv on drained pub indicates the end of messages, only do that if there're any + # certain processes use drain_sock. need to cause empty recv to break from this loop trigger_empty_recv = False if self.cfg.main_pub and self.cfg.main_pub_drained: - trigger_empty_recv = next((True for m in self.msg_queue if m.which() == self.cfg.main_pub), False) + trigger_empty_recv = any(m.which() == self.cfg.main_pub for m in self.msg_queue) + + # get output msgs from previous inputs + output_msgs = self.get_output_msgs(msg.logMonoTime) for m in self.msg_queue: self.pm.send(m.which(), m.as_builder()) @@ -303,14 +306,8 @@ class ProcessContainer: self.msg_queue = [] self.rc.unlock_sockets() - self.rc.wait_for_next_recv(trigger_empty_recv) - - for socket in self.sockets: - ms = messaging.drain_sock(socket) - for m in ms: - m = m.as_builder() - m.logMonoTime = msg.logMonoTime + int(self.cfg.processing_time * 1e9) - output_msgs.append(m.as_reader()) + if trigger_empty_recv: + self.rc.unlock_sockets() self.cnt += 1 assert self.process.proc.is_alive() @@ -320,7 +317,7 @@ class ProcessContainer: def card_fingerprint_callback(rc, pm, msgs, fingerprint): print("start fingerprinting") params = Params() - canmsgs = [msg for msg in msgs if msg.which() == "can"][:300] + canmsgs = list(islice((m for m in msgs if m.which() == "can"), 300)) # card expects one arbitrary can and pandaState rc.send_sync(pm, "can", messaging.new_message("can", 1)) @@ -345,36 +342,27 @@ def get_car_params_callback(rc, pm, msgs, fingerprint): CP = CarInterface.get_non_essential_params(fingerprint) CP_SP = CarInterface.get_non_essential_params_sp(CP, fingerprint) else: - can = DummySocket() - sendcan = DummySocket() - - canmsgs = [msg for msg in msgs if msg.which() == "can"] + can_msgs = ([CanData(can.address, can.dat, can.src) for can in m.can] for m in msgs if m.which() == "can") cached_params_raw = params.get("CarParamsCache") - has_cached_cp = cached_params_raw is not None - assert len(canmsgs) != 0, "CAN messages are required for fingerprinting" - assert os.environ.get("SKIP_FW_QUERY", False) or has_cached_cp, \ + assert next(can_msgs, None), "CAN messages are required for fingerprinting" + assert os.environ.get("SKIP_FW_QUERY", False) or cached_params_raw is not None, \ "CarParamsCache is required for fingerprinting. Make sure to keep carParams msgs in the logs." - for m in canmsgs[:300]: - can.send(m.as_builder().to_bytes()) - can_callbacks = can_comm_callbacks(can, sendcan) + def can_recv(wait_for_one: bool = False) -> list[list[CanData]]: + return [next(can_msgs, [])] cached_params = None - if has_cached_cp: + if cached_params_raw is not None: with car.CarParams.from_bytes(cached_params_raw) as _cached_params: cached_params = _cached_params - _CI = get_car(*can_callbacks, lambda obd: None, Params().get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params) + _CI = get_car(can_recv, lambda _msgs: None, lambda obd: None, params.get_bool("AlphaLongitudinalEnabled"), False, cached_params=cached_params) CP, CP_SP = _CI.CP, _CI.CP_SP params.put("CarParams", CP.to_bytes()) params.put("CarParamsSP", convert_to_capnp(CP_SP).to_bytes()) -def selfdrived_rcv_callback(msg, cfg, frame): - return (frame - 1) == 0 or msg.which() == 'carState' - - def card_rcv_callback(msg, cfg, frame): # no sendcan until card is initialized if msg.which() != "can": @@ -389,21 +377,6 @@ def card_rcv_callback(msg, cfg, frame): return len(socks) > 0 -def calibration_rcv_callback(msg, cfg, frame): - # calibrationd publishes 1 calibrationData every 5 cameraOdometry packets. - # should_recv always true to increment frame - return (frame - 1) == 0 or msg.which() == 'cameraOdometry' - - -def torqued_rcv_callback(msg, cfg, frame): - # should_recv always true to increment frame - return (frame - 1) == 0 or msg.which() == 'livePose' - - -def dmonitoringmodeld_rcv_callback(msg, cfg, frame): - return msg.which() == "driverCameraState" - - class ModeldCameraSyncRcvCallback: def __init__(self): self.road_present = False @@ -428,26 +401,13 @@ class ModeldCameraSyncRcvCallback: class MessageBasedRcvCallback: - def __init__(self, trigger_msg_type): + def __init__(self, trigger_msg_type: str, first_frame: bool = False): self.trigger_msg_type = trigger_msg_type + self.first_frame = first_frame def __call__(self, msg, cfg, frame): - return msg.which() == self.trigger_msg_type - - -class FrequencyBasedRcvCallback: - def __init__(self, trigger_msg_type): - self.trigger_msg_type = trigger_msg_type - - def __call__(self, msg, cfg, frame): - if msg.which() != self.trigger_msg_type: - return False - - resp_sockets = [ - s for s in cfg.subs - if frame % max(1, int(SERVICE_LIST[msg.which()].frequency / SERVICE_LIST[s].frequency)) == 0 - ] - return bool(len(resp_sockets)) + # publish on first frame or trigger msg + return ((frame - 1) == 0 and self.first_frame) or msg.which() == self.trigger_msg_type def selfdrived_config_callback(params, cfg, lr): @@ -462,16 +422,16 @@ CONFIGS = [ proc_name="selfdrived", pubs=[ "carState", "deviceState", "pandaStates", "peripheralState", "liveCalibration", "driverMonitoringState", - "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", - "modelV2", "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", - "liveTorqueParameters", "accelerometer", "gyroscope", "carOutput", - "gpsLocationExternal", "gpsLocation", "controlsState", "carControl", "driverAssistance", "alertDebug", + "longitudinalPlan", "livePose", "liveDelay", "liveParameters", "radarState", "modelV2", + "driverCameraState", "roadCameraState", "wideRoadCameraState", "managerState", "liveTorqueParameters", + "accelerometer", "gyroscope", "carOutput", "gpsLocationExternal", "gpsLocation", "controlsState", + "carControl", "driverAssistance", "alertDebug", "audioFeedback", ], subs=["selfdriveState", "onroadEvents"], ignore=["logMonoTime"], config_callback=selfdrived_config_callback, init_callback=get_car_params_callback, - should_recv_callback=selfdrived_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("carState", True), tolerance=NUMPY_TOLERANCE, processing_time=0.004, ), @@ -496,6 +456,7 @@ CONFIGS = [ tolerance=NUMPY_TOLERANCE, processing_time=0.004, main_pub="can", + main_pub_drained=True, ), ProcessConfig( proc_name="radard", @@ -503,7 +464,7 @@ CONFIGS = [ subs=["radarState"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("modelV2"), + should_recv_callback=MessageBasedRcvCallback("modelV2"), ), ProcessConfig( proc_name="plannerd", @@ -511,7 +472,7 @@ CONFIGS = [ subs=["longitudinalPlan", "driverAssistance"], ignore=["logMonoTime", "longitudinalPlan.processingDelay", "longitudinalPlan.solverExecutionTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("modelV2"), + should_recv_callback=MessageBasedRcvCallback("modelV2"), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -520,14 +481,14 @@ CONFIGS = [ subs=["liveCalibration"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=calibration_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("cameraOdometry", True), ), ProcessConfig( proc_name="dmonitoringd", pubs=["driverStateV2", "liveCalibration", "carState", "modelV2", "selfdriveState"], subs=["driverMonitoringState"], ignore=["logMonoTime"], - should_recv_callback=FrequencyBasedRcvCallback("driverStateV2"), + should_recv_callback=MessageBasedRcvCallback("driverStateV2"), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -539,7 +500,6 @@ CONFIGS = [ ignore=["logMonoTime"], should_recv_callback=MessageBasedRcvCallback("cameraOdometry"), tolerance=NUMPY_TOLERANCE, - unlocked_pubs=["accelerometer", "gyroscope"], ), ProcessConfig( proc_name="paramsd", @@ -547,7 +507,7 @@ CONFIGS = [ subs=["liveParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=FrequencyBasedRcvCallback("livePose"), + should_recv_callback=MessageBasedRcvCallback("livePose"), tolerance=NUMPY_TOLERANCE, processing_time=0.004, ), @@ -572,7 +532,7 @@ CONFIGS = [ subs=["liveTorqueParameters"], ignore=["logMonoTime"], init_callback=get_car_params_callback, - should_recv_callback=torqued_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("livePose", True), tolerance=NUMPY_TOLERANCE, ), ProcessConfig( @@ -584,7 +544,6 @@ CONFIGS = [ tolerance=NUMPY_TOLERANCE, processing_time=0.020, main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("roadCameraState").stream), - main_pub_drained=False, vision_pubs=["roadCameraState", "wideRoadCameraState"], ignore_alive_pubs=["wideRoadCameraState"], init_callback=get_car_params_callback, @@ -594,11 +553,10 @@ CONFIGS = [ pubs=["liveCalibration", "driverCameraState"], subs=["driverStateV2"], ignore=["logMonoTime", "driverStateV2.modelExecutionTime", "driverStateV2.gpuExecutionTime"], - should_recv_callback=dmonitoringmodeld_rcv_callback, + should_recv_callback=MessageBasedRcvCallback("driverCameraState"), tolerance=NUMPY_TOLERANCE, processing_time=0.020, main_pub=vipc_get_endpoint_name("camerad", meta_from_camera_state("driverCameraState").stream), - main_pub_drained=False, vision_pubs=["driverCameraState"], ignore_alive_pubs=["driverCameraState"], ), @@ -706,8 +664,8 @@ def _replay_multi_process( all_msgs = sorted(lr, key=lambda msg: msg.logMonoTime) log_msgs = [] + containers = [] try: - containers = [] for cfg in cfgs: container = ProcessContainer(cfg) containers.append(container) @@ -742,6 +700,11 @@ def _replay_multi_process( internal_pub_queue.append(m) heapq.heappush(internal_pub_index_heap, (m.logMonoTime, len(internal_pub_queue) - 1)) log_msgs.extend(output_msgs) + + # flush last set of messages from each process + for container in containers: + last_time = log_msgs[-1].logMonoTime if len(log_msgs) > 0 else int(time.monotonic() * 1e9) + log_msgs.extend(container.get_output_msgs(last_time)) finally: for container in containers: container.stop() diff --git a/selfdrive/test/process_replay/ref_commit b/selfdrive/test/process_replay/ref_commit index f839238098..a4297096c0 100644 --- a/selfdrive/test/process_replay/ref_commit +++ b/selfdrive/test/process_replay/ref_commit @@ -1 +1 @@ -f440c9e0469d32d350aa99ddaa8f44591a2ce690 \ No newline at end of file +6d3219bca9f66a229b38a5382d301a92b0147edb \ No newline at end of file diff --git a/selfdrive/test/process_replay/test_processes.py b/selfdrive/test/process_replay/test_processes.py index 39c1e408eb..e49a8b0f8c 100755 --- a/selfdrive/test/process_replay/test_processes.py +++ b/selfdrive/test/process_replay/test_processes.py @@ -63,7 +63,7 @@ segments = [ ] # dashcamOnly makes don't need to be tested until a full port is done -excluded_interfaces = ["mock", "body"] +excluded_interfaces = ["mock", "body", "psa"] BASE_URL = "https://commadataci.blob.core.windows.net/openpilotci/" REF_COMMIT_FN = os.path.join(PROC_REPLAY_DIR, "ref_commit") diff --git a/selfdrive/test/setup_device_ci.sh b/selfdrive/test/setup_device_ci.sh index d717698514..98909bfb52 100755 --- a/selfdrive/test/setup_device_ci.sh +++ b/selfdrive/test/setup_device_ci.sh @@ -54,6 +54,7 @@ while true; do # /data/ciui.py & #fi + awk '{print \$1}' /proc/uptime > /var/tmp/power_watchdog sleep 5s done diff --git a/selfdrive/test/test_onroad.py b/selfdrive/test/test_onroad.py index f98e3bd88c..0149653c84 100644 --- a/selfdrive/test/test_onroad.py +++ b/selfdrive/test/test_onroad.py @@ -55,6 +55,7 @@ PROCS = { "selfdrive.locationd.paramsd": 9.0, "selfdrive.locationd.lagd": 11.0, "selfdrive.ui.soundd": 3.0, + "selfdrive.ui.feedback.feedbackd": 1.0, "selfdrive.monitoring.dmonitoringd": 4.0, "./proclogd": 2.0, "system.logmessaged": 1.0, @@ -147,7 +148,7 @@ class TestOnroad: while not sm.seen['carState']: sm.update(1000) - route = params.get("CurrentRoute", encoding="utf-8") + route = params.get("CurrentRoute") assert route is not None segs = list(Path(Paths.log_root()).glob(f"{route}--*")) @@ -332,20 +333,18 @@ class TestOnroad: assert np.all(eof_sof_diff > 0) assert np.all(eof_sof_diff < 50*1e6) - first_fid = {c: min(self.ts[c]['frameId']) for c in cams} + first_fid = {min(self.ts[c]['frameId']) for c in cams} + assert len(first_fid) == 1, "Cameras don't start on same frame ID" if cam.endswith('CameraState'): # camerad guarantees that all cams start on frame ID 0 # (note loggerd also needs to start up fast enough to catch it) - assert set(first_fid.values()) == {0, }, "Cameras don't start on frame ID 0" - else: - # encoder guarantees all cams start on the same frame ID - assert len(set(first_fid.values())) == 1, "Cameras don't start on same frame ID" + assert next(iter(first_fid)) < 100, "Cameras start on frame ID too high" # we don't do a full segment rotation, so these might not match exactly - last_fid = {c: max(self.ts[c]['frameId']) for c in cams} - assert max(last_fid.values()) - min(last_fid.values()) < 10 + last_fid = {max(self.ts[c]['frameId']) for c in cams} + assert max(last_fid) - min(last_fid) < 10 - start, end = min(first_fid.values()), min(last_fid.values()) + start, end = min(first_fid), min(last_fid) for i in range(end-start): ts = {c: round(self.ts[c]['timestampSof'][i]/1e6, 1) for c in cams} diff = (max(ts.values()) - min(ts.values())) diff --git a/selfdrive/test/test_updated.py b/selfdrive/test/test_updated.py index ea945d94c2..95365640f5 100644 --- a/selfdrive/test/test_updated.py +++ b/selfdrive/test/test_updated.py @@ -105,7 +105,7 @@ class TestUpdated: ret = None start_time = time.monotonic() while ret is None: - ret = self.params.get(key, encoding='utf8') + ret = self.params.get(key) if time.monotonic() - start_time > timeout: break time.sleep(0.01) @@ -162,8 +162,7 @@ class TestUpdated: def _check_update_state(self, update_available): # make sure LastUpdateTime is recent - t = self._read_param("LastUpdateTime") - last_update_time = datetime.datetime.fromisoformat(t) + last_update_time = self._read_param("LastUpdateTime") td = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - last_update_time assert td.total_seconds() < 10 self.params.remove("LastUpdateTime") @@ -174,7 +173,7 @@ class TestUpdated: # check params update = self._read_param("UpdateAvailable") assert update == "1" == update_available, f"UpdateAvailable: {repr(update)}" - assert self._read_param("UpdateFailedCount") == "0" + assert self._read_param("UpdateFailedCount") == 0 # TODO: check that the finalized update actually matches remote # check the .overlay_init and .overlay_consistent flags diff --git a/selfdrive/ui/SConscript b/selfdrive/ui/SConscript index d883ca5b15..2a88832ef2 100644 --- a/selfdrive/ui/SConscript +++ b/selfdrive/ui/SConscript @@ -70,18 +70,8 @@ if GetOption('extras'): qt_src.remove("main.cc") # replaced by test_runner qt_env.Program('tests/test_translations', [asset_obj, 'tests/test_runner.cc', 'tests/test_translations.cc'] + qt_src, LIBS=qt_libs) - qt_env.SharedLibrary("qt/python_helpers", ["qt/qt_window.cc"], LIBS=qt_libs) - - # setup and factory resetter - qt_env.Program("qt/setup/reset", ["qt/setup/reset.cc"], LIBS=qt_libs) - qt_env.Program("qt/setup/setup", ["qt/setup/setup.cc", asset_obj], - LIBS=qt_libs + ['curl', 'common']) - - # build updater UI - qt_env.Program("qt/setup/updater", ["qt/setup/updater.cc", asset_obj], LIBS=qt_libs) - + # build installers if arch != "Darwin": - # build installers raylib_env = env.Clone() raylib_env['LIBPATH'] += [f'#third_party/raylib/{arch}/'] raylib_env['LINKFLAGS'].append('-Wl,-strip-debug') @@ -113,7 +103,3 @@ if GetOption('extras'): f = raylib_env.Program(f"installer/installers/installer_{name}", [obj, cont, inter], LIBS=raylib_libs) # keep installers small assert f[0].get_size() < 1900*1e3, f[0].get_size() - -# build watch3 -if arch in ['x86_64', 'aarch64', 'Darwin'] or GetOption('extras'): - qt_env.Program("watch3", ["watch3.cc"], LIBS=qt_libs + ['common', 'msgq', 'visionipc']) diff --git a/selfdrive/ui/feedback/feedbackd.py b/selfdrive/ui/feedback/feedbackd.py new file mode 100755 index 0000000000..24f27874eb --- /dev/null +++ b/selfdrive/ui/feedback/feedbackd.py @@ -0,0 +1,72 @@ +#!/usr/bin/env python3 +import cereal.messaging as messaging +from openpilot.common.params import Params +from openpilot.common.swaglog import cloudlog +from cereal import car +from openpilot.system.micd import SAMPLE_RATE, SAMPLE_BUFFER + +FEEDBACK_MAX_DURATION = 10.0 +ButtonType = car.CarState.ButtonEvent.Type + + +def main(): + params = Params() + pm = messaging.PubMaster(['userBookmark', 'audioFeedback']) + sm = messaging.SubMaster(['rawAudioData', 'bookmarkButton', 'carState', 'selfdriveStateSP']) + should_record_audio = False + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + + while True: + sm.update() + should_send_bookmark = False + + # TODO: https://github.com/commaai/openpilot/issues/36015 + # only allow the LKAS button to record feedback when MADS is disabled + if False and sm.updated['carState'] and sm['carState'].canValid and not sm['selfdriveStateSP'].mads.available: + for be in sm['carState'].buttonEvents: + if be.type == ButtonType.lkas: + if be.pressed: + if not should_record_audio: + if params.get_bool("RecordAudioFeedback"): # Start recording on first press if toggle set + should_record_audio = True + block_num = 0 + waiting_for_release = False + early_stop_triggered = False + cloudlog.info("LKAS button pressed - starting 10-second audio feedback") + else: + should_send_bookmark = True # immediately send bookmark if toggle false + cloudlog.info("LKAS button pressed - bookmarking") + elif should_record_audio and not waiting_for_release: # Wait for release of second press to stop recording early + waiting_for_release = True + elif waiting_for_release: # Second press released + waiting_for_release = False + early_stop_triggered = True + cloudlog.info("LKAS button released - ending recording early") + + if should_record_audio and sm.updated['rawAudioData']: + raw_audio = sm['rawAudioData'] + msg = messaging.new_message('audioFeedback', valid=True) + msg.audioFeedback.audio.data = raw_audio.data + msg.audioFeedback.audio.sampleRate = raw_audio.sampleRate + msg.audioFeedback.blockNum = block_num + block_num += 1 + if (block_num * SAMPLE_BUFFER / SAMPLE_RATE) >= FEEDBACK_MAX_DURATION or early_stop_triggered: # Check for timeout or early stop + should_send_bookmark = True # send bookmark at end of audio segment + should_record_audio = False + early_stop_triggered = False + cloudlog.info("10-second recording completed or second button press - stopping audio feedback") + pm.send('audioFeedback', msg) + + if sm.updated['bookmarkButton']: + cloudlog.info("Bookmark button pressed!") + should_send_bookmark = True + + if should_send_bookmark: + msg = messaging.new_message('userBookmark', valid=True) + pm.send('userBookmark', msg) + + +if __name__ == '__main__': + main() diff --git a/selfdrive/ui/installer/installer.cc b/selfdrive/ui/installer/installer.cc index 7326e089ab..a9b84b5c06 100644 --- a/selfdrive/ui/installer/installer.cc +++ b/selfdrive/ui/installer/installer.cc @@ -24,11 +24,13 @@ const std::string BRANCH_STR = get_str(BRANCH "? #define GIT_SSH_URL "git@github.com:commaai/openpilot.git" #define CONTINUE_PATH "/data/continue.sh" -const std::string CACHE_PATH = "/data/openpilot.cache"; +const std::string INSTALL_PATH = "/data/openpilot"; +const std::string VALID_CACHE_PATH = "/data/.openpilot_cache"; -#define INSTALL_PATH "/data/openpilot" #define TMP_INSTALL_PATH "/data/tmppilot" +const int FONT_SIZE = 120; + extern const uint8_t str_continue[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_start"); extern const uint8_t str_continue_end[] asm("_binary_selfdrive_ui_installer_continue_openpilot_sh_end"); extern const uint8_t inter_ttf[] asm("_binary_selfdrive_ui_installer_inter_ascii_ttf_start"); @@ -41,6 +43,16 @@ void run(const char* cmd) { assert(err == 0); } +void finishInstall() { + BeginDrawing(); + ClearBackground(BLACK); + const char *m = "Finishing install..."; + int text_width = MeasureText(m, FONT_SIZE); + DrawTextEx(font, m, (Vector2){(float)(GetScreenWidth() - text_width)/2 + FONT_SIZE, (float)(GetScreenHeight() - FONT_SIZE)/2}, FONT_SIZE, 0, WHITE); + EndDrawing(); + util::sleep_for(60 * 1000); +} + void renderProgress(int progress) { BeginDrawing(); ClearBackground(BLACK); @@ -62,11 +74,11 @@ int doInstall() { } // cleanup previous install attempts - run("rm -rf " TMP_INSTALL_PATH " " INSTALL_PATH); + run("rm -rf " TMP_INSTALL_PATH); // do the install - if (util::file_exists(CACHE_PATH)) { - return cachedFetch(CACHE_PATH); + if (util::file_exists(INSTALL_PATH) && util::file_exists(VALID_CACHE_PATH)) { + return cachedFetch(INSTALL_PATH); } else { return freshClone(); } @@ -135,7 +147,9 @@ void cloneFinished(int exitCode) { run("git submodule update --init"); // move into place - run("mv " TMP_INSTALL_PATH " " INSTALL_PATH); + run(("rm -f " + VALID_CACHE_PATH).c_str()); + run(("rm -rf " + INSTALL_PATH).c_str()); + run(util::string_format("mv %s %s", TMP_INSTALL_PATH, INSTALL_PATH.c_str()).c_str()); #ifdef INTERNAL run("mkdir -p /data/params/d/"); @@ -153,9 +167,9 @@ void cloneFinished(int exitCode) { param << value; param.close(); } - run("cd " INSTALL_PATH " && " + run(("cd " + INSTALL_PATH + " && " "git remote set-url origin --push " GIT_SSH_URL " && " - "git config --replace-all remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\""); + "git config --replace-all remote.origin.fetch \"+refs/heads/*:refs/remotes/origin/*\"").c_str()); #endif // write continue.sh @@ -171,16 +185,22 @@ void cloneFinished(int exitCode) { run("mv /data/continue.sh.new " CONTINUE_PATH); // wait for the installed software's UI to take over - util::sleep_for(60 * 1000); + finishInstall(); } int main(int argc, char *argv[]) { InitWindow(2160, 1080, "Installer"); - font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, 120, NULL, 0); + font = LoadFontFromMemory(".ttf", inter_ttf, inter_ttf_end - inter_ttf, FONT_SIZE, NULL, 0); SetTextureFilter(font.texture, TEXTURE_FILTER_BILINEAR); - renderProgress(0); - int result = doInstall(); - cloneFinished(result); + + if (util::file_exists(CONTINUE_PATH)) { + finishInstall(); + } else { + renderProgress(0); + int result = doInstall(); + cloneFinished(result); + } + CloseWindow(); UnloadFont(font); return 0; diff --git a/selfdrive/ui/layouts/home.py b/selfdrive/ui/layouts/home.py index 0fa91da552..f102481c3a 100644 --- a/selfdrive/ui/layouts/home.py +++ b/selfdrive/ui/layouts/home.py @@ -9,7 +9,7 @@ from openpilot.selfdrive.ui.widgets.prime import PrimeWidget from openpilot.selfdrive.ui.widgets.setup import SetupWidget from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_COLOR -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget HEADER_HEIGHT = 80 HEAD_BUTTON_FONT_SIZE = 40 @@ -67,7 +67,7 @@ class HomeLayout(Widget): self.current_state = state def _render(self, rect: rl.Rectangle): - current_time = time.time() + current_time = time.monotonic() if current_time - self.last_refresh >= REFRESH_INTERVAL: self._refresh() self.last_refresh = current_time @@ -210,5 +210,5 @@ class HomeLayout(Widget): def _get_version_text(self) -> str: brand = "openpilot" - description = self.params.get("UpdaterCurrentDescription", encoding='utf-8') + description = self.params.get("UpdaterCurrentDescription") return f"{brand} {description}" if description else brand diff --git a/selfdrive/ui/layouts/main.py b/selfdrive/ui/layouts/main.py index a3081143f4..3ab8525d33 100644 --- a/selfdrive/ui/layouts/main.py +++ b/selfdrive/ui/layouts/main.py @@ -4,9 +4,9 @@ import cereal.messaging as messaging from openpilot.selfdrive.ui.layouts.sidebar import Sidebar, SIDEBAR_WIDTH from openpilot.selfdrive.ui.layouts.home import HomeLayout from openpilot.selfdrive.ui.layouts.settings.settings import SettingsLayout, PanelType -from openpilot.selfdrive.ui.ui_state import device, ui_state from openpilot.selfdrive.ui.onroad.augmented_road_view import AugmentedRoadView -from openpilot.system.ui.lib.widget import Widget +from openpilot.selfdrive.ui.ui_state import device, ui_state +from openpilot.system.ui.widgets import Widget class MainState(IntEnum): @@ -19,7 +19,7 @@ class MainLayout(Widget): def __init__(self): super().__init__() - self._pm = messaging.PubMaster(['userFlag']) + self._pm = messaging.PubMaster(['bookmarkButton']) self._sidebar = Sidebar() self._current_mode = MainState.HOME @@ -40,7 +40,7 @@ class MainLayout(Widget): def _setup_callbacks(self): self._sidebar.set_callbacks(on_settings=self._on_settings_clicked, - on_flag=self._on_flag_clicked) + on_flag=self._on_bookmark_clicked) self._layouts[MainState.HOME]._setup_widget.set_open_settings_callback(lambda: self.open_settings(PanelType.FIREHOSE)) self._layouts[MainState.SETTINGS].set_callbacks(on_close=self._set_mode_for_state) self._layouts[MainState.ONROAD].set_callbacks(on_click=self._on_onroad_clicked) @@ -76,10 +76,10 @@ class MainLayout(Widget): def _on_settings_clicked(self): self.open_settings(PanelType.DEVICE) - def _on_flag_clicked(self): - user_flag = messaging.new_message('userFlag') - user_flag.valid = True - self._pm.send('userFlag', user_flag) + def _on_bookmark_clicked(self): + user_bookmark = messaging.new_message('bookmarkButton') + user_bookmark.valid = True + self._pm.send('bookmarkButton', user_bookmark) def _on_onroad_clicked(self): self._sidebar.set_visible(not self._sidebar.is_visible) diff --git a/selfdrive/ui/layouts/network.py b/selfdrive/ui/layouts/network.py index c227facd04..856be26e97 100644 --- a/selfdrive/ui/layouts/network.py +++ b/selfdrive/ui/layouts/network.py @@ -1,6 +1,6 @@ import pyray as rl -from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.widgets.network import WifiManagerUI diff --git a/selfdrive/ui/layouts/settings/developer.py b/selfdrive/ui/layouts/settings/developer.py index ce6e9e4af6..cfef0f84d1 100644 --- a/selfdrive/ui/layouts/settings/developer.py +++ b/selfdrive/ui/layouts/settings/developer.py @@ -1,8 +1,8 @@ -from openpilot.system.ui.lib.list_view import toggle_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget from openpilot.common.params import Params from openpilot.selfdrive.ui.widgets.ssh_key import ssh_key_item +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.list_view import toggle_item +from openpilot.system.ui.widgets.scroller import Scroller # Description constants DESCRIPTIONS = { diff --git a/selfdrive/ui/layouts/settings/device.py b/selfdrive/ui/layouts/settings/device.py index 69b7822ba9..df8bba030c 100644 --- a/selfdrive/ui/layouts/settings/device.py +++ b/selfdrive/ui/layouts/settings/device.py @@ -5,15 +5,15 @@ from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params from openpilot.selfdrive.ui.onroad.driver_camera_dialog import DriverCameraDialog from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.list_view import text_item, button_item, dual_button_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget, DialogResult -from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog -from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog +from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog, alert_dialog from openpilot.system.ui.widgets.html_render import HtmlRenderer +from openpilot.system.ui.widgets.list_view import text_item, button_item, dual_button_item +from openpilot.system.ui.widgets.option_dialog import MultiOptionDialog +from openpilot.system.ui.widgets.scroller import Scroller # Description constants DESCRIPTIONS = { @@ -41,7 +41,7 @@ class DeviceLayout(Widget): self._scroller = Scroller(items, line_separator=True, spacing=0) def _initialize_items(self): - dongle_id = self._params.get("DongleId", encoding="utf-8") or "N/A" + dongle_id = self._params.get("DongleId") or "N/A" serial = self._params.get("HardwareSerial") or "N/A" items = [ diff --git a/selfdrive/ui/layouts/settings/firehose.py b/selfdrive/ui/layouts/settings/firehose.py index 298f66e276..74a8f317d5 100644 --- a/selfdrive/ui/layouts/settings/firehose.py +++ b/selfdrive/ui/layouts/settings/firehose.py @@ -1,17 +1,16 @@ import pyray as rl -import json import time import threading from openpilot.common.api import Api, api_get from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.widget import Widget -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget TITLE = "Firehose Mode" @@ -51,11 +50,11 @@ class FirehoseLayout(Widget): self.last_update_time = 0 def _get_segment_count(self) -> int: - stats = self.params.get(self.PARAM_KEY, encoding='utf8') + stats = self.params.get(self.PARAM_KEY) if not stats: return 0 try: - return int(json.loads(stats).get("firehose", 0)) + return int(stats.get("firehose", 0)) except Exception: cloudlog.exception(f"Failed to decode firehose stats: {stats}") return 0 @@ -161,7 +160,7 @@ class FirehoseLayout(Widget): def _fetch_firehose_stats(self): try: - dongle_id = self.params.get("DongleId", encoding='utf8') + dongle_id = self.params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return identity_token = Api(dongle_id).get_token() @@ -169,7 +168,7 @@ class FirehoseLayout(Widget): if response.status_code == 200: data = response.json() self.segment_count = data.get("firehose", 0) - self.params.put(self.PARAM_KEY, json.dumps(data)) + self.params.put(self.PARAM_KEY, data) except Exception as e: cloudlog.error(f"Failed to fetch firehose stats: {e}") diff --git a/selfdrive/ui/layouts/settings/settings.py b/selfdrive/ui/layouts/settings/settings.py index 4a29f408ee..674e5005f4 100644 --- a/selfdrive/ui/layouts/settings/settings.py +++ b/selfdrive/ui/layouts/settings/settings.py @@ -2,19 +2,20 @@ 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 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 +from openpilot.system.ui.lib.application import gui_app, FontWeight, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.selfdrive.ui.layouts.network import NetworkLayout -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget -# Import individual panels +# Settings close button +SETTINGS_CLOSE_TEXT = "×" +SETTINGS_CLOSE_TEXT_Y_OFFSET = 8 # The '×' character isn't quite vertically centered in the font so we need to offset it a bit to fully center it -SETTINGS_CLOSE_TEXT = "X" # Constants SIDEBAR_WIDTH = 500 CLOSE_BTN_SIZE = 200 @@ -27,7 +28,7 @@ PANEL_COLOR = rl.Color(41, 41, 41, 255) CLOSE_BTN_COLOR = rl.Color(41, 41, 41, 255) CLOSE_BTN_PRESSED = rl.Color(59, 59, 59, 255) TEXT_NORMAL = rl.Color(128, 128, 128, 255) -TEXT_SELECTED = rl.Color(255, 255, 255, 255) +TEXT_SELECTED = rl.WHITE class PanelType(IntEnum): @@ -62,7 +63,6 @@ class SettingsLayout(Widget): } self._font_medium = gui_app.font(FontWeight.MEDIUM) - self._font_bold = gui_app.font(FontWeight.SEMI_BOLD) # Callbacks self._close_callback: Callable | None = None @@ -84,7 +84,7 @@ class SettingsLayout(Widget): # Close button close_btn_rect = rl.Rectangle( - rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 45, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE + rect.x + (rect.width - CLOSE_BTN_SIZE) / 2, rect.y + 60, CLOSE_BTN_SIZE, CLOSE_BTN_SIZE ) pressed = (rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and @@ -92,12 +92,12 @@ class SettingsLayout(Widget): close_color = CLOSE_BTN_PRESSED if pressed else CLOSE_BTN_COLOR rl.draw_rectangle_rounded(close_btn_rect, 1.0, 20, close_color) - close_text_size = measure_text_cached(self._font_bold, SETTINGS_CLOSE_TEXT, 140) + close_text_size = measure_text_cached(self._font_medium, SETTINGS_CLOSE_TEXT, 140) close_text_pos = rl.Vector2( close_btn_rect.x + (close_btn_rect.width - close_text_size.x) / 2, - close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2, + close_btn_rect.y + (close_btn_rect.height - close_text_size.y) / 2 - SETTINGS_CLOSE_TEXT_Y_OFFSET, ) - rl.draw_text_ex(self._font_bold, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED) + rl.draw_text_ex(self._font_medium, SETTINGS_CLOSE_TEXT, close_text_pos, 140, 0, TEXT_SELECTED) # Store close button rect for click detection self._close_btn_rect = close_btn_rect @@ -132,7 +132,7 @@ class SettingsLayout(Widget): if panel.instance: panel.instance.render(content_rect) - def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool: + def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: # Check close button if rl.check_collision_point_rec(mouse_pos, self._close_btn_rect): if self._close_callback: diff --git a/selfdrive/ui/layouts/settings/software.py b/selfdrive/ui/layouts/settings/software.py index b97e306ff9..4361725a1b 100644 --- a/selfdrive/ui/layouts/settings/software.py +++ b/selfdrive/ui/layouts/settings/software.py @@ -1,9 +1,9 @@ from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.list_view import button_item, text_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget, DialogResult +from openpilot.system.ui.widgets import Widget, DialogResult from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog +from openpilot.system.ui.widgets.list_view import button_item, text_item +from openpilot.system.ui.widgets.scroller import Scroller class SoftwareLayout(Widget): diff --git a/selfdrive/ui/layouts/settings/toggles.py b/selfdrive/ui/layouts/settings/toggles.py index 5c17082769..58afcec5ef 100644 --- a/selfdrive/ui/layouts/settings/toggles.py +++ b/selfdrive/ui/layouts/settings/toggles.py @@ -1,7 +1,7 @@ -from openpilot.system.ui.lib.list_view import multiple_button_item, toggle_item -from openpilot.system.ui.lib.scroller import Scroller -from openpilot.system.ui.lib.widget import Widget from openpilot.common.params import Params +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.list_view import multiple_button_item, toggle_item +from openpilot.system.ui.widgets.scroller import Scroller # Description constants DESCRIPTIONS = { @@ -22,6 +22,7 @@ DESCRIPTIONS = { "AlwaysOnDM": "Enable driver monitoring even when openpilot is not engaged.", 'RecordFront': "Upload data from the driver facing camera and help improve the driver monitoring algorithm.", "IsMetric": "Display speed in km/h instead of mph.", + "RecordAudio": "Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect.", } @@ -53,7 +54,7 @@ class TogglesLayout(Widget): buttons=["Aggressive", "Standard", "Relaxed"], button_width=255, callback=self._set_longitudinal_personality, - selected_index=int(self._params.get("LongitudinalPersonality") or 0), + selected_index=self._params.get("LongitudinalPersonality", return_default=True), icon="speed_limit.png" ), toggle_item( @@ -75,7 +76,13 @@ class TogglesLayout(Widget): icon="monitoring.png", ), toggle_item( - "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="monitoring.png" + "Record Microphone Audio", + DESCRIPTIONS["RecordAudio"], + self._params.get_bool("RecordAudio"), + icon="microphone.png", + ), + toggle_item( + "Use Metric System", DESCRIPTIONS["IsMetric"], self._params.get_bool("IsMetric"), icon="metric.png" ), ] @@ -85,4 +92,4 @@ class TogglesLayout(Widget): self._scroller.render(rect) def _set_longitudinal_personality(self, button_index: int): - self._params.put("LongitudinalPersonality", str(button_index)) + self._params.put("LongitudinalPersonality", button_index) diff --git a/selfdrive/ui/layouts/sidebar.py b/selfdrive/ui/layouts/sidebar.py index 13f69b79d4..cdbfa4c04c 100644 --- a/selfdrive/ui/layouts/sidebar.py +++ b/selfdrive/ui/layouts/sidebar.py @@ -4,14 +4,15 @@ from dataclasses import dataclass from collections.abc import Callable from cereal import log from openpilot.selfdrive.ui.ui_state import ui_state -from openpilot.system.ui.lib.application import gui_app, FontWeight +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.widget import Widget +from openpilot.system.ui.widgets import Widget SIDEBAR_WIDTH = 300 METRIC_HEIGHT = 126 METRIC_WIDTH = 240 METRIC_MARGIN = 30 +FONT_SIZE = 35 SETTINGS_BTN = rl.Rectangle(50, 35, 200, 117) HOME_BTN = rl.Rectangle(60, 860, 180, 180) @@ -23,18 +24,18 @@ NetworkType = log.DeviceState.NetworkType # Color scheme class Colors: SIDEBAR_BG = rl.Color(57, 57, 57, 255) - WHITE = rl.Color(255, 255, 255, 255) + WHITE = rl.WHITE WHITE_DIM = rl.Color(255, 255, 255, 85) GRAY = rl.Color(84, 84, 84, 255) # Status colors - GOOD = rl.Color(255, 255, 255, 255) + GOOD = rl.WHITE WARNING = rl.Color(218, 202, 37, 255) DANGER = rl.Color(201, 34, 49, 255) # UI elements METRIC_BORDER = rl.Color(255, 255, 255, 85) - BUTTON_NORMAL = rl.Color(255, 255, 255, 255) + BUTTON_NORMAL = rl.WHITE BUTTON_PRESSED = rl.Color(255, 255, 255, 166) @@ -135,7 +136,7 @@ class Sidebar(Widget): else: self._panda_status.update("VEHICLE", "ONLINE", Colors.GOOD) - def _handle_mouse_release(self, mouse_pos: rl.Vector2): + def _handle_mouse_release(self, mouse_pos: MousePos): if rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN): if self._on_settings_click: self._on_settings_click() @@ -145,7 +146,7 @@ class Sidebar(Widget): def _draw_buttons(self, rect: rl.Rectangle): mouse_pos = rl.get_mouse_position() - mouse_down = self._is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) + mouse_down = self.is_pressed and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) # Settings button settings_down = mouse_down and rl.check_collision_point_rec(mouse_pos, SETTINGS_BTN) @@ -175,7 +176,7 @@ class Sidebar(Widget): # Network type text text_y = rect.y + 247 text_pos = rl.Vector2(rect.x + 58, text_y) - rl.draw_text_ex(self._font_regular, self._net_type, text_pos, 35, 0, Colors.WHITE) + rl.draw_text_ex(self._font_regular, self._net_type, text_pos, FONT_SIZE, 0, Colors.WHITE) def _draw_metrics(self, rect: rl.Rectangle): metrics = [(self._temp_status, 338), (self._panda_status, 496), (self._connect_status, 654)] @@ -194,11 +195,14 @@ class Sidebar(Widget): # Draw border rl.draw_rectangle_rounded_lines_ex(metric_rect, 0.15, 10, 2, Colors.METRIC_BORDER) - # Draw text - text = f"{metric.label}\n{metric.value}" - text_size = measure_text_cached(self._font_bold, text, 35) - text_pos = rl.Vector2( - metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2, - metric_rect.y + (metric_rect.height - text_size.y) / 2 - ) - rl.draw_text_ex(self._font_bold, text, text_pos, 35, 0, Colors.WHITE) + # Draw label and value + labels = [metric.label, metric.value] + text_y = metric_rect.y + (metric_rect.height / 2 - len(labels) * FONT_SIZE) + for text in labels: + text_size = measure_text_cached(self._font_bold, text, FONT_SIZE) + text_y += text_size.y + text_pos = rl.Vector2( + metric_rect.x + 22 + (metric_rect.width - 22 - text_size.x) / 2, + text_y + ) + rl.draw_text_ex(self._font_bold, text, text_pos, FONT_SIZE, 0, Colors.WHITE) diff --git a/selfdrive/ui/lib/prime_state.py b/selfdrive/ui/lib/prime_state.py index 6b87540f65..da2ff899dd 100644 --- a/selfdrive/ui/lib/prime_state.py +++ b/selfdrive/ui/lib/prime_state.py @@ -2,12 +2,15 @@ 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.params import Params from openpilot.common.swaglog import cloudlog from openpilot.system.athena.registration import UNREGISTERED_DONGLE_ID +TOKEN_EXPIRY_HOURS = 2 + class PrimeType(IntEnum): UNKNOWN = -2, @@ -20,6 +23,12 @@ class PrimeType(IntEnum): PURPLE = 5, +@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) + + class PrimeState: FETCH_INTERVAL = 5.0 # seconds between API calls API_TIMEOUT = 10.0 # seconds for API requests @@ -35,7 +44,7 @@ class PrimeState: self.start() def _load_initial_state(self) -> PrimeType: - prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType", encoding='utf8') + prime_type_str = os.getenv("PRIME_TYPE") or self._params.get("PrimeType") try: if prime_type_str is not None: return PrimeType(int(prime_type_str)) @@ -44,18 +53,20 @@ class PrimeState: return PrimeType.UNKNOWN def _fetch_prime_status(self) -> None: - dongle_id = self._params.get("DongleId", encoding='utf8') + dongle_id = self._params.get("DongleId") if not dongle_id or dongle_id == UNREGISTERED_DONGLE_ID: return try: - identity_token = Api(dongle_id).get_token() + identity_token = get_token(dongle_id, int(time.monotonic() / (TOKEN_EXPIRY_HOURS / 2 * 60 * 60))) 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}") @@ -63,7 +74,7 @@ class PrimeState: with self._lock: if prime_type != self.prime_type: self.prime_type = prime_type - self._params.put("PrimeType", str(int(prime_type))) + self._params.put("PrimeType", int(prime_type)) cloudlog.info(f"Prime type updated to {prime_type}") def _worker_thread(self) -> None: diff --git a/selfdrive/ui/onroad/alert_renderer.py b/selfdrive/ui/onroad/alert_renderer.py index 7a17291bbe..e8817f24b4 100644 --- a/selfdrive/ui/onroad/alert_renderer.py +++ b/selfdrive/ui/onroad/alert_renderer.py @@ -2,12 +2,15 @@ import time import pyray as rl from dataclasses import dataclass from cereal import messaging, log +from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_FPS -from openpilot.system.ui.lib.label import gui_text_box from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget -from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import gui_text_box + +AlertSize = log.SelfdriveState.AlertSize +AlertStatus = log.SelfdriveState.AlertStatus ALERT_MARGIN = 40 ALERT_PADDING = 60 @@ -23,9 +26,9 @@ SELFDRIVE_UNRESPONSIVE_TIMEOUT = 10 # Seconds # Constants ALERT_COLORS = { - log.SelfdriveState.AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black - log.SelfdriveState.AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange - log.SelfdriveState.AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red + AlertStatus.normal: rl.Color(0, 0, 0, 235), # Black + AlertStatus.userPrompt: rl.Color(0xFE, 0x8C, 0x34, 235), # Orange + AlertStatus.critical: rl.Color(0xC9, 0x22, 0x31, 235), # Red } @@ -41,22 +44,22 @@ class Alert: ALERT_STARTUP_PENDING = Alert( text1="openpilot Unavailable", text2="Waiting to start", - size=log.SelfdriveState.AlertSize.mid, - status=log.SelfdriveState.AlertStatus.normal, + size=AlertSize.mid, + status=AlertStatus.normal, ) ALERT_CRITICAL_TIMEOUT = Alert( text1="TAKE CONTROL IMMEDIATELY", text2="System Unresponsive", - size=log.SelfdriveState.AlertSize.full, - status=log.SelfdriveState.AlertStatus.critical, + size=AlertSize.full, + status=AlertStatus.critical, ) ALERT_CRITICAL_REBOOT = Alert( text1="System Unresponsive", text2="Reboot Device", - size=log.SelfdriveState.AlertSize.full, - status=log.SelfdriveState.AlertStatus.critical, + size=AlertSize.full, + status=AlertStatus.critical, ) @@ -93,7 +96,7 @@ class AlertRenderer(Widget): return None # Return current alert - return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize, status=ss.alertStatus) + return Alert(text1=ss.alertText1, text2=ss.alertText2, size=ss.alertSize.raw, status=ss.alertStatus.raw) def _render(self, rect: rl.Rectangle) -> bool: alert = self.get_alert(ui_state.sm) @@ -113,10 +116,10 @@ class AlertRenderer(Widget): return True def _get_alert_rect(self, rect: rl.Rectangle, size: int) -> rl.Rectangle: - if size == log.SelfdriveState.AlertSize.full: + if size == AlertSize.full: return rect - height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == log.SelfdriveState.AlertSize.small else + height = (ALERT_FONT_MEDIUM + 2 * ALERT_PADDING if size == AlertSize.small else ALERT_FONT_BIG + ALERT_LINE_SPACING + ALERT_FONT_SMALL + 2 * ALERT_PADDING) return rl.Rectangle( @@ -127,19 +130,19 @@ class AlertRenderer(Widget): ) def _draw_background(self, rect: rl.Rectangle, alert: Alert) -> None: - color = ALERT_COLORS.get(alert.status, ALERT_COLORS[log.SelfdriveState.AlertStatus.normal]) + color = ALERT_COLORS.get(alert.status, ALERT_COLORS[AlertStatus.normal]) - if alert.size != log.SelfdriveState.AlertSize.full: + if alert.size != AlertSize.full: roundness = ALERT_BORDER_RADIUS / (min(rect.width, rect.height) / 2) rl.draw_rectangle_rounded(rect, roundness, 10, color) else: rl.draw_rectangle_rec(rect, color) def _draw_text(self, rect: rl.Rectangle, alert: Alert) -> None: - if alert.size == log.SelfdriveState.AlertSize.small: + if alert.size == AlertSize.small: self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_MEDIUM) - elif alert.size == log.SelfdriveState.AlertSize.mid: + elif alert.size == AlertSize.mid: self._draw_centered(alert.text1, rect, self.font_bold, ALERT_FONT_BIG, center_y=False) rect.y += ALERT_FONT_BIG + ALERT_LINE_SPACING self._draw_centered(alert.text2, rect, self.font_regular, ALERT_FONT_SMALL, center_y=False) diff --git a/selfdrive/ui/onroad/cameraview.py b/selfdrive/ui/onroad/cameraview.py index 33f1961d2b..ca031e2f17 100644 --- a/selfdrive/ui/onroad/cameraview.py +++ b/selfdrive/ui/onroad/cameraview.py @@ -2,12 +2,12 @@ import platform import numpy as np import pyray as rl -from openpilot.system.hardware import TICI from msgq.visionipc import VisionIpcClient, VisionStreamType, VisionBuf from openpilot.common.swaglog import cloudlog +from openpilot.system.hardware import TICI from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.egl import init_egl, create_egl_image, destroy_egl_image, bind_egl_image_to_texture, EGLImage -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget CONNECTION_RETRY_INTERVAL = 0.2 # seconds between connection attempts @@ -141,6 +141,9 @@ class CameraView(Widget): self.client = None + def __del__(self): + self.close() + def _calc_frame_matrix(self, rect: rl.Rectangle) -> np.ndarray: if not self.frame: return np.eye(3) @@ -179,6 +182,9 @@ class CameraView(Widget): transform = self._calc_frame_matrix(rect) src_rect = rl.Rectangle(0, 0, float(self.frame.width), float(self.frame.height)) + # Flip driver camera horizontally + if self._stream_type == VisionStreamType.VISION_STREAM_DRIVER: + src_rect.width = -src_rect.width # Calculate scale scale_x = rect.width * transform[0, 0] # zx @@ -334,16 +340,7 @@ class CameraView(Widget): if __name__ == "__main__": - gui_app.init_window("watch3") - road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) - driver_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) - wide_road_camera_view = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) - try: - for _ in gui_app.render(): - road_camera_view.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) - driver_camera_view.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) - wide_road_camera_view.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) - finally: - road_camera_view.close() - driver_camera_view.close() - wide_road_camera_view.close() + gui_app.init_window("camera view") + road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + for _ in gui_app.render(): + road.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/selfdrive/ui/onroad/driver_camera_dialog.py b/selfdrive/ui/onroad/driver_camera_dialog.py index 40c72f292b..c8b4e62030 100644 --- a/selfdrive/ui/onroad/driver_camera_dialog.py +++ b/selfdrive/ui/onroad/driver_camera_dialog.py @@ -5,7 +5,7 @@ from openpilot.selfdrive.ui.onroad.cameraview import CameraView from openpilot.selfdrive.ui.onroad.driver_state import DriverStateRenderer from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.label import gui_label +from openpilot.system.ui.widgets.label import gui_label class DriverCameraDialog(CameraView): diff --git a/selfdrive/ui/onroad/driver_state.py b/selfdrive/ui/onroad/driver_state.py index 8d5fb951d0..a25d9bd316 100644 --- a/selfdrive/ui/onroad/driver_state.py +++ b/selfdrive/ui/onroad/driver_state.py @@ -3,7 +3,7 @@ import pyray as rl from dataclasses import dataclass from openpilot.selfdrive.ui.ui_state import ui_state, UI_BORDER_SIZE from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget # Default 3D coordinates for face keypoints as a NumPy array DEFAULT_FACE_KPTS_3D = np.array([ diff --git a/selfdrive/ui/onroad/exp_button.py b/selfdrive/ui/onroad/exp_button.py index 8c835090d0..27f5763077 100644 --- a/selfdrive/ui/onroad/exp_button.py +++ b/selfdrive/ui/onroad/exp_button.py @@ -1,9 +1,9 @@ import time import pyray as rl +from openpilot.common.params import Params from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.widget import Widget -from openpilot.common.params import Params +from openpilot.system.ui.widgets import Widget class ExpButton(Widget): @@ -41,7 +41,7 @@ class ExpButton(Widget): # Hold new state temporarily self._held_mode = new_mode - self._hold_end_time = time.time() + self._hold_duration + self._hold_end_time = time.monotonic() + self._hold_duration return True return False @@ -50,7 +50,7 @@ class ExpButton(Widget): center_y = int(self._rect.y + self._rect.height // 2) mouse_over = rl.check_collision_point_rec(rl.get_mouse_position(), self._rect) - mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed + mouse_down = rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed self._white_color.a = 180 if (mouse_down and mouse_over) or not self._engageable else 255 texture = self._txt_exp if self._held_or_actual_mode() else self._txt_wheel @@ -58,7 +58,7 @@ class ExpButton(Widget): rl.draw_texture(texture, center_x - texture.width // 2, center_y - texture.height // 2, self._white_color) def _held_or_actual_mode(self): - now = time.time() + now = time.monotonic() if self._hold_end_time and now < self._hold_end_time: return self._held_mode diff --git a/selfdrive/ui/onroad/hud_renderer.py b/selfdrive/ui/onroad/hud_renderer.py index da292d38d5..536d993389 100644 --- a/selfdrive/ui/onroad/hud_renderer.py +++ b/selfdrive/ui/onroad/hud_renderer.py @@ -1,11 +1,11 @@ import pyray as rl from dataclasses import dataclass -from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus +from openpilot.common.constants import CV from openpilot.selfdrive.ui.onroad.exp_button import ExpButton +from openpilot.selfdrive.ui.ui_state import ui_state, UIStatus from openpilot.system.ui.lib.application import gui_app, FontWeight from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.common.conversions import Conversions as CV -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget # Constants SET_SPEED_NA = 255 @@ -34,7 +34,7 @@ class FontSizes: @dataclass(frozen=True) class Colors: - white: rl.Color = rl.Color(255, 255, 255, 255) + white: rl.Color = rl.WHITE disengaged: rl.Color = rl.Color(145, 155, 149, 255) override: rl.Color = rl.Color(145, 155, 149, 255) # Added engaged: rl.Color = rl.Color(128, 216, 166, 255) @@ -47,7 +47,7 @@ class Colors: white_translucent: rl.Color = rl.Color(255, 255, 255, 200) border_translucent: rl.Color = rl.Color(255, 255, 255, 75) header_gradient_start: rl.Color = rl.Color(0, 0, 0, 114) - header_gradient_end: rl.Color = rl.Color(0, 0, 0, 0) + header_gradient_end: rl.Color = rl.BLANK UI_CONFIG = UIConfig() diff --git a/selfdrive/ui/onroad/model_renderer.py b/selfdrive/ui/onroad/model_renderer.py index ab2b7aaefb..932773755d 100644 --- a/selfdrive/ui/onroad/model_renderer.py +++ b/selfdrive/ui/onroad/model_renderer.py @@ -4,11 +4,11 @@ import pyray as rl from cereal import messaging, car from dataclasses import dataclass, field from openpilot.common.params import Params +from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import DEFAULT_FPS from openpilot.system.ui.lib.shader_polygon import draw_polygon -from openpilot.system.ui.lib.widget import Widget -from openpilot.selfdrive.locationd.calibrationd import HEIGHT_INIT +from openpilot.system.ui.widgets import Widget CLIP_MARGIN = 500 MIN_DRAW_DISTANCE = 10.0 @@ -187,9 +187,9 @@ class ModelRenderer(Widget): self._path.raw_points, 0.9, self._path_offset_z, max_idx, allow_invert=False ) - self._update_experimental_gradient(self._rect.height) + self._update_experimental_gradient() - def _update_experimental_gradient(self, height): + def _update_experimental_gradient(self): """Pre-calculate experimental mode gradient colors""" if not self._experimental_mode: return @@ -201,22 +201,21 @@ class ModelRenderer(Widget): i = 0 while i < max_len: - track_idx = max_len - i - 1 # flip idx to start from bottom right - track_y = self._path.projected_points[track_idx][1] - if track_y < 0 or track_y > height: + # Some points (screen space) are out of frame (rect space) + track_y = self._path.projected_points[i][1] + if track_y < self._rect.y or track_y > (self._rect.y + self._rect.height): i += 1 continue - # Calculate color based on acceleration - lin_grad_point = (height - track_y) / height + # Calculate color based on acceleration (0 is bottom, 1 is top) + lin_grad_point = 1 - (track_y - self._rect.y) / self._rect.height # speed up: 120, slow down: 0 - path_hue = max(min(60 + self._acceleration_x[i] * 35, 120), 0) - path_hue = int(path_hue * 100 + 0.5) / 100 + path_hue = np.clip(60 + self._acceleration_x[i] * 35, 0, 120) saturation = min(abs(self._acceleration_x[i] * 1.5), 1) - lightness = self._map_val(saturation, 0.0, 1.0, 0.95, 0.62) - alpha = self._map_val(lin_grad_point, 0.75 / 2.0, 0.75, 0.4, 0.0) + lightness = np.interp(saturation, [0.0, 1.0], [0.95, 0.62]) + alpha = np.interp(lin_grad_point, [0.75 / 2.0, 0.75], [0.4, 0.0]) # Use HSL to RGB conversion color = self._hsla_to_color(path_hue / 360.0, saturation, lightness, alpha) @@ -280,7 +279,7 @@ class ModelRenderer(Widget): if self._experimental_mode: # Draw with acceleration coloring - if len(self._exp_gradient['colors']) > 2: + if len(self._exp_gradient['colors']) > 1: draw_polygon(self._rect, self._path.projected_points, gradient=self._exp_gradient) else: draw_polygon(self._rect, self._path.projected_points, rl.Color(255, 255, 255, 30)) @@ -409,13 +408,6 @@ class ModelRenderer(Widget): return np.vstack((left_screen.T, right_screen[:, ::-1].T)).astype(np.float32) - @staticmethod - def _map_val(x, x0, x1, y0, y1): - x = np.clip(x, x0, x1) - ra = x1 - x0 - rb = y1 - y0 - return (x - x0) * rb / ra + y0 if ra != 0 else y0 - @staticmethod def _hsla_to_color(h, s, l, a): rgb = colorsys.hls_to_rgb(h, l, s) diff --git a/selfdrive/ui/qt/offroad/developer_panel.cc b/selfdrive/ui/qt/offroad/developer_panel.cc index 3e9647d2cf..fbfb16294b 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.cc +++ b/selfdrive/ui/qt/offroad/developer_panel.cc @@ -45,17 +45,6 @@ DeveloperPanel::DeveloperPanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(experimentalLongitudinalToggle); - enableGithubRunner = new ParamControl("EnableGithubRunner", tr("Enable GitHub runner service"), tr("Enables or disables the github runner service."), ""); - addItem(enableGithubRunner); - - // error log button - errorLogBtn = new ButtonControl(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes.")); - connect(errorLogBtn, &ButtonControl::clicked, [=]() { - std::string txt = util::read_file("/data/community/crashes/error.log"); - ConfirmationDialog::rich(QString::fromStdString(txt), this); - }); - addItem(errorLogBtn); - // Joystick and longitudinal maneuvers should be hidden on release branches is_release = params.getBool("IsReleaseBranch"); @@ -104,8 +93,6 @@ void DeveloperPanel::updateToggles(bool _offroad) { experimentalLongitudinalToggle->refresh(); // Handle specific controls visibility for release branches - enableGithubRunner->setVisible(!is_release); - errorLogBtn->setVisible(!is_release); joystickToggle->setVisible(!is_release); offroad = _offroad; diff --git a/selfdrive/ui/qt/offroad/developer_panel.h b/selfdrive/ui/qt/offroad/developer_panel.h index c15b574f5a..51b1b0b6f0 100644 --- a/selfdrive/ui/qt/offroad/developer_panel.h +++ b/selfdrive/ui/qt/offroad/developer_panel.h @@ -16,10 +16,8 @@ private: Params params; ParamControl* adbToggle; ParamControl* joystickToggle; - ButtonControl* errorLogBtn; ParamControl* longManeuverToggle; ParamControl* experimentalLongitudinalToggle; - ParamControl* enableGithubRunner; bool is_release; bool offroad = false; diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index 6b97342966..205a14624a 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -68,6 +68,13 @@ TogglesPanel::TogglesPanel(SettingsWindow *parent) : ListWidget(parent) { "../assets/icons/monitoring.png", true, }, + { + "RecordAudio", + tr("Record and Upload Microphone Audio"), + tr("Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect."), + "../assets/icons/microphone.png", + true, + }, { "IsMetric", tr("Use Metric System"), @@ -342,26 +349,22 @@ void DevicePanel::updateCalibDescription() { } } - const bool is_release = params.getBool("IsReleaseBranch"); - if (!is_release) { - int lag_perc = 0; - std::string lag_bytes = params.get("LiveDelay"); - if (!lag_bytes.empty()) { - try { - AlignedBuffer aligned_buf; - capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); - lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); - } catch (kj::Exception) { - qInfo() << "invalid LiveDelay"; - } - } - desc += "\n\n"; - if (lag_perc < 100) { - desc += tr("Steering lag calibration is %1% complete.").arg(lag_perc); - } else { - desc += tr("Steering lag calibration is complete."); + int lag_perc = 0; + std::string lag_bytes = params.get("LiveDelay"); + if (!lag_bytes.empty()) { + try { + AlignedBuffer aligned_buf; + capnp::FlatArrayMessageReader cmsg(aligned_buf.align(lag_bytes.data(), lag_bytes.size())); + lag_perc = cmsg.getRoot().getLiveDelay().getCalPerc(); + } catch (kj::Exception) { + qInfo() << "invalid LiveDelay"; } } + if (lag_perc < 100) { + desc += tr("\n\nSteering lag calibration is %1% complete.").arg(lag_perc); + } else { + desc += tr("\n\nSteering lag calibration is complete."); + } std::string torque_bytes = params.get("LiveTorqueParameters"); if (!torque_bytes.empty()) { @@ -372,11 +375,10 @@ void DevicePanel::updateCalibDescription() { // don't add for non-torque cars if (torque.getUseParams()) { int torque_perc = torque.getCalPerc(); - desc += is_release ? "\n\n" : " "; if (torque_perc < 100) { - desc += tr("Steering torque response calibration is %1% complete.").arg(torque_perc); + desc += tr(" Steering torque response calibration is %1% complete.").arg(torque_perc); } else { - desc += tr("Steering torque response calibration is complete."); + desc += tr(" Steering torque response calibration is complete."); } } } catch (kj::Exception) { diff --git a/selfdrive/ui/qt/onroad/model.cc b/selfdrive/ui/qt/onroad/model.cc index dc801e591f..a1e3756cb6 100644 --- a/selfdrive/ui/qt/onroad/model.cc +++ b/selfdrive/ui/qt/onroad/model.cc @@ -34,6 +34,7 @@ void ModelRenderer::draw(QPainter &painter, const QRect &surface_rect) { drawLead(painter, lead_two, lead_vertices[1], surface_rect); } } + drawLeadStatus(painter, surface_rect.height(), surface_rect.width()); painter.restore(); } @@ -173,6 +174,173 @@ QColor ModelRenderer::blendColors(const QColor &start, const QColor &end, float (1 - t) * start.alphaF() + t * end.alphaF()); } + +void ModelRenderer::drawLeadStatus(QPainter &painter, int height, int width) { + auto *s = uiState(); + auto &sm = *(s->sm); + + if (!sm.alive("radarState")) return; + + const auto &radar_state = sm["radarState"].getRadarState(); + const auto &lead_one = radar_state.getLeadOne(); + const auto &lead_two = radar_state.getLeadTwo(); + + // Check if we have any active leads + bool has_lead_one = lead_one.getStatus(); + bool has_lead_two = lead_two.getStatus(); + + if (!has_lead_one && !has_lead_two) { + // Fade out status display + lead_status_alpha = std::max(0.0f, lead_status_alpha - 0.05f); + if (lead_status_alpha <= 0.0f) return; + } else { + // Fade in status display + lead_status_alpha = std::min(1.0f, lead_status_alpha + 0.1f); + } + + // Draw status for each lead vehicle under its chevron + if (true) { + drawLeadStatusAtPosition(painter, lead_one, lead_vertices[0], height, width, "L1"); + } + + if (has_lead_two && std::abs(lead_one.getDRel() - lead_two.getDRel()) > 3.0) { + drawLeadStatusAtPosition(painter, lead_two, lead_vertices[1], height, width, "L2"); + } +} + +void ModelRenderer::drawLeadStatusAtPosition(QPainter &painter, + const cereal::RadarState::LeadData::Reader &lead_data, + const QPointF &chevron_pos, + int height, int width, + const QString &label) { + + float d_rel = lead_data.getDRel(); + float v_rel = lead_data.getVRel(); + auto *s = uiState(); + auto &sm = *(s->sm); + float v_ego = sm["carState"].getCarState().getVEgo(); + + int chevron_data = std::atoi(Params().get("ChevronInfo").c_str()); + + // Calculate chevron size (same logic as drawLead) + float sz = std::clamp((25 * 30) / (d_rel / 3 + 30), 15.0f, 30.0f) * 2.35; + + QFont content_font = painter.font(); + content_font.setPixelSize(35); + content_font.setBold(true); + painter.setFont(content_font); + + QFontMetrics fm(content_font); + bool is_metric = s->scene.is_metric; + + QStringList text_lines; + + const int chevron_types = 3; + const int chevron_all = chevron_types + 1; // All metrics (value 4) + QStringList chevron_text[chevron_types]; + int position; + float val; + + // Distance display (chevron_data == 1 or all) + if (chevron_data == 1 || chevron_data == chevron_all) { + position = 0; + val = std::max(0.0f, d_rel); + QString distance_unit = is_metric ? "m" : "ft"; + if (!is_metric) { + val *= 3.28084f; // Convert meters to feet + } + chevron_text[position].append(QString::number(val, 'f', 0) + " " + distance_unit); + } + + // Absolute velocity display (chevron_data == 2 or all) + if (chevron_data == 2 || chevron_data == chevron_all) { + position = (chevron_data == 2) ? 0 : 1; + val = std::max(0.0f, (v_rel + v_ego) * (is_metric ? static_cast(MS_TO_KPH) : static_cast(MS_TO_MPH))); + chevron_text[position].append(QString::number(val, 'f', 0) + " " + (is_metric ? "km/h" : "mph")); + } + + // Time-to-contact display (chevron_data == 3 or all) + if (chevron_data == 3 || chevron_data == chevron_all) { + position = (chevron_data == 3) ? 0 : 2; + val = (d_rel > 0 && v_ego > 0) ? std::max(0.0f, d_rel / v_ego) : 0.0f; + QString ttc_str = (val > 0 && val < 200) ? QString::number(val, 'f', 1) + "s" : "---"; + chevron_text[position].append(ttc_str); + } + + // Collect all non-empty text lines + for (int i = 0; i < chevron_types; ++i) { + if (!chevron_text[i].isEmpty()) { + text_lines.append(chevron_text[i]); + } + } + + // If no text to display, return early + if (text_lines.isEmpty()) { + return; + } + + // Text box dimensions + float str_w = 150; // Width of text area + float str_h = 45; // Height per line + + // Position text below chevron, centered horizontally + float text_x = chevron_pos.x() - str_w / 2; + float text_y = chevron_pos.y() + sz + 15; + + // Clamp to screen bounds + text_x = std::clamp(text_x, 10.0f, (float)width - str_w - 10); + + // Shadow offset + QPoint shadow_offset(2, 2); + + // Draw each line of text with shadow + for (int i = 0; i < text_lines.size(); ++i) { + if (!text_lines[i].isEmpty()) { + QRect textRect(text_x, text_y + (i * str_h), str_w, str_h); + + // Draw shadow + painter.setPen(QColor(0x0, 0x0, 0x0, (int)(200 * lead_status_alpha))); + painter.drawText(textRect.translated(shadow_offset.x(), shadow_offset.y()), + Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]); + + // Determine text color based on content and danger level + QColor text_color; + + // Check if this is a distance line (contains 'm' or 'ft') + if (text_lines[i].contains("m") || text_lines[i].contains("ft")) { + if (d_rel < 20.0f) { + text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - danger + } else if (d_rel < 40.0f) { + text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution + } else { + text_color = QColor(80, 255, 120, (int)(255 * lead_status_alpha)); // Green - safe + } + } + // Enhanced color coding for time-to-contact + else if (text_lines[i].contains("s") && !text_lines[i].contains("---")) { + float ttc_val = text_lines[i].left(text_lines[i].length() - 1).toFloat(); + if (ttc_val < 3.0f) { + text_color = QColor(255, 80, 80, (int)(255 * lead_status_alpha)); // Red - urgent + } else if (ttc_val < 6.0f) { + text_color = QColor(255, 200, 80, (int)(255 * lead_status_alpha)); // Yellow - caution + } else { + text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White - safe + } + } + else { + text_color = QColor(0xff, 0xff, 0xff, (int)(255 * lead_status_alpha)); // White for other lines + } + + // Draw main text + painter.setPen(text_color); + painter.drawText(textRect, Qt::AlignBottom | Qt::AlignHCenter, text_lines[i]); + } + } + + // Reset pen + painter.setPen(Qt::NoPen); +} + void ModelRenderer::drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect) { const float speedBuff = 10.; diff --git a/selfdrive/ui/qt/onroad/model.h b/selfdrive/ui/qt/onroad/model.h index 85eb236e76..e40f832c37 100644 --- a/selfdrive/ui/qt/onroad/model.h +++ b/selfdrive/ui/qt/onroad/model.h @@ -34,6 +34,12 @@ protected: bool mapToScreen(float in_x, float in_y, float in_z, QPointF *out); void mapLineToPolygon(const cereal::XYZTData::Reader &line, float y_off, float z_off, QPolygonF *pvd, int max_idx, bool allow_invert = true); + void drawLeadStatus(QPainter &painter, int height, int width); + void drawLeadStatusAtPosition(QPainter &painter, + const cereal::RadarState::LeadData::Reader &lead_data, + const QPointF &chevron_pos, + int height, int width, + const QString &label); void drawLead(QPainter &painter, const cereal::RadarState::LeadData::Reader &lead_data, const QPointF &vd, const QRect &surface_rect); void update_leads(const cereal::RadarState::Reader &radar_state, const cereal::XYZTData::Reader &line); virtual void update_model(const cereal::ModelDataV2::Reader &model, const cereal::RadarState::LeadData::Reader &lead); @@ -58,4 +64,9 @@ protected: QPointF lead_vertices[2] = {}; Eigen::Matrix3f car_space_transform = Eigen::Matrix3f::Zero(); QRectF clip_region; + + float lead_status_alpha = 0.0f; + QPointF lead_status_pos; + QString lead_status_text; + QColor lead_status_color; }; diff --git a/selfdrive/ui/qt/python_helpers.py b/selfdrive/ui/qt/python_helpers.py deleted file mode 100644 index 1f6d43f309..0000000000 --- a/selfdrive/ui/qt/python_helpers.py +++ /dev/null @@ -1,23 +0,0 @@ -import os -import platform -from cffi import FFI - -import sip - -from openpilot.common.basedir import BASEDIR - -def suffix(): - return ".dylib" if platform.system() == "Darwin" else ".so" - - -def get_ffi(): - lib = os.path.join(BASEDIR, "selfdrive", "ui", "qt", "libpython_helpers" + suffix()) - - ffi = FFI() - ffi.cdef("void set_main_window(void *w);") - return ffi, ffi.dlopen(lib) - - -def set_main_window(widget): - ffi, lib = get_ffi() - lib.set_main_window(ffi.cast('void*', sip.unwrapinstance(widget))) diff --git a/selfdrive/ui/qt/setup/reset.cc b/selfdrive/ui/qt/setup/reset.cc deleted file mode 100644 index 82fd5a7820..0000000000 --- a/selfdrive/ui/qt/setup/reset.cc +++ /dev/null @@ -1,141 +0,0 @@ -#include -#include -#include -#include -#include -#include - -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/setup/reset.h" - -#define NVME "/dev/nvme0n1" -#define USERDATA "/dev/disk/by-partlabel/userdata" - -void Reset::doErase() { - // best effort to wipe nvme - std::system("sudo umount " NVME); - std::system("yes | sudo mkfs.ext4 " NVME); - - int rm = std::system("sudo rm -rf /data/*"); - std::system("sudo umount " USERDATA); - int fmt = std::system("yes | sudo mkfs.ext4 " USERDATA); - - if (rm == 0 || fmt == 0) { - std::system("sudo reboot"); - } - body->setText(tr("Reset failed. Reboot to try again.")); - rebootBtn->show(); -} - -void Reset::startReset() { - body->setText(tr("Resetting device...\nThis may take up to a minute.")); - rejectBtn->hide(); - rebootBtn->hide(); - confirmBtn->hide(); -#ifdef __aarch64__ - QTimer::singleShot(100, this, &Reset::doErase); -#endif -} - -void Reset::confirm() { - const QString confirm_txt = tr("Are you sure you want to reset your device?"); - if (body->text() != confirm_txt) { - body->setText(confirm_txt); - } else { - startReset(); - } -} - -Reset::Reset(ResetMode mode, QWidget *parent) : QWidget(parent) { - QVBoxLayout *main_layout = new QVBoxLayout(this); - main_layout->setContentsMargins(45, 220, 45, 45); - main_layout->setSpacing(0); - - QLabel *title = new QLabel(tr("System Reset")); - title->setStyleSheet("font-size: 90px; font-weight: 600;"); - main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - main_layout->addSpacing(60); - - body = new QLabel(tr("System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot.")); - body->setWordWrap(true); - body->setStyleSheet("font-size: 80px; font-weight: light;"); - main_layout->addWidget(body, 1, Qt::AlignTop | Qt::AlignLeft); - - QHBoxLayout *blayout = new QHBoxLayout(); - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - rejectBtn = new QPushButton(tr("Cancel")); - blayout->addWidget(rejectBtn); - QObject::connect(rejectBtn, &QPushButton::clicked, QCoreApplication::instance(), &QCoreApplication::quit); - - rebootBtn = new QPushButton(tr("Reboot")); - blayout->addWidget(rebootBtn); -#ifdef __aarch64__ - QObject::connect(rebootBtn, &QPushButton::clicked, [=]{ - std::system("sudo reboot"); - }); -#endif - - confirmBtn = new QPushButton(tr("Confirm")); - confirmBtn->setStyleSheet(R"( - QPushButton { - background-color: #465BEA; - } - QPushButton:pressed { - background-color: #3049F4; - } - )"); - blayout->addWidget(confirmBtn); - QObject::connect(confirmBtn, &QPushButton::clicked, this, &Reset::confirm); - - bool recover = mode == ResetMode::RECOVER; - rejectBtn->setVisible(!recover); - rebootBtn->setVisible(recover); - if (recover) { - body->setText(tr("Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device.")); - } - - // automatically start if we're just finishing up an ABL reset - if (mode == ResetMode::FORMAT) { - startReset(); - } - - setStyleSheet(R"( - * { - font-family: Inter; - color: white; - background-color: black; - } - QLabel { - margin-left: 140; - } - QPushButton { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #333333; - } - QPushButton:pressed { - background-color: #444444; - } - )"); -} - -int main(int argc, char *argv[]) { - ResetMode mode = ResetMode::USER_RESET; - if (argc > 1) { - if (strcmp(argv[1], "--recover") == 0) { - mode = ResetMode::RECOVER; - } else if (strcmp(argv[1], "--format") == 0) { - mode = ResetMode::FORMAT; - } - } - - QApplication a(argc, argv); - Reset reset(mode); - setMainWindow(&reset); - return a.exec(); -} diff --git a/selfdrive/ui/qt/setup/reset.h b/selfdrive/ui/qt/setup/reset.h deleted file mode 100644 index 2e0784cdc9..0000000000 --- a/selfdrive/ui/qt/setup/reset.h +++ /dev/null @@ -1,27 +0,0 @@ -#include -#include -#include - -enum ResetMode { - USER_RESET, // user initiated a factory reset from openpilot - RECOVER, // userdata is corrupt for some reason, give a chance to recover - FORMAT, // finish up a factory reset from a tool that doesn't flash an empty partition to userdata -}; - -class Reset : public QWidget { - Q_OBJECT - -public: - explicit Reset(ResetMode mode, QWidget *parent = 0); - -private: - QLabel *body; - QPushButton *rejectBtn; - QPushButton *rebootBtn; - QPushButton *confirmBtn; - void doErase(); - void startReset(); - -private slots: - void confirm(); -}; diff --git a/selfdrive/ui/qt/setup/setup.cc b/selfdrive/ui/qt/setup/setup.cc deleted file mode 100644 index 90ed973b0d..0000000000 --- a/selfdrive/ui/qt/setup/setup.cc +++ /dev/null @@ -1,541 +0,0 @@ -#include "selfdrive/ui/qt/setup/setup.h" - -#include -#include -#include -#include - -#include -#include -#include - -#include - -#include "common/util.h" -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/api.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/network/networking.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/input.h" - -const std::string USER_AGENT = "AGNOSSetup-"; -const QString OPENPILOT_URL = "https://openpilot.comma.ai"; - -bool is_elf(char *fname) { - FILE *fp = fopen(fname, "rb"); - if (fp == NULL) { - return false; - } - char buf[4]; - size_t n = fread(buf, 1, 4, fp); - fclose(fp); - return n == 4 && buf[0] == 0x7f && buf[1] == 'E' && buf[2] == 'L' && buf[3] == 'F'; -} - -void Setup::download(QString url) { - // autocomplete incomplete urls - if (QRegularExpression("^([^/.]+)/([^/]+)$").match(url).hasMatch()) { - url.prepend("https://installer.comma.ai/"); - } - - CURL *curl = curl_easy_init(); - if (!curl) { - emit finished(url, tr("Something went wrong. Reboot the device.")); - return; - } - - auto version = util::read_file("/VERSION"); - - struct curl_slist *list = NULL; - std::string header = "X-openpilot-serial: " + Hardware::get_serial(); - list = curl_slist_append(list, header.c_str()); - - char tmpfile[] = "/tmp/installer_XXXXXX"; - FILE *fp = fdopen(mkstemp(tmpfile), "wb"); - - curl_easy_setopt(curl, CURLOPT_URL, url.toStdString().c_str()); - curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, NULL); - curl_easy_setopt(curl, CURLOPT_WRITEDATA, fp); - curl_easy_setopt(curl, CURLOPT_VERBOSE, 1L); - curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); - curl_easy_setopt(curl, CURLOPT_USERAGENT, (USER_AGENT + version).c_str()); - curl_easy_setopt(curl, CURLOPT_HTTPHEADER, list); - curl_easy_setopt(curl, CURLOPT_TIMEOUT, 30L); - - int ret = curl_easy_perform(curl); - long res_status = 0; - curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &res_status); - - if (ret != CURLE_OK || res_status != 200) { - emit finished(url, tr("Ensure the entered URL is valid, and the device’s internet connection is good.")); - } else if (!is_elf(tmpfile)) { - emit finished(url, tr("No custom software found at this URL.")); - } else { - rename(tmpfile, "/tmp/installer"); - - FILE *fp_url = fopen("/tmp/installer_url", "w"); - fprintf(fp_url, "%s", url.toStdString().c_str()); - fclose(fp_url); - - emit finished(url); - } - - curl_slist_free_all(list); - curl_easy_cleanup(curl); - fclose(fp); -} - -QWidget * Setup::low_voltage() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 0, 55, 55); - main_layout->setSpacing(0); - - // inner text layout: warning icon, title, and body - QVBoxLayout *inner_layout = new QVBoxLayout(); - inner_layout->setContentsMargins(110, 144, 365, 0); - main_layout->addLayout(inner_layout); - - QLabel *triangle = new QLabel(); - triangle->setPixmap(QPixmap(ASSET_PATH + "icons/warning.png")); - inner_layout->addWidget(triangle, 0, Qt::AlignTop | Qt::AlignLeft); - inner_layout->addSpacing(80); - - QLabel *title = new QLabel(tr("WARNING: Low Voltage")); - title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); - inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - inner_layout->addSpacing(25); - - QLabel *body = new QLabel(tr("Power your device in a car with a harness or proceed at your own risk.")); - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 80px; font-weight: 300;"); - inner_layout->addWidget(body); - - inner_layout->addStretch(); - - // power off + continue buttons - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *poweroff = new QPushButton(tr("Power off")); - poweroff->setObjectName("navBtn"); - blayout->addWidget(poweroff); - QObject::connect(poweroff, &QPushButton::clicked, this, [=]() { - Hardware::poweroff(); - }); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - blayout->addWidget(cont); - QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); - - return widget; -} - -QWidget * Setup::custom_software_warning() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 0, 55, 55); - main_layout->setSpacing(0); - - QVBoxLayout *inner_layout = new QVBoxLayout(); - inner_layout->setContentsMargins(110, 110, 300, 0); - main_layout->addLayout(inner_layout); - - QLabel *title = new QLabel(tr("WARNING: Custom Software")); - title->setStyleSheet("font-size: 90px; font-weight: 500; color: #FF594F;"); - inner_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - inner_layout->addSpacing(25); - - QLabel *body = new QLabel(tr("Use caution when installing third-party software. Third-party software has not been tested by comma, and may cause damage to your device and/or vehicle.\n\nIf you'd like to proceed, use https://flash.comma.ai to restore your device to a factory state later.")); - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 65px; font-weight: 300;"); - inner_layout->addWidget(body); - - inner_layout->addStretch(); - - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - blayout->addWidget(back); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - blayout->addWidget(cont); - QObject::connect(cont, &QPushButton::clicked, this, [=]() { - QTimer::singleShot(0, [=]() { - setCurrentWidget(downloading_widget); - }); - QString url = InputDialog::getText(tr("Enter URL"), this, tr("for Custom Software")); - if (!url.isEmpty()) { - QTimer::singleShot(1000, this, [=]() { - download(url); - }); - } else { - setCurrentWidget(software_selection_widget); - } - }); - - return widget; -} - -QWidget * Setup::getting_started() { - QWidget *widget = new QWidget(); - - QHBoxLayout *main_layout = new QHBoxLayout(widget); - main_layout->setMargin(0); - - QVBoxLayout *vlayout = new QVBoxLayout(); - vlayout->setContentsMargins(165, 280, 100, 0); - main_layout->addLayout(vlayout); - - QLabel *title = new QLabel(tr("Getting Started")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - vlayout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - vlayout->addSpacing(90); - QLabel *desc = new QLabel(tr("Before we get on the road, let’s finish installation and cover some details.")); - desc->setWordWrap(true); - desc->setStyleSheet("font-size: 80px; font-weight: 300;"); - vlayout->addWidget(desc, 0, Qt::AlignTop | Qt::AlignLeft); - - vlayout->addStretch(); - - QPushButton *btn = new QPushButton(); - btn->setIcon(QIcon(":/images/button_continue_triangle.svg")); - btn->setIconSize(QSize(54, 106)); - btn->setFixedSize(310, 1080); - btn->setProperty("primary", true); - btn->setStyleSheet("border: none;"); - main_layout->addWidget(btn, 0, Qt::AlignRight); - QObject::connect(btn, &QPushButton::clicked, this, &Setup::nextPage); - - return widget; -} - -QWidget * Setup::network_setup() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 50, 55, 50); - - // title - QLabel *title = new QLabel(tr("Connect to Wi-Fi")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - - main_layout->addSpacing(25); - - // wifi widget - Networking *networking = new Networking(this, false); - networking->setStyleSheet("Networking {background-color: #292929; border-radius: 13px;}"); - main_layout->addWidget(networking, 1); - - main_layout->addSpacing(35); - - // back + continue buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - blayout->addWidget(back); - - QPushButton *cont = new QPushButton(); - cont->setObjectName("navBtn"); - cont->setProperty("primary", true); - cont->setEnabled(false); - QObject::connect(cont, &QPushButton::clicked, this, &Setup::nextPage); - blayout->addWidget(cont); - - // setup timer for testing internet connection - HttpRequest *request = new HttpRequest(this, false, 2500); - QObject::connect(request, &HttpRequest::requestDone, [=](const QString &, bool success) { - cont->setEnabled(success); - if (success) { - const bool wifi = networking->wifi->currentNetworkType() == NetworkType::WIFI; - cont->setText(wifi ? tr("Continue") : tr("Continue without Wi-Fi")); - } else { - cont->setText(tr("Waiting for internet")); - } - repaint(); - }); - request->sendRequest(OPENPILOT_URL); - QTimer *timer = new QTimer(this); - QObject::connect(timer, &QTimer::timeout, [=]() { - if (!request->active() && cont->isVisible()) { - request->sendRequest(OPENPILOT_URL); - } - }); - timer->start(1000); - - return widget; -} - -QWidget * radio_button(QString title, QButtonGroup *group) { - QPushButton *btn = new QPushButton(title); - btn->setCheckable(true); - group->addButton(btn); - btn->setStyleSheet(R"( - QPushButton { - height: 230; - padding-left: 100px; - padding-right: 100px; - text-align: left; - font-size: 80px; - font-weight: 400; - border-radius: 10px; - background-color: #4F4F4F; - } - QPushButton:checked { - background-color: #465BEA; - } - )"); - - // checkmark icon - QPixmap pix(":/icons/circled_check.svg"); - btn->setIcon(pix); - btn->setIconSize(QSize(0, 0)); - btn->setLayoutDirection(Qt::RightToLeft); - QObject::connect(btn, &QPushButton::toggled, [=](bool checked) { - btn->setIconSize(checked ? QSize(104, 104) : QSize(0, 0)); - }); - return btn; -} - -QWidget * Setup::software_selection() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 50, 55, 50); - main_layout->setSpacing(0); - - // title - QLabel *title = new QLabel(tr("Choose Software to Install")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignLeft | Qt::AlignTop); - - main_layout->addSpacing(50); - - // sunnypilot + custom radio buttons - QButtonGroup *group = new QButtonGroup(widget); - group->setExclusive(true); - - QWidget *openpilot = radio_button(tr("sunnypilot"), group); - main_layout->addWidget(openpilot); - - main_layout->addSpacing(30); - - QWidget *custom = radio_button(tr("Custom Software"), group); - main_layout->addWidget(custom); - - main_layout->addStretch(); - - // back + continue buttons - QHBoxLayout *blayout = new QHBoxLayout; - main_layout->addLayout(blayout); - blayout->setSpacing(50); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - QObject::connect(back, &QPushButton::clicked, this, &Setup::prevPage); - blayout->addWidget(back); - - QPushButton *cont = new QPushButton(tr("Continue")); - cont->setObjectName("navBtn"); - cont->setEnabled(false); - cont->setProperty("primary", true); - blayout->addWidget(cont); - - QObject::connect(cont, &QPushButton::clicked, [=]() { - if (group->checkedButton() != openpilot) { - QTimer::singleShot(0, [=]() { - setCurrentWidget(custom_software_warning_widget); - }); - } else { - QTimer::singleShot(0, [=]() { - setCurrentWidget(downloading_widget); - }); - QTimer::singleShot(1000, this, [=]() { - download(OPENPILOT_URL); - }); - } - }); - - connect(group, QOverload::of(&QButtonGroup::buttonClicked), [=](QAbstractButton *btn) { - btn->setChecked(true); - cont->setEnabled(true); - }); - - return widget; -} - -QWidget * Setup::downloading() { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - QLabel *txt = new QLabel(tr("Downloading...")); - txt->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(txt, 0, Qt::AlignCenter); - return widget; -} - -QWidget * Setup::download_failed(QLabel *url, QLabel *body) { - QWidget *widget = new QWidget(); - QVBoxLayout *main_layout = new QVBoxLayout(widget); - main_layout->setContentsMargins(55, 185, 55, 55); - main_layout->setSpacing(0); - - QLabel *title = new QLabel(tr("Download Failed")); - title->setStyleSheet("font-size: 90px; font-weight: 500;"); - main_layout->addWidget(title, 0, Qt::AlignTop | Qt::AlignLeft); - - main_layout->addSpacing(67); - - url->setWordWrap(true); - url->setAlignment(Qt::AlignTop | Qt::AlignLeft); - url->setStyleSheet("font-family: \"JetBrains Mono\"; font-size: 64px; font-weight: 400; margin-right: 100px;"); - main_layout->addWidget(url); - - main_layout->addSpacing(48); - - body->setWordWrap(true); - body->setAlignment(Qt::AlignTop | Qt::AlignLeft); - body->setStyleSheet("font-size: 80px; font-weight: 300; margin-right: 100px;"); - main_layout->addWidget(body); - - main_layout->addStretch(); - - // reboot + start over buttons - QHBoxLayout *blayout = new QHBoxLayout(); - blayout->setSpacing(50); - main_layout->addLayout(blayout, 0); - - QPushButton *reboot = new QPushButton(tr("Reboot device")); - reboot->setObjectName("navBtn"); - blayout->addWidget(reboot); - QObject::connect(reboot, &QPushButton::clicked, this, [=]() { - Hardware::reboot(); - }); - - QPushButton *restart = new QPushButton(tr("Start over")); - restart->setObjectName("navBtn"); - restart->setProperty("primary", true); - blayout->addWidget(restart); - QObject::connect(restart, &QPushButton::clicked, this, [=]() { - setCurrentIndex(1); - }); - - widget->setStyleSheet(R"( - QLabel { - margin-left: 117; - } - )"); - return widget; -} - -void Setup::prevPage() { - setCurrentIndex(currentIndex() - 1); -} - -void Setup::nextPage() { - setCurrentIndex(currentIndex() + 1); -} - -Setup::Setup(QWidget *parent) : QStackedWidget(parent) { - if (std::getenv("MULTILANG")) { - selectLanguage(); - } - - std::stringstream buffer; - buffer << std::ifstream("/sys/class/hwmon/hwmon1/in1_input").rdbuf(); - float voltage = (float)std::atoi(buffer.str().c_str()) / 1000.; - if (voltage < 7) { - addWidget(low_voltage()); - } - - addWidget(getting_started()); - addWidget(network_setup()); - software_selection_widget = software_selection(); - addWidget(software_selection_widget); - custom_software_warning_widget = custom_software_warning(); - addWidget(custom_software_warning_widget); - downloading_widget = downloading(); - addWidget(downloading_widget); - - QLabel *url_label = new QLabel(); - QLabel *body_label = new QLabel(); - failed_widget = download_failed(url_label, body_label); - addWidget(failed_widget); - - QObject::connect(this, &Setup::finished, [=](const QString &url, const QString &error) { - qDebug() << "finished" << url << error; - if (error.isEmpty()) { - // hide setup on success - QTimer::singleShot(3000, this, &QWidget::hide); - } else { - url_label->setText(url); - body_label->setText(error); - setCurrentWidget(failed_widget); - } - }); - - // TODO: revisit pressed bg color - setStyleSheet(R"( - * { - color: white; - font-family: Inter; - } - Setup { - background-color: black; - } - QPushButton#navBtn { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #333333; - } - QPushButton#navBtn:disabled, QPushButton[primary='true']:disabled { - color: #808080; - background-color: #333333; - } - QPushButton#navBtn:pressed { - background-color: #444444; - } - QPushButton[primary='true'], #navBtn[primary='true'] { - background-color: #465BEA; - } - QPushButton[primary='true']:pressed, #navBtn:pressed[primary='true'] { - background-color: #3049F4; - } - )"); -} - -void Setup::selectLanguage() { - QMap langs = getSupportedLanguages(); - QString selection = MultiOptionDialog::getSelection(tr("Select a language"), langs.keys(), "", this); - if (!selection.isEmpty()) { - QString selectedLang = langs[selection]; - Params().put("LanguageSetting", selectedLang.toStdString()); - if (translator.load(":/" + selectedLang)) { - qApp->installTranslator(&translator); - } - } -} - -int main(int argc, char *argv[]) { - QApplication a(argc, argv); - Setup setup; - setMainWindow(&setup); - return a.exec(); -} diff --git a/selfdrive/ui/qt/setup/setup.h b/selfdrive/ui/qt/setup/setup.h deleted file mode 100644 index 986956c902..0000000000 --- a/selfdrive/ui/qt/setup/setup.h +++ /dev/null @@ -1,38 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include - -class Setup : public QStackedWidget { - Q_OBJECT - -public: - explicit Setup(QWidget *parent = 0); - -private: - void selectLanguage(); - QWidget *low_voltage(); - QWidget *custom_software_warning(); - QWidget *getting_started(); - QWidget *network_setup(); - QWidget *software_selection(); - QWidget *downloading(); - QWidget *download_failed(QLabel *url, QLabel *body); - - QWidget *failed_widget; - QWidget *downloading_widget; - QWidget *custom_software_warning_widget; - QWidget *software_selection_widget; - QTranslator translator; - -signals: - void finished(const QString &url, const QString &error = ""); - -public slots: - void nextPage(); - void prevPage(); - void download(QString url); -}; diff --git a/selfdrive/ui/qt/setup/updater.cc b/selfdrive/ui/qt/setup/updater.cc deleted file mode 100644 index ed47590aa3..0000000000 --- a/selfdrive/ui/qt/setup/updater.cc +++ /dev/null @@ -1,186 +0,0 @@ -#include -#include -#include - -#include "system/hardware/hw.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/setup/updater.h" -#include "selfdrive/ui/qt/network/networking.h" - -Updater::Updater(const QString &updater_path, const QString &manifest_path, QWidget *parent) - : updater(updater_path), manifest(manifest_path), QStackedWidget(parent) { - - assert(updater.size()); - assert(manifest.size()); - - // initial prompt screen - prompt = new QWidget; - { - QVBoxLayout *layout = new QVBoxLayout(prompt); - layout->setContentsMargins(100, 250, 100, 100); - - QLabel *title = new QLabel(tr("Update Required")); - title->setStyleSheet("font-size: 80px; font-weight: bold;"); - layout->addWidget(title); - - layout->addSpacing(75); - - QLabel *desc = new QLabel(tr("An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB.")); - desc->setWordWrap(true); - desc->setStyleSheet("font-size: 65px;"); - layout->addWidget(desc); - - layout->addStretch(); - - QHBoxLayout *hlayout = new QHBoxLayout; - hlayout->setSpacing(30); - layout->addLayout(hlayout); - - QPushButton *connect = new QPushButton(tr("Connect to Wi-Fi")); - connect->setObjectName("navBtn"); - QObject::connect(connect, &QPushButton::clicked, [=]() { - setCurrentWidget(wifi); - }); - hlayout->addWidget(connect); - - QPushButton *install = new QPushButton(tr("Install")); - install->setObjectName("navBtn"); - install->setStyleSheet(R"( - QPushButton { - background-color: #465BEA; - } - QPushButton:pressed { - background-color: #3049F4; - } - )"); - QObject::connect(install, &QPushButton::clicked, this, &Updater::installUpdate); - hlayout->addWidget(install); - } - - // wifi connection screen - wifi = new QWidget; - { - QVBoxLayout *layout = new QVBoxLayout(wifi); - layout->setContentsMargins(100, 100, 100, 100); - - Networking *networking = new Networking(this, false); - networking->setStyleSheet("Networking { background-color: #292929; border-radius: 13px; }"); - layout->addWidget(networking, 1); - - QPushButton *back = new QPushButton(tr("Back")); - back->setObjectName("navBtn"); - back->setStyleSheet("padding-left: 60px; padding-right: 60px;"); - QObject::connect(back, &QPushButton::clicked, [=]() { - setCurrentWidget(prompt); - }); - layout->addWidget(back, 0, Qt::AlignLeft); - } - - // progress screen - progress = new QWidget; - { - QVBoxLayout *layout = new QVBoxLayout(progress); - layout->setContentsMargins(150, 330, 150, 150); - layout->setSpacing(0); - - text = new QLabel(tr("Loading...")); - text->setStyleSheet("font-size: 90px; font-weight: 600;"); - layout->addWidget(text, 0, Qt::AlignTop); - - layout->addSpacing(100); - - bar = new QProgressBar(); - bar->setRange(0, 100); - bar->setTextVisible(false); - bar->setFixedHeight(72); - layout->addWidget(bar, 0, Qt::AlignTop); - - layout->addStretch(); - - reboot = new QPushButton(tr("Reboot")); - reboot->setObjectName("navBtn"); - reboot->setStyleSheet("padding-left: 60px; padding-right: 60px;"); - QObject::connect(reboot, &QPushButton::clicked, [=]() { - Hardware::reboot(); - }); - layout->addWidget(reboot, 0, Qt::AlignLeft); - reboot->hide(); - - layout->addStretch(); - } - - addWidget(prompt); - addWidget(wifi); - addWidget(progress); - - setStyleSheet(R"( - * { - color: white; - outline: none; - font-family: Inter; - } - Updater { - color: white; - background-color: black; - } - QPushButton#navBtn { - height: 160; - font-size: 55px; - font-weight: 400; - border-radius: 10px; - background-color: #333333; - } - QPushButton#navBtn:pressed { - background-color: #444444; - } - QProgressBar { - border: none; - background-color: #292929; - } - QProgressBar::chunk { - background-color: #364DEF; - } - )"); -} - -void Updater::installUpdate() { - setCurrentWidget(progress); - QObject::connect(&proc, &QProcess::readyReadStandardOutput, this, &Updater::readProgress); - QObject::connect(&proc, QOverload::of(&QProcess::finished), this, &Updater::updateFinished); - proc.setProcessChannelMode(QProcess::ForwardedErrorChannel); - proc.start(updater, {"--swap", manifest}); -} - -void Updater::readProgress() { - auto lines = QString(proc.readAllStandardOutput()); - for (const QString &line : lines.trimmed().split("\n")) { - auto parts = line.split(":"); - if (parts.size() == 2) { - text->setText(parts[0]); - bar->setValue((int)parts[1].toDouble()); - } else { - qDebug() << line; - } - } - update(); -} - -void Updater::updateFinished(int exitCode, QProcess::ExitStatus exitStatus) { - qDebug() << "finished with " << exitCode; - if (exitCode == 0) { - Hardware::reboot(); - } else { - text->setText(tr("Update failed")); - reboot->show(); - } -} - -int main(int argc, char *argv[]) { - initApp(argc, argv); - QApplication a(argc, argv); - Updater updater(argv[1], argv[2]); - setMainWindow(&updater); - a.installEventFilter(&updater); - return a.exec(); -} diff --git a/selfdrive/ui/qt/setup/updater.h b/selfdrive/ui/qt/setup/updater.h deleted file mode 100644 index ce46c0aabd..0000000000 --- a/selfdrive/ui/qt/setup/updater.h +++ /dev/null @@ -1,29 +0,0 @@ -#pragma once - -#include -#include -#include -#include -#include -#include - -class Updater : public QStackedWidget { - Q_OBJECT - -public: - explicit Updater(const QString &updater_path, const QString &manifest_path, QWidget *parent = 0); - -private slots: - void installUpdate(); - void readProgress(); - void updateFinished(int exitCode, QProcess::ExitStatus exitStatus); - -private: - QProcess proc; - QString updater, manifest; - - QLabel *text; - QProgressBar *bar; - QPushButton *reboot; - QWidget *prompt, *wifi, *progress; -}; diff --git a/selfdrive/ui/qt/sidebar.cc b/selfdrive/ui/qt/sidebar.cc index fd8ea5bcf5..72400bd945 100644 --- a/selfdrive/ui/qt/sidebar.cc +++ b/selfdrive/ui/qt/sidebar.cc @@ -24,10 +24,12 @@ void Sidebar::drawMetric(QPainter &p, const QPair &label, QCol p.drawText(rect.adjusted(22, 0, 0, 0), Qt::AlignCenter, label.first + "\n" + label.second); } -Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false) { +Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed(false), settings_pressed(false), mic_indicator_pressed(false) { home_img = loadPixmap("../assets/images/button_home.png", home_btn.size()); flag_img = loadPixmap("../assets/images/button_flag.png", home_btn.size()); settings_img = loadPixmap("../assets/images/button_settings.png", settings_btn.size(), Qt::IgnoreAspectRatio); + mic_img = loadPixmap("../assets/icons/microphone.png", QSize(30, 30)); + link_img = loadPixmap("../assets/icons/link.png", QSize(60, 60)); connect(this, &Sidebar::valueChanged, [=] { update(); }); @@ -37,7 +39,7 @@ Sidebar::Sidebar(QWidget *parent) : QFrame(parent), onroad(false), flag_pressed( QObject::connect(uiState(), &UIState::uiUpdate, this, &Sidebar::updateState); - pm = std::make_unique(std::vector{"userFlag"}); + pm = std::make_unique(std::vector{"bookmarkButton"}); } void Sidebar::mousePressEvent(QMouseEvent *event) { @@ -47,20 +49,25 @@ void Sidebar::mousePressEvent(QMouseEvent *event) { } else if (settings_btn.contains(event->pos())) { settings_pressed = true; update(); + } else if (recording_audio && mic_indicator_btn.contains(event->pos())) { + mic_indicator_pressed = true; + update(); } } void Sidebar::mouseReleaseEvent(QMouseEvent *event) { - if (flag_pressed || settings_pressed) { - flag_pressed = settings_pressed = false; + if (flag_pressed || settings_pressed || mic_indicator_pressed) { + flag_pressed = settings_pressed = mic_indicator_pressed = false; update(); } if (onroad && home_btn.contains(event->pos())) { MessageBuilder msg; - msg.initEvent().initUserFlag(); - pm->send("userFlag", msg); + msg.initEvent().initBookmarkButton(); + pm->send("bookmarkButton", msg); } else if (settings_btn.contains(event->pos())) { emit openSettings(); + } else if (recording_audio && mic_indicator_btn.contains(event->pos())) { + emit openSettings(2, "RecordAudio"); } } @@ -106,6 +113,8 @@ void Sidebar::updateState(const UIState &s) { pandaStatus = {{tr("NO"), tr("PANDA")}, danger_color}; } setProperty("pandaStatus", QVariant::fromValue(pandaStatus)); + + setProperty("recordingAudio", s.scene.recording_audio); } void Sidebar::paintEvent(QPaintEvent *event) { @@ -124,6 +133,14 @@ void Sidebar::drawSidebar(QPainter &p) { p.drawPixmap(settings_btn.x(), settings_btn.y(), settings_img); p.setOpacity(onroad && flag_pressed ? 0.65 : 1.0); p.drawPixmap(home_btn.x(), home_btn.y(), onroad ? flag_img : home_img); + if (recording_audio) { + p.setBrush(danger_color); + p.setOpacity(mic_indicator_pressed ? 0.65 : 1.0); + p.drawRoundedRect(mic_indicator_btn, mic_indicator_btn.height() / 2, mic_indicator_btn.height() / 2); + int icon_x = mic_indicator_btn.x() + (mic_indicator_btn.width() - mic_img.width()) / 2; + int icon_y = mic_indicator_btn.y() + (mic_indicator_btn.height() - mic_img.height()) / 2; + p.drawPixmap(icon_x, icon_y, mic_img); + } p.setOpacity(1.0); // network @@ -138,7 +155,12 @@ void Sidebar::drawSidebar(QPainter &p) { p.setFont(InterFont(35)); p.setPen(QColor(0xff, 0xff, 0xff)); const QRect r = QRect(58, 247, width() - 100, 50); - p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type); + + if (net_type == "Hotspot") { + p.drawPixmap(r.x(), r.y() + (r.height() - link_img.height()) / 2, link_img); + } else { + p.drawText(r, Qt::AlignLeft | Qt::AlignVCenter, net_type); + } #ifndef SUNNYPILOT // metrics diff --git a/selfdrive/ui/qt/sidebar.h b/selfdrive/ui/qt/sidebar.h index 3c57d10a1a..30a92c972e 100644 --- a/selfdrive/ui/qt/sidebar.h +++ b/selfdrive/ui/qt/sidebar.h @@ -23,6 +23,7 @@ class Sidebar : public QFrame { Q_PROPERTY(ItemStatus tempStatus MEMBER temp_status NOTIFY valueChanged); Q_PROPERTY(QString netType MEMBER net_type NOTIFY valueChanged); Q_PROPERTY(int netStrength MEMBER net_strength NOTIFY valueChanged); + Q_PROPERTY(bool recordingAudio MEMBER recording_audio NOTIFY valueChanged); public: explicit Sidebar(QWidget* parent = 0); @@ -42,8 +43,8 @@ protected: void drawMetric(QPainter &p, const QPair &label, QColor c, int y); virtual void drawSidebar(QPainter &p); - QPixmap home_img, flag_img, settings_img; - bool onroad, flag_pressed, settings_pressed; + QPixmap home_img, flag_img, settings_img, mic_img, link_img; + bool onroad, recording_audio, flag_pressed, settings_pressed, mic_indicator_pressed; const QMap network_type = { {cereal::DeviceState::NetworkType::NONE, tr("--")}, {cereal::DeviceState::NetworkType::WIFI, tr("Wi-Fi")}, @@ -56,6 +57,7 @@ protected: const QRect home_btn = QRect(60, 860, 180, 180); const QRect settings_btn = QRect(50, 35, 200, 117); + const QRect mic_indicator_btn = QRect(158, 252, 75, 40); const QColor good_color = QColor(255, 255, 255); const QColor warning_color = QColor(218, 202, 37); const QColor danger_color = QColor(201, 34, 49); diff --git a/selfdrive/ui/qt/util.cc b/selfdrive/ui/qt/util.cc index dedc59f0e4..9af76e4aea 100644 --- a/selfdrive/ui/qt/util.cc +++ b/selfdrive/ui/qt/util.cc @@ -18,6 +18,7 @@ #include #include "common/swaglog.h" +#include "common/util.h" #include "system/hardware/hw.h" QString getVersion() { @@ -57,6 +58,10 @@ QMap getSupportedLanguages() { } QString timeAgo(const QDateTime &date) { + if (!util::system_time_valid()) { + return date.date().toString(); + } + int diff = date.secsTo(QDateTime::currentDateTimeUtc()); QString s; diff --git a/selfdrive/ui/qt/widgets/input.cc b/selfdrive/ui/qt/widgets/input.cc index 0cbf14931b..efd330587a 100644 --- a/selfdrive/ui/qt/widgets/input.cc +++ b/selfdrive/ui/qt/widgets/input.cc @@ -2,6 +2,7 @@ #include #include +#include #include "system/hardware/hw.h" #include "selfdrive/ui/qt/util.h" @@ -334,3 +335,272 @@ QString MultiOptionDialog::getSelection(const QString &prompt_text, const QStrin } return ""; } + +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; } + #confirm_btn[enabled="false"] { background-color: #2B2B2B; } + #confirm_btn:enabled { background-color: #465BEA; } + #confirm_btn:enabled:pressed { background-color: #3049F4; } + QTreeWidget { + background-color: transparent; + border: none; + } + QTreeWidget::item { + height: 135; + padding: 0px 50px; + margin: 5px; + text-align: left; + font-size: 55px; + font-weight: 300; + border-radius: 10px; + background-color: #4F4F4F; + color: white; + } + QTreeWidget::item:selected { + background-color: #465BEA; + } + QTreeWidget::branch { + background-color: transparent; + } + )"); + + QVBoxLayout *main_layout = new QVBoxLayout(container); + main_layout->setContentsMargins(55, 50, 55, 50); + + QLabel *title = new QLabel(prompt_text, this); + title->setStyleSheet("font-size: 70px; font-weight: 500;"); + main_layout->addWidget(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); + treeWidget->setExpandsOnDoubleClick(false); // Disable double-click expansion + treeWidget->setAnimated(true); + treeWidget->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + treeWidget->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + treeWidget->setVerticalScrollMode(QAbstractItemView::ScrollPerPixel); + treeWidget->setDragEnabled(false); + treeWidget->setMouseTracking(true); + + // Connect single-click to expand/collapse + QObject::connect(treeWidget, &QTreeWidget::itemClicked, [=](QTreeWidgetItem *item, int) { + if (item->childCount() > 0) { + item->setExpanded(!item->isExpanded()); + treeWidget->scrollToItem(item->child(0), QAbstractItemView::EnsureVisible); + } + }); + + QScroller::grabGesture(treeWidget->viewport(), 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); + while (iter.hasNext()) { + 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.displayName); + topLevel->setData(0, Qt::UserRole, item.ref); + topLevel->setFlags(topLevel->flags() | Qt::ItemIsSelectable); + treeWidget->addTopLevelItem(topLevel); + if (item.ref == current) { + topLevel->setSelected(true); + } + } + } else { + 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.folder); + folderItem->setFlags(folderItem->flags() | Qt::ItemIsAutoTristate); + folderItem->setFlags(folderItem->flags() & ~Qt::ItemIsSelectable); + + for (const TreeNode item : currItem.items) + { + QTreeWidgetItem *childItem = addChildItem(item.displayName, item.ref, folderItem); + if (item.ref == current) { + childItem->setSelected(true); + folderItem->setExpanded(true); + } + } + } + } + + // Create favorites folder + favorites = new QTreeWidgetItem(); + favorites->setIcon(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); + + QObject::connect(treeWidget, &QTreeWidget::itemSelectionChanged, [=]() { + QList selectedItems = treeWidget->selectedItems(); + if (!selectedItems.isEmpty()) { + selection = selectedItems.first()->data(0, Qt::UserRole).toString(); + confirm_btn->setEnabled(selection != current); + } + }); + + ScrollView *scroll_view = new ScrollView(treeWidget, this); + scroll_view->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded); + + main_layout->addWidget(scroll_view); + main_layout->addSpacing(35); + + // cancel + confirm buttons + QHBoxLayout *blayout = new QHBoxLayout; + main_layout->addLayout(blayout); + blayout->setSpacing(50); + + QPushButton *cancel_btn = new QPushButton(tr("Cancel")); + QObject::connect(cancel_btn, &QPushButton::clicked, this, &ConfirmationDialog::reject); + QObject::connect(confirm_btn, &QPushButton::clicked, this, &ConfirmationDialog::accept); + blayout->addWidget(cancel_btn); + blayout->addWidget(confirm_btn); + + QVBoxLayout *outer_layout = new QVBoxLayout(this); + outer_layout->setContentsMargins(50, 50, 50, 50); + outer_layout->addWidget(container); +} + +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 089e54e4a0..3fb1ebfe1a 100644 --- a/selfdrive/ui/qt/widgets/input.h +++ b/selfdrive/ui/qt/widgets/input.h @@ -6,10 +6,24 @@ #include #include #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 @@ -69,3 +83,25 @@ public: static QString getSelection(const QString &prompt_text, const QStringList &l, const QString ¤t, QWidget *parent); QString selection; }; + +class TreeOptionDialog : public DialogBase { + Q_OBJECT + +public: + 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/qt/widgets/offroad_alerts.cc b/selfdrive/ui/qt/widgets/offroad_alerts.cc index f630875978..3a4828a2a8 100644 --- a/selfdrive/ui/qt/widgets/offroad_alerts.cc +++ b/selfdrive/ui/qt/widgets/offroad_alerts.cc @@ -32,15 +32,19 @@ AbstractAlert::AbstractAlert(bool hasRebootBtn, QWidget *parent) : QFrame(parent footer_layout->addWidget(dismiss_btn, 0, Qt::AlignBottom | Qt::AlignLeft); QObject::connect(dismiss_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); - snooze_btn = new QPushButton(tr("Snooze Update")); - snooze_btn->setVisible(false); - snooze_btn->setFixedSize(550, 125); - footer_layout->addWidget(snooze_btn, 0, Qt::AlignBottom | Qt::AlignRight); - QObject::connect(snooze_btn, &QPushButton::clicked, [=]() { - params.putBool("SnoozeUpdate", true); + action_btn = new QPushButton(); + action_btn->setVisible(false); + action_btn->setFixedHeight(125); + footer_layout->addWidget(action_btn, 0, Qt::AlignBottom | Qt::AlignRight); + QObject::connect(action_btn, &QPushButton::clicked, [=]() { + if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { + params.remove("Offroad_ExcessiveActuation"); + } else { + params.putBool("SnoozeUpdate", true); + } }); - QObject::connect(snooze_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); - snooze_btn->setStyleSheet(R"(color: white; background-color: #4F4F4F;)"); + QObject::connect(action_btn, &QPushButton::clicked, this, &AbstractAlert::dismiss); + action_btn->setStyleSheet("color: white; background-color: #4F4F4F; padding-left: 60px; padding-right: 60px;"); if (hasRebootBtn) { QPushButton *rebootBtn = new QPushButton(tr("Reboot and Update")); @@ -107,7 +111,14 @@ int OffroadAlert::refresh() { label->setVisible(!text.isEmpty()); alertCount += !text.isEmpty(); } - snooze_btn->setVisible(!alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); + + action_btn->setVisible(!alerts["Offroad_ExcessiveActuation"]->text().isEmpty() || !alerts["Offroad_ConnectivityNeeded"]->text().isEmpty()); + if (!alerts["Offroad_ExcessiveActuation"]->text().isEmpty()) { + action_btn->setText(tr("Acknowledge Excessive Actuation")); + } else { + action_btn->setText(tr("Snooze Update")); + } + return alertCount; } diff --git a/selfdrive/ui/qt/widgets/offroad_alerts.h b/selfdrive/ui/qt/widgets/offroad_alerts.h index ace2e75456..2dcf4f9d8c 100644 --- a/selfdrive/ui/qt/widgets/offroad_alerts.h +++ b/selfdrive/ui/qt/widgets/offroad_alerts.h @@ -15,9 +15,10 @@ class AbstractAlert : public QFrame { protected: AbstractAlert(bool hasRebootBtn, QWidget *parent = nullptr); - QPushButton *snooze_btn; + QPushButton *action_btn; QVBoxLayout *scrollable_layout; Params params; + std::map alerts; signals: void dismiss(); @@ -40,7 +41,4 @@ class OffroadAlert : public AbstractAlert { public: explicit OffroadAlert(QWidget *parent = 0) : AbstractAlert(false, parent) {} int refresh(); - -private: - std::map alerts; }; diff --git a/selfdrive/ui/soundd.py b/selfdrive/ui/soundd.py index facb398339..a94456efe0 100644 --- a/selfdrive/ui/soundd.py +++ b/selfdrive/ui/soundd.py @@ -139,7 +139,7 @@ class Soundd(QuietMode): # sounddevice must be imported after forking processes import sounddevice as sd - sm = messaging.SubMaster(['selfdriveState', 'microphone']) + sm = messaging.SubMaster(['selfdriveState', 'soundPressure']) with self.get_stream(sd) as stream: rk = Ratekeeper(20) @@ -150,8 +150,8 @@ class Soundd(QuietMode): self.load_param() - if sm.updated['microphone'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert - self.spl_filter_weighted.update(sm["microphone"].soundPressureWeightedDb) + if sm.updated['soundPressure'] and self.current_alert == AudibleAlert.none: # only update volume filter when not playing alert + self.spl_filter_weighted.update(sm["soundPressure"].soundPressureWeightedDb) self.current_volume = self.calculate_volume(float(self.spl_filter_weighted.x)) self.get_audible_alert(sm) diff --git a/selfdrive/ui/sunnypilot/SConscript b/selfdrive/ui/sunnypilot/SConscript index afa5a4d628..1ea9bb8e13 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", @@ -21,6 +22,7 @@ qt_src = [ "sunnypilot/qt/home.cc", "sunnypilot/qt/offroad/exit_offroad_button.cc", "sunnypilot/qt/offroad/offroad_home.cc", + "sunnypilot/qt/offroad/settings/developer_panel.cc", "sunnypilot/qt/offroad/settings/device_panel.cc", "sunnypilot/qt/offroad/settings/lateral_panel.cc", "sunnypilot/qt/offroad/settings/longitudinal_panel.cc", diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc new file mode 100644 index 0000000000..9c36097f1b --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.cc @@ -0,0 +1,89 @@ +/** + * 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/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); + + QObject::connect(showAdvancedControls, &ParamControlSP::toggleFlipped, this, [=](bool) { + AbstractControlSP::UpdateAllAdvancedControls(); + updateToggles(!uiState()->scene.started); + }); + showAdvancedControls->showDescription(); + + // Github Runner Toggle + 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); + + QObject::connect(prebuiltToggle, &ParamControl::toggleFlipped, [=](bool state) { + QString prebuiltPath = "/data/openpilot/prebuilt"; + state ? QFile(prebuiltPath).open(QIODevice::WriteOnly) : QFile::remove(prebuiltPath); + prebuiltToggle->refresh(); + }); + prebuiltToggle->setVisible(false); + + // Error log button + errorLogBtn = new ButtonControlSP(tr("Error Log"), tr("VIEW"), tr("View the error log for sunnypilot crashes.")); + connect(errorLogBtn, &ButtonControlSP::clicked, [=]() { + QFileInfo file("/data/community/crashes/error.log"); + QString text; + if (file.exists()) { + text = "" + file.lastModified().toString("dd-MMM-yyyy hh:mm:ss ").toUpper() + "

"; + } + text += QString::fromStdString(util::read_file("/data/community/crashes/error.log")); + ConfirmationDialog::rich(text, this); + }); + addItem(errorLogBtn); + + QObject::connect(uiState(), &UIState::offroadTransition, this, &DeveloperPanelSP::updateToggles); + + is_release = params.getBool("IsReleaseBranch"); + is_tested = params.getBool("IsTestedBranch"); + is_development = params.getBool("IsDevelopmentBranch"); +} + +void DeveloperPanelSP::updateToggles(bool offroad) { + bool disable_updates = params.getBool("DisableUpdates"); + + prebuiltToggle->setVisible(!is_release && !is_tested && !is_development); + prebuiltToggle->setEnabled(disable_updates); + params.putBool("QuickBootToggle", QFile::exists("/data/openpilot/prebuilt")); + prebuiltToggle->refresh(); + + prebuiltToggle->setDescription(disable_updates + ? tr("When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, " + "it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. " + "

To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.") + : tr("Quickboot mode requires updates to be disabled.
Enable 'Disable Updates' in the Software panel first.")); + prebuiltToggle->showDescription(); + + enableGithubRunner->setVisible(!is_release); + errorLogBtn->setVisible(!is_release); + showAdvancedControls->setEnabled(true); +} + +void DeveloperPanelSP::showEvent(QShowEvent *event) { + DeveloperPanel::showEvent(event); + AbstractControlSP::UpdateAllAdvancedControls(); + updateToggles(!uiState()->scene.started); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h new file mode 100644 index 0000000000..7f67512b5d --- /dev/null +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.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 +#include + +#include "selfdrive/ui/qt/offroad/developer_panel.h" + +class DeveloperPanelSP : public DeveloperPanel { + Q_OBJECT +public: + explicit DeveloperPanelSP(SettingsWindow *parent); + +private: + ParamControlSP *enableCopyparty; + ParamControlSP *enableGithubRunner; + ButtonControlSP *errorLogBtn; + ParamControlSP *prebuiltToggle; + Params params; + ParamControlSP *showAdvancedControls; + bool is_development; + bool is_release; + bool is_tested; + +private slots: + void updateToggles(bool offroad); + +protected: + void showEvent(QShowEvent *event) override; +}; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc index b89116d424..7e3d18bd81 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.cc @@ -87,6 +87,17 @@ DevicePanelSP::DevicePanelSP(SettingsWindowSP *parent) : DevicePanel(parent) { params.put("DeviceBootMode", QString::number(index).toStdString()); updateState(); }); + + interactivityTimeout = new OptionControlSP("InteractivityTimeout", tr("Interactivity Timeout"), + tr("Apply a custom timeout for settings UI." + "\nThis is the time after which settings UI closes automatically if user is not interacting with the screen."), + "", {0, 120}, 10, true, nullptr, false); + + connect(interactivityTimeout, &OptionControlSP::updateLabels, [=]() { + updateState(); + }); + + addItem(interactivityTimeout); // Brightness brightness = new Brightness(); @@ -198,4 +209,11 @@ void DevicePanelSP::updateState() { currStatus = DeviceSleepModeStatus::OFFROAD; } toggleDeviceBootMode->setDescription(deviceSleepModeDescription(currStatus)); + + QString timeoutValue = QString::fromStdString(params.get("InteractivityTimeout")); + if (timeoutValue == "0" || timeoutValue.isEmpty()) { + interactivityTimeout->setLabel("Default"); + } else { + interactivityTimeout->setLabel(timeoutValue + "s"); + } } diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h index 2cd3281d1f..899a942dae 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h @@ -33,6 +33,7 @@ private: MaxTimeOffroad *maxTimeOffroad; ButtonParamControlSP *toggleDeviceBootMode; Brightness *brightness; + OptionControlSP *interactivityTimeout; const QString alwaysOffroadStyle = R"( PushButtonSP { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc index b8237c3627..c3f795e18c 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.cc @@ -8,6 +8,8 @@ #include #include #include +#include +#include #include "common/model.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" @@ -66,6 +68,19 @@ ModelsPanel::ModelsPanel(QWidget *parent) : QWidget(parent) { connect(uiStateSP(), &UIStateSP::uiUpdate, this, &ModelsPanel::updateLabels); list->addItem(currentModelLblBtn); + refreshAvailableModelsBtn = new ButtonControlSP(tr("Refresh Model List"), tr("REFRESH"), "", this); + connect(refreshAvailableModelsBtn, &ButtonControlSP::clicked, this, [=]() { + params.put("ModelManager_LastSyncTime", "0"); + ConfirmationDialog::alert(tr("Fetching Latest Models"), this); + }); + + list->addItem(refreshAvailableModelsBtn); + + clearModelCacheBtn = new ButtonControlSP(tr("Clear Model Cache"), tr("CLEAR"), "", this); + connect(clearModelCacheBtn, &ButtonControlSP::clicked, this, &ModelsPanel::clearModelCache); + + list->addItem(clearModelCacheBtn); + // Create progress bars for downloads supercomboProgressBar = createProgressBar(this); QString supercomboType = tr("Driving Model"); @@ -86,15 +101,52 @@ 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 - list->addItem(new ParamControlSP("LagdToggle", - tr("Live Learning Steer Delay"), - tr("Enable this for the car to learn and adapt its steering response time. " - "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."), - "../assets/offroad/icon_shell.png")); + lagd_toggle_control = new ParamControlSP("LagdToggle", tr("Live Learning Steer Delay"), "", "../assets/offroad/icon_shell.png"); + lagd_toggle_control->showDescription(); + list->addItem(lagd_toggle_control); + + // Software delay control + 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, 50}, 1, false, nullptr, true, true); + + connect(delay_control, &OptionControlSP::updateLabels, [=]() { + float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); + delay_control->setLabel(QString::number(value, 'f', 2) + "s"); + }); + connect(lagd_toggle_control, &ParamControlSP::toggleFlipped, [=](bool state) { + delay_control->setVisible(!state); + }); + delay_control->showDescription(); + list->addItem(delay_control); } QProgressBar* ModelsPanel::createProgressBar(QWidget *parent) { @@ -117,6 +169,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 @@ -217,6 +282,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(); @@ -230,42 +307,93 @@ void ModelsPanel::handleCurrentModelLblBtnClicked() { currentModelLblBtn->setEnabled(false); currentModelLblBtn->setValue(tr("Fetching models...")); - // Create mapping of bundle indices to display names - QMap index_to_bundle; + QList sortedModels; + QSet modelFolders; + QRegularExpression re("\\(([^)]*)\\)[^(]*$"); const auto bundles = model_manager.getAvailableBundles(); - for (const auto &bundle: bundles) { - index_to_bundle.insert(bundle.getIndex(), QString::fromStdString(bundle.getDisplayName())); + + for (const auto &bundle : bundles) { + auto overrides = bundle.getOverrides(); + QString folder; + for (const auto &override : overrides) { + if (override.getKey() == "folder") { + folder = QString::fromStdString(override.getValue().cStr()); + } + } + + modelFolders.insert(folder); + sortedModels.append(TreeNode{ + folder, + QString::fromStdString(bundle.getDisplayName()), + QString::fromStdString(bundle.getRef()), + static_cast(bundle.getIndex()) + }); } - // Sort bundles by index in descending order - QStringList bundleNames; - // Add "Default" as the first option - bundleNames.append(DEFAULT_MODEL); + std::sort(sortedModels.begin(), sortedModels.end(), + [](const TreeNode &a, const TreeNode &b) { + return a.index > b.index; + }); - auto indices = index_to_bundle.keys(); - std::sort(indices.begin(), indices.end(), std::greater()); - for (const auto &index: indices) { - bundleNames.append(index_to_bundle[index]); + // Create a list of folder-maxIndex pairs for sorting + QList> folderMaxIndices; + for (const auto &folder : modelFolders) { + int maxIndex = -1; + for (const auto &model : sortedModels) { + if (model.folder == folder) { + maxIndex = std::max(maxIndex, model.index); + } + } + folderMaxIndices.append(qMakePair(folder, maxIndex)); } + // Sort folders by their highest model index + std::sort(folderMaxIndices.begin(), folderMaxIndices.end(), + [](const QPair &a, const QPair &b) { + return a.second > b.second; + }); + + // Create the final items list using sorted folders + QList items; + for (const auto &folderPair : folderMaxIndices) { + QList folderModels; + QString folder = folderPair.first; + for (const auto &model : sortedModels) { + if (model.folder == folderPair.first) { + 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(TreeFolder{folder, folderModels}); + } + + items.insert(0, TreeFolder{"", { + TreeNode{"", DEFAULT_MODEL, DEFAULT_MODEL, -1} + }}); + currentModelLblBtn->setValue(GetActiveModelInternalName()); - const QString selectedBundleName = MultiOptionDialog::getSelection( - tr("Select a Model"), bundleNames, 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(); @@ -290,6 +418,48 @@ void ModelsPanel::updateLabels() { handleBundleDownloadProgress(); currentModelLblBtn->setEnabled(!is_onroad && !isDownloading()); currentModelLblBtn->setValue(GetActiveModelInternalName()); + + // Update lagdToggle description with current value + QString desc = tr("Enable this for the car to learn and adapt its steering response time. " + "Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience."); + bool lagdEnabled = params.getBool("LagdToggle"); + if (lagdEnabled) { + 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)); + } + } else { + auto carParamsBytes = params.get("CarParamsPersistent"); + if (!carParamsBytes.empty()) { + 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)); + } + } + lagd_toggle_control->setDescription(desc); + + delay_control->setVisible(!params.getBool("LagdToggle")); + if (delay_control->isVisible()) { + float value = QString::fromStdString(params.get("LagdToggleDelay")).toFloat(); + 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"); } /** @@ -310,3 +480,39 @@ void ModelsPanel::showResetParamsDialog() { params.remove("LiveTorqueParameters"); } } + +void ModelsPanel::clearModelCache() { + QString confirmMsg = tr("This will delete ALL downloaded models from the cache" + "
except the currently active model." + "

Are you sure you want to continue?"); + QString content("

" + tr("Driving Model Selector") + "


" + "

" + confirmMsg + "

"); + if (showConfirmationDialog( + content, + tr("Clear Cache"))) { + params.putBool("ModelManager_ClearCache", true); + } +} + +double ModelsPanel::calculateCacheSize() { + QFuture future_ModelCacheSize = QtConcurrent::run([=]() { + + QDir model_dir(QString::fromStdString(Path::model_root())); + QFileInfoList model_files = model_dir.entryInfoList(QDir::Files | QDir::NoDotAndDotDot); + qint64 totalSize = 0; + for (const QFileInfo &model_file : model_files) { + if (model_file.isFile()) { + totalSize += model_file.size(); + } + } + return totalSize; + }); + return static_cast(future_ModelCacheSize) / (1024.0 * 1024.0); +} + +void ModelsPanel::showEvent(QShowEvent *event) { + lagd_toggle_control->showDescription(); + if (delay_control->isVisible()) { + delay_control->showDescription(); + } +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h index 8b8cf69759..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 { @@ -20,7 +21,9 @@ public: private: QString GetActiveModelName(); QString GetActiveModelInternalName(); + QString GetActiveModelRef(); void updateModelManagerState(); + void showEvent(QShowEvent *event) override; bool isDownloading() const { if (!model_manager.hasSelectedBundle()) { @@ -35,12 +38,15 @@ private: void updateLabels(); void handleCurrentModelLblBtnClicked(); void handleBundleDownloadProgress(); + void refreshLaneTurnValueControl(); void showResetParamsDialog(); QProgressBar* createProgressBar(QWidget *parent); QFrame* createModelDetailFrame(QWidget *parent, QString &typeName, QProgressBar *progressBar); cereal::ModelManagerSP::Reader model_manager; cereal::ModelManagerSP::DownloadStatus download_status{}; cereal::ModelManagerSP::DownloadStatus prev_download_status{}; + void clearModelCache(); + double calculateCacheSize(); bool canContinueOnMeteredDialog() { if (!is_metered) return true; @@ -64,6 +70,8 @@ private: bool is_onroad = false; ButtonControlSP *currentModelLblBtn; + ParamControlSP *lagd_toggle_control; + OptionControlSP *delay_control; QProgressBar *supercomboProgressBar; QFrame *supercomboFrame; QProgressBar *navigationProgressBar; @@ -73,5 +81,8 @@ private: QProgressBar *policyProgressBar; QFrame *policyFrame; Params params; - + ButtonControlSP *clearModelCacheBtn; + ButtonControlSP *refreshAvailableModelsBtn; + ParamControlSP *lane_turn_desire_toggle; + OptionControlSP *lane_turn_value_control; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc index 05ea5ff899..5415e75ea9 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/settings.cc @@ -8,10 +8,10 @@ #include "selfdrive/ui/sunnypilot/qt/offroad/settings/settings.h" #include "selfdrive/ui/sunnypilot/qt/widgets/scrollview.h" -#include "selfdrive/ui/qt/offroad/developer_panel.h" #include "selfdrive/ui/qt/offroad/firehose.h" #include "selfdrive/ui/sunnypilot/qt/network/networking.h" +#include "selfdrive/ui/sunnypilot/qt/offroad/settings/developer_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/device_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/models_panel.h" #include "selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h" @@ -91,7 +91,7 @@ SettingsWindowSP::SettingsWindowSP(QWidget *parent) : SettingsWindow(parent) { PanelInfo(" " + tr("Trips"), new TripsPanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_trips.png"), PanelInfo(" " + tr("Vehicle"), new VehiclePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_vehicle.png"), PanelInfo(" " + tr("Firehose"), new FirehosePanel(this), "../../sunnypilot/selfdrive/assets/offroad/icon_firehose.svg"), - PanelInfo(" " + tr("Developer"), new DeveloperPanel(this), "../assets/icons/shell.png"), + PanelInfo(" " + tr("Developer"), new DeveloperPanelSP(this), "../assets/icons/shell.png"), }; nav_btns = new QButtonGroup(this); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc index 9430d02ba8..a1961cb1ea 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.cc @@ -18,6 +18,18 @@ SoftwarePanelSP::SoftwarePanelSP(QWidget *parent) : SoftwarePanel(parent) { searchBranches(d.text()); } }); + + // Disable Updates toggle + disableUpdatesToggle = new ParamControl("DisableUpdates", + tr("Disable Updates"), + tr("When enabled, software updates will be disabled. This requires a reboot to take effect."), + "../assets/icons/icon_warning.png", + this, true); + disableUpdatesToggle->showDescription(); + addItem(disableUpdatesToggle); + connect(disableUpdatesToggle, &ParamControl::toggleFlipped, this, &SoftwarePanelSP::handleDisableUpdatesToggled); + connect(uiState(), &UIState::offroadTransition, this, &SoftwarePanelSP::updateDisableUpdatesToggle); + updateDisableUpdatesToggle(!uiState()->scene.started); } /** @@ -49,3 +61,27 @@ void SoftwarePanelSP::searchBranches(const QString &query) { checkForUpdates(); } } + +void SoftwarePanelSP::handleDisableUpdatesToggled(bool state) { + if (ConfirmationDialog::confirm(tr("%1 updates requires a reboot.
Reboot now?") + .arg(state ? "Disabling" : "Enabling"), tr("Reboot"), this)) { + params.putBool("DoReboot", true); + } else { + params.putBool("DisableUpdates", !state); + disableUpdatesToggle->refresh(); + } +} + +void SoftwarePanelSP::updateDisableUpdatesToggle(bool offroad) { + bool enabled = offroad; + disableUpdatesToggle->setEnabled(enabled); + disableUpdatesToggle->setDescription(enabled + ? tr("When enabled, software updates will be disabled.
This requires a reboot to take effect.") + : tr("Please enable always offroad mode or turn off vehicle to adjust these toggles")); +} + +void SoftwarePanelSP::showEvent(QShowEvent *event) { + SoftwarePanel::showEvent(event); + updateDisableUpdatesToggle(!uiState()->scene.started); + disableUpdatesToggle->showDescription(); +} diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h index 56d8b9be5b..efaa836a21 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/software_panel.h @@ -19,4 +19,10 @@ public: private: void searchBranches(const QString &query); + ParamControl *disableUpdatesToggle = nullptr; + void handleDisableUpdatesToggled(bool state); +private slots: + void updateDisableUpdatesToggle(bool offroad); +protected: + void showEvent(QShowEvent *event) override; }; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.cc index 1358cbc959..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", @@ -66,6 +87,14 @@ SunnylinkPanel::SunnylinkPanel(QWidget *parent) : QFrame(parent) { }); list->addItem(horizontal_line()); + QString sunnylinkUploaderDesc = tr("Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.)"); + sunnylinkUploaderEnabledBtn = new ParamControlSP( + "EnableSunnylinkUploader", + tr("[Don't use] Enable sunnylink uploader"), + sunnylinkUploaderDesc, + "", nullptr, true); + list->addItem(sunnylinkUploaderEnabledBtn); + connect(sunnylinkEnabledBtn, &ParamControl::showDescriptionEvent, [=]() { // resets the description to the default one for the Easter egg sunnylinkEnabledBtn->setDescription(sunnylinkEnabledBtnDesc); @@ -261,6 +290,8 @@ void SunnylinkPanel::updatePanel() { pairSponsorBtn->setEnabled(!is_onroad && is_sunnylink_enabled); pairSponsorBtn->setValue(is_paired ? tr("Paired") : tr("Not Paired")); + sunnylinkUploaderEnabledBtn->setEnabled(max_current_sponsor_rule.roleTier == SponsorTier::Guardian && is_sunnylink_enabled); + if (!is_sunnylink_enabled) { sunnylinkEnabledBtn->setValue(""); sponsorBtn->setValue(""); diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h index e17c68d3a3..c56d8ebd1a 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/sunnylink_panel.h @@ -49,6 +49,7 @@ private: QString sunnylinkBtnDescription; PushButtonSP *restoreSettings; PushButtonSP *backupSettings; + ParamControlSP * sunnylinkUploaderEnabledBtn; void stopSunnylink() const; void startSunnylink() const; diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_live_tuning.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_live_tuning.cc index d7eff4b9bd..fd02d81ba1 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_live_tuning.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/vehicle/hyundai_live_tuning.cc @@ -16,11 +16,9 @@ struct ParamConfig { // Clipping limits on all tuning params to prevent unsafe values. static const std::vector PARAM_CONFIGS = { {"LongTuningAccelMax", QObject::tr("Acceleration Max"), QObject::tr("Acceleration limit (m/s^2)"), "2.00", 0.5f, 2.0f, 0.1f}, - {"LongTuningVEgoStopping", QObject::tr("Stopping Threshold"), QObject::tr("Velocity threshold for stopping behavior (m/s)"), "0.25", 0.05f, 0.5f, 0.01f}, - {"LongTuningStoppingDecelRate", QObject::tr("Stopping Decel Rate"), QObject::tr("Deceleration rate when stopping (m/s^2)"), "0.40", 0.01f, 0.8f, 0.01f}, {"LongTuningMinUpperJerk", QObject::tr("Min Upper Jerk"), QObject::tr("Minimum accel jerk limit (m/s^3)"), "0.50", 0.5f, 3.0f, 0.1f}, - {"LongTuningMinLowerJerk", QObject::tr("Min Lower Jerk"), QObject::tr("Minimum braking jerk limit (m/s^3)"), "0.50", 0.5f, 8.0f, 0.1f}, - {"LongTuningJerkLimits", QObject::tr("Dynamic Tune Jerk Max"), QObject::tr("Maximum lower jerk limit the tune cannot exceed (m/s^3)"), "4.00", 2.0f, 8.0f, 0.1f}, + {"LongTuningMinLowerJerk", QObject::tr("Min Lower Jerk"), QObject::tr("Minimum braking jerk limit (m/s^3)"), "0.50", 0.5f, 5.0f, 0.1f}, + {"LongTuningJerkLimits", QObject::tr("Dynamic Tune Jerk Max"), QObject::tr("Maximum lower jerk limit the tune cannot exceed (m/s^3)"), "4.00", 2.0f, 5.0f, 0.1f}, }; QWidget* createButtonWidget(const QString &text, std::function callback, bool isPushButton = false) { diff --git a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc index 0d42bd74f4..dd2f05416d 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.cc @@ -12,7 +12,7 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { connect(param_watcher, &ParamWatcher::paramChanged, [=](const QString ¶m_name, const QString ¶m_value) { paramsRefresh(); }); - + main_layout = new QStackedLayout(this); ListWidgetSP *list = new ListWidgetSP(this, false); @@ -28,8 +28,16 @@ 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 for (auto &[param, title, desc, icon, needs_restart] : toggle_defs) { auto toggle = new ParamControlSP(param, title, desc, icon, this); @@ -53,9 +61,20 @@ VisualsPanel::VisualsPanel(QWidget *parent) : QWidget(parent) { param_watcher->addParam(param); } + // Visuals: Display Metrics below Chevron + std::vector chevron_info_settings_texts{tr("Off"), tr("Distance"), tr("Speed"), tr("Time"), tr("All")}; + chevron_info_settings = new ButtonParamControlSP( + "ChevronInfo", tr("Display Metrics Below Chevron"), tr("Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control)."), + "", + chevron_info_settings_texts, + 200); + chevron_info_settings->showDescription(); + list->addItem(chevron_info_settings); + param_watcher->addParam("ChevronInfo"); + sunnypilotScroller = new ScrollViewSP(list, this); vlayout->addWidget(sunnypilotScroller); - + main_layout->addWidget(sunnypilotScreen); } @@ -67,4 +86,8 @@ void VisualsPanel::paramsRefresh() { for (auto toggle : toggles) { toggle.second->refresh(); } + + if (chevron_info_settings) { + chevron_info_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 42e0688957..f342662c22 100644 --- a/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h +++ b/selfdrive/ui/sunnypilot/qt/offroad/settings/visuals_panel.h @@ -27,4 +27,5 @@ protected: Params params; std::map toggles; ParamWatcher * param_watcher; + ButtonParamControlSP *chevron_info_settings; }; 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/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); diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc index d873913b57..28374f3a9f 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.cc +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.cc @@ -30,9 +30,24 @@ QFrame *vertical_space(int height, QWidget *parent) { } // AbstractControlSP +std::vector AbstractControlSP::advanced_controls_; +AbstractControlSP::~AbstractControlSP() { UnregisterAdvancedControl(this); } -AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : AbstractControl(title, desc, icon, parent) { +void AbstractControlSP::RegisterAdvancedControl(AbstractControlSP *ctrl) { advanced_controls_.push_back(ctrl); } + +void AbstractControlSP::UnregisterAdvancedControl(AbstractControlSP *ctrl) { + advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), ctrl), advanced_controls_.end()); +} + +void AbstractControlSP::UpdateAllAdvancedControls() { + bool visibility = Params().getBool("ShowAdvancedControls"); + advanced_controls_.erase(std::remove(advanced_controls_.begin(), advanced_controls_.end(), nullptr), advanced_controls_.end()); + for (auto *ctrl : advanced_controls_) ctrl->setVisible(visibility); +} + +AbstractControlSP::AbstractControlSP(const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) + : AbstractControl(title, desc, icon, parent), isAdvancedControl(advancedControl) { + if (isAdvancedControl) RegisterAdvancedControl(this); main_layout = new QVBoxLayout(this); main_layout->setMargin(0); @@ -82,8 +97,8 @@ void AbstractControlSP::hideEvent(QHideEvent *e) { } } -AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : AbstractControlSP(title, desc, icon, parent) { +AbstractControlSP_SELECTOR::AbstractControlSP_SELECTOR(const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) + : AbstractControlSP(title, desc, icon, parent, advancedControl) { if (title_label != nullptr) { delete title_label; @@ -169,8 +184,8 @@ void AbstractControlSP_SELECTOR::hideEvent(QHideEvent *e) { // controls -ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent) - : AbstractControlSP(title, desc, "", parent) { +ButtonControlSP::ButtonControlSP(const QString &title, const QString &text, const QString &desc, QWidget *parent, bool advancedControl) + : AbstractControlSP(title, desc, "", parent, advancedControl) { btn.setText(text); btn.setStyleSheet(R"( @@ -225,8 +240,8 @@ void ElidedLabelSP::paintEvent(QPaintEvent *event) { // ParamControlSP -ParamControlSP::ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent) - : ToggleControlSP(title, desc, icon, false, parent) { +ParamControlSP::ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent, bool advancedControl) + : ToggleControlSP(title, desc, icon, false, parent, advancedControl){ key = param.toStdString(); QObject::connect(this, &ParamControlSP::toggleFlipped, this, &ParamControlSP::toggleClicked); diff --git a/selfdrive/ui/sunnypilot/qt/widgets/controls.h b/selfdrive/ui/sunnypilot/qt/widgets/controls.h index c8849faf53..03cb461385 100644 --- a/selfdrive/ui/sunnypilot/qt/widgets/controls.h +++ b/selfdrive/ui/sunnypilot/qt/widgets/controls.h @@ -57,6 +57,7 @@ class AbstractControlSP : public AbstractControl { Q_OBJECT public: + ~AbstractControlSP(); void setDescription(const QString &desc) override { if (description) description->setText(desc); } @@ -81,13 +82,30 @@ public slots: description->setVisible(true); } + void setVisible(bool visible) override { + bool _visible = visible; + if (isAdvancedControl && !params.getBool("ShowAdvancedControls")) { + _visible = false; + } + AbstractControl::setVisible(_visible); + } + + static void RegisterAdvancedControl(AbstractControlSP *ctrl); + static void UnregisterAdvancedControl(AbstractControlSP *ctrl); + static void UpdateAllAdvancedControls(); + protected: - AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + AbstractControlSP(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr, bool advancedControl = false); void hideEvent(QHideEvent *e) override; QVBoxLayout *main_layout; ElidedLabelSP *value; QLabel *description = nullptr; + bool isAdvancedControl; + +private: + Params params; + static std::vector advanced_controls_; }; // AbstractControlSP_SELECTOR @@ -97,7 +115,7 @@ class AbstractControlSP_SELECTOR : public AbstractControlSP { protected: QSpacerItem *spacingItem = new QSpacerItem(44, 44, QSizePolicy::Minimum, QSizePolicy::Fixed); - AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr); + AbstractControlSP_SELECTOR(const QString &title, const QString &desc = "", const QString &icon = "", QWidget *parent = nullptr, bool advancedControl = false); void hideEvent(QHideEvent *e) override; }; @@ -123,7 +141,7 @@ class ButtonControlSP : public AbstractControlSP { Q_OBJECT public: - ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr); + ButtonControlSP(const QString &title, const QString &text, const QString &desc = "", QWidget *parent = nullptr, bool advancedControl = false); inline void setText(const QString &text) { btn.setText(text); } inline QString text() const { return btn.text(); } inline void click() { btn.click(); } @@ -142,7 +160,7 @@ class ToggleControlSP : public AbstractControlSP { Q_OBJECT public: - ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr) : AbstractControlSP(title, desc, icon, parent) { + ToggleControlSP(const QString &title, const QString &desc = "", const QString &icon = "", const bool state = false, QWidget *parent = nullptr, bool advancedControl = false) : AbstractControlSP(title, desc, icon, parent, advancedControl) { // space between toggle and title icon_label = new QLabel(this); hlayout->addWidget(icon_label); @@ -173,7 +191,7 @@ class ParamControlSP : public ToggleControlSP { Q_OBJECT public: - ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr); + ParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, QWidget *parent = nullptr, bool advancedControl = false); void setConfirmation(bool _confirm, bool _store_confirm) { confirm = _confirm; store_confirm = _store_confirm; @@ -219,7 +237,7 @@ class MultiButtonControlSP : public AbstractControlSP_SELECTOR { public: MultiButtonControlSP(const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false) : AbstractControlSP_SELECTOR(title, desc, icon), button_texts(button_texts), is_inline_layout(inline_layout) { + const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), button_texts(button_texts), is_inline_layout(inline_layout) { const QString style = R"( QPushButton { border-radius: 20px; @@ -371,8 +389,8 @@ class ButtonParamControlSP : public MultiButtonControlSP { Q_OBJECT public: ButtonParamControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false) : MultiButtonControlSP(title, desc, icon, - button_texts, minimum_button_width, inline_layout) { + const std::vector &button_texts, const int minimum_button_width = 225, const bool inline_layout = false, bool advancedControl = false) : MultiButtonControlSP(title, desc, icon, + button_texts, minimum_button_width, inline_layout, advancedControl) { key = param.toStdString(); int value = atoi(params.get(key).c_str()); @@ -480,6 +498,16 @@ private: return result.toInt(); } + int getParamValueScaled() { + const auto param_value = QString::fromStdString(params.get(key)); + return static_cast(param_value.toFloat() * 100); + } + + void setParamValueScaled(const int new_value) { + const float scaled_value = new_value / 100.0f; + params.put(key, QString::number(scaled_value, 'f', 2).toStdString()); + } + // Although the method is not static, and thus has access to the value property, I prefer to be explicit about the value. void setParamValue(const int new_value) { const auto value_str = valueMap != nullptr ? valueMap->value(QString::number(new_value)) : QString::number(new_value); @@ -488,7 +516,8 @@ private: public: OptionControlSP(const QString ¶m, const QString &title, const QString &desc, const QString &icon, - const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, const QMap *valMap = nullptr) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr), _title(title), valueMap(valMap), is_inline_layout(inline_layout) { + const MinMaxValue &range, const int per_value_change = 1, const bool inline_layout = false, + const QMap *valMap = nullptr, bool scale_float = false, bool advancedControl = false) : AbstractControlSP_SELECTOR(title, desc, icon, nullptr, advancedControl), _title(title), valueMap(valMap), is_inline_layout(inline_layout), use_float_scaling(scale_float) { const QString style = R"( QPushButton { border-radius: 20px; @@ -528,7 +557,7 @@ public: const std::vector button_texts{"ï¼", "+"}; key = param.toStdString(); - value = getParamValue(); + value = use_float_scaling ? getParamValueScaled() : getParamValue(); button_group = new QButtonGroup(this); button_group->setExclusive(true); @@ -546,10 +575,15 @@ public: QObject::connect(button, &QPushButton::clicked, [=]() { int change_value = (i == 0) ? -per_value_change : per_value_change; - value = getParamValue(); // in case it changed externally, we need to get the latest value. + value = use_float_scaling ? getParamValueScaled() : getParamValue(); value += change_value; value = std::clamp(value, range.min_value, range.max_value); - setParamValue(value); + + if (use_float_scaling) { + setParamValueScaled(value); + } else { + setParamValue(value); + } button_group->button(0)->setEnabled(!(value <= range.min_value)); button_group->button(1)->setEnabled(!(value >= range.max_value)); @@ -642,6 +676,7 @@ private: const QString label_disabled_style = "font-size: 50px; font-weight: 450; color: #5C5C5C;"; bool button_enabled = true; + bool use_float_scaling = false; }; class PushButtonSP : public QPushButton { @@ -715,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(); + } 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/selfdrive/ui/tests/test_feedbackd.py b/selfdrive/ui/tests/test_feedbackd.py new file mode 100644 index 0000000000..6b7ec44863 --- /dev/null +++ b/selfdrive/ui/tests/test_feedbackd.py @@ -0,0 +1,53 @@ +import pytest +import cereal.messaging as messaging +from cereal import car +from openpilot.common.params import Params +from openpilot.system.manager.process_config import managed_processes + + +@pytest.mark.skip("tmp disabled") +class TestFeedbackd: + def setup_method(self): + self.pm = messaging.PubMaster(['carState', 'rawAudioData']) + self.sm = messaging.SubMaster(['audioFeedback']) + + def _send_lkas_button(self, pressed: bool): + msg = messaging.new_message('carState') + msg.carState.canValid = True + msg.carState.buttonEvents = [{'type': car.CarState.ButtonEvent.Type.lkas, 'pressed': pressed}] + self.pm.send('carState', msg) + + def _send_audio_data(self, count: int = 5): + for _ in range(count): + audio_msg = messaging.new_message('rawAudioData') + audio_msg.rawAudioData.data = bytes(1600) # 800 samples of int16 + audio_msg.rawAudioData.sampleRate = 16000 + self.pm.send('rawAudioData', audio_msg) + self.sm.update(timeout=100) + + @pytest.mark.parametrize("record_feedback", [False, True]) + def test_audio_feedback(self, record_feedback): + Params().put_bool("RecordAudioFeedback", record_feedback) + + managed_processes["feedbackd"].start() + assert self.pm.wait_for_readers_to_update('carState', timeout=5) + assert self.pm.wait_for_readers_to_update('rawAudioData', timeout=5) + + self._send_lkas_button(pressed=True) + self._send_audio_data() + self._send_lkas_button(pressed=False) + self._send_audio_data() + + if record_feedback: + assert self.sm.updated['audioFeedback'], "audioFeedback should be published when enabled" + else: + assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published when disabled" + + self._send_lkas_button(pressed=True) + self._send_audio_data() + self._send_lkas_button(pressed=False) + self._send_audio_data() + + assert not self.sm.updated['audioFeedback'], "audioFeedback should not be published after second press" + + managed_processes["feedbackd"].stop() diff --git a/selfdrive/ui/tests/test_raylib_ui.py b/selfdrive/ui/tests/test_raylib_ui.py new file mode 100644 index 0000000000..3f23301972 --- /dev/null +++ b/selfdrive/ui/tests/test_raylib_ui.py @@ -0,0 +1,8 @@ +import time +from openpilot.selfdrive.test.helpers import with_processes + + +@with_processes(["raylib_ui"]) +def test_raylib_ui(): + """Test initialization of the UI widgets is successful.""" + time.sleep(1) diff --git a/selfdrive/ui/tests/test_ui/run.py b/selfdrive/ui/tests/test_ui/run.py index b263e28297..653395ba1d 100755 --- a/selfdrive/ui/tests/test_ui/run.py +++ b/selfdrive/ui/tests/test_ui/run.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 import capnp -import json import pathlib import shutil import sys @@ -265,12 +264,12 @@ def setup_settings_trips(click, pm: PubMaster, scroll=None): time.sleep(UI_DELAY) def setup_settings_vehicle(click, pm: PubMaster, scroll=None): - Params().put("CarPlatformBundle", json.dumps( + Params().put("CarPlatformBundle", { "platform": "HONDA_CIVIC_2022", "name": "Honda Civic 2022-24" } - )) + ) setup_settings_device(click, pm) scroll(-400, 278, 962) @@ -408,9 +407,9 @@ def create_screenshots(): params.put("DongleId", "123456789012345") params.put("SunnylinkDongleId", "123456789012345") if name == 'prime': - params.put('PrimeType', '1') + params.put('PrimeType', 1) elif name == 'pair_device': - params.put('ApiCache_Device', '{"is_paired":0, "prime_type":-1}') + params.put('ApiCache_Device', {"is_paired":0, "prime_type":-1}) t.test_ui(name, setup) diff --git a/selfdrive/ui/translations/main_ar.ts b/selfdrive/ui/translations/main_ar.ts index de24097d8f..ef7110e72e 100644 --- a/selfdrive/ui/translations/main_ar.ts +++ b/selfdrive/ui/translations/main_ar.ts @@ -7,10 +7,6 @@ Close إغلاق - - Snooze Update - تأخير التحديث - Reboot and Update إعادة التشغيل والتحديث @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - يجب عليك قبول الشروط والأحكام من أجل استخدام sunnypilot. - Back السابق @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Ø±ÙØ¶ØŒ إلغاء التثبيت %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode وضع المناورة الطولية + + openpilot Longitudinal Control (Alpha) + التحكم الطولي openpilot (Ø£Ù„ÙØ§) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + تحذير: التحكم الطولي ÙÙŠ openpilot ÙÙŠ المرحلة Ø£Ù„ÙØ§ لهذه السيارة، وسيقوم بتعطيل مكابح الطوارئ الآلية (AEB). + Enable ADB تمكين ADB @@ -179,6 +202,22 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. أداة ADB (Android Debug Bridge) تسمح بالاتصال بجهازك عبر USB أو عبر الشبكة. راجع هذا الرابط: https://docs.comma.ai/how-to/connect-to-comma لمزيد من المعلومات. + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW مراجعة - - Review the rules, features, and limitations of sunnypilot - مراجعة الأدوار والميزات والقيود ÙÙŠ sunnypilot - Are you sure you want to review the training guide? هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off إيقا٠التشغيل - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - يحتاج sunnypilot أن يتم ضبط الجهاز ضمن حدود 4 درجات يميناً أو يساراً Ùˆ5 درجات نحو الأعلى أو 9 نحو الأسÙÙ„. يقوم sunnypilot بالمعايرة باستمرار، ونادراً ما يحتاج إلى عملية إعادة الضبط. - Your device is pointed %1° %2 and %3° %4. يشير جهازك إلى %1 درجة %2ØŒ Ùˆ%3 درجة %4. @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + + + + + +Steering lag calibration is %1% complete. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + Steering torque response calibration is complete. + + + + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? هل أنت متأكد أنك تريد مراجعة دليل التدريب؟ @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language اختر لغة + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot إعادة التشغيل @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - تأكيد + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an بدء تشغيل الكاميرا + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 وضع خرطوم الحريق 🔥 - Firehose Mode: ACTIVE وضع خرطوم الحريق: نشط @@ -509,10 +604,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ACTIVE نشط - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - للحصول على أقصى ÙØ¹Ø§Ù„ية، أحضر جهازك إلى الداخل واتصل بمحول USB-C جيد وشبكة Wi-Fi أسبوعياً.<br><br>يمكن أن يعمل وضع خرطوم الحريق أيضاً أثناء القيادة إذا كنت متصلاً بنقطة اتصال أو ببطاقة SIM غير محدودة.<br><br><br><b>الأسئلة المتكررة</b><br><br><i>هل يهم كي٠أو أين أقود؟</i> لا، Ùقط قد كما ØªÙØ¹Ù„ عادة.<br><br><i>هل يتم سحب كل مقاطع رحلاتي ÙÙŠ وضع خرطوم الحريق؟</i> لا، نقوم بسحب مجموعة مختارة من مقاطع رحلاتك.<br><br><i>ما هو محول USB-C الجيد؟</i> أي شاحن سريع للهات٠أو اللابتوب يجب أن يكون مناسباً.<br><br><i>هل يهم أي برنامج أستخدم؟</i> نعم، Ùقط النسخة الأصلية من sunnypilot (ÙˆØ£ÙØ±Ø¹ معينة) يمكن استخدامها للتدريب. - <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -524,6 +615,14 @@ Please use caution when using this feature. Only use the blinker when traffic an حتى الآن، يوجد </b>%n مقطع<b>%n من قيادتك ÙÙŠ مجموعة بيانات التدريب. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -531,7 +630,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -644,6 +743,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -661,6 +768,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -668,11 +798,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -683,18 +817,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -711,6 +833,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -723,6 +849,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -770,31 +900,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp اختيار - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -810,21 +953,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -853,6 +992,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -869,6 +1016,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel إلغاء + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -976,14 +1155,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Device temperature too high. System cooling down before starting. Current internal component temperature: %1 درجة حرارة الجهاز Ù…Ø±ØªÙØ¹Ø© جداً. يقوم النظام بالتبريد قبل البدء. درجة الحرارة الحالية للمكونات الداخلية: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - اتصل Ùوراً بالإنترنت للتحقق من وجود تحديثات. إذا لم تكم متصلاً بالإنترنت ÙØ¥Ù† sunnypilot لن يساهم ÙÙŠ %1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - اتصل بالإنترنت للتحقق من وجود تحديثات. لا يعمل sunnypilot تلقائياً إلا إذا اتصل بالإنترنت من أجل التحقق من التحديثات. - Unable to download updates %1 @@ -998,28 +1169,46 @@ 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. يتم تنزيل تحديث لنظام تشغيل جهازك ÙÙŠ الخلÙية. سيطلَب منك التحديث عندما يصبح جاهزاً للتثبيت. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - ÙØ´Ù„ تسجيل الجهاز. لن يقوم بالاتصال أو تحميل خوادم comma.aiØŒ ولا تلقي الدعم من comma.ai. إذا كان هذا الجهاز نظامياً Ùيرجى زيارة الموقع https://comma.ai/support. - NVMe drive not mounted. محرك NVMe غير مثبَّت. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - تم اكتشا٠محرك NVMe غير مدعوم. قد يستهلك الجهاز قدراً أكبر بكثير من الطاقة، وزيادة ÙÙŠ Ø§Ø±ØªÙØ§Ø¹ درجة الحرارة بسبب وجود NVMe غير مدعوم. + 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. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + تأخير التحديث + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot 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. - لم يكن sunnypilot قادراً على تحديد سيارتك. إما أن تكون سيارتك غير مدعومة أو أنه لم يتم التعر٠على وحدة التحكم الإلكتروني (ECUs) Ùيها. يرجى تقديم طلب سحب من أجل Ø¥Ø¶Ø§ÙØ© نسخ برمجيات ثابتة إلى السيارة المناسبة. هل تحتاج إلى أي مساعدة؟ لا تتردد ÙÙŠ التواصل مع doscord.comma.ai. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - لقد اكتش٠sunnypilot تغييراً ÙÙŠ موقع تركيب الجهاز. تأكد من تثبيت الجهاز بشكل كامل ÙÙŠ موقعه وتثبيته بإحكام على الزجاج الأمامي. + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1039,11 +1228,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot غير Ù…ØªÙˆÙØ± + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY تحكم على الÙور @@ -1060,6 +1252,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive النظام لا يستجيب + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + التحقق + + + Country + + + + SELECT + اختيار + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + تحديث + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1146,7 +1473,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - تأكيد + Cancel @@ -1229,10 +1556,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1270,46 +1593,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now الآن - - - Reset - Reset failed. Reboot to try again. - ÙØ´Ù„ إعاة الضبط. أعد التشغيل للمحاولة من جديد. - - - Are you sure you want to reset your device? - هل أنت متأكد أنك تريد إعادة ضبط جهازك؟ - - - System Reset - إعادة ضبط النظام - - - Cancel - إلغاء - - - Reboot - إعادة التشغيل - - - Confirm - تأكيد - - - Resetting device... -This may take up to a minute. - يتم إعادة ضبط الجهاز... -قد يستغرق الأمر حوالي الدقيقة. - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - غير قادر على تحميل جزء البيانات. قد يكون الجزء ØªØ§Ù„ÙØ§Ù‹. اضغط على تأكيد لمسح جهازك وإعادة ضبطه. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - تم ØªÙØ¹ÙŠÙ„ إعادة ضبط النظام. اضغط على تأكيد لمسح جميع المحتويات والإعدادات. اضغط على إلغاء لاستئنا٠التمهيد. + sunnypilot + @@ -1370,29 +1656,13 @@ This may take up to a minute. البرنامج - Trips + Models - - Vehicle - - - - Developer - المطور - - - Firehose - خرطوم الحريق - Steering - - Models - - Cruise @@ -1401,100 +1671,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - تحذير: الجهد Ù…Ù†Ø®ÙØ¶ + OSM + - Power your device in a car with a harness or proceed at your own risk. - شغل جهازك ÙÙŠ السيارة عن طريق شرطان التوصيل، أو تابع على مسؤوليتك. + Trips + - Power off - إيقا٠التشغيل + Vehicle + - Continue - متابعة + Firehose + خرطوم الحريق - Getting Started - البدء - - - Before we get on the road, let’s finish installation and cover some details. - قبل أن ننطلق ÙÙŠ الطريق، دعنا ننتهي من التثبيت ونغطي بعض Ø§Ù„ØªÙØ§ØµÙŠÙ„. - - - Connect to Wi-Fi - الاتصال بشبكة الواي ÙØ§ÙŠ - - - Back - السابق - - - Continue without Wi-Fi - المتابعة بدون شبكة الواي ÙØ§ÙŠ - - - Waiting for internet - بانتظار الاتصال بالإنترنت - - - Enter URL - أدخل رابط URL - - - for Custom Software - للبرامج المخصصة - - - Downloading... - يتم الآن التنزيل... - - - Download Failed - ÙØ´Ù„ التنزيل - - - Ensure the entered URL is valid, and the device’s internet connection is good. - تأكد من أن رابط URL الذي أدخلته صالح، وأن اتصال الجهاز بالإنترنت جيد. - - - Reboot device - إعادة التشغيل - - - Start over - البدء من جديد - - - Something went wrong. Reboot the device. - حدث خطأ ما. أعد التشغيل الجهاز. - - - No custom software found at this URL. - لم يتم العثور على برنامج خاص لعنوان URL ها. - - - Select a language - اختر لغة - - - Choose Software to Install - اختر البرنامج للتثبيت - - - sunnypilot - sunnypilot - - - Custom Software - البرمجيات المخصصة + Developer + المطور @@ -1699,6 +1894,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1707,6 +1910,22 @@ This may take up to a minute. Select a branch اختر ÙØ±Ø¹Ø§Ù‹ + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + إعادة التشغيل + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1764,22 +1983,6 @@ This may take up to a minute. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - غير متاح - Sponsor Status @@ -1804,24 +2007,20 @@ This may take up to a minute. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + غير متاح + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1876,6 +2075,50 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1928,11 +2171,11 @@ This may take up to a minute. Welcome to sunnypilot - مرحباً بكم ÙÙŠ sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - يجب عليك قبول الشروط والأحكام لاستخدام sunnypilot. اقرأ أحدث الشروط على <span style='color: #465BEA;'>https://comma.ai/terms</span> قبل الاستمرار. + @@ -1965,10 +2208,6 @@ This may take up to a minute. Disengage on Accelerator Pedal ÙÙƒ الارتباط عن دواسة الوقود - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - عند تمكين هذه الميزة، ÙØ¥Ù† الضغط على دواسة الوقود سيؤدي إلى ÙÙƒ ارتباط sunnypilot. - Experimental Mode الوضع التجريبي @@ -1989,18 +2228,10 @@ This may take up to a minute. Driving Personality شخصية القيادة - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - يتم وضع sunnypilot بشكل قياسي ÙÙŠ <b>وضعية الراحة</b>. يمكن الوضع التجريبي <b>ميزات المستوى Ø£Ù„ÙØ§</b> التي لا تكون جاهزة ÙÙŠ وضع الراحة: - End-to-End Longitudinal Control التحكم الطولي من طر٠إلى طر٠- - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - دع نظام القيادة يتحكم بالوقود والمكابح. سيقوم sunnypilot بالقيادة كما لو أنه كائن بشري، بما ÙÙŠ ذلك التوق٠عند الإشارة الحمراء، وإشارات التوقÙ. وبما أن نمط القيادة يحدد سرعة القيادة، ÙØ¥Ù† السرعة المضبوطة تشكل الحد الأقصى Ùقط. هذه خاصية الجودة Ø£Ù„ÙØ§ØŒ Ùيجب توقع حدوث الأخطاء. - New Driving Visualization تصور القيادة الديد @@ -2010,16 +2241,8 @@ This may take up to a minute. الوضع التجريبي غير Ù…ØªÙˆÙØ± حالياً ÙÙŠ هذه السيارة نظراً لاستخدام رصيد التحكم التكيÙÙŠ بالسرعة من أجل التحكم الطولي. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - يمكن اختبار نسخة Ø£Ù„ÙØ§ من التحكم الطولي من sunnypilotØŒ مع الوضع التجريبي، لكن على Ø§Ù„ÙØ±ÙˆØ¹ غير المطلقة. - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - تمكين التحكم الطولي من sunnypilot (Ø£Ù„ÙØ§) للسماح بالوضع التجريبي. - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - يوصى بالمعيار. ÙÙŠ الوضع العدواني، سيتبع الطيار Ø§Ù„Ù…ÙØªÙˆØ­ السيارات الرائدة بشكل أقرب ويكون أكثر عدوانية مع البنزين ÙˆØ§Ù„ÙØ±Ø§Ù…Ù„. ÙÙŠ الوضع المريح، سيبقى sunnypilot بعيدًا عن السيارات الرائدة. ÙÙŠ السيارات المدعومة، يمكنك التنقل بين هذه الشخصيات باستخدام زر Ù…Ø³Ø§ÙØ© عجلة القيادة. + openpilot longitudinal control may come in a future update. + قد يتم الحصول على التحكم الطولي ÙÙŠ openpilot ÙÙŠ عمليات التحديث المستقبلية. The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -2030,8 +2253,24 @@ This may take up to a minute. مراقبة السائق المستمرة - Enable driver monitoring even when sunnypilot is not engaged. - تمكين مراقبة السائق حتى عندما لا يكون نظام OpenPilot Ù…ÙÙØ¹Ù‘لاً. + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2042,11 +2281,53 @@ This may take up to a minute. - Enable sunnypilot - تمكين sunnypilot + When enabled, pressing the accelerator pedal will disengage sunnypilot. + - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable driver monitoring even when sunnypilot is not engaged. + + + + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + + + + + TreeOptionDialog + + Select + اختيار + + + Cancel + إلغاء + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2054,43 +2335,32 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - التحديث مطلوب + Distance + - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - تحديث نظام التشغيل مطلوب. قم بوصل جهازك بشبكة واي ÙØ§ÙŠ Ù…Ù† أجل تحديث أسرع. حجم التحميل حوالي 1 غيغا بايت تقريباً. + Speed + - Connect to Wi-Fi - الاتصال بشبكة الواي ÙØ§ÙŠ + Time + - Install - تثبيت + All + - Back - السابق + Display Metrics Below Chevron + - Loading... - يتم التحميل... - - - Reboot - إعادة التشغيل - - - Update failed - ÙØ´Ù„ التحديث + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2100,12 +2370,12 @@ This may take up to a minute. Ø§Ù†ÙØªØ­ - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> وضع خرطوم الحريق <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + قم بزيادة تحميلات بيانات التدريب الخاصة بك لتحسين نماذج القيادة الخاصة بـ openpilot. - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> وضع خرطوم الحريق <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_de.ts b/selfdrive/ui/translations/main_de.ts index 6229920121..dde9241460 100644 --- a/selfdrive/ui/translations/main_de.ts +++ b/selfdrive/ui/translations/main_de.ts @@ -7,10 +7,6 @@ Close Schließen - - Snooze Update - Update pausieren - Reboot and Update Aktualisieren und neu starten @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -191,6 +206,18 @@ Please use caution when using this feature. Only use the blinker when traffic an On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -199,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -211,6 +242,14 @@ Please use caution when using this feature. Only use the blinker when traffic an View the error log for sunnypilot crashes. + + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> + + + + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. + + DevicePanel @@ -350,16 +389,36 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + + + + + +Steering lag calibration is %1% complete. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + Steering torque response calibration is complete. + + Review the rules, features, and limitations of sunnypilot - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - - - - Resetting calibration will restart openpilot if the car is powered on. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. @@ -401,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language Sprache wählen + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Neustart @@ -419,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Bestätigen + Are you sure you want to enter Always Offroad mode? @@ -449,6 +521,18 @@ Please use caution when using this feature. Only use the blinker when traffic an Always Offroad + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an Kamera startet + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 Firehose-Modus 🔥 - Firehose Mode: ACTIVE Firehose-Modus: AKTIV @@ -520,6 +615,10 @@ Please use caution when using this feature. Only use the blinker when traffic an <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INAKTIV</span>: Verbinde dich mit einem ungedrosselten Netzwerk + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -636,6 +735,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -653,6 +760,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -762,31 +892,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp AUSWÄHLEN - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +945,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -845,6 +984,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -861,6 +1008,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel Abbrechen + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -978,22 +1157,30 @@ 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. Ein Update für das Betriebssystem deines Geräts wird im Hintergrund heruntergeladen. Du wirst aufgefordert, das Update zu installieren, sobald es bereit ist. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - Gerät konnte nicht registriert werden. Es wird keine Verbindung zu den comma.ai-Servern herstellen oder Daten hochladen und erhält keinen Support von comma.ai. Wenn dies ein offizielles Gerät ist, besuche https://comma.ai/support. - NVMe drive not mounted. NVMe-Laufwerk nicht gemounted. - - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Nicht unterstütztes NVMe-Laufwerk erkannt. Das Gerät kann dadurch deutlich mehr Strom verbrauchen und überhitzen. - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 Gerätetemperatur zu hoch. Das System kühlt ab, bevor es startet. Aktuelle interne Komponententemperatur: %1 + + 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. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Update pausieren + + + 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. + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 @@ -1011,7 +1198,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1030,6 +1219,13 @@ Firehose Mode allows you to maximize your training data uploads to improve openp HINWEIS + + OffroadHomeSP + + ALWAYS OFFROAD ACTIVE + + + OnroadAlerts @@ -1053,6 +1249,137 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + ÜBERPRÜFEN + + + Country + + + + SELECT + AUSWÄHLEN + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + Aktualisieren + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + + PairingPopup @@ -1138,7 +1465,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Bestätigen + Cancel @@ -1251,47 +1578,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - - Reset - - Reset failed. Reboot to try again. - Zurücksetzen fehlgeschlagen. Starte das Gerät neu und versuche es wieder. - - - Are you sure you want to reset your device? - Bist du sicher, dass du das Gerät auf Werkseinstellungen zurücksetzen möchtest? - - - System Reset - System auf Werkseinstellungen zurücksetzen - - - Cancel - Abbrechen - - - Reboot - Neustart - - - Confirm - Bestätigen - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Datenpartition konnte nicht gemounted werden. Die Partition ist möglicherweise beschädigt. Drücke Bestätigen, um das Gerät zu löschen und zurückzusetzen. - - - Resetting device... -This may take up to a minute. - Gerät wird zurückgesetzt... -Dies kann bis zu einer Minute dauern. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - System-Reset ausgelöst. Drücke Bestätigen, um alle Inhalte und Einstellungen zu löschen. Drücke Abbrechen, um den Startvorgang fortzusetzen. - - SettingsWindow @@ -1361,6 +1647,14 @@ Dies kann bis zu einer Minute dauern. Cruise + + Visuals + + + + OSM + + Trips @@ -1377,105 +1671,6 @@ Dies kann bis zu einer Minute dauern. Developer Entwickler - - Visuals - - - - - Setup - - WARNING: Low Voltage - Warnung: Batteriespannung niedrig - - - Power your device in a car with a harness or proceed at your own risk. - Versorge dein Gerät über einen Kabelbaum im Auto mit Strom, oder fahre auf eigene Gefahr fort. - - - Power off - Ausschalten - - - Continue - Fortsetzen - - - Getting Started - Loslegen - - - Before we get on the road, let’s finish installation and cover some details. - Bevor wir uns auf die Straße begeben, lass uns die Installation fertigstellen und einige Details prüfen. - - - Connect to Wi-Fi - Mit WLAN verbinden - - - Back - Zurück - - - Continue without Wi-Fi - Ohne WLAN fortsetzen - - - Waiting for internet - Auf Internet warten - - - Enter URL - URL eingeben - - - for Custom Software - für spezifische Software - - - Downloading... - Herunterladen... - - - Download Failed - Herunterladen fehlgeschlagen - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Stelle sicher, dass die eingegebene URL korrekt ist und dein Gerät eine stabile Internetverbindung hat. - - - Reboot device - Gerät neustarten - - - Start over - Von neuem beginnen - - - No custom software found at this URL. - Keine benutzerdefinierte Software unter dieser URL gefunden. - - - Something went wrong. Reboot the device. - Etwas ist schiefgelaufen. Starte das Gerät neu. - - - Select a language - Sprache wählen - - - Choose Software to Install - Wähle die zu installierende Software - - - Custom Software - Benutzerdefinierte Software - - - sunnypilot - - SetupWidget @@ -1681,6 +1876,14 @@ Dies kann bis zu einer Minute dauern. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1689,6 +1892,22 @@ Dies kann bis zu einer Minute dauern. Select a branch Wähle einen Branch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Neustart + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1858,6 +2077,30 @@ Dies kann bis zu einer Minute dauern. Not Paired + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1991,6 +2234,18 @@ Dies kann bis zu einer Minute dauern. Always-On Driver Monitoring Dauerhaft aktive Fahrerüberwachung + + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + Enable sunnypilot @@ -2019,10 +2274,6 @@ Dies kann bis zu einer Minute dauern. Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - - Changing this setting will restart openpilot if the car is powered on. - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: @@ -2041,38 +2292,57 @@ Dies kann bis zu einer Minute dauern. - Updater + TreeOptionDialog - Update Required - Aktualisierung notwendig + Select + Auswählen - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Eine Aktualisierung des Betriebssystems ist notwendig. Verbinde dein Gerät mit WLAN für ein schnelleres Update. Die Download Größe ist ungefähr 1GB. + Cancel + Abbrechen + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Mit WLAN verbinden + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - Installieren + Changing this setting will restart openpilot if the car is powered on. + - Back - Zurück + Off + - Loading... - Laden... + Distance + - Reboot - Neustart + Speed + - Update failed - Aktualisierung fehlgeschlagen + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + diff --git a/selfdrive/ui/translations/main_es.ts b/selfdrive/ui/translations/main_es.ts index 3d9622166a..3b53e7f944 100644 --- a/selfdrive/ui/translations/main_es.ts +++ b/selfdrive/ui/translations/main_es.ts @@ -7,10 +7,6 @@ Close Cerrar - - Snooze Update - Posponer Actualización - Reboot and Update Reiniciar y Actualizar @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + Evite cargas de grandes cantidades de datos cuando utilice una conexión celular medida default - + por defecto metered - + medido unmetered - + sin medidor Wi-Fi Network Metered - + Red Wi-Fi medida Prevent large data uploads when on a metered Wi-Fi connection - + Evite cargas de grandes cantidades de datos cuando esté en una conexión Wi-Fi medida @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Debe aceptar los términos y condiciones para poder utilizar sunnypilot. - Back Atrás @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Rechazar, desinstalar %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,12 +186,36 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo de maniobra longitudinal + + openpilot Longitudinal Control (Alpha) + Control longitudinal de openpilot (fase experimental) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + AVISO: el control longitudinal de openpilot está en fase experimental para este automóvil y desactivará el Frenado Automático de Emergencia (AEB). + Enable ADB - + Activar ADB ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. + ADB (Android Debug Bridge) permite conectar a su dispositivo por USB o por red. Visite https://docs.comma.ai/how-to/connect-to-comma para más información. + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -274,10 +313,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW REVISAR - - Review the rules, features, and limitations of sunnypilot - Revisar las reglas, características y limitaciones de sunnypilot - Are you sure you want to review the training guide? ¿Seguro que quiere revisar la guía de entrenamiento? @@ -314,10 +349,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Apagar - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot requiere que el dispositivo sea montado entre 4° grados a la izquierda o derecha y entre 5° grados hacia arriba o 9° grados hacia abajo. sunnypilot está constantemente en calibración, formatear rara vez es necesario. - Your device is pointed %1° %2 and %3° %4. Su dispositivo está apuntando %1° %2 y %3° %4. @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration + Desactivar para restablecer la calibración + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + Openpilot se calibra continuamente, por lo que rara vez es necesario reiniciarlo. Restablecer la calibración reiniciará Openpilot si el automóvil está encendido. + + + + +Steering lag calibration is %1% complete. + + +La calibración del retraso de dirección está %1% completada. + + + + +Steering lag calibration is complete. + + +La calibración del retraso de la dirección está completa. + + + Steering torque response calibration is %1% complete. + La calibración de la respuesta del par de dirección está %1% completada. + + + Steering torque response calibration is complete. + La calibración de la respuesta del par de dirección está completa. + + + Review the rules, features, and limitations of sunnypilot - Resetting calibration will restart openpilot if the car is powered on. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? ¿Seguro que quiere revisar la guía de entrenamiento? @@ -391,6 +462,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language + Seleccione el idioma + + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Confirmar + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an iniciando cámara + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,29 +600,29 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - Firehose Mode: ACTIVE - + Modo Firehose: ACTIVO ACTIVE - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - + ACTIVO <b>%n segment(s)</b> of your driving is in the training dataset so far. - - - + + <b>%n segmento</b> de tu conducción está en el conjunto de datos de entrenamiento hasta ahora. + <b>%n segmentos</b> de tu conducción están en el conjunto de datos de entrenamiento hasta ahora. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVO</span>: conéctate a una red sin límite de datos + + + Firehose Mode + Modo manguera contra incendios + sunnypilot learns to drive by watching humans, like you, drive. @@ -527,7 +630,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -636,6 +739,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -653,6 +764,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -660,11 +794,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -675,18 +813,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -703,6 +829,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -715,6 +845,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -762,31 +896,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECCIONAR - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +949,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -845,6 +988,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -861,6 +1012,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel Cancelar + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -968,14 +1151,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Device temperature too high. System cooling down before starting. Current internal component temperature: %1 La temperatura del dispositivo es muy alta. El sistema se está enfriando antes de iniciar. Temperatura actual del componente interno: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Conéctese inmediatamente al internet para buscar actualizaciones. Si no se conecta al internet, sunnypilot no iniciará en %1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - Conectese al internet para buscar actualizaciones. sunnypilot no iniciará automáticamente hasta conectarse al internet para buscar actualizaciones. - Unable to download updates %1 @@ -990,28 +1165,46 @@ 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. 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. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - El dispositivo no pudo registrarse. No se conectará ni subirá datos a los servidores de comma.ai y no recibe soporte de comma.ai. Si este es un dispositivo oficial, visite https://comma.ai/support. - NVMe drive not mounted. Unidad NVMe no montada. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Se detectó una unidad NVMe incompatible. El dispositivo puede consumir mucha más energía y sobrecalentarse debido a esto. + 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. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Posponer Actualización + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot 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. - sunnypilot 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. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot detectó un cambio en la posición de montaje del dispositivo. Asegúrese de que el dispositivo esté completamente asentado en el soporte y que el soporte esté firmemente asegurado al parabrisas. + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1031,11 +1224,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot no disponible + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY TOME CONTROL INMEDIATAMENTE @@ -1052,6 +1248,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive Systema no responde + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + VERIFICAR + + + Country + + + + SELECT + SELECCIONAR + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + ACTUALIZAR + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1138,7 +1469,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Confirmar + Cancel @@ -1221,10 +1552,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - now ahora @@ -1250,46 +1577,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp hace %n días - - - Reset - Reset failed. Reboot to try again. - Formateo fallido. Reinicie para reintentar. - - - Resetting device... -This may take up to a minute. - formateando dispositivo... -Esto puede tardar un minuto. - - - Are you sure you want to reset your device? - ¿Seguro que quiere formatear su dispositivo? - - - System Reset - Formatear Sistema - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Formateo del sistema activado. Presione confirmar para borrar todo el contenido y la configuración. Presione cancelar para reanudar el inicio. - - - Cancel - Cancelar - - - Reboot - Reiniciar - - - Confirm - Confirmar - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - No es posible montar una partición de datos. La partición podría estar corrompida. Confirme para borrar y formatear su dispositivo. + sunnypilot + @@ -1320,7 +1610,7 @@ Esto puede tardar un minuto. Firehose - + Firehose @@ -1350,29 +1640,13 @@ Esto puede tardar un minuto. Software - Trips - - - - Vehicle - - - - Developer - Desarrollador - - - Firehose + Models Steering - - Models - - Cruise @@ -1381,100 +1655,25 @@ Esto puede tardar un minuto. Visuals - - - Setup - Something went wrong. Reboot the device. - Algo ha ido mal. Reinicie el dispositivo. + OSM + - Ensure the entered URL is valid, and the device’s internet connection is good. - Asegúrese de que la URL insertada es válida y que el dispositivo tiene buena conexión. + Trips + - No custom software found at this URL. - No encontramos software personalizado en esta URL. + Vehicle + - WARNING: Low Voltage - ALERTA: Voltaje bajo + Firehose + Firehose - Power your device in a car with a harness or proceed at your own risk. - Encienda su dispositivo en un auto con el arnés o proceda bajo su propio riesgo. - - - Power off - Apagar - - - Continue - Continuar - - - Getting Started - Comenzando - - - Before we get on the road, let’s finish installation and cover some details. - Antes de ponernos en marcha, terminemos la instalación y cubramos algunos detalles. - - - Connect to Wi-Fi - Conectarse al Wi-Fi - - - Back - Volver - - - Continue without Wi-Fi - Continuar sin Wi-Fi - - - Waiting for internet - Esperando conexión a internet - - - Choose Software to Install - Elija el software a instalar - - - sunnypilot - sunnypilot - - - Custom Software - Software personalizado - - - Enter URL - Insertar URL - - - for Custom Software - para Software personalizado - - - Downloading... - Descargando... - - - Download Failed - Descarga fallida - - - Reboot device - Reiniciar Dispositivo - - - Start over - Comenzar de nuevo - - - Select a language - Seleccione un idioma + Developer + Desarrollador @@ -1679,6 +1878,14 @@ Esto puede tardar un minuto. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1687,6 +1894,22 @@ Esto puede tardar un minuto. Select a branch Selecione una rama + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Reiniciar + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1744,37 +1967,21 @@ Esto puede tardar un minuto. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status - Estado de patrocinio + SPONSOR - PATROCINADOR + Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - Conviértete en patrocinador de sunnypilot para obtener acceso anticipado a las funciones de sunnylink cuando estén disponibles. + Pair GitHub Account - Emparejar cuenta de GitHub + PAIR @@ -1782,27 +1989,23 @@ Esto puede tardar un minuto. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - Empareja tu cuenta de GitHub para otorgar a tu dispositivo beneficios de patrocinador, incluido acceso a la API en sunnylink. + + + + N/A + N/A sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - ID del dongle de sunnylink no encontrado. Esto puede deberse a una conexión débil o a un problema de registro en sunnylink. Por favor, reinicia y vuelve a intentarlo. + - Not Sponsor - No patrocinador + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + - Paired - Emparejado - - - Not Paired - No emparejado - - - THANKS ♥ - GRACIAS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + Backup Settings @@ -1856,44 +2059,88 @@ Esto puede tardar un minuto. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 Scan the QR code to login to your GitHub account - Escanea el código QR para iniciar sesión en tu cuenta de GitHub + Follow the prompts to complete the pairing process - Sigue las indicaciones para completar el proceso de emparejamiento + Re-enter the "sunnylink" panel to verify sponsorship status - Vuelve a ingresar al panel de "sunnylink" para verificar el estado de patrocinio + If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - Si el estado de patrocinio no se actualizó, por favor contacta a un moderador en Discord en https://discord.gg/sunnypilot + Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - Escanea el código QR para visitar la página de GitHub Sponsors de sunnyhaibin + Choose your sponsorship tier and confirm your support - Elige tu nivel de patrocinio y confirma tu apoyo + Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - Únete a nuestra comunidad en Discord en https://discord.gg/sunnypilot y contacta a un moderador para confirmar tu estado de patrocinio + Pair your GitHub account - Empareja tu cuenta de GitHub + Early Access: Become a sunnypilot Sponsor - Acceso temprano: conviértete en patrocinador de sunnypilot + @@ -1925,10 +2172,6 @@ Esto puede tardar un minuto. Disengage on Accelerator Pedal Desactivar con el Acelerador - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Cuando esté activado, presionar el acelerador deshabilitará sunnypilot. - Enable Lane Departure Warnings Activar Avisos de Salida de Carril @@ -1941,10 +2184,6 @@ Esto puede tardar un minuto. Always-On Driver Monitoring Monitoreo Permanente del Conductor - - Enable driver monitoring even when sunnypilot is not engaged. - Habilitar el monitoreo del conductor incluso cuando Openpilot no esté activado. - Record and Upload Driver Camera Grabar y Subir Cámara del Conductor @@ -1977,22 +2216,10 @@ Esto puede tardar un minuto. Driving Personality Personalidad de conducción - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Se recomienda el modo estándar. En el modo agresivo, sunnypilot seguirá más cerca a los autos delante suyo y será más agresivo con el acelerador y el freno. En modo relajado, sunnypilot se mantendrá más alejado de los autos delante suyo. En automóviles compatibles, puede recorrer estas personalidades con el botón de distancia del volante. - - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot por defecto conduce en <b>modo chill</b>. El modo Experimental activa <b>funcionalidades en fase experimental</b>, que no están listas para el modo chill. Las funcionalidades del modo expeimental están listados abajo: - End-to-End Longitudinal Control Control Longitudinal de Punta a Punta - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Dajar que el modelo de conducción controle la aceleración y el frenado. sunnypilot conducirá como piensa que lo haría una persona, incluiyendo parar en los semáforos en rojo y las señales de alto. Dado que el modelo decide la velocidad de conducción, la velocidad de crucero establecida solo actuará como el límite superior. Este recurso aún está en fase experimental; deberían esperarse errores. - New Driving Visualization Nueva Visualización de la conducción @@ -2006,12 +2233,28 @@ Esto puede tardar un minuto. El modo Experimental no está disponible actualmente para este auto, ya que el ACC default del auto está siendo usado para el control longitudinal. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Se puede probar una versión experimental del control longitudinal sunnypilot, junto con el modo Experimental, en ramas no liberadas. + openpilot longitudinal control may come in a future update. + El control longitudinal de openpilot podrá llegar en futuras actualizaciones. - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Activar el control longitudinal (fase experimental) para permitir el modo Experimental. + Changing this setting will restart openpilot if the car is powered on. + Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. + + + Record and Upload Microphone Audio + Grabar y cargar audio de micrófono + + + 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. + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2022,70 +2265,101 @@ Esto puede tardar un minuto. - Enable sunnypilot - Activar sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Updater + TreeOptionDialog - Update Required - Actualización Requerida + Select + Seleccionar - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Es necesario la actualización del sistema operativo. Conecte su dispositivo al Wi-Fi para una actualización rápida. El tamaño de descarga es de aproximadamente 1GB. + Cancel + Cancelar + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Conectarse a la Wi-Fi + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - Instalar + Changing this setting will restart openpilot if the car is powered on. + Cambiar esta configuración reiniciará Openpilot si el automóvil está encendido. - Back - Volver + Off + - Loading... - Cargando... + Distance + - Reboot - Reiniciar + Speed + - Update failed - Actualización fallida + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + WiFiPromptWidget Open - - - - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - + Abrir Maximize your training data uploads to improve openpilot's driving models. - + Maximice sus cargas de datos de entrenamiento para mejorar los modelos de conducción de openpilot. + + + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_fr.ts b/selfdrive/ui/translations/main_fr.ts index 741cc606a7..45705defd9 100644 --- a/selfdrive/ui/translations/main_fr.ts +++ b/selfdrive/ui/translations/main_fr.ts @@ -7,10 +7,6 @@ Close Fermer - - Snooze Update - Reporter la mise à jour - Reboot and Update Redémarrer et mettre à jour @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Vous devez accepter les conditions générales pour utiliser sunnypilot. - Back Retour @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Refuser, désinstaller %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Mode manÅ“uvre longitudinale + + openpilot Longitudinal Control (Alpha) + Contrôle longitudinal openpilot (Alpha) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + ATTENTION : le contrôle longitudinal openpilot est en alpha pour cette voiture et désactivera le freinage d'urgence automatique (AEB). + Enable ADB @@ -179,6 +202,22 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -262,10 +301,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW REVOIR - - Review the rules, features, and limitations of sunnypilot - Revoir les règles, fonctionnalités et limitations d'sunnypilot - Are you sure you want to review the training guide? Êtes-vous sûr de vouloir revoir le guide de formation ? @@ -302,10 +337,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Éteindre - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot nécessite que l'appareil soit monté à 4° à gauche ou à droite et à 5° vers le haut ou 9° vers le bas. sunnypilot se calibre en continu, la réinitialisation est rarement nécessaire. - Your device is pointed %1° %2 and %3° %4. Votre appareil est orienté %1° %2 et %3° %4. @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + + + + + +Steering lag calibration is %1% complete. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + Steering torque response calibration is complete. + + + + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? Êtes-vous sûr de vouloir revoir le guide de formation ? @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language Choisir une langue + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Redémarrer @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Confirmer + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an démarrage de la caméra + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - Firehose Mode: ACTIVE @@ -509,10 +604,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ACTIVE - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -520,6 +611,14 @@ Please use caution when using this feature. Only use the blinker when traffic an + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -527,7 +626,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -636,6 +735,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -653,6 +760,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -660,11 +790,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -675,18 +809,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -703,6 +825,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -715,6 +841,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -762,31 +892,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SÉLECTIONNER - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +945,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -845,6 +984,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -861,6 +1008,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel Annuler + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -968,14 +1147,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Device temperature too high. System cooling down before starting. Current internal component temperature: %1 Température de l'appareil trop élevée. Le système doit refroidir avant de démarrer. Température actuelle de l'appareil : %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Connectez-vous immédiatement à internet pour vérifier les mises à jour. Si vous ne vous connectez pas à internet, sunnypilot ne s'engagera pas dans %1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - Connectez l'appareil à internet pour vérifier les mises à jour. sunnypilot ne démarrera pas automatiquement tant qu'il ne se connecte pas à internet pour vérifier les mises à jour. - Unable to download updates %1 @@ -990,28 +1161,46 @@ 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. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - L'appareil n'a pas réussi à s'enregistrer. Il ne se connectera pas aux serveurs de comma.ai, n'enverra rien et ne recevra aucune assistance de comma.ai. S'il s'agit d'un appareil officiel, visitez https://comma.ai/support. - NVMe drive not mounted. Le disque NVMe n'est pas monté. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Disque NVMe non supporté détecté. L'appareil peut consommer beaucoup plus d'énergie et surchauffer en raison du NVMe non supporté. + 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. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Reporter la mise à jour + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot 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. - sunnypilot 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. + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot a détecté un changement dans la position de montage de l'appareil. Assurez-vous que l'appareil est totalement inséré dans le support et que le support est fermement fixé au pare-brise. + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1031,11 +1220,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot indisponible + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY REPRENEZ LE CONTRÔLE IMMÉDIATEMENT @@ -1052,6 +1244,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive Système inopérant + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + VÉRIFIER + + + Country + + + + SELECT + SÉLECTIONNER + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + MISE À JOUR + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1138,7 +1465,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Confirmer + Cancel @@ -1221,10 +1548,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1250,46 +1573,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now maintenant - - - Reset - Reset failed. Reboot to try again. - Réinitialisation échouée. Redémarrez pour réessayer. - - - Resetting device... -This may take up to a minute. - Réinitialisation de l'appareil... -Cela peut prendre jusqu'à une minute. - - - Are you sure you want to reset your device? - Êtes-vous sûr de vouloir réinitialiser votre appareil ? - - - System Reset - Réinitialisation du système - - - Cancel - Annuler - - - Reboot - Redémarrer - - - Confirm - Confirmer - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Impossible de monter la partition data. La partition peut être corrompue. Appuyez sur confirmer pour effacer et réinitialiser votre appareil. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Réinitialisation système déclenchée. Appuyez sur confirmer pour effacer tout le contenu et les paramètres. Appuyez sur annuler pour reprendre le démarrage. + sunnypilot + @@ -1350,29 +1636,13 @@ Cela peut prendre jusqu'à une minute. Logiciel - Trips - - - - Vehicle - - - - Developer - Dév. - - - Firehose + Models Steering - - Models - - Cruise @@ -1381,100 +1651,25 @@ Cela peut prendre jusqu'à une minute. Visuals - - - Setup - Something went wrong. Reboot the device. - Un problème est survenu. Redémarrez l'appareil. + OSM + - Ensure the entered URL is valid, and the device’s internet connection is good. - Assurez-vous que l'URL saisie est valide et que la connexion internet de l'appareil est bonne. + Trips + - No custom software found at this URL. - Aucun logiciel personnalisé trouvé à cette URL. + Vehicle + - WARNING: Low Voltage - ATTENTION : Tension faible + Firehose + - Power your device in a car with a harness or proceed at your own risk. - Alimentez votre appareil dans une voiture avec un harness ou continuez à vos risques et périls. - - - Power off - Éteindre - - - Continue - Continuer - - - Getting Started - Commencer - - - Before we get on the road, let’s finish installation and cover some details. - Avant de prendre la route, terminons l'installation et passons en revue quelques détails. - - - Connect to Wi-Fi - Se connecter au Wi-Fi - - - Back - Retour - - - Enter URL - Entrer l'URL - - - for Custom Software - pour logiciel personnalisé - - - Continue without Wi-Fi - Continuer sans Wi-Fi - - - Waiting for internet - En attente d'internet - - - Downloading... - Téléchargement... - - - Download Failed - Échec du téléchargement - - - Reboot device - Redémarrer l'appareil - - - Start over - Recommencer - - - Select a language - Choisir une langue - - - Choose Software to Install - Choisir le logiciel à installer - - - sunnypilot - sunnypilot - - - Custom Software - Logiciel personnalisé + Developer + Dév. @@ -1679,6 +1874,14 @@ Cela peut prendre jusqu'à une minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1687,6 +1890,22 @@ Cela peut prendre jusqu'à une minute. Select a branch Sélectionner une branche + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Redémarrer + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1744,22 +1963,6 @@ Cela peut prendre jusqu'à une minute. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -1784,24 +1987,20 @@ Cela peut prendre jusqu'à une minute. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1856,6 +2055,50 @@ Cela peut prendre jusqu'à une minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1925,10 +2168,6 @@ Cela peut prendre jusqu'à une minute. Disengage on Accelerator Pedal Désengager avec la pédale d'accélérateur - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Lorsqu'il est activé, appuyer sur la pédale d'accélérateur désengagera sunnypilot. - Enable Lane Departure Warnings Activer les avertissements de sortie de voie @@ -1969,14 +2208,6 @@ Cela peut prendre jusqu'à une minute. Driving Personality Personnalité de conduite - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - Par défaut, sunnypilot conduit en <b>mode détente</b>. Le mode expérimental permet d'activer des <b>fonctionnalités alpha</b> qui ne sont pas prêtes pour le mode détente. Les fonctionnalités expérimentales sont listées ci-dessous : - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Laissez le modèle de conduite contrôler l'accélérateur et les freins. sunnypilot conduira comme il pense qu'un humain le ferait, y compris s'arrêter aux feux rouges et aux panneaux stop. Comme le modèle de conduite décide de la vitesse à adopter, la vitesse définie ne servira que de limite supérieure. Cette fonctionnalité est de qualité alpha ; des erreurs sont à prévoir. - New Driving Visualization Nouvelle visualisation de la conduite @@ -1986,21 +2217,13 @@ Cela peut prendre jusqu'à une minute. Le mode expérimental est actuellement indisponible pour cette voiture car le régulateur de vitesse adaptatif d'origine est utilisé pour le contrôle longitudinal. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Une version alpha du contrôle longitudinal sunnypilot peut être testée, avec le mode expérimental, sur des branches non publiées. + openpilot longitudinal control may come in a future update. + Le contrôle longitudinal openpilot pourrait être disponible dans une future mise à jour. End-to-End Longitudinal Control Contrôle longitudinal de bout en bout - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Activer le contrôle longitudinal d'sunnypilot (en alpha) pour autoriser le mode expérimental. - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Le mode Standard est recommandé. En mode Agressif, sunnypilot suivra les véhicules de plus près et sera plus dynamique avec l'accélérateur et le frein. En mode Détendu, sunnypilot maintiendra une distance plus importante avec les véhicules qui précèdent. Sur les véhicules compatibles, vous pouvez alterner entre ces personnalités à l'aide du bouton de distance au volant. - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. La visualisation de la conduite passera sur la caméra grand angle dirigée vers la route à faible vitesse afin de mieux montrer certains virages. Le logo du mode expérimental s'affichera également dans le coin supérieur droit. @@ -2010,8 +2233,24 @@ Cela peut prendre jusqu'à une minute. Surveillance continue du conducteur - Enable driver monitoring even when sunnypilot is not engaged. - Activer la surveillance conducteur lorsque sunnypilot n'est pas actif. + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2022,11 +2261,53 @@ Cela peut prendre jusqu'à une minute. - Enable sunnypilot - Activer sunnypilot + When enabled, pressing the accelerator pedal will disengage sunnypilot. + - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable driver monitoring even when sunnypilot is not engaged. + + + + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + + + + + TreeOptionDialog + + Select + Sélectionner + + + Cancel + Annuler + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2034,43 +2315,32 @@ Cela peut prendre jusqu'à une minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - Mise à jour requise + Distance + - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Une mise à jour du système d'exploitation est requise. Connectez votre appareil au Wi-Fi pour une mise à jour plus rapide. La taille du téléchargement est d'environ 1 Go. + Speed + - Connect to Wi-Fi - Se connecter au Wi-Fi + Time + - Install - Installer + All + - Back - Retour + Display Metrics Below Chevron + - Loading... - Chargement... - - - Reboot - Redémarrer - - - Update failed - Échec de la mise à jour + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2080,11 +2350,11 @@ Cela peut prendre jusqu'à une minute. - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. - Maximize your training data uploads to improve openpilot's driving models. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_ja.ts b/selfdrive/ui/translations/main_ja.ts index f85ee37689..3faa183954 100644 --- a/selfdrive/ui/translations/main_ja.ts +++ b/selfdrive/ui/translations/main_ja.ts @@ -7,10 +7,6 @@ Close é–‰ã˜ã‚‹ - - Snooze Update - æ›´æ–°ã®ä¸€æ™‚åœæ­¢ - Reboot and Update å†èµ·å‹•ã—ã¦ã‚¢ãƒƒãƒ—デート @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + モãƒã‚¤ãƒ«ãƒ‡ãƒ¼ã‚¿å›žç·šã‚’使用ã—ã¦ã„ã‚‹ã¨ãã¯å¤§å®¹é‡ãƒ‡ãƒ¼ã‚¿ã‚’アップロードã—ã¾ã›ã‚“ default - + 標準設定 metered - + 従é‡åˆ¶ unmetered - + 定é¡åˆ¶ Wi-Fi Network Metered - + 従é‡åˆ¶ã®Wi-Fiãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ Prevent large data uploads when on a metered Wi-Fi connection - + 通信制é™ã®ã‚ã‚‹Wi-Fi接続ã§ã¯å¤§å®¹é‡ãƒ‡ãƒ¼ã‚¿ã‚’アップロードã—ã¾ã›ã‚“ @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - sunnypilotを使用ã™ã‚‹ãŸã‚ã«ã¯åˆ©ç”¨è¦ç´„ã«åŒæ„ã™ã‚‹å¿…è¦ãŒã‚りã¾ã™ - Back 戻る @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 åŒæ„ã—ãªã„(%1をアンインストール) + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode アクセル制御マニューãƒãƒ¼ + + openpilot Longitudinal Control (Alpha) + openpilotアクセル制御(Alpha) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + ã“ã®è»Šã§ã¯openpilotã®ã‚¢ã‚¯ã‚»ãƒ«åˆ¶å¾¡ã¯ã‚¢ãƒ«ãƒ•ァ版ã§ã‚りã€è‡ªå‹•緊急ブレーキ(AEB)ãŒç„¡åŠ¹åŒ–ã•れã¾ã™ã€‚ + Enable ADB ADBを有効ã«ã™ã‚‹ @@ -179,6 +202,22 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android Debug Bridge)ã«ã‚ˆã‚Šã€USBã¾ãŸã¯ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯çµŒç”±ã§ãƒ‡ãƒã‚¤ã‚¹ã«æŽ¥ç¶šã§ãã¾ã™ã€‚詳細ã¯ã€https://docs.comma.ai/how-to/connect-to-comma ã‚’å‚ç…§ã—ã¦ãã ã•ã„。 + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW ç¢ºèª - - Review the rules, features, and limitations of sunnypilot - sunnypilotã®ãƒ«ãƒ¼ãƒ«ã€æ©Ÿèƒ½ã€ãŠã‚ˆã³åˆ¶é™ã‚’確èªã—ã¦ãã ã•ã„ - Are you sure you want to review the training guide? トレーニングガイドを始ã‚ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off パワーオフ - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilotã®æœ¬ä½“ã¯å·¦å³4°以内ã€ä¸Š5°下9°以内ã®è§’度ã§å–付ã‘ã‚‹å¿…è¦ãŒã‚りã¾ã™ã€‚常ã«ã‚­ãƒ£ãƒªãƒ–レーションã•れã¦ãŠã‚Šãƒªã‚»ãƒƒãƒˆã¯ã»ã¨ã‚“ã©å¿…è¦ã‚りã¾ã›ã‚“。 - Your device is pointed %1° %2 and %3° %4. ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã¯%2 %1°ã€%4 %3°ã®å‘ãã«è¨­ç½®ã•れã¦ã„ã¾ã™ã€‚ @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration + キャリブレーションをリセットã™ã‚‹ã«ã¯é‹è»¢æ”¯æ´ã‚’解除ã—ã¦ä¸‹ã•ã„。 + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot ã¯ç¶™ç¶šçš„ã«ã‚­ãƒ£ãƒªãƒ–レーションを行ã£ã¦ãŠã‚Šã€ãƒªã‚»ãƒƒãƒˆãŒå¿…è¦ã«ãªã‚‹ã“ã¨ã¯ã»ã¨ã‚“ã©ã‚りã¾ã›ã‚“。キャリブレーションをリセットã™ã‚‹ã¨ã€è»Šã®é›»æºãŒå…¥ã£ã¦ã„ã‚‹å ´åˆã¯openpilotãŒå†èµ·å‹•ã—ã¾ã™ã€‚ + + + + +Steering lag calibration is %1% complete. + + +ステアリングé…å»¶ã®ã‚­ãƒ£ãƒªãƒ–レーションãŒ%1%完了。 + + + + +Steering lag calibration is complete. + + +ステアリングé…å»¶ã®ã‚­ãƒ£ãƒªãƒ–レーション完了。 + + + Steering torque response calibration is %1% complete. + ステアリングトルク応答ã®ã‚­ãƒ£ãƒªãƒ–レーションãŒ%1%完了。 + + + Steering torque response calibration is complete. + ステアリングトルク応答ã®ã‚­ãƒ£ãƒªãƒ–レーション完了。 + + + Review the rules, features, and limitations of sunnypilot - Resetting calibration will restart openpilot if the car is powered on. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? トレーニングガイドを始ã‚ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language è¨€èªžã‚’é¸æŠž + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot å†èµ·å‹• @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - ç¢ºèª + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an カメラ起動中 + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +600,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 Firehoseモード 🔥 - Firehose Mode: ACTIVE - + Firehoseモード: 作動中 ACTIVE - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - + 動作中 <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + ã‚ãªãŸã®é‹è»¢ã®<b>%nセグメント</b>ãŒã“れã¾ã§ã®ãƒˆãƒ¬ãƒ¼ãƒ‹ãƒ³ã‚°ãƒ‡ãƒ¼ã‚¿ã«å«ã¾ã‚Œã¦ã„ã¾ã™ã€‚ + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>å‹•ä½œåœæ­¢</span>: 大容é‡ã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«æŽ¥ç¶šã—ã¦ãã ã•ã„ + + + Firehose Mode + Firehoseモード + sunnypilot learns to drive by watching humans, like you, drive. @@ -526,8 +629,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>å‹•ä½œåœæ­¢</span>: 大容é‡ã®ãƒãƒƒãƒˆãƒ¯ãƒ¼ã‚¯ã«æŽ¥ç¶šã—ã¦ãã ã•ã„ + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + @@ -634,6 +737,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -651,6 +762,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,11 +792,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -673,18 +811,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -701,6 +827,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -713,6 +843,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -760,31 +894,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp é¸æŠž - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +947,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -843,6 +986,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -859,6 +1010,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel キャンセル + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -962,14 +1145,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - インターãƒãƒƒãƒˆã¸æŽ¥ç¶šã—ã¦ã‚¢ãƒƒãƒ—デートを確èªã—ã¦ãã ã•ã„。未接続ã®ã¾ã¾ã§ã¯sunnypilotを使用ã§ããªããªã‚Šã¾ã™ã€‚ã‚ã¨[%1] - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - インターãƒãƒƒãƒˆã«æŽ¥ç¶šã—ã¦ã‚¢ãƒƒãƒ—デートを確èªã—ã¦ãã ã•ã„。接続ã™ã‚‹ã¾ã§sunnypilotã¯ä½¿ç”¨ã§ãã¾ã›ã‚“。 - Unable to download updates %1 @@ -984,32 +1159,50 @@ 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. オペレーティングシステムãŒãƒãƒƒã‚¯ã‚°ãƒ©ã‚¦ãƒ³ãƒ‰ã§ãƒ€ã‚¦ãƒ³ãƒ­ãƒ¼ãƒ‰ã•れã¦ã„ã¾ã™ã€‚ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã®æº–å‚™ãŒæ•´ã†ã¨æ›´æ–°ã‚’促ã•れã¾ã™ã€‚ - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - 登録ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã¯comma.aiã®ã‚µãƒ¼ãƒãƒ¼ã«æŽ¥ç¶šã—ãŸã‚Šãƒ‡ãƒ¼ã‚¿ã‚’アップロードã—ãŸã‚Šã§ãã¾ã›ã‚“。ã¾ãŸcomma.aiã®ã‚µãƒãƒ¼ãƒˆã‚‚å—ã‘られã¾ã›ã‚“。公å¼ãƒ‡ãƒã‚¤ã‚¹ã§ã‚ã‚‹å ´åˆã¯ https://comma.ai/support ã«å•ã„åˆã‚ã›ã¦ä¸‹ã•ã„。 - NVMe drive not mounted. SSDドライブ(NVMe)ãŒãƒžã‚¦ãƒ³ãƒˆã•れã¦ã„ã¾ã›ã‚“。 - - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - éžã‚µãƒãƒ¼ãƒˆã®SSDドライブ(NVMe)ãŒæ¤œå‡ºã•れã¾ã—ãŸã€‚ã“ã®ãƒ‰ãƒ©ã‚¤ãƒ–を使用ã™ã‚‹ã¨ãƒ‡ãƒã‚¤ã‚¹ãŒå¤šå¤§ãªé›»åŠ›ã‚’æ¶ˆè²»ã—éŽç†±ã™ã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ - - - sunnypilot 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. - sunnypilotãŒè»Šä¸¡ã‚’識別ã§ãã¾ã›ã‚“ã§ã—ãŸã€‚è»ŠãŒæœªå¯¾å¿œã¾ãŸã¯ECUãŒèªè­˜ã•れã¦ã„ãªã„å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚該当車両ã®ãƒ•ァームウェアãƒãƒ¼ã‚¸ãƒ§ãƒ³ã‚’追加ã™ã‚‹ãŸã‚ã«ãƒ—ルリクエストã—ã¦ãã ã•ã„。サãƒãƒ¼ãƒˆãŒå¿…è¦ãªå ´åˆã¯ discord.comma.ai ã«å‚加ã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilotãŒãƒ‡ãƒã‚¤ã‚¹ã®å–り付ã‘ä½ç½®ã«ãšã‚Œã‚’検出ã—ã¾ã—ãŸã€‚デãƒã‚¤ã‚¹ã®å›ºå®šã¨ãƒžã‚¦ãƒ³ãƒˆãŒãƒ•ロントガラスã«ã—ã£ã‹ã‚Šã¨å–り付ã‘られã¦ã„ã‚‹ã“ã¨ã‚’確èªã—ã¦ãã ã•ã„。 - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 デãƒã‚¤ã‚¹ã®æ¸©åº¦ãŒé«˜ã™ãŽã‚‹ãŸã‚システム起動å‰ã®å†·å´ä¸­ã§ã™ã€‚ç¾åœ¨ã®ãƒ‡ãƒã‚¤ã‚¹å†…部温度: %1 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + 製å“ã®comma.aiã¸ã®ç™»éŒ²ã«å¤±æ•—ã—ã¾ã—ãŸã€‚ã“ã®ãƒ‡ãƒã‚¤ã‚¹ã¯comma.aiã®ã‚µãƒ¼ãƒãƒ¼ã«æŽ¥ç¶šã—ãŸã‚Šã‚¢ãƒƒãƒ—ロードを行ã£ãŸã‚Šã™ã‚‹ã“ã¨ã¯ã§ããšã€comma.aiã‹ã‚‰ã®ã‚µãƒãƒ¼ãƒˆã‚‚å—ã‘られã¾ã›ã‚“。もã—comma.ai/shopã§è³¼å…¥ã—ãŸå ´åˆã¯ã€https://comma.ai/support ã«ã¦ã‚µãƒãƒ¼ãƒˆãƒã‚±ãƒƒãƒˆã‚’ã”æå‡ºãã ã•ã„。 + + + Acknowledge Excessive Actuation + éŽå‰°ãªä½œå‹•ã®æ¤œçŸ¥ + + + Snooze Update + ã¾ãŸå¾Œã§æ›´æ–°ã™ã‚‹ + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + + + + sunnypilot 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. + + + + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + + + + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1029,11 +1222,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilotã¯ä½¿ç”¨ã§ãã¾ã›ã‚“ + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY ç›´ã¡ã«è»Šã®é‹è»¢ã«æˆ»ã£ã¦ä¸‹ã•ã„ @@ -1050,6 +1246,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive システムãŒå¿œç­”ã—ã¾ã›ã‚“ + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + ç¢ºèª + + + Country + + + + SELECT + é¸æŠž + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + æ›´æ–° + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1136,7 +1467,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - ç¢ºèª + Cancel @@ -1219,10 +1550,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1572,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now ãŸã£ãŸä»Š - - - Reset - Reset failed. Reboot to try again. - åˆæœŸåŒ–ã«å¤±æ•—ã—ã¾ã—ãŸã€‚å†èµ·å‹•後ã«å†è©¦è¡Œã—ã¦ãã ã•ã„。 - - - Are you sure you want to reset your device? - åˆæœŸåŒ–ã—ã¦ã‚‚よã‚ã—ã„ã§ã™ã‹ï¼Ÿ - - - System Reset - ã‚·ã‚¹ãƒ†ãƒ ã‚’åˆæœŸåŒ– - - - Cancel - キャンセル - - - Reboot - å†èµ·å‹• - - - Confirm - ç¢ºèª - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Dataパーティションをマウントã§ãã¾ã›ã‚“。パーティションãŒç ´æã—ã¦ã„ã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚デãƒã‚¤ã‚¹ã‚’消去ã—ã¦ãƒªã‚»ãƒƒãƒˆã—ã¾ã™ã®ã§ç¢ºèªã‚’押ã—ã¦ä¸‹ã•ã„。 - - - Resetting device... -This may take up to a minute. - デãƒã‚¤ã‚¹ã‚’リセットã—ã¦ã„ã¾ã™â€¦ -ã“ã®å‡¦ç†ã«ã¯æœ€å¤§ã§1分ã»ã©ã‹ã‹ã‚‹å ´åˆãŒã‚りã¾ã™ã€‚ - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - ã‚·ã‚¹ãƒ†ãƒ ãƒªã‚»ãƒƒãƒˆã®æº–å‚™ãŒæ•´ã„ã¾ã—ãŸã€‚ã™ã¹ã¦ã®ãƒ‡ãƒ¼ã‚¿ã¨è¨­å®šã‚’消去ã™ã‚‹ã«ã¯ã€Œç¢ºèªã€ã‚’押ã—ã¦ãã ã•ã„。「キャンセルã€ã‚’押ã™ã¨ãƒ–ートをå†é–‹ã—ã¾ã™ã€‚ + sunnypilot + @@ -1345,29 +1635,13 @@ This may take up to a minute. ソフト - Trips + Models - - Vehicle - - - - Developer - 開発 - - - Firehose - データ学習 - Steering - - Models - - Cruise @@ -1376,100 +1650,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - 警告:電圧低下 + OSM + - Power your device in a car with a harness or proceed at your own risk. - ãƒãƒ¼ãƒã‚¹ã‚’使ã£ã¦è»Šã§ãƒ‡ãƒã‚¤ã‚¹ã«é›»æºã‚’供給ã™ã‚‹ã‹ã€è‡ªå·±è²¬ä»»ã§ã“ã®ã¾ã¾ç¶™ç¶šã—ã¦ä¸‹ã•ã„。 + Trips + - Power off - é›»æºã‚’切る + Vehicle + - Continue - ç¶šã‘ã‚‹ + Firehose + データ学習 - Getting Started - ã¯ã˜ã‚ã« - - - Before we get on the road, let’s finish installation and cover some details. - 出発ã™ã‚‹å‰ã«ã€ã‚¤ãƒ³ã‚¹ãƒˆãƒ¼ãƒ«ã‚’完了ã•ã›ã¦å°‘ã—詳細を確èªã—ã¾ã—ょã†ã€‚ - - - Connect to Wi-Fi - Wi-Fiã«æŽ¥ç¶š - - - Back - 戻る - - - Continue without Wi-Fi - Wi-Fiã«æŽ¥ç¶šã›ãšã«ç¶šè¡Œ - - - Waiting for internet - インターãƒãƒƒãƒˆæŽ¥ç¶šã‚’待機中 - - - Enter URL - URLã®å…¥åŠ› - - - for Custom Software - カスタムソフトウェア - - - Downloading... - ダウンロード中... - - - Download Failed - ダウンロードã«å¤±æ•—ã—ã¾ã—㟠- - - Ensure the entered URL is valid, and the device’s internet connection is good. - 入力ã•れãŸURLãŒæ­£ã—ã„ã‹ã©ã†ã‹ã€ã‚¤ãƒ³ã‚¿ãƒ¼ãƒãƒƒãƒˆã«æ­£å¸¸ã«æŽ¥ç¶šã§ãã¦ã„ã‚‹ã‹ã‚’確èªã—ã¦ãã ã•ã„。 - - - Reboot device - デãƒã‚¤ã‚¹ã‚’å†èµ·å‹• - - - Start over - やり直㙠- - - No custom software found at this URL. - ã“ã®URLã¯ã‚«ã‚¹ã‚¿ãƒ ã‚½ãƒ•トウェアã§ã¯ã‚りã¾ã›ã‚“。 - - - Something went wrong. Reboot the device. - 何ã‹ã®å•題ãŒç™ºç”Ÿã—ã¾ã—ãŸã€‚デãƒã‚¤ã‚¹ã‚’å†èµ·å‹•ã—ã¦ãã ã•ã„。 - - - Select a language - è¨€èªžã‚’é¸æŠž - - - Choose Software to Install - インストールã™ã‚‹ã‚½ãƒ•ãƒˆã‚¦ã‚§ã‚¢ã‚’é¸æŠžã—ã¦ãã ã•ã„。 - - - sunnypilot - sunnypilot - - - Custom Software - カスタムソフトウェア + Developer + 開発 @@ -1674,6 +1873,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1889,22 @@ This may take up to a minute. Select a branch ブランãƒã‚’é¸æŠž + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + å†èµ·å‹• + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1691,7 +1914,7 @@ This may take up to a minute. Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. - 警告: ã“れã¯ã€GitHub ã®è¨­å®šã«ã‚ã‚‹ã™ã¹ã¦ã®å…¬é–‹éµã¸ã® SSH アクセスを許å¯ã™ã‚‹ã‚‚ã®ã§ã™ã€‚自分以外㮠GitHub ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åを入力ã—ãªã„ã§ãã ã•ã„。commaã®ã‚¹ã‚¿ãƒƒãƒ•㌠GitHub ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åを追加ã™ã‚‹ã‚ˆã†ãŠé¡˜ã„ã™ã‚‹ã“ã¨ã¯ã‚りã¾ã›ã‚“。 + 警告: ã“れã¯GitHubã®è¨­å®šã«ã‚ã‚‹ã™ã¹ã¦ã®å…¬é–‹éµã¸ã® SSH アクセスを許å¯ã™ã‚‹ã‚‚ã®ã§ã™ã€‚自分以外ã®GitHubユーザーåを入力ã—ãªã„ã§ãã ã•ã„。commaã®ã‚¹ã‚¿ãƒƒãƒ•ãŒGitHubã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åを追加ã™ã‚‹ã‚ˆã†ãŠé¡˜ã„ã™ã‚‹ã“ã¨ã¯ã‚りã¾ã›ã‚“。 ADD @@ -1699,7 +1922,7 @@ This may take up to a minute. Enter your GitHub username - GitHub ã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åを入力ã—ã¦ãã ã•ã„ + GitHubã®ãƒ¦ãƒ¼ã‚¶ãƒ¼åを入力ã—ã¦ãã ã•ã„ LOADING @@ -1739,22 +1962,6 @@ This may take up to a minute. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - 該当ãªã— - Sponsor Status @@ -1779,24 +1986,20 @@ This may take up to a minute. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + 該当ãªã— + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1851,6 +2054,50 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1914,7 +2161,7 @@ This may take up to a minute. TogglesPanel Enable Lane Departure Warnings - è»Šç·šé€¸è„±è­¦å ±æ©Ÿèƒ½ã®æœ‰åŠ¹åŒ– + è»Šç·šé€¸è„±è­¦å ±ã®æœ‰åŠ¹åŒ– Receive alerts to steer back into the lane when your vehicle drifts over a detected lane line without a turn signal activated while driving over 31 mph (50 km/h). @@ -1940,22 +2187,10 @@ This may take up to a minute. Disengage on Accelerator Pedal アクセルをè¸ã‚€ã¨é‹è»¢ã‚µãƒãƒ¼ãƒˆã‚’中断 - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - ã“ã®æ©Ÿèƒ½ã‚’有効化ã™ã‚‹ã¨ã€sunnypilotを利用中ã«ã‚¢ã‚¯ã‚»ãƒ«ã‚’è¸ã‚€ã¨sunnypilotã«ã‚ˆã‚‹é‹è»¢ã‚µãƒãƒ¼ãƒˆã‚’中断ã—ã¾ã™ã€‚ - Experimental Mode Experimentalモード - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilotã¯æ¨™æº–ã§ã¯ã‚†ã£ãりã¨ãã¤ã‚ã’ã‚‹é‹è»¢ã‚’æä¾›ã—ã¾ã™ã€‚ã“ã®Experimental(実験)モードを有効ã«ã™ã‚‹ã¨ã€ä»¥ä¸‹ã®ã‚¢ã‚°ãƒ¬ãƒƒã‚·ãƒ–ãªé–‹ç™ºä¸­ã®æ©Ÿèƒ½ã‚’利用ã™ã‚‹äº‹ãŒã§ãã¾ã™ã€‚ - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - sunnypilotã«ã‚¢ã‚¯ã‚»ãƒ«ã¨ãƒ–レーキを任ã›ã¾ã™ã€‚sunnypilotã¯èµ¤ä¿¡å·ã‚„ä¸€æ™‚åœæ­¢ã‚µã‚¤ãƒ³ã§ã®åœæ­¢ã‚’å«ã¿ã€äººé–“ã¨åŒã˜ã‚ˆã†ã«è€ƒãˆã¦é‹è»¢ã‚’行ã„ã¾ã™ã€‚sunnypilotãŒé‹è»¢é€Ÿåº¦ã‚’決定ã™ã‚‹ãŸã‚ã€ã‚ãªãŸãŒè¨­å®šã™ã‚‹é€Ÿåº¦ã¯ä¸Šé™é€Ÿåº¦ã«ãªã‚Šã¾ã™ã€‚ã“ã®æ©Ÿèƒ½ã¯å®Ÿé¨“段階ã®ãŸã‚ã€sunnypilotã®é‹è»¢ãƒŸã‚¹ã«å¸¸ã«å‚™ãˆã¦æ³¨æ„ã—ã¦ãã ã•ã„。 - New Driving Visualization æ–°ã—ã„é‹è»¢ç”»é¢ @@ -1985,16 +2220,8 @@ This may take up to a minute. End-to-Endアクセル制御 - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - sunnypilotã®ã‚¢ãƒ«ãƒ•ァ版アクセル制御ã¯ã€Experimentalモードã¨å…±ã«éžãƒªãƒªãƒ¼ã‚¹ã®ãƒ–ランãƒã§ãƒ†ã‚¹ãƒˆã™ã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - sunnypilotã®ã‚¢ã‚¯ã‚»ãƒ«åˆ¶å¾¡æ©Ÿèƒ½(アルファ)を有効ã«ã—ã¦ã€Experimentalモードを許å¯ã—ã¦ãã ã•ã„。 - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - æ¨™æº–ãƒ¢ãƒ¼ãƒ‰ãŒæŽ¨å¥¨ã•れã¾ã™ã€‚アグレッシブモードã§ã¯sunnypilotã¯å…ˆè¡Œè»Šã«è¿‘ã¥ã„ã¦è¿½å¾“ã—ã€ã‚¢ã‚¯ã‚»ãƒ«ã¨ãƒ–レーキãŒã‚ˆã‚Šå¼·æ°—ã«ãªã‚Šã¾ã™ã€‚リラックスモードã§ã¯sunnypilotã¯å…ˆè¡Œè»Šã‹ã‚‰è·é›¢ã‚’å–ã£ã¦èµ°è¡Œã—ã¾ã™ã€‚サãƒãƒ¼ãƒˆã•れã¦ã„る車両ã§ã¯ã‚¹ãƒ†ã‚¢ãƒªãƒ³ã‚°ãƒ›ã‚¤ãƒ¼ãƒ«ã®è·é›¢ãƒœã‚¿ãƒ³ã§ã“れらã®ãƒ¢ãƒ¼ãƒ‰ã‚’切り替ãˆã‚‹ã“ã¨ãŒã§ãã¾ã™ã€‚ + openpilot longitudinal control may come in a future update. + openpilotã®ã‚¢ã‚¯ã‚»ãƒ«åˆ¶å¾¡ã¯å°†æ¥ã®ã‚¢ãƒƒãƒ—デートã§åˆ©ç”¨ã§ãã‚‹å¯èƒ½æ€§ãŒã‚りã¾ã™ã€‚ The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. @@ -2005,8 +2232,24 @@ This may take up to a minute. é‹è»¢è€…ã®å¸¸æ™‚モニタリング - Enable driver monitoring even when sunnypilot is not engaged. - sunnypilotãŒä½œå‹•ã—ã¦ã„ãªã„å ´åˆã§ã‚‚é‹è»¢è€…モニタリングを有効ã«ã™ã‚‹ã€‚ + Changing this setting will restart openpilot if the car is powered on. + ã“ã®è¨­å®šã‚’変更ã™ã‚‹ã¨è»Šã®é›»æºãŒå…¥ã£ã¦ã„ã‚‹å ´åˆã¯openpilotãŒå†èµ·å‹•ã—ã¾ã™ã€‚ + + + Record and Upload Microphone Audio + マイク音声ã®éŒ²éŸ³ã¨ã‚¢ãƒƒãƒ—ロード + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + é‹è»¢ä¸­ã«ãƒžã‚¤ã‚¯éŸ³å£°ã‚’録音・ä¿å­˜ã—ã¾ã™ã€‚音声㯠comma connect ã®ãƒ‰ãƒ©ã‚¤ãƒ–レコーダー映åƒã«å«ã¾ã‚Œã¾ã™ã€‚ + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2017,55 +2260,86 @@ This may take up to a minute. - Enable sunnypilot - sunnypilot を有効化 - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Updater + TreeOptionDialog - Update Required - アップデートãŒå¿…è¦ã§ã™ + Select + é¸æŠž - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - OSã®ã‚¢ãƒƒãƒ—デートãŒå¿…è¦ã§ã™ã€‚Wi-Fiã«æŽ¥ç¶šã—ã¦ã‚¢ãƒƒãƒ—デートã™ã‚‹äº‹ã‚’ãŠå‹§ã‚ã—ã¾ã™ã€‚ダウンロードサイズã¯ç´„1GBã§ã™ã€‚ + Cancel + キャンセル + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Wi-Fiã«æŽ¥ç¶š + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - インストール + Changing this setting will restart openpilot if the car is powered on. + ã“ã®è¨­å®šã‚’変更ã™ã‚‹ã¨è»Šã®é›»æºãŒå…¥ã£ã¦ã„ã‚‹å ´åˆã¯openpilotãŒå†èµ·å‹•ã—ã¾ã™ã€‚ - Back - 戻る + Off + - Loading... - 読ã¿è¾¼ã¿ä¸­... + Distance + - Reboot - å†èµ·å‹• + Speed + - Update failed - 更新失敗 + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2075,12 +2349,12 @@ This may take up to a minute. é–‹ã - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehoseモード <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + openpilotã®é‹è»¢ãƒ¢ãƒ‡ãƒ«ã‚’改善ã™ã‚‹ãŸã‚ã«ã€å¤§å®¹é‡ã®å­¦ç¿’データをアップロードã—ã¦ä¸‹ã•ã„。 - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehoseモード <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_ko.ts b/selfdrive/ui/translations/main_ko.ts index 89f30ea545..756cad6045 100644 --- a/selfdrive/ui/translations/main_ko.ts +++ b/selfdrive/ui/translations/main_ko.ts @@ -7,10 +7,6 @@ Close 닫기 - - Snooze Update - ì—…ë°ì´íЏ ì¼ì‹œ 중지 - Reboot and Update ì—…ë°ì´íЏ ë° ìž¬ë¶€íŒ… @@ -36,7 +32,7 @@ Enter new tethering password - 새 í…Œë”ë§ ë¹„ë°€ë²ˆí˜¸ë¥¼ 입력하세요 + 새 í…Œë”ë§ ë¹„ë°€ë²ˆí˜¸ ìž…ë ¥ IP Address @@ -56,11 +52,11 @@ leave blank for automatic configuration - ìžë™ 설정하려면 빈 칸으로 ë‘세요 + ìžë™ 설정하려면 비워 ë‘세요 Cellular Metered - ë°ì´í„° 요금제 + 셀룰러 종량제 Hidden Network @@ -68,7 +64,7 @@ CONNECT - ì—°ê²°ë¨ + ì—°ê²° Enter SSID @@ -76,63 +72,83 @@ Enter password - 비밀번호를 입력하세요 + 비밀번호 ìž…ë ¥ for "%1" - "%1"ì— ì ‘ì†í•˜ë ¤ë©´ 비밀번호가 필요합니다 + "%1"ì˜ ë¹„ë°€ë²ˆí˜¸ë¥¼ 입력하세요 Prevent large data uploads when on a metered cellular connection - + ë°ì´í„° 사용량 ì œí•œì´ ìžˆëŠ” 셀룰러 ì—°ê²° 시 대용량 ë°ì´í„° 업로드 ë°©ì§€ default - + 기본 metered - + 종량제 unmetered - + 무제한 Wi-Fi Network Metered - + Wi-Fi ë„¤íŠ¸ì›Œí¬ ì¢…ëŸ‰ì œ Prevent large data uploads when on a metered Wi-Fi connection - + ë°ì´í„° 사용량 ì œí•œì´ ìžˆëŠ” Wi-Fi ì—°ê²° 시 대용량 ë°ì´í„° 업로드 ë°©ì§€ AutoLaneChangeTimer Auto Lane Change by Blinker - + ë°©í–¥ì§€ì‹œë“±ì„ ì´ìš©í•œ ìžë™ 차로 변경 Set a timer to delay the auto lane change operation when the blinker is used. No nudge on the steering wheel is required to auto lane change if a timer is set. Default is Nudge. Please use caution when using this feature. Only use the blinker when traffic and road conditions permit. - + ë°©í–¥ì§€ì‹œë“±ì„ ì‚¬ìš©í•  때 ìžë™ 차로 변경 ë™ìž‘ì„ ì§€ì—°í•˜ë„ë¡ íƒ€ì´ë¨¸ë¥¼ 설정하세요. 타ì´ë¨¸ê°€ 설정ë˜ì–´ 있으면 ìžë™ 차로 ë³€ê²½ì„ ìœ„í•´ 핸들 ì¡°ìž‘ì´ í•„ìš”í•˜ì§€ 않습니다. ê¸°ë³¸ê°’ì€ í•¸ë“¤ ì¡°ìž‘ 필요입니다. +ì´ ê¸°ëŠ¥ì„ ì‚¬ìš©í•  때ì—는 주ì˜í•˜ì„¸ìš”. êµí†µ ë° ë„로 ìƒí™©ì´ 허용할 때ì—ë§Œ ë°©í–¥ì§€ì‹œë“±ì„ ì‚¬ìš©í•˜ì„¸ìš”. s - + ì´ˆ Off - + 사용 안 함 Nudge - + 핸들 ì¡°ìž‘ í•„ìš” Nudgeless - + ì¡°ìž‘ í•„ìš” ì—†ìŒ + + + + Brightness + + Brightness + ë°ê¸° + + + Overrides the brightness of the device. + 기기 ë°ê¸°ë¥¼ ì§ì ‘ 설정합니다. + + + Auto (Dark) + ìžë™ (어둡게) + + + Auto + ìžë™ @@ -148,10 +164,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - sunnypilotì„ ì‚¬ìš©í•˜ë ¤ë©´ ì´ìš©ì•½ê´€ì— ë™ì˜í•´ì•¼ 합니다. - Back 뒤로 @@ -160,6 +172,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 ê±°ì ˆ, %1 제거 + + You must accept the Terms and Conditions in order to use sunnypilot. + sunnypilotì„ ì‚¬ìš©í•˜ë ¤ë©´ ì´ìš© ì•½ê´€ì— ë™ì˜í•´ì•¼ 합니다. + DeveloperPanel @@ -169,7 +185,15 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode - 롱컨 제어 모드 + ê°€ê°ì† 제어 모드 + + + openpilot Longitudinal Control (Alpha) + openpilot ê°€ê°ì† 제어 (알파) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 경고: ì´ ì°¨ëŸ‰ì—서 openpilot ê°€ê°ì† 제어는 알파 단계ì´ë©° ìžë™ 긴급 ì œë™(AEB)ì„ ë¹„í™œì„±í™”í•©ë‹ˆë‹¤. Enable ADB @@ -177,39 +201,58 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. - ADB (안드로ì´ë“œ 디버그 브릿지) USB ë˜ëŠ” 네트워í¬ë¥¼ 통해 ìž¥ì¹˜ì— ì—°ê²°í•  수 있습니다. ìžì„¸í•œ ë‚´ìš©ì€ https://docs.comma.ai/how-to/connect-to-comma를 참조하세요. - - - Enable GitHub runner service - - - - Enables or disables the github runner service. - - - - Error Log - - - - VIEW - 보기 - - - View the error log for sunnypilot crashes. - - - - openpilot Longitudinal Control (Alpha) - - - - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - + ADB(Android Debug Bridge)는 USB ë˜ëŠ” 네트워í¬ë¥¼ 통해 ê¸°ê¸°ì— ì—°ê²°í•  수 있게 í•´ì¤ë‹ˆë‹¤. ìžì„¸í•œ ë‚´ìš©ì€ https://docs.comma.ai/how-to/connect-to-comma 를 참고하세요. On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. - + ì´ ì°¨ëŸ‰ì—서는 openpilotì˜ ê°€ê°ì† 제어 대신 sunnypilotì´ ì°¨ëŸ‰ì˜ ë‚´ìž¥ ACC를 기본값으로 사용합니다. +ì´ë¥¼ 활성화하면 openpilot ê°€ê°ì† 제어로 전환합니다. +openpilot ê°€ê°ì† 제어(알파)를 활성화할 때는 ì‹¤í—˜ì  ëª¨ë“œ 활성화를 권장합니다. + + + + DeveloperPanelSP + + Show Advanced Controls + 고급 설정 표시 + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + sunnypilot 고급 ì„¤ì •ì˜ í‘œì‹œ 여부를 전환합니다. +ì´ ì„¤ì •ì€ ì„¤ì •ì˜ í‘œì‹œ 여부만 전환하며, 실제 ê¸°ëŠ¥ì˜ ì¼œì§/êº¼ì§ ìƒíƒœëŠ” 바꾸지 않습니다. + + + Enable GitHub runner service + GitHub runner 서비스 사용 + + + Enables or disables the github runner service. + GitHub Runner 서비스를 켜거나 ë•니다. + + + Enable Quickboot Mode + 빠른 부팅 모드 사용 + + + Error Log + 오류 로그 + + + VIEW + 보기 + + + View the error log for sunnypilot crashes. + sunnypilot ì¶©ëŒì— 대한 오류 로그를 확ì¸í•˜ì„¸ìš”. + + + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> + í† ê¸€ì„ ì¼œë©´ 부팅 ì‹œê°„ì„ ë‹¨ì¶•í•˜ê¸° 위해 미리 ë¹Œë“œëœ íŒŒì¼ì„ ìƒì„±í•©ë‹ˆë‹¤. í† ê¸€ì„ ë„ë©´ 로컬ì—서 편집한 cpp 파ì¼ì„ 컴파ì¼í•  수 있ë„ë¡ í•´ë‹¹ 미리 ë¹Œë“œëœ íŒŒì¼ì„ 즉시 제거합니다. <br><br><b>기기ì—서 C++ 파ì¼ì„ 로컬로 편집하려면, 변경 ì‚¬í•­ì´ ë‹¤ì‹œ 컴파ì¼ë  수 있ë„ë¡ ë°˜ë“œì‹œ ì´ í† ê¸€ì„ ë¨¼ì € 꺼야 합니다.</b> + + + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. + 빠른 부팅 모드는 ì—…ë°ì´íŠ¸ê°€ 비활성화ë˜ì–´ 있어야 합니다.<br>먼저 소프트웨어 패ë„ì—서 'ì—…ë°ì´íЏ 비활성화'를 활성화하세요. @@ -224,7 +267,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Serial - 시리얼 + ì¼ë ¨ë²ˆí˜¸ Driver Camera @@ -236,7 +279,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Preview the driver facing camera to ensure that driver monitoring has good visibility. (vehicle must be off) - ìš´ì „ìž ëª¨ë‹ˆí„°ë§ì´ 잘 ë˜ëŠ”ì§€ 확ì¸í•˜ê¸° 위해 후면 ì¹´ë©”ë¼ë¥¼ 미리 봅니다. (차량 시ë™ì´ 꺼져 있어야 합니다) + ìš´ì „ìž ëª¨ë‹ˆí„°ë§ì´ 잘 ë³´ì´ëŠ”ì§€ 확ì¸í•˜ê¸° 위해 ìš´ì „ìž ë°©í–¥ ì¹´ë©”ë¼ë¥¼ 미리 확ì¸í•˜ì„¸ìš”. (ì°¨ëŸ‰ì€ ì‹œë™ì´ 꺼져 있어야 합니다) Reset Calibration @@ -258,10 +301,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 다시보기 - - Review the rules, features, and limitations of sunnypilot - sunnypilotì˜ ê·œì¹™, 기능 ë° ì œí•œ 다시 í™•ì¸ - Are you sure you want to review the training guide? 트레ì´ë‹ ê°€ì´ë“œë¥¼ 다시 확ì¸í•˜ì‹œê² ìŠµë‹ˆê¹Œ? @@ -294,13 +333,9 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off ì „ì› ë„기 - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot 장치는 좌우 4°, 위로 5°, 아래로 9° ì´ë‚´ ê°ë„로 장착ë˜ì–´ì•¼ 합니다. sunnypilotì€ ì§€ì†ì ìœ¼ë¡œ ìžë™ ë³´ì •ë˜ë©° ìž¬ì„¤ì •ì€ ê±°ì˜ í•„ìš”í•˜ì§€ 않습니다. - Your device is pointed %1° %2 and %3° %4. - 사용ìžì˜ 장치는 %2 %1° ë° %4 %3° ì˜ ë°©í–¥ìœ¼ë¡œ 장착ë˜ì–´ 있습니다. + 사용ìžì˜ 기기는 %2 %1° ë° %4 %3° ì˜ ë°©í–¥ìœ¼ë¡œ 장착ë˜ì–´ 있습니다. down @@ -344,137 +379,195 @@ Please use caution when using this feature. Only use the blinker when traffic an Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 장치를 comma connect (connect.comma.ai)ì—서 ë™ê¸°í™”하고 comma prime 무료 ì´ìš©ê¶Œì„ 사용하세요. + 기기를 comma connect (connect.comma.ai)와 페어ë§í•˜ê³  comma prime 혜íƒì„ 받으세요. Pair Device - 장치 ë™ê¸°í™” + 기기 íŽ˜ì–´ë§ PAIR - ë™ê¸°í™” + íŽ˜ì–´ë§ Disengage to Reset Calibration - + 캘리브레ì´ì…˜ì„ 초기화하려면 해제하세요 - Resetting calibration will restart openpilot if the car is powered on. - + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilotì€ ì§€ì†ì ìœ¼ë¡œ 캘리브레ì´ì…˜ì„ 수행하며, 캘리브레ì´ì…˜ 초기화가 필요한 경우는 드뭅니다. 차량 ì „ì›ì´ 켜져 있는 ìƒíƒœì—서 캘리브레ì´ì…˜ì„ 초기화하면 openpilotì´ ìž¬ì‹œìž‘ë©ë‹ˆë‹¤. + + + + +Steering lag calibration is %1% complete. + + +ì¡°í–¥ 지연 캘리브레ì´ì…˜ì´ %1% 완료ë˜ì—ˆìŠµë‹ˆë‹¤. + + + + +Steering lag calibration is complete. + + +ì¡°í–¥ 지연 캘리브레ì´ì…˜ì´ 완료ë˜ì—ˆìŠµë‹ˆë‹¤. + + + Steering torque response calibration is %1% complete. + ì¡°í–¥ í† í¬ ì‘답 캘리브레ì´ì…˜ì´ %1% 완료ë˜ì—ˆìŠµë‹ˆë‹¤. + + + Steering torque response calibration is complete. + ì¡°í–¥ í† í¬ ì‘답 캘리브레ì´ì…˜ì´ 완료ë˜ì—ˆìŠµë‹ˆë‹¤. + + + Review the rules, features, and limitations of sunnypilot + sunnypilotì˜ ê·œì¹™, 기능 ë° ì œí•œ ì‚¬í•­ì„ ê²€í† í•˜ì„¸ìš”. + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. + sunnypilotì€ ê¸°ê¸°ë¥¼ 좌우 4° ì´ë‚´, 위로는 5° ì´ë‚´, 아래로는 9° ì´ë‚´ë¡œ 장착해야 합니다. DevicePanelSP + + Quiet Mode + 정숙 모드 + Driver Camera Preview - + ìš´ì „ìž ì¹´ë©”ë¼ ë¯¸ë¦¬ë³´ê¸° Training Guide - + êµìœ¡ ê°€ì´ë“œ Regulatory - 규제 + 규제 Language - - - - Are you sure you want to review the training guide? - 트레ì´ë‹ ê°€ì´ë“œë¥¼ 다시 확ì¸í•˜ì‹œê² ìŠµë‹ˆê¹Œ? - - - Review - 다시보기 - - - Select a language - 언어를 ì„ íƒí•˜ì„¸ìš” - - - Reboot - 재부팅 - - - Power Off - ì „ì› ë„기 - - - Offroad Mode - - - - Are you sure you want to exit Always Offroad mode? - - - - Confirm - í™•ì¸ - - - Are you sure you want to enter Always Offroad mode? - - - - Disengage to Enter Always Offroad Mode - - - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - + 언어 Reset Settings - + 설정 초기화 + + + Are you sure you want to review the training guide? + êµìœ¡ ê°€ì´ë“œë¥¼ 검토하시겠습니까? + + + Review + 검토 + + + Select a language + 언어를 ì„ íƒí•˜ì„¸ìš” + + + Wake-Up Behavior + 깨우기 ë™ìž‘ + + + Interactivity Timeout + ìƒí˜¸ìž‘ìš© 타임아웃 + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + 설정 í™”ë©´ì— ì‚¬ìš©ìž ì§€ì • íƒ€ìž„ì•„ì›ƒì„ ì ìš©í•˜ì„¸ìš”. +사용ìžê°€ 화면과 ìƒí˜¸ìž‘용하지 않으면 설정 í™”ë©´ì´ ìžë™ìœ¼ë¡œ 닫히는 시간입니다. + + + Reboot + 재부팅 + + + Power Off + ì „ì› ë„기 + + + Offroad Mode + 오프로드 모드 + + + Are you sure you want to exit Always Offroad mode? + í•­ìƒ ì˜¤í”„ë¡œë“œ 모드를 종료하시겠습니까? + + + Confirm + í™•ì¸ + + + Are you sure you want to enter Always Offroad mode? + í•­ìƒ ì˜¤í”„ë¡œë“œ 모드로 진입하시겠습니까? + + + Disengage to Enter Always Offroad Mode + í•­ìƒ ì˜¤í”„ë¡œë“œ 모드로 진입하려면 해제하세요 Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. - + 모든 Sunnypilot ì„¤ì •ì„ ê¸°ë³¸ê°’ìœ¼ë¡œ 재설정하시겠습니까? ì„¤ì •ì„ ìž¬ì„¤ì •í•˜ë©´ ë˜ëŒë¦´ 수 없습니다. Reset - 초기화 + 재설정 The reset cannot be undone. You have been warned. - + ìž¬ì„¤ì •ì€ ë˜ëŒë¦´ 수 없습니다. 경고 드립니다. + + + Exit Always Offroad + í•­ìƒ ì˜¤í”„ë¡œë“œ 종료 + + + Always Offroad + í•­ìƒ ì˜¤í”„ë¡œë“œ + + + â Default: Device will boot/wake-up normally & will be ready to engage. + â 기본값: 기기가 ì •ìƒì ìœ¼ë¡œ 부팅/절전 모드ì—서 í•´ì œë˜ë©°, ìž‘ë™ ì¤€ë¹„ê°€ 완료ë©ë‹ˆë‹¤. + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + â 오프로드: 부팅/절전 í•´ì œ 후 기기가 í•­ìƒ ì˜¤í”„ë¡œë“œ 모드로 유지ë©ë‹ˆë‹¤. + + + Controls state of the device after boot/sleep. + 부팅/절전 모드 후 ê¸°ê¸°ì˜ ìƒíƒœë¥¼ 제어합니다. DriveStats Drives - + 주행 Hours - + 시간 ALL TIME - + ì „ì²´ 기간 PAST WEEK - + 지난주 KM - + KM Miles - + mi @@ -484,50 +577,67 @@ Please use caution when using this feature. Only use the blinker when traffic an ì¹´ë©”ë¼ ì‹œìž‘ 중 + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + í•­ìƒ ì˜¤í”„ë¡œë“œ 모드를 종료하시겠습니까? + + + Confirm + í™•ì¸ + + + EXIT ALWAYS OFFROAD MODE + í•­ìƒ ì˜¤í”„ë¡œë“œ 모드 종료 + + ExperimentalModeButton EXPERIMENTAL MODE ON - 실험 모드 사용 + 실험 모드 ì¼œì§ CHILL MODE ON - 안정 모드 사용 + 안정 모드 ì¼œì§ FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 파ì´ì–´í˜¸ìФ 모드 🔥 - Firehose Mode: ACTIVE - 파ì´ì–´í˜¸ìФ 모드: 활성화 + Firehose 모드: 활성 ACTIVE - 활성화 - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - ìµœëŒ€í•œì˜ íš¨ê³¼ë¥¼ 얻으려면 매주 장치를 실내로 가져와 ì¢‹ì€ USB-C ì¼€ì´ë¸”ì— ì—°ê²°í•˜ê³  Wi-Fiì— ì—°ê²°í•˜ì„¸ìš”.<br><br>파ì´ì–´í˜¸ìФ 모드는 핫스팟 ë˜ëŠ” 무제한 SIM ì¹´ë“œì— ì—°ê²°ëœ ê²½ìš°ì—는 ìš´ì „ 중ì—ë„ ìž‘ë™í•  수 있습니다.<br><br><br><b>ìžì£¼ 묻는 질문</b><br><br><i>ìš´ì „ 방법ì´ë‚˜ 장소가 중요한가요?</i> 아니요, í‰ì†Œì²˜ëŸ¼ 운전하시면 ë©ë‹ˆë‹¤.<br><br><i>파ì´ì–´í˜¸ìФ 모드ì—서 ì œ 모든 êµ¬ê°„ì„ ê°€ì ¸ì˜¤ë‚˜ìš”?.<br><br><i> 아니요, ì €í¬ëŠ” ì—¬ëŸ¬ë¶„ì˜ êµ¬ê°„ 중 ì¼ë¶€ë¥¼ 선별ì ìœ¼ë¡œ 가져옵니다.<br><br><i>ì¢‹ì€ USB-C ì¼€ì´ë¸”ì€ ë¬´ì—‡ì¸ê°€ìš”?</i> 휴대í°ì´ë‚˜ ë…¸íŠ¸ë¶ ê³ ì† ì¶©ì „ê¸°ê°€ 있으면 ë©ë‹ˆë‹¤.<br><br><i>ì–´ë–¤ 소프트웨어를 실행하는지가 중요한가요?</i> 예, ì˜¤ì§ ê³µì‹ sunnypilotì˜ íŠ¹ì • í¬í¬ë§Œ 트레ì´ë‹ì— 사용할 수 있습니다. + 활성 <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + <b>%nê°œ 구간</b> ì˜ ì£¼í–‰ì´ í˜„ìž¬ê¹Œì§€ 학습 ë°ì´í„°ì…‹ì— í¬í•¨ë˜ì–´ 있습니다. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>비활성</span>: 무제한 네트워í¬ì— ì—°ê²° 하세요 + + + Firehose Mode + Firehose 모드 + sunnypilot learns to drive by watching humans, like you, drive. Firehose Mode allows you to maximize your training data uploads to improve openpilot's driving models. More data means bigger models, which means better Experimental Mode. - + sunnypilotì€ ë‹¹ì‹ ê³¼ ê°™ì€ ì‚¬ëžŒë“¤ì˜ ìš´ì „ì„ ë³´ë©´ì„œ 스스로 ìš´ì „ì„ ë°°ì›ë‹ˆë‹¤. + +Firehose 모드를 사용하면 훈련 ë°ì´í„° 업로드를 극대화하여 openpilotì˜ ìš´ì „ 모ë¸ì„ 개선할 수 있습니다. ë” ë§Žì€ ë°ì´í„°ëŠ” ë” í° ëª¨ë¸ì„ ì˜ë¯¸í•˜ë©°, ì´ëŠ” ë” ë‚˜ì€ ì‹¤í—˜ 모드를 ì˜ë¯¸í•©ë‹ˆë‹¤. - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + 최대 효과를 위해 기기를 실내로 가져와 ì¢‹ì€ USB-C 어댑터와 Wi-Fiì— ë§¤ì£¼ 연결하세요.<br><br>핫스팟ì´ë‚˜ 무제한 SIM ì¹´ë“œì— ì—°ê²°ë˜ì–´ 있다면 ìš´ì „ 중ì—ë„ Firehose 모드를 사용할 수 있습니다.<br><br><br><b>ìžì£¼ 묻는 질문</b><br><br><i>ìš´ì „ ë°©ì‹ì´ë‚˜ 장소가 중요한가요?</i> 아니요, í‰ì†Œì²˜ëŸ¼ 운전하시면 ë©ë‹ˆë‹¤.<br><br><i>모든 세그먼트가 Firehose 모드로 가져와지나요?</i> 아니요, ì„¸ê·¸ë¨¼íŠ¸ì˜ ì¼ë¶€ë§Œ ì„ íƒì ìœ¼ë¡œ 가져옵니다.<br><br><i>ì¢‹ì€ USB-C 어댑터는 무엇ì¸ê°€ìš”?</i> 모든 ê³ ì† íœ´ëŒ€í° ë˜ëŠ” ë…¸íŠ¸ë¶ ì¶©ì „ê¸°ëŠ” 괜찮습니다.<br><br><i>ì–´ë–¤ 소프트웨어를 실행하는지가 중요한가요?</i> 예, 업스트림 sunnypilot(ë° íŠ¹ì • í¬í¬)ë§Œ í›ˆë ¨ì— ì‚¬ìš©í•  수 있습니다. @@ -542,50 +652,50 @@ Firehose Mode allows you to maximize your training data uploads to improve openp MAX - MAX + 최대 HyundaiSettings Off - + 사용 안 함 Dynamic - + ë™ì  Predictive - + 예측 Custom Longitudinal Tuning - + ì‚¬ìš©ìž ì§€ì • ê°€ê°ì† íŠœë‹ This feature can only be used with openpilot longitudinal control enabled. - + ì´ ê¸°ëŠ¥ì€ openpilot ê°€ê°ì† 제어가 í™œì„±í™”ëœ ê²½ìš°ì—ë§Œ 사용할 수 있습니다. Enable "Always Offroad" in Device panel, or turn vehicle off to select an option. - + ì˜µì…˜ì„ ì„ íƒí•˜ë ¤ë©´ 기기 패ë„ì—서 "í•­ìƒ ì˜¤í”„ë¡œë“œ"를 활성화하거나, ì°¨ëŸ‰ì˜ ì‹œë™ì„ ë„세요. Off: Uses default tuning - + 사용 안 함: 기본 튜ë‹ì„ 사용합니다 Dynamic: Adjusts acceleration limits based on current speed - + ë™ì : 현재 ì†ë„ì— ë”°ë¼ ê°€ì† ì œí•œì„ ì¡°ì •í•©ë‹ˆë‹¤. Predictive: Uses future trajectory data to anticipate needed adjustments - + 예측: 향후 주행 ê¶¤ì  ë°ì´í„°ë¥¼ 활용해 필요한 ì¡°ì •ì„ ë¯¸ë¦¬ 파악합니다. Fine-tune your driving experience by adjusting acceleration smoothness with openpilot longitudinal control. - + openpilot ê°€ê°ì† ì œì–´ì˜ ê°€ì† ë¶€ë“œëŸ¬ì›€ì„ ì¡°ì •í•˜ì—¬ 주행 ê²½í—˜ì„ ì„¸ë°€í•˜ê²Œ 조정하세요. @@ -605,259 +715,340 @@ Firehose Mode allows you to maximize your training data uploads to improve openp LaneChangeSettings Back - 뒤로 + 뒤로 Auto Lane Change: Delay with Blind Spot - + ìžë™ 차로 변경: 사ê°ì§€ëŒ€ ê°ì§€ 시 지연 Toggle to enable a delay timer for seamless lane changes when blind spot monitoring (BSM) detects a obstructing vehicle, ensuring safe maneuvering. - + í† ê¸€ì„ ì¼œë©´ 사ê°ì§€ëŒ€ 모니터ë§(BSM)ì´ ê°€ë¡œë§‰ëŠ” ì°¨ëŸ‰ì„ ê°ì§€í•  때 차로 ë³€ê²½ì„ ì•ˆì „í•˜ê²Œ 수행할 수 있ë„ë¡ ì§€ì—° 타ì´ë¨¸ë¥¼ 활성화합니다. LateralPanel Modular Assistive Driving System (MADS) - + 모듈형 ë³´ì¡° 주행 시스템 (MADS) Enable the beloved MADS feature. Disable toggle to revert back to stock sunnypilot engagement/disengagement. - + ì¸ê¸° 있는 MADS ê¸°ëŠ¥ì„ í™œì„±í™”í•˜ì„¸ìš”. í† ê¸€ì„ ë„ë©´ 기본 sunnypilot ìž‘ë™/í•´ì œ ë°©ì‹ìœ¼ë¡œ ë˜ëŒì•„갑니다. Customize MADS - + MADS ì‚¬ìš©ìž ì§€ì • Customize Lane Change - + 차로 변경 ì‚¬ìš©ìž ì§€ì • + + + Pause Lateral Control with Blinker + 방향지시등으로 ì¡°í–¥ 제어 ì¼ì‹œ 중지 + + + Pause lateral control with blinker when traveling below the desired speed selected. + 설정한 ì†ë„보다 ë‚®ì€ ì†ë„로 주행 ì¤‘ì¼ ë•Œ 방향지시등으로 ì¡°í–¥ 제어를 ì¼ì‹œ 중지합니다. Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). - + 차로 중앙 유지(ALC) ë° ì–´ëŒ‘í‹°ë¸Œ í¬ë£¨ì¦ˆ 컨트롤(ACC) ê¸°ëŠ¥ì„ ë…립ì ìœ¼ë¡œ 사용할 수 있습니다. Start the vehicle to check vehicle compatibility. - + 차량 í˜¸í™˜ì„±ì„ í™•ì¸í•˜ë ¤ë©´ 차량 시ë™ì„ 걸어주세요. This platform supports all MADS settings. - + ì´ í”Œëž«í¼ì€ 모든 MADS ì„¤ì •ì„ ì§€ì›í•©ë‹ˆë‹¤. This platform supports limited MADS settings. - + ì´ í”Œëž«í¼ì€ ì œí•œëœ MADS ì„¤ì •ì„ ì§€ì›í•©ë‹ˆë‹¤. + + + + LongitudinalPanel + + Custom ACC Speed Increments + ì‚¬ìš©ìž ì§€ì • ACC ì†ë„ ì¡°ì ˆ 단위 + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + í¬ë£¨ì¦ˆ ì†ë„ ì¦ê°€/ê°ì†Œ 시 짧게 누르기 ë° ê¸¸ê²Œ 누르기 단위를 개별 설정합니다. + + + This feature can only be used with openpilot longitudinal control enabled. + ì´ ê¸°ëŠ¥ì€ openpilotì˜ ê°€ê°ì† 제어가 활성화ë˜ì–´ 있어야만 사용할 수 있습니다. + + + This feature is not supported on this platform due to vehicle limitations. + ì´ ê¸°ëŠ¥ì€ ì°¨ëŸ‰ 제한으로 ì¸í•´ ì´ í”Œëž«í¼ì—서 ì§€ì›ë˜ì§€ 않습니다. + + + Start the vehicle to check vehicle compatibility. + 차량 í˜¸í™˜ì„±ì„ í™•ì¸í•˜ë ¤ë©´ 차량 시ë™ì„ 걸어주세요. MadsSettings Toggle with Main Cruise - - - - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. - + ë©”ì¸ í¬ë£¨ì¦ˆì™€ ì—°ë™ Unified Engagement Mode (UEM) - - - - Engage lateral and longitudinal control with cruise control engagement. - - - - Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - - - Remain Active - - - - Disengage - + 통합 주행 ë³´ì¡° 모드 (UEM) Steering Mode on Brake Pedal - + 브레ì´í¬ ì¡°ìž‘ 시 ì¡°í–¥ 모드 + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + 참고: LFA/LKAS ë²„íŠ¼ì´ ì—†ëŠ” ì°¨ëŸ‰ì˜ ê²½ìš°, ì´ ê¸°ëŠ¥ì„ ë¹„í™œì„±í™”í•˜ë©´ ì¡°í–¥ 제어가 ìž‘ë™í•˜ì§€ 않습니다. + + + Engage lateral and longitudinal control with cruise control engagement. + í¬ë£¨ì¦ˆ ì»¨íŠ¸ë¡¤ì„ ì¼œë©´ ê°€ê°ì†ê³¼ ì¡°í–¥ì„ ë™ì‹œì— 시작합니다. + + + Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. + 참고: 통합 주행 ë³´ì¡° 모드(UEM)로 ì¡°í–¥ 제어가 ìž‘ë™í•˜ë©´, MADS 버튼으로 ì§ì ‘ ë„거나 차량 시ë™ì„ ë„기 전까지 ê³„ì† ìœ ì§€ë©ë‹ˆë‹¤. Start the vehicle to check vehicle compatibility. - + 차량 í˜¸í™˜ì„±ì„ í™•ì¸í•˜ë ¤ë©´ 차량 시ë™ì„ 걸어주세요. This feature defaults to OFF, and does not allow selection due to vehicle limitations. - + ì´ ê¸°ëŠ¥ì€ ì°¨ëŸ‰ì˜ í•œê³„ë¡œ ì¸í•´ 기본ì ìœ¼ë¡œ 비활성화ë˜ì–´ 있으며, ì„ íƒí•  수 없습니다. This feature defaults to ON, and does not allow selection due to vehicle limitations. - + ì´ ê¸°ëŠ¥ì€ ì°¨ëŸ‰ì˜ í•œê³„ë¡œ ì¸í•´ 기본ì ìœ¼ë¡œ 활성화ë˜ì–´ 있으며, ì„ íƒí•  수 없습니다. This platform only supports Disengage mode due to vehicle limitations. - + ì´ í”Œëž«í¼ì€ ì°¨ëŸ‰ì˜ í•œê³„ë¡œ ì¸í•´ 'í•´ì œ 모드'ë§Œ ì§€ì›í•©ë‹ˆë‹¤. + + + Remain Active + ê³„ì† í™œì„± 유지 Remain Active: ALC will remain active when the brake pedal is pressed. - + ê³„ì† í™œì„± 유지: 브레ì´í¬ íŽ˜ë‹¬ì„ ë°Ÿì•„ë„ ì°¨ë¡œ 유지 ë³´ì¡°(ALC) ê¸°ëŠ¥ì´ ê³„ì† ìž‘ë™í•©ë‹ˆë‹¤. Pause - + ì¼ì‹œ ì •ì§€ Pause: ALC will pause when the brake pedal is pressed. - + ì¼ì‹œ ì •ì§€: 브레ì´í¬ íŽ˜ë‹¬ì„ ë°Ÿìœ¼ë©´ 차로 유지 ë³´ì¡°(ALC) ê¸°ëŠ¥ì´ ì¼ì‹œ ì •ì§€ë©ë‹ˆë‹¤. + + + Disengage + í•´ì œ Disengage: ALC will disengage when the brake pedal is pressed. - + í•´ì œ: 브레ì´í¬ íŽ˜ë‹¬ì„ ë°Ÿìœ¼ë©´ 차로 유지 ë³´ì¡°(ALC) ê¸°ëŠ¥ì´ í•´ì œë©ë‹ˆë‹¤. Choose how Automatic Lane Centering (ALC) behaves after the brake pedal is manually pressed in sunnypilot. - + 브레ì´í¬ íŽ˜ë‹¬ì„ ì§ì ‘ ë°Ÿì•˜ì„ ë•Œ ìžë™ 차로 중앙 유지(ALC) ê¸°ëŠ¥ì´ ì–´ë–»ê²Œ ìž‘ë™í• ì§€ ì„ íƒí•˜ì„¸ìš”. MaxTimeOffroad Max Time Offroad - + 최대 오프로드 시간 Device will automatically shutdown after set time once the engine is turned off.<br/>(30h is the default) - + 차량 시ë™ì´ 꺼지면 ì„¤ì •ëœ ì‹œê°„ì´ ì§€ë‚œ 후 기기가 ìžë™ìœ¼ë¡œ 꺼집니다.<br/>(ê¸°ë³¸ê°’ì€ 30시간입니다) Always On - + ì¼œì§ ìœ ì§€ h - + 시간 m - + ë¶„ (default) - + (기본값) ModelsPanel Current Model - + 현재 ëª¨ë¸ SELECT - ì„ íƒ + ì„ íƒ - No custom model selected! - + Clear Model Cache + ëª¨ë¸ ìºì‹œ 지우기 - Driving - + CLEAR + 지우기 - Navigation - + Driving Model + 주행 ëª¨ë¸ - Vision - + Navigation Model + 내비게ì´ì…˜ ëª¨ë¸ - Policy - + Vision Model + 비전 ëª¨ë¸ - Downloading %1 model [%2]... (%3%) - + Policy Model + ì •ì±… ëª¨ë¸ - %1 model [%2] %3 - + Live Learning Steer Delay + ì¡°í–¥ 지연 실시간 학습 + + + Adjust Software Delay + 소프트웨어 지연 ì¡°ì ˆ + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + ì¡°í–¥ 지연 실시간 학습 ê¸°ëŠ¥ì´ êº¼ì ¸ ìžˆì„ ë•Œ, 소프트웨어 ì§€ì—°ì„ ì¡°ì ˆí•©ë‹ˆë‹¤. +ê¸°ë³¸ê°’ì€ 0.2입니다. + + + %1 - %2 + %1 - %2 downloaded - + 다운로드 완료 ready - + 준비 완료 from cache - + ìºì‹œì—서 가져옴 - %1 model [%2] download failed - + download failed - %1 + 다운로드 실패 - %1 - %1 model [%2] pending... - + pending - %1 + 대기 중 - %1 Fetching models... - - - - Use Default - + ëª¨ë¸ ê°€ì ¸ì˜¤ëŠ” 중... Select a Model - + ëª¨ë¸ ì„ íƒ Default - + 기본값 Model download has started in the background. - + ëª¨ë¸ ë‹¤ìš´ë¡œë“œê°€ 백그ë¼ìš´ë“œì—서 시작ë˜ì—ˆìŠµë‹ˆë‹¤. We STRONGLY suggest you to reset calibration. - + 캘리브레ì´ì…˜ì„ 초기화하는 ê²ƒì„ ê°•ë ¥ížˆ 권장합니다. Would you like to do that now? - + 지금 바로 하시겠습니까? Reset Calibration - 캘리브레ì´ì…˜ + 캘리브레ì´ì…˜ 초기화 Driving Model Selector - + 주행 ëª¨ë¸ ì„ íƒê¸° + + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + <u>현재 사용 ì¤‘ì¸ ëª¨ë¸ì„ 제외한</u><br/>모든 ë‹¤ìš´ë¡œë“œëœ ëª¨ë¸ì´ ìºì‹œì—서 ì‚­ì œë©ë‹ˆë‹¤.<br/><br/>ê³„ì† ì§„í–‰í•˜ì‹œê² ìŠµë‹ˆê¹Œ? + + + Clear Cache + ìºì‹œ 지우기 Warning: You are on a metered connection! - + 경고: ë°ì´í„° 사용량 ì œí•œì´ ìžˆëŠ” 네트워í¬ì— ì—°ê²°ë˜ì–´ 있습니다! Continue - ê³„ì† + ê³„ì† on Metered - + ë°ì´í„° 사용량 제한 ë„¤íŠ¸ì›Œí¬ ì‚¬ìš© Cancel - 취소 + 취소 + + + Refresh Model List + + + + REFRESH + 새로 고침 + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + @@ -894,82 +1085,74 @@ Firehose Mode allows you to maximize your training data uploads to improve openp NetworkingSP Scan - + 검색 Scanning... - + 검색 중... NeuralNetworkLateralControl Neural Network Lateral Control (NNLC) - + ì‹ ê²½ë§ ê¸°ë°˜ ì¡°í–¥ 제어 (NNLC) NNLC is currently not available on this platform. - + ì´ í”Œëž«í¼ì—서는 현재 NNLC를 사용할 수 없습니다. Start the car to check car compatibility - + 차량 í˜¸í™˜ì„±ì„ í™•ì¸í•˜ë ¤ë©´ 차량 시ë™ì„ 걸어주세요. NNLC Not Loaded - + NNLCê°€ 불러와지지 ì•ŠìŒ NNLC Loaded - + NNLCê°€ ë¶ˆëŸ¬ì™€ì§ Match - + ì¼ì¹˜ Exact - + 정확 Fuzzy - + 유사 Match: "Exact" is ideal, but "Fuzzy" is fine too. - + ì¼ì¹˜: "정확"ì´ ê°€ìž¥ 좋지만, "유사"ë„ ê´œì°®ìŠµë‹ˆë‹¤. Formerly known as <b>"NNFF"</b>, this replaces the lateral <b>"torque"</b> controller, with one using a neural network trained on each car's (actually, each separate EPS firmware) driving data for increased controls accuracy. - + 과거 <b>"NNFF"</b>ë¼ê³  ë¶ˆë ¸ë˜ ì´ ê¸°ëŠ¥ì€ ê¸°ì¡´ì˜ ì¡°í–¥ <b>"토í¬"</b> 제어기를 대체합니다. ì´ëŠ” ê° ì°¨ëŸ‰ì˜ ì£¼í–‰ ë°ì´í„°(ë” ì •í™•ížˆëŠ”, ê°ê¸° 다른 EPS 펌웨어 ë°ì´í„°)를 기반으로 í›ˆë ¨ëœ ì‹ ê²½ë§ì„ 사용하여 제어 정확ë„를 높ì´ê¸° 위함입니다. Reach out to the sunnypilot team in the following channel at the sunnypilot Discord server - + sunnypilot 팀ì—게 ì—°ë½í•˜ì‹œë ¤ë©´, sunnypilot Discord 서버 ë‚´ì˜ ë‹¤ìŒ ì±„ë„ì„ ì´ìš©í•´ 주세요. with feedback, or to provide log data for your car if your car is currently unsupported: - + í”¼ë“œë°±ì„ ë³´ë‚´ê±°ë‚˜, 현재 ì§€ì›ë˜ì§€ 않는 ì°¨ëŸ‰ì˜ ë¡œê·¸ ë°ì´í„°ë¥¼ 제공하고 ì‹¶ì„ ë•Œ: if there are any issues: - + 문제가 ë°œìƒí•˜ë©´: and donate logs to get NNLC loaded for your car: - + 그리고 ì°¨ëŸ‰ì— NNLCê°€ í™œì„±í™”ë  ìˆ˜ 있ë„ë¡ ë¡œê·¸ë¥¼ 제공하려면: OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - 즉시 ì¸í„°ë„·ì— 연결하여 ì—…ë°ì´íŠ¸ë¥¼ 확ì¸í•˜ì„¸ìš”. ì¸í„°ë„·ì— ì—°ê²°ë˜ì–´ 있지 않으면 %1 ì´í›„ì—는 sunnypilotì´ í™œì„±í™”ë˜ì§€ 않습니다. - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - ì—…ë°ì´íŠ¸ë¥¼ 확ì¸í•˜ë ¤ë©´ ì¸í„°ë„·ì— 연결하세요. sunnypilotì€ ì—…ë°ì´íŠ¸ë¥¼ 확ì¸í•˜ê¸° 위해 ì¸í„°ë„·ì— ì—°ê²°í•  때까지 ìžë™ìœ¼ë¡œ 시작ë˜ì§€ 않습니다. - Unable to download updates %1 @@ -978,39 +1161,59 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Taking camera snapshots. System won't start until finished. - ì¹´ë©”ë¼ ìŠ¤ëƒ…ìƒ· ì°ê¸°ê°€ ì™„ë£Œë  ë•Œê¹Œì§€ ì‹œìŠ¤í…œì´ ì‹œìž‘ë˜ì§€ 않습니다. + ì¹´ë©”ë¼ ìŠ¤ëƒ…ìƒ·ì„ ì´¬ì˜í•˜ëŠ” 중입니다. ì™„ë£Œë  ë•Œê¹Œì§€ ì‹œìŠ¤í…œì´ ì‹œìž‘ë˜ì§€ 않습니다. 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. - 백그ë¼ìš´ë“œì—서 ìš´ì˜ ì²´ì œì— ëŒ€í•œ ì—…ë°ì´íŠ¸ê°€ 다운로드ë˜ê³  있습니다. 설치가 준비ë˜ë©´ ì—…ë°ì´íЏ 메시지가 표시ë©ë‹ˆë‹¤. - - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - 장치를 등ë¡í•˜ì§€ 못했습니다. comma.ai ì„œë²„ì— ì—°ê²°í•˜ê±°ë‚˜ ë°ì´í„°ë¥¼ 업로드하지 않으며 comma.aiì—서 ì§€ì›ì„ 받지 않습니다. ê³µì‹ ìž¥ì¹˜ì¸ ê²½ìš° https://comma.ai/support ì— ë°©ë¬¸í•˜ì—¬ 문ì˜í•˜ì„¸ìš”. + 기기 ìš´ì˜ì²´ì œ ì—…ë°ì´íŠ¸ê°€ 백그ë¼ìš´ë“œì—서 다운로드 중입니다. 설치가 준비ë˜ë©´ ì—…ë°ì´íŠ¸ë¥¼ 진행하ë¼ëŠ” 메시지가 표시ë©ë‹ˆë‹¤. NVMe drive not mounted. NVMe 드ë¼ì´ë¸Œê°€ 마운트ë˜ì§€ 않았습니다. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - ì§€ì›ë˜ì§€ 않는 NVMe 드ë¼ì´ë¸Œê°€ ê°ì§€ë˜ì—ˆìŠµë‹ˆë‹¤. ì§€ì›ë˜ì§€ 않는 NVMe 드ë¼ì´ë¸ŒëŠ” ë§Žì€ ì „ë ¥ì„ ì†Œë¹„í•˜ê³  장치를 과열시킬 수 있습니다. + Device temperature too high. System cooling down before starting. Current internal component temperature: %1 + 기기 온ë„ê°€ 너무 높습니다. ì‹œìŠ¤í…œì´ ì‹œìž‘í•˜ê¸° ì „ì— ì˜¨ë„를 낮추고 있습니다. 현재 ë‚´ë¶€ 부품 온ë„는 %1입니다. + + + 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. + 기기가 comma.ai ì„œë²„ì— ë“±ë¡ë˜ì§€ 못했습니다. ë”°ë¼ì„œ comma.ai ì„œë²„ì— ì—°ê²°í•˜ê±°ë‚˜ ë°ì´í„°ë¥¼ 업로드할 수 없으며, comma.aiì˜ ì§€ì›ë„ ë°›ì„ ìˆ˜ 없습니다. 만약 ì´ ê¸°ê¸°ë¥¼ comma.ai/shopì—서 구매했다면, https://comma.ai/supportì—서 ì§€ì› í‹°ì¼“ì„ ì—´ì–´ì£¼ì„¸ìš”. + + + Acknowledge Excessive Actuation + ê³¼ë„한 ìž‘ë™ì„ ì¸ì •하십시오 + + + Snooze Update + ì—…ë°ì´íЏ ì¼ì‹œ 중지 + + + 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. + openpilotì´ ì§€ë‚œ 주행ì—서 ê³¼ë„한 %1 ìž‘ë™ì„ ê°ì§€í–ˆìŠµë‹ˆë‹¤. 문제 í•´ê²°ì„ ìœ„í•´ https://comma.ai/support로 ì§€ì›íŒ€ì— ì—°ë½í•˜ì‹œê³ , 사용 ì¤‘ì¸ ê¸°ê¸°ì˜ ë™ê¸€ ID를 공유해 주세요. + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + 즉시 ì¸í„°ë„·ì— 연결하여 ì—…ë°ì´íŠ¸ë¥¼ 확ì¸í•˜ì„¸ìš”. ì¸í„°ë„·ì— 연결하지 않으면, %1 후 sunnypilotì´ ë¹„í™œì„±í™”ë©ë‹ˆë‹¤. + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + ì—…ë°ì´íŠ¸ë¥¼ 확ì¸í•˜ë ¤ë©´ ì¸í„°ë„·ì— ì—°ê²°í•´ 주세요. ì¸í„°ë„·ì— 연결하여 ì—…ë°ì´íŠ¸ë¥¼ 확ì¸í•  때까지 sunnypilotì€ ìžë™ìœ¼ë¡œ 시작ë˜ì§€ 않습니다. sunnypilot 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. - sunnypilotì´ ì°¨ëŸ‰ì„ ì‹ë³„í•  수 없습니다. ì§€ì›ë˜ì§€ 않는 차량ì´ê±°ë‚˜ ECUê°€ ì¸ì‹ë˜ì§€ 않습니다. 해당 ì°¨ëŸ‰ì— ë§žëŠ” 펌웨어 ë²„ì „ì„ ì¶”ê°€í•˜ë ¤ë©´ PRì„ ì œì¶œí•˜ì„¸ìš”. ë„ì›€ì´ í•„ìš”í•˜ì‹œë©´ discord.comma.aiì— ê°€ìž…í•˜ì„¸ìš”. + sunnypilotì´ ë‹¹ì‹ ì˜ ì°¨ëŸ‰ì„ ì‹ë³„하지 못했습니다. 현재 ì°¨ëŸ‰ì€ ì§€ì›ë˜ì§€ 않거나, ì°¨ëŸ‰ì˜ ECUê°€ ì¸ì‹ë˜ì§€ 않습니다. 올바른 ì°¨ëŸ‰ì— íŽŒì›¨ì–´ ë²„ì „ì„ ì¶”ê°€í•˜ëŠ” í’€ 리퀘스트를 제출해 주세요. ë„ì›€ì´ í•„ìš”í•˜ë©´ discord.comma.aiì— ì°¸ì—¬í•˜ì„¸ìš”. sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot ìž¥ì¹˜ì˜ ìž¥ì°© 위치가 변경ë˜ì—ˆìŠµë‹ˆë‹¤. 장치가 ë§ˆìš´íŠ¸ì— ì™„ì „ížˆ 장착ë˜ê³  마운트가 ì•žìœ ë¦¬ì— ë‹¨ë‹¨ížˆ ê³ ì •ë˜ì—ˆëŠ”ì§€ 확ì¸í•˜ì„¸ìš”. + sunnypilotì´ ê¸°ê¸°ì˜ ìž¥ì°© 위치 변화를 ê°ì§€í–ˆìŠµë‹ˆë‹¤. 기기가 ë§ˆìš´íŠ¸ì— ì™„ì „ížˆ 장착ë˜ì—ˆëŠ”ì§€, 그리고 마운트가 ì•žìœ ë¦¬ì— ë‹¨ë‹¨ížˆ ê³ ì •ë˜ì—ˆëŠ”ì§€ 확ì¸í•˜ì„¸ìš”. - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - ìž¥ì¹˜ì˜ ì˜¨ë„ê°€ 너무 높습니다. 시작 ì „ì— ì˜¨ë„를 낮춰주세요. 현재 ë‚´ë¶€ 구성 요소 온ë„: %1 - - - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. - + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 + OpenStreetMap ë°ì´í„°ë² ì´ìŠ¤ê°€ ìµœì‹ ì´ ì•„ë‹™ë‹ˆë‹¤. í–¥ìƒëœ ì†ë„ 제어 ë° ë„로명 표시를 위해 OpenStreetMap ë°ì´í„°ë¥¼ ê³„ì† ì‚¬ìš©í•˜ë ¤ë©´, 새로운 ì§€ë„를 다운로드해야 합니다. + +%1 @@ -1028,50 +1231,192 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 알림 + + OffroadHomeSP + + ALWAYS OFFROAD ACTIVE + í•­ìƒ ì˜¤í”„ë¡œë“œ 활성 ìƒíƒœ + + OnroadAlerts - - sunnypilot Unavailable - 오픈파ì¼ëŸ¿ì„ 사용할수없습니다 - TAKE CONTROL IMMEDIATELY - í•¸ë“¤ì„ ìž¡ì•„ì£¼ì„¸ìš” + 즉시 제어하세요 Reboot Device - 장치를 재부팅하세요 + 기기 재부팅 Waiting to start - ì‹œìž‘ì„ ê¸°ë‹¤ë¦¬ëŠ”ì¤‘ + 시작 대기 중 System Unresponsive - ì‹œìŠ¤í…œì´ ì‘답하지않습니다 + 시스템 ì‘답 ì—†ìŒ + + + sunnypilot Unavailable + sunnypilot 사용 불가 + + + + OsmPanel + + Mapd Version + Mapd 버전 + + + Offline Maps ETA + 오프ë¼ì¸ ì§€ë„ ë‚¨ì€ ì˜ˆìƒ ì‹œê°„ + + + Time Elapsed + 경과 시간 + + + Downloaded Maps + ë‹¤ìš´ë¡œë“œëœ ì§€ë„ + + + DELETE + ì‚­ì œ + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + ë‹¤ìš´ë¡œë“œëœ ëª¨ë“  ì§€ë„ê°€ ì‚­ì œë©ë‹ˆë‹¤. + +모든 ì§€ë„를 ì •ë§ë¡œ 삭제하시겠습니까? + + + Yes, delete all the maps. + 네, 모든 ì§€ë„를 삭제합니다. + + + Database Update + ë°ì´í„°ë² ì´ìФ ì—…ë°ì´íЏ + + + CHECK + í™•ì¸ + + + Country + êµ­ê°€ + + + SELECT + ì„ íƒ + + + Fetching Country list... + êµ­ê°€ ëª©ë¡ ë¶ˆëŸ¬ì˜¤ëŠ” 중... + + + State + 주 + + + Fetching State list... + 주 ëª©ë¡ ë¶ˆëŸ¬ì˜¤ëŠ” 중... + + + All + ì „ì²´ + + + REFRESH + 새로 고침 + + + UPDATE + ì—…ë°ì´íЏ + + + Download starting... + 다운로드 시작 중... + + + Error: Invalid download. Retry. + 오류: 유효하지 ì•Šì€ ë‹¤ìš´ë¡œë“œìž…ë‹ˆë‹¤. 다시 시ë„í•´ 주세요. + + + Download complete! + 다운로드 완료! + + + + +Warning: You are on a metered connection! + + +경고: ë°ì´í„° 사용량 ì œí•œì´ ìžˆëŠ” 네트워í¬ì— ì—°ê²°ë˜ì–´ 있습니다! + + + This will start the download process and it might take a while to complete. + 다운로드 절차를 시작합니다. 완료하는 ë° ì‹œê°„ì´ ë‹¤ì†Œ 걸릴 수 있습니다. + + + Continue on Metered + ë°ì´í„° 사용량 제한 네트워í¬ë¡œ ê³„ì† + + + Start Download + 다운로드 시작 + + + m + ë¶„ + + + s + ì´ˆ + + + Calculating... + 계산 중... + + + Downloaded + ë‹¤ìš´ë¡œë“œë¨ + + + Calculating ETA... + ë‚¨ì€ ì˜ˆìƒ ì‹œê°„ 계산 중... + + + Ready + 준비 완료 + + + Time remaining: + ë‚¨ì€ ì‹œê°„: PairingPopup Pair your device to your comma account - 장치를 comma ê³„ì •ì— ë™ê¸°í™”합니다 + 기기를 comma ê³„ì •ì— íŽ˜ì–´ë§í•˜ì„¸ìš”. Go to https://connect.comma.ai on your phone - https://connect.comma.aiì— ì ‘ì†í•˜ì„¸ìš” + 휴대í°ì—서 https://connect.comma.ai로 ì´ë™í•˜ì„¸ìš”. Click "add new device" and scan the QR code on the right - "새 장치 추가"를 í´ë¦­í•˜ê³  오른쪽 QR 코드를 스캔하세요 + "새 기기 추가"를 누르고 ì˜¤ë¥¸ìª½ì— ìžˆëŠ” QR 코드를 스캔하세요. Bookmark connect.comma.ai to your home screen to use it like an app - connect.comma.ai를 앱처럼 사용하려면 홈 í™”ë©´ì— ë°”ë¡œê°€ê¸°ë¥¼ 만드세요 + connect.comma.ai를 홈 í™”ë©´ì— ë¶ë§ˆí¬í•˜ì—¬ 앱처럼 사용하세요. Please connect to Wi-Fi to complete initial pairing - 초기 ë™ê¸°í™”를 완료하려면 Wi-Fiì— ì—°ê²°í•˜ì„¸ìš”. + 초기 페어ë§ì„ 완료하려면 Wi-Fiì— ì—°ê²°í•´ 주세요. @@ -1089,90 +1434,90 @@ Firehose Mode allows you to maximize your training data uploads to improve openp ParamControlSP Enable - 활성화 + 활성화 Cancel - 취소 + 취소 PlatformSelector Vehicle - + 차량 SEARCH - + 검색 Search your vehicle - + 차량 검색 Enter model year (e.g., 2021) and model name (Toyota Corolla): - + 차량 ì—°ì‹(예: 2021)ê³¼ 모ë¸ëª…(예: Toyota Corolla)ì„ ìž…ë ¥í•˜ì„¸ìš”: SEARCHING - + 검색 중 REMOVE - ì‚­ì œ + 제거 This setting will take effect immediately. - + ì´ ì„¤ì •ì€ ì¦‰ì‹œ ì ìš©ë©ë‹ˆë‹¤. This setting will take effect once the device enters offroad state. - + ì´ ì„¤ì •ì€ ê¸°ê¸°ê°€ 오프로드 ìƒíƒœì— 진입하면 ì ìš©ë©ë‹ˆë‹¤. Vehicle Selector - + 차량 ì„ íƒê¸° Confirm - í™•ì¸ + í™•ì¸ Cancel - 취소 + 취소 No vehicles found for query: %1 - + 입력한 정보와 ì¼ì¹˜í•˜ëŠ” ì°¨ëŸ‰ì„ ì°¾ì„ ìˆ˜ 없습니다: %1 Select a vehicle - + 차량 ì„ íƒ Unrecognized Vehicle - + ì¸ì‹í•  수 없는 차량 Fingerprinted automatically - + ìžë™ìœ¼ë¡œ 지문 ì¸ì‹ë¨ Manually selected - + 수ë™ìœ¼ë¡œ ì„ íƒë¨ Not fingerprinted or manually selected - + 지문 ì¸ì‹ì´ ë˜ì§€ 않았거나 수ë™ìœ¼ë¡œ ì„ íƒë˜ì§€ ì•ŠìŒ Select vehicle to force fingerprint manually. - + 수ë™ìœ¼ë¡œ 지문 ì¸ì‹ì„ 강제할 ì°¨ëŸ‰ì„ ì„ íƒí•˜ì„¸ìš”. Colors represent fingerprint status: - + 색ìƒì€ 지문 ì¸ì‹ ìƒíƒœë¥¼ 나타냅니다: @@ -1183,11 +1528,11 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Become a comma prime member at connect.comma.ai - connect.comma.aiì— ì ‘ì†í•˜ì—¬ comma prime 회ì›ìœ¼ë¡œ 등ë¡í•˜ì„¸ìš” + connect.comma.aiì—서 comma 프ë¼ìž„ 회ì›ì´ ë˜ì„¸ìš”. PRIME FEATURES: - PRIME 기능: + 프ë¼ìž„ 기능: Remote access @@ -1195,11 +1540,11 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 24/7 LTE connectivity - í•­ìƒ LTE ì—°ê²° + 24시간 7ì¼ LTE ì—°ê²° 1 year of drive storage - 1ë…„ê°„ 드ë¼ì´ë¸Œ 로그 저장 + 1년치 주행 ê¸°ë¡ ì €ìž¥ 공간 Remote snapshots @@ -1210,19 +1555,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp PrimeUserWidget ✓ SUBSCRIBED - ✓ 구ë…함 + ✓ 구ë…ë¨ comma prime - comma prime + comma 프ë¼ìž„ QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1586,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now now - - - Reset - Reset failed. Reboot to try again. - 초기화 실패. 재부팅 후 다시 시ë„하세요. - - - Are you sure you want to reset your device? - 장치를 초기화하시겠습니까? - - - System Reset - 장치 초기화 - - - Cancel - 취소 - - - Reboot - 재부팅 - - - Confirm - í™•ì¸ - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - ë°ì´í„° íŒŒí‹°ì…˜ì„ ë§ˆìš´íŠ¸í•  수 없습니다. íŒŒí‹°ì…˜ì´ ì†ìƒë˜ì—ˆì„ 수 있습니다. 모든 ì„¤ì •ì„ ì‚­ì œí•˜ê³  장치를 초기화하려면 확ì¸ì„ 누르세요. - - - Resetting device... -This may take up to a minute. - 장치를 초기화하는 중... -최대 1ë¶„ì´ ì†Œìš”ë  ìˆ˜ 있습니다. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 시스템 ìž¬ì„¤ì •ì´ ì‹œìž‘ë˜ì—ˆìŠµë‹ˆë‹¤. 모든 콘í…츠와 ì„¤ì •ì„ ì§€ìš°ë ¤ë©´ 확ì¸ì„ 누르시고 ë¶€íŒ…ì„ ìž¬ê°œí•˜ë ¤ë©´ 취소를 누르세요. + sunnypilot + sunnypilot @@ -1295,7 +1599,7 @@ This may take up to a minute. Device - 장치 + 기기 Network @@ -1315,187 +1619,96 @@ This may take up to a minute. Firehose - 파ì´ì–´í˜¸ìФ + Firehose SettingsWindowSP × - × + × Device - 장치 + 기기 Network - ë„¤íŠ¸ì›Œí¬ + ë„¤íŠ¸ì›Œí¬ sunnylink - + sunnylink Toggles - 토글 + 토글 Software - 소프트웨어 - - - Trips - - - - Vehicle - - - - Developer - ê°œë°œìž - - - Firehose - 파ì´ì–´í˜¸ìФ - - - Steering - + 소프트웨어 Models - + ëª¨ë¸ + + + Steering + ì¡°í–¥ Cruise - + í¬ë£¨ì¦ˆ Visuals - - - - - Setup - - WARNING: Low Voltage - 경고: ì „ì••ì´ ë‚®ìŠµë‹ˆë‹¤ + ì‹œê° íš¨ê³¼ - Power your device in a car with a harness or proceed at your own risk. - 장치를 하네스를 통해 차량 ì „ì›ì— 연결하세요. USB ì „ì›ì—서는 예ìƒì¹˜ 못한 문제가 ìƒê¸¸ 수 있습니다. + OSM + OSM - Power off - ì „ì› ë„기 + Trips + 주행 ê¸°ë¡ - Continue - ê³„ì† + Vehicle + 차량 - Getting Started - 시작하기 + Firehose + Firehose - Before we get on the road, let’s finish installation and cover some details. - 출발 ì „ ì„¤ì •ì„ ì™„ë£Œí•˜ê³  세부 ì‚¬í•­ì„ ì‚´íŽ´ë´…ë‹ˆë‹¤. - - - Connect to Wi-Fi - Wi-Fi ì—°ê²° - - - Back - 뒤로 - - - Continue without Wi-Fi - Wi-Fi ì—°ê²° ì—†ì´ ì§„í–‰ - - - Waiting for internet - ì¸í„°ë„· ì—°ê²° 대기 중 - - - Enter URL - URL ìž…ë ¥ - - - for Custom Software - 커스텀 소프트웨어 - - - Downloading... - 다운로드 중... - - - Download Failed - 다운로드 실패 - - - Ensure the entered URL is valid, and the device’s internet connection is good. - ìž…ë ¥ëœ URLì´ ìœ íš¨í•˜ê³  ì¸í„°ë„· ì—°ê²°ì´ ì›í™œí•œì§€ 확ì¸í•˜ì„¸ìš”. - - - Reboot device - 장치 재부팅 - - - Start over - 다시 시작 - - - Something went wrong. Reboot the device. - 문제가 ë°œìƒí–ˆìŠµë‹ˆë‹¤. 장치를 재부팅하세요. - - - No custom software found at this URL. - ì´ URLì—서 커스텀 소프트웨어를 ì°¾ì„ ìˆ˜ 없습니다. - - - Select a language - 언어를 ì„ íƒí•˜ì„¸ìš” - - - Choose Software to Install - 설치할 소프트웨어 ì„ íƒ - - - sunnypilot - sunnypilot - - - Custom Software - 커스텀 소프트웨어 + Developer + ê°œë°œìž SetupWidget Finish Setup - 설정 완료 + 설정 완료하기 Pair your device with comma connect (connect.comma.ai) and claim your comma prime offer. - 장치를 comma connect (connect.comma.ai)ì—서 ë™ê¸°í™”하고 comma prime 무료 ì´ìš©ê¶Œì„ 사용하세요. + 기기를 comma connect (connect.comma.ai)와 페어ë§í•˜ê³  comma prime 혜íƒì„ 받으세요. Pair device - 장치 ë™ê¸°í™” + 기기 íŽ˜ì–´ë§ Sidebar CONNECT - 커넥트 + CONNECT OFFLINE - ì—°ê²° ì•ˆë¨ + 오프ë¼ì¸ ONLINE @@ -1515,11 +1728,11 @@ This may take up to a minute. GOOD - ì¢‹ìŒ + ì •ìƒ OK - OK + 양호 VEHICLE @@ -1527,11 +1740,11 @@ This may take up to a minute. NO - NO + íŒë‹¤ PANDA - PANDA + ì—†ìŒ -- @@ -1543,7 +1756,7 @@ This may take up to a minute. ETH - LAN + ì´ë”ë„· 2G @@ -1566,34 +1779,34 @@ This may take up to a minute. SidebarSP DISABLED - + ë¹„í™œì„±í™”ë¨ OFFLINE - ì—°ê²° ì•ˆë¨ + 오프ë¼ì¸ REGIST... - + ë“±ë¡ ì¤‘... ONLINE - 온ë¼ì¸ + 온ë¼ì¸ ERROR - 오류 + 오류 SUNNYLINK - + SUNNYLINK SoftwarePanel Updates are only downloaded while the car is off. - ì—…ë°ì´íŠ¸ëŠ” 차량 시ë™ì´ êº¼ì¡Œì„ ë•Œ 다운로드ë©ë‹ˆë‹¤. + ì—…ë°ì´íŠ¸ëŠ” 차량 시ë™ì´ 꺼져 ìžˆì„ ë•Œë§Œ 다운로드ë©ë‹ˆë‹¤. Current Version @@ -1633,7 +1846,7 @@ This may take up to a minute. Are you sure you want to uninstall? - 제거하시겠습니까? + ì •ë§ë¡œ 제거하시겠습니까? CHECK @@ -1645,7 +1858,7 @@ This may take up to a minute. failed to check for update - ì—…ë°ì´íŠ¸ë¥¼ 확ì¸í•˜ì§€ 못했습니다 + ì—…ë°ì´íЏ í™•ì¸ ì‹¤íŒ¨ up to date, last checked %1 @@ -1661,26 +1874,50 @@ This may take up to a minute. never - ì—…ë°ì´íЏ 안함 + í™•ì¸ ì•ˆí•¨ SoftwarePanelSP Search Branch - + 브랜치 검색 Enter search keywords, or leave blank to list all branches. - + 검색어를 입력하거나, 비워ë‘ë©´ 모든 브랜치가 표시ë©ë‹ˆë‹¤. + + + Disable Updates + ì—…ë°ì´íЏ 비활성화 + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + ì´ ê¸°ëŠ¥ì„ í™œì„±í™”í•˜ë©´ 소프트웨어 ì—…ë°ì´íŠ¸ê°€ 비활성화ë©ë‹ˆë‹¤. <b>ì ìš©í•˜ë ¤ë©´ ìž¬ë¶€íŒ…ì´ í•„ìš”í•©ë‹ˆë‹¤.</b> No branches found for keywords: %1 - + 입력한 키워드와 ì¼ì¹˜í•˜ëŠ” ë¸Œëžœì¹˜ì„ ì°¾ì„ ìˆ˜ 없습니다: %1 Select a branch - 브랜치 ì„ íƒ + 브랜치 ì„ íƒ + + + %1 updates requires a reboot.<br>Reboot now? + ì—…ë°ì´íЏ 기능 %1는 ìž¬ë¶€íŒ…ì´ í•„ìš”í•©ë‹ˆë‹¤.<br>지금 재부팅하시겠습니까? + + + Reboot + 재부팅 + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + ì´ ê¸°ëŠ¥ì„ í™œì„±í™”í•˜ë©´ 소프트웨어 ì—…ë°ì´íŠ¸ê°€ 비활성화ë©ë‹ˆë‹¤.<br><b>ì ìš©í•˜ë ¤ë©´ ìž¬ë¶€íŒ…ì´ í•„ìš”í•©ë‹ˆë‹¤.</b> + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + ì´ ì„¤ì •ë“¤ì„ ë³€ê²½í•˜ë ¤ë©´ í•­ìƒ ì˜¤í”„ë¡œë“œ 모드를 활성화하거나 차량 시ë™ì„ 꺼야 합니다. @@ -1691,7 +1928,7 @@ This may take up to a minute. Warning: This grants SSH access to all public keys in your GitHub settings. Never enter a GitHub username other than your own. A comma employee will NEVER ask you to add their GitHub username. - 경고: ì´ ì„¤ì •ì€ GitHubì— ë“±ë¡ëœ 모든 공용 í‚¤ì— ëŒ€í•´ SSH 액세스 ê¶Œí•œì„ ë¶€ì—¬í•©ë‹ˆë‹¤. 본ì¸ì˜ GitHub ì‚¬ìš©ìž ì•„ì´ë”” ì´ì™¸ì—는 입력하지 마십시오. commaì—서는 GitHub ì•„ì´ë””를 추가하ë¼ëŠ” ìš”ì²­ì„ í•˜ì§€ 않습니다. + 경고: ì´ê²ƒì€ ë‹¹ì‹ ì˜ GitHub ì„¤ì •ì— ìžˆëŠ” 모든 공개 í‚¤ì— ëŒ€í•œ SSH ì ‘ê·¼ì„ í—ˆìš©í•©ë‹ˆë‹¤. 당신 ìžì‹ ì˜ GitHub ì‚¬ìš©ìž ì´ë¦„ 외ì—는 절대로 입력하지 마세요. comma ì§ì›ì€ 절대 당신ì—게 ê·¸ë“¤ì˜ GitHub ì‚¬ìš©ìž ì´ë¦„ì„ ì¶”ê°€í•˜ë¼ê³  요청하지 ì•Šì„ ê²ƒìž…ë‹ˆë‹¤. ADD @@ -1699,7 +1936,7 @@ This may take up to a minute. Enter your GitHub username - GitHub ì‚¬ìš©ìž ID + GitHub ì‚¬ìš©ìž ì´ë¦„ì„ ìž…ë ¥í•˜ì„¸ìš” LOADING @@ -1707,11 +1944,11 @@ This may take up to a minute. REMOVE - ì‚­ì œ + 제거 Username '%1' has no keys on GitHub - ì‚¬ìš©ìž '%1'ì˜ GitHubì— í‚¤ê°€ 등ë¡ë˜ì–´ 있지 않습니다 + GitHubì— ì‚¬ìš©ìž ì´ë¦„ '%1'ì˜ í‚¤ê°€ 없습니다. Request timed out @@ -1719,7 +1956,7 @@ This may take up to a minute. Username '%1' doesn't exist on GitHub - GitHub ì‚¬ìš©ìž '%1'를 찾지 못했습니다 + GitHubì— ì‚¬ìš©ìž ì´ë¦„ '%1'ì´(ê°€) 존재하지 않습니다. @@ -1733,122 +1970,146 @@ This may take up to a minute. SunnylinkPanel This is the master switch, it will allow you to cutoff any sunnylink requests should you want to do that. - + ì´ê²ƒì€ 마스터 스위치입니다. ì›í•  경우 모든 sunnylink ìš”ì²­ì„ ì°¨ë‹¨í•  수 있습니다. Enable sunnylink - - - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A + sunnylink 사용 Sponsor Status - + í›„ì› ìƒíƒœ SPONSOR - + í›„ì› Become a sponsor of sunnypilot to get early access to sunnylink features when they become available. - + sunnypilotì˜ í›„ì›ìžê°€ ë˜ì–´, sunnylink ê¸°ëŠ¥ì´ ì¶œì‹œë˜ë©´ 얼리 액세스 ê¶Œí•œì„ ì–»ìœ¼ì„¸ìš”. Pair GitHub Account - + GitHub 계정 íŽ˜ì–´ë§ PAIR - ë™ê¸°í™” + íŽ˜ì–´ë§ Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. - + GitHub ê³„ì •ì„ íŽ˜ì–´ë§í•˜ì—¬ sunnylinkì˜ API ì ‘ê·¼ì„ í¬í•¨í•œ 후ì›ìž 혜íƒì„ ê¸°ê¸°ì— ë¶€ì—¬í•˜ì„¸ìš”. + + + N/A + N/A sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - + sunnylink ë™ê¸€ ID를 ì°¾ì„ ìˆ˜ 없습니다. ì¸í„°ë„· ì—°ê²°ì´ ì•½í•˜ê±°ë‚˜ sunnylink ë“±ë¡ ë¬¸ì œ ë•Œë¬¸ì¼ ìˆ˜ 있습니다. 재부팅 후 다시 시ë„í•´ 주세요. - Not Sponsor - + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 + 🎉ëŒì•„오신 ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤! sunnylink를 다시 활성화하셨군요! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ - + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. + 솔ì§ížˆ ë§í•´ì„œ, sunnylink를 비활성화한 ê²ƒì„ ë³´ë‹ˆ 좀 슬프네요 😢. 하지만 ëŒì•„올 준비가 ë˜ë©´ ì €í¬ëŠ” 언제든 여기 있ì„게요 🎉. Backup Settings - + 설정 백업 Are you sure you want to backup sunnypilot settings? - + sunnypilot ì„¤ì •ì„ ë°±ì—…í•˜ì‹œê² ìŠµë‹ˆê¹Œ? Back Up - + 백업 Restore Settings - + 설정 ë³µì› Are you sure you want to restore the last backed up sunnypilot settings? - + 마지막으로 ë°±ì—…ëœ sunnypilot ì„¤ì •ì„ ë³µì›í•˜ì‹œê² ìŠµë‹ˆê¹Œ? Restore - + ë³µì› Backup in progress %1% - + 백업 ì§„í–‰ 중 %1% Backup Failed - + 백업 실패 Settings backup completed. - + 설정 백업 완료. Restore in progress %1% - + ë³µì› ì§„í–‰ 중 %1% Restore Failed - + ë³µì› ì‹¤íŒ¨ Unable to restore the settings, try again later. - + ì„¤ì •ì„ ë³µì›í•  수 없습니다. ë‚˜ì¤‘ì— ë‹¤ì‹œ 시ë„í•´ 주세요. Settings restored. Confirm to restart the interface. + ì„¤ì •ì´ ë³µì›ë˜ì—ˆìŠµë‹ˆë‹¤. ì¸í„°íŽ˜ì´ìŠ¤ë¥¼ 재시작하려면 확ì¸í•˜ì„¸ìš”. + + + Device ID + 기기 ID + + + THANKS ♥ + ê°ì‚¬í•©ë‹ˆë‹¤ ♥ + + + Not Sponsor + 후ì›ìž 아님 + + + Paired + 페어ë§ë¨ + + + Not Paired + 페어ë§ë˜ì§€ ì•ŠìŒ + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1856,39 +2117,39 @@ This may take up to a minute. SunnylinkSponsorPopup Scan the QR code to login to your GitHub account - + GitHub ê³„ì •ì— ë¡œê·¸ì¸í•˜ë ¤ë©´ QR 코드를 스캔하세요. Follow the prompts to complete the pairing process - + ì•ˆë‚´ì— ë”°ë¼ íŽ˜ì–´ë§ ê³¼ì •ì„ ì™„ë£Œí•˜ì„¸ìš”. Re-enter the "sunnylink" panel to verify sponsorship status - + "sunnylink" 패ë„ì— ë‹¤ì‹œ 들어가서 후ì›ìž ìƒíƒœë¥¼ 확ì¸í•˜ì„¸ìš”. If sponsorship status was not updated, please contact a moderator on Discord at https://discord.gg/sunnypilot - + 후ì›ìž ìƒíƒœê°€ ì—…ë°ì´íЏë˜ì§€ 않았다면, https://discord.gg/sunnypilotì—서 ìš´ì˜ìžì—게 ì—°ë½í•´ 주세요. Scan the QR code to visit sunnyhaibin's GitHub Sponsors page - + sunnyhaibinì˜ GitHub Sponsors 페ì´ì§€ë¥¼ 방문하려면 QR 코드를 스캔하세요. Choose your sponsorship tier and confirm your support - + í›„ì› ë“±ê¸‰ì„ ì„ íƒí•˜ê³  후ì›ì„ 확ì¸í•˜ì„¸ìš”. Join our community on Discord at https://discord.gg/sunnypilot and reach out to a moderator to confirm your sponsor status - + https://discord.gg/sunnypilotì—서 우리 ì»¤ë®¤ë‹ˆí‹°ì— ì°¸ì—¬í•˜ê³ , ìš´ì˜ìžì—게 후ì›ìž ìƒíƒœë¥¼ 확ì¸í•´ 주세요. Pair your GitHub account - + GitHub 계정 íŽ˜ì–´ë§ Early Access: Become a sunnypilot Sponsor - + 얼리 액세스: sunnypilot 후ì›ìžê°€ ë˜ì„¸ìš” @@ -1903,11 +2164,11 @@ This may take up to a minute. Welcome to sunnypilot - 오픈 파ì¼ëŸ¿ì— 오신 ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤. + sunnypilotì— ì˜¤ì‹  ê²ƒì„ í™˜ì˜í•©ë‹ˆë‹¤ You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 오픈파ì¼ëŸ¿ì„ 사용하려면 ì´ìš©ì•½ê´€ì— ë™ì˜í•´ì•¼ 합니다. 최신 ì•½ê´€ì€ <span style='color: #465BEA;'>https://comma.ai/terms</span> ì—서 최신 ì•½ê´€ì„ ì½ì€ 후 계ì†í•˜ì„¸ìš”. + sunnypilotì„ ì‚¬ìš©í•˜ë ¤ë©´ ì´ìš© ì•½ê´€ì— ë™ì˜í•´ì•¼ 합니다. 계ì†í•˜ê¸° ì „ì— <span style='color: #465BEA;'>https://comma.ai/terms</span>ì—서 최신 ì•½ê´€ì„ ì½ì–´ë³´ì„¸ìš”. @@ -1940,29 +2201,21 @@ This may take up to a minute. Disengage on Accelerator Pedal ê°€ì†íŽ˜ë‹¬ ì¡°ìž‘ 시 í•´ì œ - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - í™œì„±í™”ëœ ê²½ìš° ê°€ì† íŽ˜ë‹¬ì„ ë°Ÿìœ¼ë©´ sunnypilotì´ í•´ì œë©ë‹ˆë‹¤. - Experimental Mode 실험 모드 - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilotì€ ê¸°ë³¸ì ìœ¼ë¡œ <b>안정 모드</b>로 주행합니다. 실험 모드는 안정화ë˜ì§€ ì•Šì€ <b>알파 ìˆ˜ì¤€ì˜ ê¸°ëŠ¥</b>ì„ í™œì„±í™”í•©ë‹ˆë‹¤. 실험 ëª¨ë“œì˜ ê¸°ëŠ¥ì€ ì•„ëž˜ì™€ 같습니다: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - sunnypilotì˜ ì£¼í–‰ëª¨ë¸ì´ ê°€ê°ì†ì„ 제어합니다. sunnypilotì€ ì‹ í˜¸ë“±ê³¼ ì •ì§€ 표지íŒì„ ë³´ê³  멈추는 ê²ƒì„ í¬í•¨í•˜ì—¬ ì¸ê°„ì´ ìš´ì „í•˜ëŠ” 것처럼 ìƒê°í•˜ê³  주행합니다. 주행 모ë¸ì´ 주행할 ì†ë„를 결정하므로 ì„¤ì •ëœ ì†ë„는 최대 주행 ì†ë„로만 기능합니다. ì´ ê¸°ëŠ¥ì€ ì•ŒíŒŒ 수준ì´ë¯€ë¡œ ì‚¬ìš©ì— ê°ë³„히 주ì˜í•´ì•¼ 합니다. - New Driving Visualization 새로운 주행 시ê°í™” Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. - ì°¨ëŸ‰ì— ìž¥ì°©ëœ ACC로 ê°€ê°ì†ì„ 제어하기 ë•Œë¬¸ì— í˜„ìž¬ ì´ ì°¨ëŸ‰ì—서는 실험 모드를 사용할 수 없습니다. + ì°¨ëŸ‰ì˜ ìˆœì • ACCê°€ ê°€ê°ì† ì œì–´ì— ì‚¬ìš©ë˜ê¸° 때문ì—, ì´ ì°¨ì—서는 현재 실험 모드를 사용할 수 없습니다. + + + openpilot longitudinal control may come in a future update. + openpilotì˜ ê°€ê°ì† 제어 ê¸°ëŠ¥ì€ í–¥í›„ ì—…ë°ì´íЏì—서 ì œê³µë  ìˆ˜ 있습니다. Aggressive @@ -1974,47 +2227,35 @@ This may take up to a minute. Relaxed - 편안한 + ëŠê¸‹í•œ Driving Personality - 주행 모드 - - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - sunnypilot ê°€ê°ì† 제어 알파 ë²„ì „ì€ ë¹„ 릴리즈 브랜치ì—서 실험 모드와 함께 테스트할 수 있습니다. - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 실험 모드를 사용하려면 sunnypilot E2E ê°€ê°ì† 제어 (알파) í† ê¸€ì„ í™œì„±í™”í•˜ì„¸ìš”. + 주행 성향 End-to-End Longitudinal Control - E2E ê°€ê°ì† 제어 - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 표준 모드를 권장합니다. ê³µê²©ì  ëª¨ë“œì˜ sunnypilotì€ ì„ ë‘ ì°¨ëŸ‰ì„ ë” ê°€ê¹Œì´ ë”°ë¼ê°€ê³  ê°€ê°ì†ì œì–´ë¥¼ 사용하여 ë”ìš± 공격ì ìœ¼ë¡œ 움ì§ìž…니다. 편안한 ëª¨ë“œì˜ sunnypilotì€ ì„ ë‘ ì°¨ëŸ‰ìœ¼ë¡œë¶€í„° ë” ë©€ë¦¬ 떨어져 있습니다. ì§€ì›ë˜ëŠ” 차량ì—서는 ìŠ¤í‹°ì–´ë§ íœ  거리 ë²„íŠ¼ì„ ì‚¬ìš©í•˜ì—¬ ì´ëŸ¬í•œ íŠ¹ì„±ì„ ìˆœí™˜í•  수 있습니다. + End-to-End ê°€ê°ì† 제어 The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. - ìš´ì „ 시ê°í™”는 ì¼ë¶€ íšŒì „ì„ ë” ìž˜ 보여주기 위해 ì €ì†ì—서 ë„로를 향한 ê´‘ê° ì¹´ë©”ë¼ë¡œ 전환ë©ë‹ˆë‹¤. 우측 ìƒë‹¨ì— 실험 모드 로고가 표시ë©ë‹ˆë‹¤. + 주행 시ê°í™” í™”ë©´ì€ ì €ì†ì—서 ì¼ë¶€ 회전 êµ¬ê°„ì„ ë” ìž˜ 보여주기 위해 ì „ë°© ê´‘ê° ì¹´ë©”ë¼ë¡œ 전환ë©ë‹ˆë‹¤. ë˜í•œ, 실험 모드 로고가 오른쪽 ìƒë‹¨ì— 표시ë©ë‹ˆë‹¤. Always-On Driver Monitoring ìƒì‹œ ìš´ì „ìž ëª¨ë‹ˆí„°ë§ - Enable driver monitoring even when sunnypilot is not engaged. - sunnypilotì´ í™œì„±í™”ë˜ì§€ ì•Šì€ ê²½ìš°ì—ë„ ë“œë¼ì´ë²„ 모니터ë§ì„ 활성화합니다. + Changing this setting will restart openpilot if the car is powered on. + ì´ ì„¤ì •ì„ ë³€ê²½í•˜ë©´ 차량 ì „ì›ì´ 켜져 ìžˆì„ ë•Œ openpilotì´ ìž¬ì‹œìž‘ë©ë‹ˆë‹¤. - Enable Dynamic Experimental Control - + Record and Upload Microphone Audio + 마ì´í¬ 오디오 ë…¹ìŒ ë° ì—…ë¡œë“œ - Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. - + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + ìš´ì „ 중 마ì´í¬ 오디오를 ë…¹ìŒí•˜ê³  저장합니다. ì´ ì˜¤ë””ì˜¤ëŠ” comma connectì˜ ëŒ€ì‹œìº  ì˜ìƒì— í¬í•¨ë©ë‹ˆë‹¤. Enable sunnypilot @@ -2022,50 +2263,97 @@ This may take up to a minute. Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. - + 어댑티브 í¬ë£¨ì¦ˆ 컨트롤 ë° ì°¨ë¡œ 유지 ìš´ì „ìž ë³´ì¡°ë¥¼ 위해 sunnypilot ì‹œìŠ¤í…œì„ ì‚¬ìš©í•˜ì„¸ìš”. ì´ ê¸°ëŠ¥ì„ ì‚¬ìš©í•˜ëŠ” ë™ì•ˆì—는 í•­ìƒ ì£¼ì˜ë¥¼ 기울여야 합니다. - Changing this setting will restart openpilot if the car is powered on. - + Enable Dynamic Experimental Control + ë™ì  실험 제어 활성화 - openpilot longitudinal control may come in a future update. - + Enable toggle to allow the model to determine when to use sunnypilot ACC or sunnypilot End to End Longitudinal. + 모ë¸ì´ sunnypilot ACC ë˜ëŠ” sunnypilot End to End ê°€ê°ì† 제어를 언제 사용할지 결정하ë„ë¡ í† ê¸€ì„ í™œì„±í™”í•˜ì„¸ìš”. + + + When enabled, pressing the accelerator pedal will disengage sunnypilot. + 활성화하면, ê°€ì† íŽ˜ë‹¬ì„ ë°Ÿì„ ë•Œ sunnypilotì´ í•´ì œë©ë‹ˆë‹¤. + + + Enable driver monitoring even when sunnypilot is not engaged. + sunnypilotì´ í•´ì œë˜ì–´ ìžˆì„ ë•Œë„ ìš´ì „ìž ëª¨ë‹ˆí„°ë§ì„ 사용합니다. + + + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + 표준 모드가 권장ë©ë‹ˆë‹¤. ê³µê²©ì  ëª¨ë“œì—서는 sunnypilotì´ ì•žì°¨ë¥¼ ë” ê°€ê¹ê²Œ ë”°ë¼ê°€ê³  ê°€ì† ë° ì œë™ì— ë” ì ê·¹ì ìœ¼ë¡œ ë°˜ì‘합니다. ëŠê¸‹í•œ 모드ì—서는 sunnypilotì´ ì•žì°¨ë¡œë¶€í„° ë” ë©€ë¦¬ 떨어져 주행합니다. ì§€ì›ë˜ëŠ” 차량ì—서는 í•¸ë“¤ì˜ ê±°ë¦¬ ì¡°ì ˆ 버튼으로 ì´ ì£¼í–‰ ì„±í–¥ë“¤ì„ ìˆœí™˜í•˜ë©° 변경할 수 있습니다. + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + sunnypilotì€ ê¸°ë³¸ì ìœ¼ë¡œ <b>안정 모드</b>로 주행합니다. 실험 모드는 안정 모드ì—서 사용하기ì—는 ì•„ì§ ì¤€ë¹„ë˜ì§€ ì•Šì€ <b>알파 수준 기능</b>ì„ í™œì„±í™”í•©ë‹ˆë‹¤. 실험 ê¸°ëŠ¥ì€ ì•„ëž˜ì— ë‚˜ì—´ë˜ì–´ 있습니다: + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + 주행 모ë¸ì´ ê°€ì† íŽ˜ë‹¬ê³¼ 브레ì´í¬ë¥¼ 제어하ë„ë¡ í—ˆìš©í•©ë‹ˆë‹¤. sunnypilotì€ ì‹ í˜¸ë“±ê³¼ ì •ì§€ 표지íŒì—서 정지하는 ê²ƒì„ í¬í•¨í•˜ì—¬ ì‚¬ëžŒì´ ìš´ì „í•˜ëŠ” 것처럼 주행할 것입니다. 주행 모ë¸ì´ ìš´ì „ ì†ë„를 결정하므로, 설정 ì†ë„는 ìƒí•œì„  ì—­í• ë§Œ 합니다. ì´ê²ƒì€ 알파 í’ˆì§ˆì˜ ê¸°ëŠ¥ìž…ë‹ˆë‹¤. 실수가 예ìƒë˜ë‹ˆ 주ì˜í•´ 주세요. + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + sunnypilot ê°€ê°ì† ì œì–´ì˜ ì•ŒíŒŒ ë²„ì „ì€ ì‹¤í—˜ 모드와 함께 비공개(비 릴리스) 브랜치ì—서 테스트할 수 있습니다. + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + 실험 모드를 활성화하려면 sunnypilot ê°€ê°ì† 제어(알파) í† ê¸€ì„ ì¼œì„¸ìš”. - Updater + TreeOptionDialog - Update Required - ì—…ë°ì´íЏ í•„ìš” + Select + ì„ íƒ - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - OS ì—…ë°ì´íŠ¸ê°€ 필요합니다. 장치를 Wi-Fiì— ì—°ê²°í•˜ë©´ 가장 빠르게 ì—…ë°ì´íŠ¸í•  수 있습니다. 다운로드 í¬ê¸°ëŠ” 약 1GB입니다. + Cancel + 취소 + + + + VisualsPanel + + Show Blind Spot Warnings + 사ê°ì§€ëŒ€ 경고 표시 - Connect to Wi-Fi - Wi-Fi ì—°ê²° + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + ì´ ê¸°ëŠ¥ì„ í™œì„±í™”í•˜ë©´, ì°¨ëŸ‰ì— BSM(사ê°ì§€ëŒ€ 모니터ë§) ê¸°ëŠ¥ì´ ì§€ì›ë˜ëŠ” 한 사ê°ì§€ëŒ€ì— ì°¨ëŸ‰ì´ ê°ì§€ë˜ì—ˆì„ 때 경고가 표시ë©ë‹ˆë‹¤. - Install - 설치 + Changing this setting will restart openpilot if the car is powered on. + ì´ ì„¤ì •ì„ ë³€ê²½í•˜ë©´ 차량 ì „ì›ì´ 켜져 ìžˆì„ ë•Œ openpilotì´ ìž¬ì‹œìž‘ë©ë‹ˆë‹¤. - Back - 뒤로 + Off + ë„기 - Loading... - 로딩 중... + Distance + 거리 - Reboot - 재부팅 + Speed + ì†ë„ - Update failed - ì—…ë°ì´íЏ 실패 + Time + 시간 + + + All + ì „ì²´ + + + Display Metrics Below Chevron + 역삼ê°í˜• ì•„ëž˜ì— ì¸¡ì • 지표 표시 + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + 앞차를 ì¶”ì í•˜ëŠ” 역삼ê°í˜• ì•„ëž˜ì— ìœ ìš©í•œ 측정 지표를 표시합니다. (openpilot ê°€ê°ì† 제어 ê¸°ëŠ¥ì´ ìžˆëŠ” 차량ì—ë§Œ 해당ë©ë‹ˆë‹¤) @@ -2075,12 +2363,12 @@ This may take up to a minute. 열기 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> 파ì´ì–´í˜¸ìФ 모드 <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + openpilotì˜ ì£¼í–‰ ëª¨ë¸ ê°œì„ ì„ ìœ„í•´ 학습 ë°ì´í„° ì—…ë¡œë“œëŸ‰ì„ ê·¹ëŒ€í™”í•˜ì„¸ìš”. - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose 모드 <span style='font-family: Noto Color Emoji;'>🔥</span> @@ -2099,7 +2387,7 @@ This may take up to a minute. Forget Wi-Fi Network "%1"? - Wi-Fi "%1"ì— ìžë™ìœ¼ë¡œ 연결하지 않겠습니까? + Wi-Fi ë„¤íŠ¸ì›Œí¬ "%1"ì„(를) 삭제하시겠습니까? Forget diff --git a/selfdrive/ui/translations/main_pt-BR.ts b/selfdrive/ui/translations/main_pt-BR.ts index bc9e50ee0a..0a886d435c 100644 --- a/selfdrive/ui/translations/main_pt-BR.ts +++ b/selfdrive/ui/translations/main_pt-BR.ts @@ -7,10 +7,6 @@ Close Fechar - - Snooze Update - Adiar Atualização - Reboot and Update Reiniciar e Atualizar @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + Previna o envio de grandes volumes de dados em conexões de celular com franquia de limite de dados default - + padrão metered - + limitada unmetered - + ilimitada Wi-Fi Network Metered - + Rede Wi-Fi com Franquia Prevent large data uploads when on a metered Wi-Fi connection - + Previna o envio de grandes volumes de dados em conexões Wi-Fi com franquia de limite de dados @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Você precisa aceitar os Termos e Condições para utilizar sunnypilot. - Back Voltar @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Rejeitar, desintalar %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode Modo Longitudinal Maneuver + + openpilot Longitudinal Control (Alpha) + Controle Longitudinal openpilot (Embrionário) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + AVISO: o controle longitudinal openpilot está em estado embrionário para este carro e desativará a Frenagem Automática de Emergência (AEB). + Enable ADB Habilitar ADB @@ -179,6 +202,22 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB (Android Debug Bridge) permite conectar ao seu dispositivo por meio do USB ou através da rede. Veja https://docs.comma.ai/how-to/connect-to-comma para maiores informações. + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW REVISAR - - Review the rules, features, and limitations of sunnypilot - Revisar regras, aprimoramentos e limitações do sunnypilot - Are you sure you want to review the training guide? Tem certeza que quer rever o treinamento? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Desligar - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - O sunnypilot requer que o dispositivo seja montado dentro de 4° esquerda ou direita e dentro de 5° para cima ou 9° para baixo. O sunnypilot está continuamente calibrando, resetar raramente é necessário. - Your device is pointed %1° %2 and %3° %4. Seu dispositivo está montado %1° %2 e %3° %4. @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration + Desacione para Resetar a Calibração + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + O openpilot está em constante calibração, raramente sendo necessário redefini-lo. Redefinir a calibração reiniciará o openpilot se o carro estiver ligado. + + + + +Steering lag calibration is %1% complete. + + +A calibração do atraso da direção está %1% concluída. + + + + +Steering lag calibration is complete. + + +A calibração do atraso da direção foi concluída. + + + Steering torque response calibration is %1% complete. + A calibração da resposta de torque da direção está %1% concluída. + + + Steering torque response calibration is complete. + A calibração da resposta do torque da direção foi concluída. + + + Review the rules, features, and limitations of sunnypilot - Resetting calibration will restart openpilot if the car is powered on. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? Tem certeza que quer rever o treinamento? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language Selecione o Idioma + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Reiniciar @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Confirmar + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an câmera iniciando + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +600,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 Modo Firehose 🔥 - Firehose Mode: ACTIVE Modo Firehose: ATIVO @@ -509,10 +608,6 @@ Please use caution when using this feature. Only use the blinker when traffic an ACTIVE ATIVO - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - Para maior eficácia, leve seu dispositivo para dentro de casa e conecte-o a um bom adaptador USB-C e Wi-Fi semanalmente.<br><br>O Modo Firehose também pode funcionar enquanto você dirige, se estiver conectado a um hotspot ou a um chip SIM com dados ilimitados.<br><br><br><b>Perguntas Frequentes</b><br><br><i>Importa como ou onde eu dirijo?</i> Não, basta dirigir normalmente.<br><br><i>Todos os meus segmentos são enviados no Modo Firehose?</i> Não, selecionamos apenas um subconjunto dos seus segmentos.<br><br><i>Qual é um bom adaptador USB-C?</i> Qualquer carregador rápido de telefone ou laptop deve ser suficiente.<br><br><i>Importa qual software eu uso?</i> Sim, apenas o sunnypilot oficial (e alguns forks específicos) podem ser usados para treinamento. - <b>%n segment(s)</b> of your driving is in the training dataset so far. @@ -520,6 +615,14 @@ Please use caution when using this feature. Only use the blinker when traffic an <b>%n segmentos</b> da sua direção estão no conjunto de dados de treinamento até agora. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INATIVO</span>: conecte-se a uma rede sem limite <br> de dados + + + Firehose Mode + Modo Firehose + sunnypilot learns to drive by watching humans, like you, drive. @@ -527,8 +630,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INATIVO</span>: conecte-se a uma rede sem limite <br> de dados + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + @@ -636,6 +739,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -653,6 +764,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -660,11 +794,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -675,18 +813,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -703,6 +829,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -715,6 +845,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -762,31 +896,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp SELECIONE - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -802,21 +949,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -845,6 +988,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -861,6 +1012,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel Cancelar + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -964,14 +1147,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - Conecte-se imediatamente à internet para verificar se há atualizações. Se você não se conectar à internet em %1 não será possível acionar o sunnypilot. - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - Conecte-se à internet para verificar se há atualizações. O sunnypilot não será iniciado automaticamente até que ele se conecte à internet para verificar se há atualizações. - Unable to download updates %1 @@ -986,32 +1161,50 @@ 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. 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. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - Falha ao registrar o dispositivo. Ele não se conectará ou fará upload para os servidores comma.ai e não receberá suporte da comma.ai. Se este for um dispositivo oficial, visite https://comma.ai/support. - NVMe drive not mounted. Unidade NVMe não montada. - - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - Unidade NVMe não suportada detectada. O dispositivo pode consumir significativamente mais energia e superaquecimento devido ao NVMe não suportado. - - - sunnypilot 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 sunnypilot 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. - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - O sunnypilot detectou uma mudança na posição de montagem do dispositivo. Verifique se o dispositivo está totalmente encaixado no suporte e se o suporte está firmemente preso ao para-brisa. - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 Temperatura do dispositivo muito alta. O sistema está sendo resfriado antes de iniciar. A temperatura atual do componente interno é: %1 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + O dispositivo não conseguiu se registrar no backend da comma.ai. Ele não se conecta nem faz upload para os servidores da comma.ai e não recebe suporte da comma.ai. Se este for um dispositivo adquirido em comma.ai/shop, abra um ticket em https://comma.ai/support. + + + Acknowledge Excessive Actuation + Reconhecer Atuação Excessiva + + + Snooze Update + Adiar Atualização + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + + + + sunnypilot 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. + + + + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + + + + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1031,11 +1224,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot Indisponível + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY ASSUMA IMEDIATAMENTE @@ -1052,6 +1248,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive Sistema sem Resposta + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + VERIFICAR + + + Country + + + + SELECT + SELECIONE + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + ATUALIZAÇÃO + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1138,7 +1469,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Confirmar + Cancel @@ -1221,10 +1552,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1250,46 +1577,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now agora - - - Reset - Reset failed. Reboot to try again. - Reset falhou. Reinicie para tentar novamente. - - - Are you sure you want to reset your device? - Tem certeza que quer resetar seu dispositivo? - - - System Reset - Resetar Sistema - - - Cancel - Cancelar - - - Reboot - Reiniciar - - - Confirm - Confirmar - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - Não é possível montar a partição de dados. Partição corrompida. Confirme para apagar e redefinir o dispositivo. - - - Resetting device... -This may take up to a minute. - Redefinindo o dispositivo -Isso pode levar até um minuto. - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - Reinicialização do sistema acionada. Pressione confirmar para apagar todo o conteúdo e configurações. Pressione cancel para retomar a inicialização. + sunnypilot + @@ -1350,29 +1640,13 @@ Isso pode levar até um minuto. Software - Trips + Models - - Vehicle - - - - Developer - Desenvdor - - - Firehose - Firehose - Steering - - Models - - Cruise @@ -1381,100 +1655,25 @@ Isso pode levar até um minuto. Visuals - - - Setup - WARNING: Low Voltage - ALERTA: Baixa Voltagem + OSM + - Power your device in a car with a harness or proceed at your own risk. - Ligue seu dispositivo em um carro com um chicote ou prossiga por sua conta e risco. + Trips + - Power off - Desligar + Vehicle + - Continue - Continuar + Firehose + Firehose - Getting Started - Começando - - - Before we get on the road, let’s finish installation and cover some details. - Antes de pegarmos a estrada, vamos terminar a instalação e cobrir alguns detalhes. - - - Connect to Wi-Fi - Conectar ao Wi-Fi - - - Back - Voltar - - - Continue without Wi-Fi - Continuar sem Wi-Fi - - - Waiting for internet - Esperando pela internet - - - Enter URL - Preencher URL - - - for Custom Software - para o Software Customizado - - - Downloading... - Baixando... - - - Download Failed - Download Falhou - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Garanta que a URL inserida é valida, e uma boa conexão à internet. - - - Reboot device - Reiniciar Dispositivo - - - Start over - Inicializar - - - No custom software found at this URL. - Não há software personalizado nesta URL. - - - Something went wrong. Reboot the device. - Algo deu errado. Reinicie o dispositivo. - - - Select a language - Selecione o Idioma - - - Choose Software to Install - Escolha o Software a ser Instalado - - - sunnypilot - sunnypilot - - - Custom Software - Software Customizado + Developer + Desenvdor @@ -1679,6 +1878,14 @@ Isso pode levar até um minuto. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1687,6 +1894,22 @@ Isso pode levar até um minuto. Select a branch Selecione uma branch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Reiniciar + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1744,22 +1967,6 @@ Isso pode levar até um minuto. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -1784,24 +1991,20 @@ Isso pode levar até um minuto. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1856,6 +2059,50 @@ Isso pode levar até um minuto. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1908,11 +2155,11 @@ Isso pode levar até um minuto. Welcome to sunnypilot - Bem vindo ao sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - Você deve aceitar os Termos e Condições para usar o sunnypilot. Leia os termos mais recentes em <span style='color: #465BEA;'>https://comma.ai/terms</span> antes de continuar. + @@ -1945,22 +2192,10 @@ Isso pode levar até um minuto. Disengage on Accelerator Pedal Desacionar com Pedal do Acelerador - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - Quando ativado, pressionar o pedal do acelerador desacionará o sunnypilot. - Experimental Mode Modo Experimental - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot por padrão funciona em <b>modo chill</b>. modo Experimental ativa <b>recursos de nível-embrionário</b> que não estão prontos para o modo chill. Recursos experimentais estão listados abaixo: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - Deixe o modelo de IA controlar o acelerador e os freios. O sunnypilot irá dirigir como pensa que um humano faria, incluindo parar em sinais vermelhos e sinais de parada. Uma vez que o modelo de condução decide a velocidade a conduzir, a velocidade definida apenas funcionará como um limite superior. Este é um recurso de qualidade embrionária; erros devem ser esperados. - New Driving Visualization Nova Visualização de Condução @@ -1969,6 +2204,10 @@ Isso pode levar até um minuto. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. O modo Experimental está atualmente indisponível para este carro já que o ACC original do carro é usado para controle longitudinal. + + openpilot longitudinal control may come in a future update. + O controle longitudinal openpilot poderá vir em uma atualização futura. + Aggressive Disputa @@ -1985,22 +2224,10 @@ Isso pode levar até um minuto. Driving Personality Temperamento de Direção - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - Uma versão embrionária do controle longitudinal sunnypilot pode ser testada em conjunto com o modo Experimental, em branches que não sejam de produção. - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Habilite o controle longitudinal (embrionário) sunnypilot para permitir o modo Experimental. - End-to-End Longitudinal Control Controle Longitudinal de Ponta a Ponta - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - Neutro é o recomendado. No modo disputa o sunnypilot seguirá o carro da frente mais de perto e será mais agressivo com a aceleração e frenagem. No modo calmo o sunnypilot se manterá mais longe do carro da frente. Em carros compatíveis, você pode alternar esses temperamentos com o botão de distância do volante. - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. A visualização de condução fará a transição para a câmera grande angular voltada para a estrada em baixas velocidades para mostrar melhor algumas curvas. O logotipo do modo Experimental também será mostrado no canto superior direito. @@ -2010,8 +2237,24 @@ Isso pode levar até um minuto. Monitoramento do Motorista Sempre Ativo - Enable driver monitoring even when sunnypilot is not engaged. - Habilite o monitoramento do motorista mesmo quando o sunnypilot não estiver acionado. + Changing this setting will restart openpilot if the car is powered on. + Alterar esta configuração fará com que o openpilot reinicie se o carro estiver ligado. + + + Record and Upload Microphone Audio + Gravar e Fazer Upload do Ãudio do Microfone + + + 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. + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2022,55 +2265,86 @@ Isso pode levar até um minuto. - Enable sunnypilot - Ativar sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Updater + TreeOptionDialog - Update Required - Atualização Necessária + Select + Selecione - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - Uma atualização do sistema operacional é necessária. Conecte seu dispositivo ao Wi-Fi para a experiência de atualização mais rápida. O tamanho do download é de aproximadamente 1GB. + Cancel + Cancelar + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - Conecte-se ao Wi-Fi + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - Instalar + Changing this setting will restart openpilot if the car is powered on. + Alterar esta configuração fará com que o openpilot reinicie se o carro estiver ligado. - Back - Voltar + Off + - Loading... - Carregando... + Distance + - Reboot - Reiniciar + Speed + - Update failed - Falha na atualização + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2080,12 +2354,12 @@ Isso pode levar até um minuto. Abrir - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + Maximize seus envios de dados de treinamento para melhorar os modelos de direção do openpilot. - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Modo Firehose <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_th.ts b/selfdrive/ui/translations/main_th.ts index 2c3d0bdb6c..7b89f2a694 100644 --- a/selfdrive/ui/translations/main_th.ts +++ b/selfdrive/ui/translations/main_th.ts @@ -7,10 +7,6 @@ Close ปิด - - Snooze Update - เลื่อนà¸à¸²à¸£à¸­à¸±à¸›à¹€à¸”ต - Reboot and Update รีบูตà¹à¸¥à¸°à¸­à¸±à¸›à¹€à¸”ต @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - คุณต้องยอมรับเงื่อนไขà¹à¸¥à¸°à¸‚้อตà¸à¸¥à¸‡ เพื่อใช้งาน sunnypilot - Back ย้อนà¸à¸¥à¸±à¸š @@ -160,23 +171,51 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 ปà¸à¸´à¹€à¸ªà¸˜ à¹à¸¥à¸°à¸–อนà¸à¸²à¸£à¸•ิดตั้ง %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel Joystick Debug Mode - + โหมดดีบัà¸à¸ˆà¸­à¸¢à¸ªà¸•ิ๊ภLongitudinal Maneuver Mode - + โหมดà¸à¸²à¸£à¸„วบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรค + + + openpilot Longitudinal Control (Alpha) + ระบบควบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรคโดย openpilot (Alpha) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + คำเตือน: à¸à¸²à¸£à¸„วบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรคโดย openpilot สำหรับรถคันนี้ยังอยู่ในสถานะ alpha à¹à¸¥à¸°à¸£à¸°à¸šà¸šà¹€à¸šà¸£à¸„ฉุà¸à¹€à¸‰à¸´à¸™à¸­à¸±à¸•โนมัติ (AEB) จะถูà¸à¸›à¸´à¸” Enable ADB - + เปิด ADB ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. + ADB (Android Debug Bridge) อนุà¸à¸²à¸•ให้เชื่อมต่ออุปà¸à¸£à¸“์ของคุณผ่าน USB หรือผ่านเครือข่าย ดูข้อมูลเพิ่มเติมที่ https://docs.comma.ai/how-to/connect-to-comma + + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW ทบทวน - - Review the rules, features, and limitations of sunnypilot - ตรวจสอบà¸à¸Ž คุณสมบัติ à¹à¸¥à¸°à¸‚้อจำà¸à¸±à¸”ของ sunnypilot - Are you sure you want to review the training guide? คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¹„ม่ว่าต้องà¸à¸²à¸£à¸—บทวนคู่มือà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off ปิดเครื่อง - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot à¸à¸³à¸«à¸™à¸”ให้ติดตั้งอุปà¸à¸£à¸“์ โดยสามารถเอียงด้านซ้ายหรือขวาไม่เà¸à¸´à¸™ 4° à¹à¸¥à¸°à¹€à¸­à¸µà¸¢à¸‡à¸‚ึ้นด้านบนไม่เà¸à¸´à¸™ 5° หรือเอียงลงด้านล่างไม่เà¸à¸´à¸™ 9° sunnypilot ทำà¸à¸²à¸£à¸„าลิเบรทอย่างต่อเนื่อง à¹à¸—บจะไม่จำเป็นต้องทำà¸à¸²à¸£à¸£à¸µà¹€à¸‹à¹‡à¸•à¸à¸²à¸£à¸„าลิเบรท - Your device is pointed %1° %2 and %3° %4. อุปà¸à¸£à¸“์ของคุณเอียงไปทาง %2 %1° à¹à¸¥à¸° %4 %3° @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + + + + + +Steering lag calibration is %1% complete. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + Steering torque response calibration is complete. + + + + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¹„ม่ว่าต้องà¸à¸²à¸£à¸—บทวนคู่มือà¸à¸²à¸£à¹ƒà¸Šà¹‰à¸‡à¸²à¸™? @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language เลือà¸à¸ à¸²à¸©à¸² + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot รีบูต @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - ยืนยัน + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an à¸à¸³à¸¥à¸±à¸‡à¹€à¸›à¸´à¸”à¸à¸¥à¹‰à¸­à¸‡ + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +596,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - Firehose Mode: ACTIVE - + โหมดสายยางดับเพลิง: เปิดใช้งาน ACTIVE - - - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - + เปิดใช้งาน <b>%n segment(s)</b> of your driving is in the training dataset so far. - - + + มีà¸à¸²à¸£à¸‚ับขี่ของคุณ <b>%n เซà¸à¹€à¸¡à¸™à¸•์</b> อยู่ในชุดข้อมูลà¸à¸²à¸£à¸à¸¶à¸à¸à¸™à¹à¸¥à¹‰à¸§à¹ƒà¸™à¸‚ณะนี้ + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>ไม่เปิดใช้งาน</span>: เชื่อมต่อà¸à¸±à¸šà¹€à¸„รือข่ายที่ไม่จำà¸à¸±à¸”ข้อมูล + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -526,7 +625,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -634,6 +733,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -651,6 +758,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,11 +788,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -673,18 +807,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -701,6 +823,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -713,6 +839,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -760,31 +890,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp เลือภ- No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +943,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -843,6 +982,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -859,6 +1006,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel ยà¸à¹€à¸¥à¸´à¸ + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -966,14 +1145,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Device temperature too high. System cooling down before starting. Current internal component temperature: %1 อุณหภูมิของอุปà¸à¸£à¸“์สูงเà¸à¸´à¸™à¹„ป ระบบà¸à¸³à¸¥à¸±à¸‡à¸—ำความเย็นà¸à¹ˆà¸­à¸™à¹€à¸£à¸´à¹ˆà¸¡ อุณหภูมิของชิ้นส่วนภายในปัจจุบัน: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - à¸à¸£à¸¸à¸“าเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดทเดี๋ยวนี้ ถ้าคุณไม่เชื่อมต่ออินเตอร์เน็ต sunnypilot จะไม่ทำงานในอีภ%1 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - à¸à¸£à¸¸à¸“าเชื่อมต่ออินเตอร์เน็ตเพื่อตรวจสอบอัปเดท sunnypilot จะไม่เริ่มทำงานอัตโนมัติจนà¸à¸§à¹ˆà¸²à¸ˆà¸°à¹„ด้เชื่อมต่อà¸à¸±à¸šà¸­à¸´à¸™à¹€à¸•อร์เน็ตเพื่อตรวจสอบอัปเดท - Unable to download updates %1 @@ -988,28 +1159,46 @@ 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. à¸à¸³à¸¥à¸±à¸‡à¸”าวน์โหลดอัปเดทสำหรับระบบปà¸à¸´à¸šà¸±à¸•ิà¸à¸²à¸£à¸­à¸¢à¸¹à¹ˆà¹€à¸šà¸·à¹‰à¸­à¸‡à¸«à¸¥à¸±à¸‡ คุณจะได้รับà¸à¸²à¸£à¹à¸ˆà¹‰à¸‡à¹€à¸•ือนเมื่อระบบพร้อมสำหรับà¸à¸²à¸£à¸•ิดตั้ง - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - ไม่สามารถลงทะเบียนอุปà¸à¸£à¸“์ได้ อุปà¸à¸£à¸“์จะไม่สามารถเชื่อมต่อหรืออัปโหลดไปยังเซิร์ฟเวอร์ของ comma.ai ได้à¹à¸¥à¸°à¸ˆà¸°à¹„ม่ได้รับà¸à¸²à¸£à¸ªà¸™à¸±à¸šà¸ªà¸™à¸¸à¸™à¸ˆà¸²à¸ comma.ai ถ้านี่คืออุปà¸à¸£à¸“์อย่างเป็นทางà¸à¸²à¸£ à¸à¸£à¸¸à¸“าติดต่อ https://comma.ai/support - NVMe drive not mounted. ไม่ได้ติดตั้งไดร์ฟ NVMe - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - ตรวจพบไดร์ฟ NVMe ที่ไม่รองรับ อุปà¸à¸£à¸“์อาจใช้พลังงานมาà¸à¸‚ึ้นà¹à¸¥à¸°à¸£à¹‰à¸­à¸™à¹€à¸à¸´à¸™à¹„ปเนื่องจาà¸à¹„ดร์ฟ NVMe ที่ไม่รองรับ + 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. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + เลื่อนà¸à¸²à¸£à¸­à¸±à¸›à¹€à¸”ต + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + sunnypilot 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. - sunnypilot ไม่สามารถระบุรถยนต์ของคุณได้ ระบบอาจไม่รองรับรถยนต์ของคุณหรือไม่รู้จัภECU à¸à¸£à¸¸à¸“าส่ง pull request เพื่อเพิ่มรุ่นของเฟิร์มà¹à¸§à¸£à¹Œà¹ƒà¸«à¹‰à¸à¸±à¸šà¸£à¸–ยนต์ที่เหมาะสม หาà¸à¸•้องà¸à¸²à¸£à¸„วามช่วยเหลือให้เข้าร่วม discord.comma.ai + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot ตรวจพบà¸à¸²à¸£à¹€à¸›à¸¥à¸µà¹ˆà¸¢à¸™à¹à¸›à¸¥à¸‡à¸‚องตำà¹à¸«à¸™à¹ˆà¸‡à¸—ี่ติดตั้ง à¸à¸£à¸¸à¸“าตรวจสอบว่าได้เลื่อนอุปà¸à¸£à¸“์เข้าà¸à¸±à¸šà¸ˆà¸¸à¸”ติดตั้งจนสุดà¹à¸¥à¹‰à¸§ à¹à¸¥à¸°à¸ˆà¸¸à¸”ติดตั้งได้ยึดติดà¸à¸±à¸šà¸à¸£à¸°à¸ˆà¸à¸«à¸™à¹‰à¸²à¸­à¸¢à¹ˆà¸²à¸‡à¹à¸™à¹ˆà¸™à¸«à¸™à¸² + - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1029,11 +1218,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - sunnypilot ไม่สามารถใช้งานได้ + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY เข้าควบคุมรถเดี๋ยวนี้ @@ -1044,10 +1236,145 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Waiting to start - + รอเริ่มทำงาน System Unresponsive + ระบบไม่ตอบสนอง + + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + ตรวจสอบ + + + Country + + + + SELECT + เลือภ+ + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + อัปเดต + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: @@ -1071,7 +1398,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Please connect to Wi-Fi to complete initial pairing - + à¸à¸£à¸¸à¸“าเชื่อมต่อ Wi-Fi เพื่อทำà¸à¸²à¸£à¸ˆà¸±à¸šà¸„ู่ครั้งà¹à¸£à¸à¹ƒà¸«à¹‰à¹€à¸ªà¸£à¹‡à¸ˆà¸ªà¸´à¹‰à¸™ @@ -1136,7 +1463,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - ยืนยัน + Cancel @@ -1203,7 +1530,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Remote snapshots - + ภาพถ่ายระยะไà¸à¸¥ @@ -1219,10 +1546,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1568,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now ตอนนี้ - - - Reset - Reset failed. Reboot to try again. - à¸à¸²à¸£à¸£à¸µà¹€à¸‹à¹‡à¸•ล้มเหลว รีบูตเพื่อลองอีà¸à¸„รั้ง - - - Are you sure you want to reset your device? - คุณà¹à¸™à¹ˆà¹ƒà¸ˆà¸«à¸£à¸·à¸­à¹„ม่ว่าต้องà¸à¸²à¸£à¸£à¸µà¹€à¸‹à¹‡à¸•อุปà¸à¸£à¸“์? - - - System Reset - รีเซ็ตระบบ - - - Cancel - ยà¸à¹€à¸¥à¸´à¸ - - - Reboot - รีบูต - - - Confirm - ยืนยัน - - - Resetting device... -This may take up to a minute. - à¸à¸³à¸¥à¸±à¸‡à¸£à¸µà¹€à¸‹à¹‡à¸•อุปà¸à¸£à¸“์... -อาจใช้เวลาถึงหนึ่งนาที - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - ไม่สามารถเมานต์พาร์ติชั่นข้อมูลได้ พาร์ติชั่นอาจเสียหาย à¸à¸”ยืนยันเพื่อลบà¹à¸¥à¸°à¸£à¸µà¹€à¸‹à¹‡à¸•อุปà¸à¸£à¸“์ของคุณ - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - ระบบถูà¸à¸£à¸µà¹€à¸‹à¹‡à¸• à¸à¸”ยืนยันเพื่อลบข้อมูลà¹à¸¥à¸°à¸à¸²à¸£à¸•ั้งค่าทั้งหมด à¸à¸”ยà¸à¹€à¸¥à¸´à¸à¹€à¸žà¸·à¹ˆà¸­à¸šà¸¹à¸•ต่อ + sunnypilot + @@ -1311,11 +1597,11 @@ This may take up to a minute. Developer - + นัà¸à¸žà¸±à¸’นา Firehose - + สายยางดับเพลิง @@ -1345,29 +1631,13 @@ This may take up to a minute. ซอฟต์à¹à¸§à¸£à¹Œ - Trips - - - - Vehicle - - - - Developer - - - - Firehose + Models Steering - - Models - - Cruise @@ -1376,100 +1646,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - คำเตือน: à¹à¸£à¸‡à¸”ันà¹à¸šà¸•เตอรี่ต่ำ + OSM + - Power your device in a car with a harness or proceed at your own risk. - โปรดต่ออุปà¸à¸£à¸“์ของคุณเข้าà¸à¸±à¸šà¸ªà¸²à¸¢à¸„วบคุมในรถยนต์ หรือดำเนินà¸à¸²à¸£à¸”้วยความเสี่ยงของคุณเอง + Trips + - Power off - ปิดเครื่อง + Vehicle + - Continue - ดำเนินà¸à¸²à¸£à¸•่อ + Firehose + สายยางดับเพลิง - Getting Started - เริ่มà¸à¸±à¸™à¹€à¸¥à¸¢ - - - Before we get on the road, let’s finish installation and cover some details. - à¸à¹ˆà¸­à¸™à¸­à¸­à¸à¹€à¸”ินทาง เรามาทำà¸à¸²à¸£à¸•ิดตั้งซอฟต์à¹à¸§à¸£à¹Œ à¹à¸¥à¸°à¸•รวจสอบà¸à¸²à¸£à¸•ั้งค่า - - - Connect to Wi-Fi - เชื่อมต่อ Wi-Fi - - - Back - ย้อนà¸à¸¥à¸±à¸š - - - Continue without Wi-Fi - ดำเนินà¸à¸²à¸£à¸•่อโดยไม่ใช้ Wi-Fi - - - Waiting for internet - à¸à¸³à¸¥à¸±à¸‡à¸£à¸­à¸ªà¸±à¸à¸à¸²à¸“อินเตอร์เน็ต - - - Enter URL - ป้อน URL - - - for Custom Software - สำหรับซอฟต์à¹à¸§à¸£à¹Œà¸—ี่à¸à¸³à¸«à¸™à¸”เอง - - - Downloading... - à¸à¸³à¸¥à¸±à¸‡à¸”าวน์โหลด... - - - Download Failed - ดาวน์โหลดล้มเหลว - - - Ensure the entered URL is valid, and the device’s internet connection is good. - ตรวจสอบให้à¹à¸™à¹ˆà¹ƒà¸ˆà¸§à¹ˆà¸² URL ที่ป้อนนั้นถูà¸à¸•้อง à¹à¸¥à¸°à¸­à¸¸à¸›à¸à¸£à¸“์เชื่อมต่ออินเทอร์เน็ตอยู่ - - - Reboot device - รีบูตอุปà¸à¸£à¸“์ - - - Start over - เริ่มต้นใหม่ - - - Something went wrong. Reboot the device. - มีบางอย่างผิดพลาด รีบูตอุปà¸à¸£à¸“์ - - - No custom software found at this URL. - ไม่พบซอฟต์à¹à¸§à¸£à¹Œà¸—ี่à¸à¸³à¸«à¸™à¸”เองที่ URL นี้ - - - Select a language - เลือà¸à¸ à¸²à¸©à¸² - - - Choose Software to Install - เลือà¸à¸‹à¸­à¸Ÿà¸•์à¹à¸§à¸£à¹Œà¸—ี่จะติดตั้ง - - - sunnypilot - sunnypilot - - - Custom Software - ซอฟต์à¹à¸§à¸£à¹Œà¸—ี่à¸à¸³à¸«à¸™à¸”เอง + Developer + นัà¸à¸žà¸±à¸’นา @@ -1674,6 +1869,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1885,22 @@ This may take up to a minute. Select a branch เลือภBranch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + รีบูต + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1739,22 +1958,6 @@ This may take up to a minute. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - ไม่มี - Sponsor Status @@ -1779,24 +1982,20 @@ This may take up to a minute. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + ไม่มี + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1851,6 +2050,50 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1940,22 +2183,10 @@ This may take up to a minute. Disengage on Accelerator Pedal ยà¸à¹€à¸¥à¸´à¸à¸£à¸°à¸šà¸šà¸Šà¹ˆà¸§à¸¢à¸‚ับเมื่อเหยียบคันเร่ง - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - เมื่อเปิดใช้งาน à¸à¸²à¸£à¸à¸”à¹à¸›à¹‰à¸™à¸„ันเร่งจะเป็นà¸à¸²à¸£à¸¢à¸à¹€à¸¥à¸´à¸à¸£à¸°à¸šà¸šà¸Šà¹ˆà¸§à¸¢à¸‚ับโดย sunnypilot - Experimental Mode โหมดทดลอง - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - โดยปà¸à¸•ิ sunnypilot จะขับใน<b>โหมดชิล</b> เปิดโหมดทดลองเพื่อใช้<b>ความสามารถในขั้นพัฒนา</b> ซึ่งยังไม่พร้อมสำหรับโหมดชิล ความสามารถในขั้นพัฒนามีดังนี้: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - ให้ sunnypilot ควบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรค โดย sunnypilot จะขับอย่างที่มนุษย์คิด รวมถึงà¸à¸²à¸£à¸«à¸¢à¸¸à¸”ที่ไฟà¹à¸”ง à¹à¸¥à¸°à¸›à¹‰à¸²à¸¢à¸«à¸¢à¸¸à¸”รถ เนื่องจาภsunnypilot จะà¸à¸³à¸«à¸™à¸”ความเร็วในà¸à¸²à¸£à¸‚ับด้วยตัวเอง à¸à¸²à¸£à¸•ั้งความเร็วจะเป็นเพียงà¸à¸²à¸£à¸à¸³à¸«à¸™à¸”ความเร็วสูงสูดเท่านั้น ความสามารถนี้ยังอยู่ในขั้นพัฒนา อาจเà¸à¸´à¸”ข้อผิดพลาดขึ้นได้ - New Driving Visualization à¸à¸²à¸£à¹à¸ªà¸”งภาพà¸à¸²à¸£à¸‚ับขี่à¹à¸šà¸šà¹ƒà¸«à¸¡à¹ˆ @@ -1964,6 +2195,10 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. ขณะนี้โหมดทดลองไม่สามารถใช้งานได้ในรถคันนี้ เนื่องจาà¸à¹€à¸›à¸´à¸”ใช้ระบบควบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรคของรถที่ติดตั้งจาà¸à¹‚รงงานอยู่ + + openpilot longitudinal control may come in a future update. + ระบบควบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรคโดย openpilot อาจมาในà¸à¸²à¸£à¸­à¸±à¸›à¹€à¸”ตในอนาคต + Aggressive ดุดัน @@ -1980,32 +2215,36 @@ This may take up to a minute. Driving Personality บุคลิà¸à¸à¸²à¸£à¸‚ับขี่ - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - ระบบควบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรคโดย sunnypilot เวอร์ชัน alpha สามารถทดสอบได้พร้อมà¸à¸±à¸šà¹‚หมดà¸à¸²à¸£à¸—ดลอง บน branch ที่à¸à¸³à¸¥à¸±à¸‡à¸žà¸±à¸’นา - End-to-End Longitudinal Control ควบคุมเร่ง/เบรคà¹à¸šà¸š End-to-End - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - เปิดระบบควบคุมà¸à¸²à¸£à¹€à¸£à¹ˆà¸‡/เบรคโดย sunnypilot (alpha) เพื่อเปิดใช้งานโหมดทดลอง - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - à¹à¸™à¸°à¸™à¸³à¹ƒà¸«à¹‰à¹ƒà¸Šà¹‰à¹à¸šà¸šà¸¡à¸²à¸•รà¸à¸²à¸™ ในโหมดดุดัน sunnypilot จะตามรถคันหน้าใà¸à¸¥à¹‰à¸‚ึ้นà¹à¸¥à¸°à¹€à¸£à¹ˆà¸‡à¹à¸¥à¸°à¹€à¸šà¸£à¸„à¹à¸šà¸šà¸”ุดันมาà¸à¸‚ึ้น ในโหมดผ่อนคลาย sunnypilot จะอยู่ห่างจาà¸à¸£à¸–คันหน้ามาà¸à¸‚ึ้น ในรถรุ่นที่รองรับคุณสามารถเปลี่ยนบุคลิà¸à¹„ปà¹à¸šà¸šà¸•่าง ๆ โดยใช้ปุ่มปรับระยะห่างบนพวงมาลัย - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. à¸à¸²à¸£à¹à¸ªà¸”งภาพà¸à¸²à¸£à¸‚ับขี่จะเปลี่ยนไปใช้à¸à¸¥à¹‰à¸­à¸‡à¸¡à¸¸à¸¡à¸à¸§à¹‰à¸²à¸‡à¸—ี่หันหน้าไปทางถนนเมื่ออยู่ในความเร็วต่ำ เพื่อà¹à¸ªà¸”งภาพà¸à¸²à¸£à¹€à¸¥à¸µà¹‰à¸¢à¸§à¸—ี่ดีขึ้น โลโà¸à¹‰à¹‚หมดà¸à¸²à¸£à¸—ดลองจะà¹à¸ªà¸”งที่มุมบนขวาด้วย Always-On Driver Monitoring + à¸à¸²à¸£à¹€à¸à¹‰à¸²à¸£à¸°à¸§à¸±à¸‡à¸œà¸¹à¹‰à¸‚ับขี่ตลอดเวลา + + + Changing this setting will restart openpilot if the car is powered on. - Enable driver monitoring even when sunnypilot is not engaged. + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2017,11 +2256,53 @@ This may take up to a minute. - Enable sunnypilot - เปิดใช้งาน sunnypilot + When enabled, pressing the accelerator pedal will disengage sunnypilot. + - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable driver monitoring even when sunnypilot is not engaged. + + + + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + + + + + TreeOptionDialog + + Select + เลือภ+ + + Cancel + ยà¸à¹€à¸¥à¸´à¸ + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2029,58 +2310,47 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - จำเป็นต้องอัปเดต + Distance + - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - จำเป็นต้องมีà¸à¸²à¸£à¸­à¸±à¸›à¹€à¸”ตระบบปà¸à¸´à¸šà¸±à¸•ิà¸à¸²à¸£ เชื่อมต่ออุปà¸à¸£à¸“์ของคุณà¸à¸±à¸š Wi-Fi เพื่อประสบà¸à¸²à¸£à¸“์à¸à¸²à¸£à¸­à¸±à¸›à¹€à¸”ตที่เร็วที่สุด ขนาดดาวน์โหลดประมาณ 1GB + Speed + - Connect to Wi-Fi - เชื่อมต่อà¸à¸±à¸š Wi-Fi + Time + - Install - ติดตั้ง + All + - Back - ย้อนà¸à¸¥à¸±à¸š + Display Metrics Below Chevron + - Loading... - à¸à¸³à¸¥à¸±à¸‡à¹‚หลด... - - - Reboot - รีบูต - - - Update failed - à¸à¸²à¸£à¸­à¸±à¸›à¹€à¸”ตล้มเหลว + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + WiFiPromptWidget Open - - - - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - + เปิด Maximize your training data uploads to improve openpilot's driving models. - + อัปโหลดข้อมูลà¸à¸²à¸£à¸à¸¶à¸à¸à¸™à¹ƒà¸«à¹‰à¹„ด้มาà¸à¸—ี่สุด เพื่อพัฒนาโมเดลà¸à¸²à¸£à¸‚ับขี่ของ openpilot + + + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> โหมดสายยางดับเพลิง <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_tr.ts b/selfdrive/ui/translations/main_tr.ts index fd06e81ef1..0598d08364 100644 --- a/selfdrive/ui/translations/main_tr.ts +++ b/selfdrive/ui/translations/main_tr.ts @@ -7,10 +7,6 @@ Close Kapat - - Snooze Update - Güncellemeyi sessize al - Reboot and Update Güncelle ve Yeniden baÅŸlat @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - Openpilotu kullanmak için Kullanıcı KoÅŸullarını kabul etmelisiniz. - Back Geri @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 Reddet, Kurulumu kaldır. %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode + + openpilot Longitudinal Control (Alpha) + + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + + Enable ADB @@ -179,6 +202,22 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW GÖZDEN GEÇİR - - Review the rules, features, and limitations of sunnypilot - sunnypilot sisteminin kurallarını ve sınırlamalarını gözden geçirin. - Are you sure you want to review the training guide? EÄŸitim kılavuzunu incelemek istediÄŸinizden emin misiniz? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off Sistemi kapat - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot, cihazın 4° sola veya 5° yukarı yada 9° aÅŸağı bakıcak ÅŸekilde monte edilmesi gerekmektedir. sunnypilot sürekli kendisini kalibre edilmektedir ve nadiren sıfırlama gerebilir. - Your device is pointed %1° %2 and %3° %4. Cihazınız %1° %2 ve %3° %4 yönünde ayarlı @@ -359,12 +390,44 @@ Please use caution when using this feature. Only use the blinker when traffic an - Resetting calibration will restart openpilot if the car is powered on. + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + + + + + +Steering lag calibration is %1% complete. + + + + + +Steering lag calibration is complete. + + + + Steering torque response calibration is %1% complete. + + + + Steering torque response calibration is complete. + + + + Review the rules, features, and limitations of sunnypilot + + + + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +444,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? EÄŸitim kılavuzunu incelemek istediÄŸinizden emin misiniz? @@ -393,6 +460,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language Dil seçin + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot Yeniden baÅŸlat @@ -411,7 +491,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - Onayla + Are you sure you want to enter Always Offroad mode? @@ -421,22 +501,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +513,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +568,21 @@ Please use caution when using this feature. Only use the blinker when traffic an kamera baÅŸlatılıyor + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,10 +596,6 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - - Firehose Mode: ACTIVE @@ -509,16 +604,20 @@ Please use caution when using this feature. Only use the blinker when traffic an ACTIVE - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - - <b>%n segment(s)</b> of your driving is in the training dataset so far. + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + + + + Firehose Mode + + sunnypilot learns to drive by watching humans, like you, drive. @@ -526,7 +625,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. @@ -634,6 +733,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -651,6 +758,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,11 +788,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -673,18 +807,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -701,6 +823,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -713,6 +839,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -760,31 +890,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +943,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -843,6 +982,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -859,6 +1006,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -966,14 +1145,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Device temperature too high. System cooling down before starting. Current internal component temperature: %1 - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - - Unable to download updates %1 @@ -987,16 +1158,32 @@ 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. - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - - NVMe drive not mounted. - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. + 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. + + + + Acknowledge Excessive Actuation + + + + Snooze Update + Güncellemeyi sessize al + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. @@ -1008,7 +1195,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1028,11 +1217,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable + ALWAYS OFFROAD ACTIVE + + + OnroadAlerts TAKE CONTROL IMMEDIATELY @@ -1049,6 +1241,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + KONTROL ET + + + Country + + + + SELECT + + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + GÜNCELLE + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1135,7 +1462,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - Onayla + Cancel @@ -1218,10 +1545,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1244,44 +1567,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now - - - Reset - Reset failed. Reboot to try again. - Sıfırlama baÅŸarız oldu. Cihazı yeniden baÅŸlatın ve tekrar deneyin. - - - Are you sure you want to reset your device? - Cihazı sıfırlamak istediÄŸinizden eminmisiniz ? - - - System Reset - Sistemi sıfırla - - - Cancel - İptal - - - Reboot - Yeniden baÅŸlat - - - Confirm - Onayla - - - Resetting device... -This may take up to a minute. - - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. + sunnypilot @@ -1343,29 +1630,13 @@ This may take up to a minute. Yazılım - Trips - - - - Vehicle - - - - Developer - - - - Firehose + Models Steering - - Models - - Cruise @@ -1374,99 +1645,24 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - UYARI: Düşük voltaj - - - Power your device in a car with a harness or proceed at your own risk. - Cihazınızı emniyet kemeri olan bir arabada çalıştırın veya riski kabul ederek devam edin. - - - Power off - Sistemi kapat - - - Continue - Devam et - - - Getting Started - BaÅŸlarken - - - Before we get on the road, let’s finish installation and cover some details. - Yola çıkmadan önce kurulumu bitirin ve bazı detayları gözden geçirin.. - - - Connect to Wi-Fi - Wi-Fi ile baÄŸlan - - - Back - Geri - - - Continue without Wi-Fi - Wi-Fi baÄŸlantısı olmadan devam edin - - - Waiting for internet - İnternet baÄŸlantısı bekleniyor. - - - Enter URL - URL girin - - - for Custom Software - özel yazılım için - - - Downloading... - İndiriliyor... - - - Download Failed - İndirme baÅŸarısız. - - - Ensure the entered URL is valid, and the device’s internet connection is good. - Girilen URL nin geçerli olduÄŸundan ve cihazın internet baÄŸlantısının olduÄŸunu kontrol edin - - - Reboot device - Cihazı yeniden baÅŸlat - - - Start over - Zacznij od poczÄ…tku - - - Something went wrong. Reboot the device. + OSM - No custom software found at this URL. + Trips - Select a language - Dil seçin - - - Choose Software to Install + Vehicle - sunnypilot - sunnypilot + Firehose + - Custom Software + Developer @@ -1672,6 +1868,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1680,6 +1884,22 @@ This may take up to a minute. Select a branch + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + Yeniden baÅŸlat + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1737,22 +1957,6 @@ This may take up to a minute. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -1777,24 +1981,20 @@ This may take up to a minute. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1849,6 +2049,50 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1934,10 +2178,6 @@ This may take up to a minute. Upload data from the driver facing camera and help improve the driver monitoring algorithm. Sürücüye bakan kamera verisini yükleyin ve Cihazın algoritmasını geliÅŸtirmemize yardımcı olun. - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - AktifleÅŸtirilirse eÄŸer gaz pedalına basınca sunnypilot devre dışı kalır. - Experimental Mode @@ -1962,18 +2202,10 @@ This may take up to a minute. Driving Personality - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - - End-to-End Longitudinal Control - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - - New Driving Visualization @@ -1983,15 +2215,7 @@ This may take up to a minute. - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - - - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + openpilot longitudinal control may come in a future update. @@ -2003,7 +2227,23 @@ This may take up to a minute. - Enable driver monitoring even when sunnypilot is not engaged. + Changing this setting will restart openpilot if the car is powered on. + + + + Record and Upload Microphone Audio + + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. @@ -2015,11 +2255,53 @@ This may take up to a minute. - Enable sunnypilot - sunnypilot'u aktifleÅŸtir + When enabled, pressing the accelerator pedal will disengage sunnypilot. + - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable driver monitoring even when sunnypilot is not engaged. + + + + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. + + + + + TreeOptionDialog + + Select + Seç + + + Cancel + + + + + VisualsPanel + + Show Blind Spot Warnings + + + + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. @@ -2027,43 +2309,32 @@ This may take up to a minute. - openpilot longitudinal control may come in a future update. + Off - - - Updater - Update Required - Güncelleme yapılması gerekli + Distance + - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - İşletim sistemi güncellemesi gerekmektedir. Lütfen Cihazı daha hızlı günceleyebilmesi için bir sonraki baÅŸlatılışında çekilir. Wi-Fi ağına baÄŸlayın. Dosyanın boyutu yaklaşık 1GB dır. + Speed + - Connect to Wi-Fi - Wi-Fi ağına baÄŸlan + Time + - Install - Yükle + All + - Back - Geri + Display Metrics Below Chevron + - Loading... - Yükleniyor... - - - Reboot - Yeniden baÅŸlat - - - Update failed - Güncelleme baÅŸarız oldu + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2073,11 +2344,11 @@ This may take up to a minute. - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. - Maximize your training data uploads to improve openpilot's driving models. + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_zh-CHS.ts b/selfdrive/ui/translations/main_zh-CHS.ts index 06c6c67152..1f4db2d6d0 100644 --- a/selfdrive/ui/translations/main_zh-CHS.ts +++ b/selfdrive/ui/translations/main_zh-CHS.ts @@ -7,10 +7,6 @@ Close 关闭 - - Snooze Update - æš‚åœæ›´æ–° - Reboot and Update é‡å¯å¹¶æ›´æ–° @@ -40,7 +36,7 @@ IP Address - IPåœ°å€ + IP åœ°å€ Enable Roaming @@ -48,11 +44,11 @@ APN Setting - APN设置 + APN 设置 Enter APN - 输入APN + 输入 APN leave blank for automatic configuration @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + 在按æµé‡è®¡è´¹çš„ç§»åŠ¨ç½‘ç»œä¸Šï¼Œé˜²æ­¢ä¸Šä¼ å¤§æ•°æ® default - + 默认 metered - + 按æµé‡è®¡è´¹ unmetered - + 䏿Œ‰æµé‡è®¡è´¹ Wi-Fi Network Metered - + 按æµé‡è®¡è´¹çš„ WLAN 网络 Prevent large data uploads when on a metered Wi-Fi connection - + 在按æµé‡è®¡è´¹çš„ WLAN ç½‘ç»œä¸Šï¼Œé˜²æ­¢ä¸Šä¼ å¤§æ•°æ® @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - æ‚¨å¿…é¡»æŽ¥å—æ¡æ¬¾å’Œæ¡ä»¶ä»¥ä½¿ç”¨sunnypilot。 - Back 返回 @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 æ‹’ç»å¹¶å¸è½½%1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode çºµå‘æ“æŽ§æµ‹è¯•æ¨¡å¼ + + openpilot Longitudinal Control (Alpha) + openpilotçºµå‘æŽ§åˆ¶ï¼ˆAlpha 版) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 警告:此车辆的 openpilot çºµå‘æŽ§åˆ¶åŠŸèƒ½ç›®å‰å¤„于Alpha版本,使用此功能将会åœç”¨è‡ªåŠ¨ç´§æ€¥åˆ¶åŠ¨ï¼ˆAEB)功能。 + Enable ADB å¯ç”¨ ADB @@ -179,6 +202,22 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android调试桥接)å…许通过USB或网络连接到您的设备。更多信æ¯è¯·å‚è§ [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 查看 - - Review the rules, features, and limitations of sunnypilot - 查看 sunnypilot 的使用规则,以åŠå…¶åŠŸèƒ½å’Œé™åˆ¶ - Are you sure you want to review the training guide? æ‚¨ç¡®å®šè¦æŸ¥çœ‹æ–°æ‰‹æŒ‡å—å—? @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 关机 - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilotè¦æ±‚设备安装的å航角在左4°和å³4°之间,俯仰角在上5°和下9°之间。一般æ¥è¯´ï¼Œsunnypilot会æŒç»­æ›´æ–°æ ¡å‡†ï¼Œå¾ˆå°‘需è¦é‡ç½®ã€‚ - Your device is pointed %1° %2 and %3° %4. 您的设备校准为%1° %2ã€%3° %4。 @@ -324,7 +355,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reboot - å–æ¶ˆsunnypilot以釿–°å¯åЍ + å–æ¶ˆopenpilot以釿–°å¯åЍ Are you sure you want to power off? @@ -332,7 +363,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Power Off - å–æ¶ˆsunnypilot以关机 + å–æ¶ˆopenpilot以关机 Reset @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration + 解除以é‡ç½®æ ¡å‡† + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot 会æŒç»­è¿›è¡Œæ ¡å‡†ï¼Œå› æ­¤å¾ˆå°‘需è¦é‡ç½®ã€‚如果车辆电æºå·²å¼€å¯ï¼Œé‡ç½®æ ¡å‡†ä¼šé‡æ–°å¯åЍ openpilot。 + + + + +Steering lag calibration is %1% complete. + + +转å‘å»¶è¿Ÿæ ¡å‡†å·²å®Œæˆ %1%。 + + + + +Steering lag calibration is complete. + + +转å‘延迟校准已完æˆã€‚ + + + Steering torque response calibration is %1% complete. + è½¬å‘æ‰­çŸ©å“åº”æ ¡å‡†å·²å®Œæˆ %1%。 + + + Steering torque response calibration is complete. + è½¬å‘æ‰­çŸ©å“应校准已完æˆã€‚ + + + Review the rules, features, and limitations of sunnypilot - Resetting calibration will restart openpilot if the car is powered on. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? æ‚¨ç¡®å®šè¦æŸ¥çœ‹æ–°æ‰‹æŒ‡å—å—? @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language 选择语言 + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot é‡å¯ @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - 确认 + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an 正在å¯åŠ¨ç›¸æœº + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +600,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 训练数æ®ä¸Šä¼ æ¨¡å¼ 🔥 - Firehose Mode: ACTIVE - 训练数æ®ä¸Šä¼ æ¨¡å¼ï¼šæ¿€æ´»ä¸­ + Firehose 模å¼ï¼šæ¿€æ´»ä¸­ ACTIVE 激活中 - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - 为了达到最佳效果,请æ¯å‘¨å°†æ‚¨çš„设备带回室内,并连接到优质的 USB-C 充电器和 Wi-Fi。<br><br>Firehose 模å¼åœ¨è¡Œé©¶æ—¶ä¹Ÿèƒ½è¿è¡Œï¼Œä½†éœ€è¿žæŽ¥åˆ°ç§»åŠ¨çƒ­ç‚¹æˆ–ä½¿ç”¨ä¸é™æµé‡çš„ SIM å¡ã€‚<br><br><br><b>常è§é—®é¢˜</b><br><br><i>æˆ‘å¼€è½¦çš„æ–¹å¼æˆ–地点有影å“å—?</i>ä¸ä¼šï¼Œè¯·åƒå¹³å¸¸ä¸€æ ·é©¾é©¶å³å¯ã€‚<br><br><i>Firehose 模å¼ä¼šä¸Šä¼ æ‰€æœ‰çš„驾驶片段å—?</i>ä¸ä¼šï¼Œæˆ‘们会选择性地上传部分片段。<br><br><i>什么是好的 USB-C 充电器?</i>任何快速手机或笔记本电脑充电器都应该适用。<br><br><i>我使用的软件版本有影å“å—?</i>æœ‰çš„ï¼Œåªæœ‰å®˜æ–¹ sunnypilot(以åŠç‰¹å®šçš„分支)å¯ä»¥ç”¨äºŽè®­ç»ƒã€‚ - <b>%n segment(s)</b> of your driving is in the training dataset so far. <b>ç›®å‰å·²æœ‰ %n 段</b> 您的驾驶数æ®è¢«çº³å…¥è®­ç»ƒæ•°æ®é›†ã€‚ + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>闲置</span>:请连接到ä¸é™æµé‡çš„网络 + + + Firehose Mode + Firehose æ¨¡å¼ + sunnypilot learns to drive by watching humans, like you, drive. @@ -526,8 +629,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>闲置</span>:请连接到ä¸é™æµé‡çš„网络 + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + @@ -634,6 +737,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -651,6 +762,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,11 +792,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -673,18 +811,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -701,6 +827,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -713,6 +843,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -760,31 +894,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp 选择 - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +947,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -843,6 +986,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -859,6 +1010,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel å–æ¶ˆ + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -962,14 +1145,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - 请立å³è¿žæŽ¥ç½‘络检查更新。如果ä¸è¿žæŽ¥ç½‘络,sunnypilot 将在 %1 åŽä¾¿æ— æ³•使用 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - 请连接至互è”网以检查更新。在连接至互è”ç½‘å¹¶å®Œæˆæ›´æ–°æ£€æŸ¥ä¹‹å‰ï¼Œsunnypilot å°†ä¸ä¼šè‡ªåЍå¯åŠ¨ã€‚ - Unable to download updates %1 @@ -984,32 +1159,50 @@ 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. 一个针对您设备的æ“作系统更新正在åŽå°ä¸‹è½½ä¸­ã€‚当更新准备好安装时,您将收到æç¤ºè¿›è¡Œæ›´æ–°ã€‚ - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - 设备注册失败。它将无法连接或上传至 comma.ai æœåŠ¡å™¨ï¼Œå¹¶ä¸”æ— æ³•èŽ·å¾— comma.ai 的支æŒã€‚如果这是一个官方设备,请访问 https://comma.ai/support。 - NVMe drive not mounted. NVMe固æ€ç¡¬ç›˜æœªè¢«æŒ‚载。 - - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - æ£€æµ‹åˆ°ä¸æ”¯æŒçš„ NVMe 固æ€ç¡¬ç›˜ã€‚æ‚¨çš„è®¾å¤‡å› ä¸ºä½¿ç”¨äº†ä¸æ”¯æŒçš„ NVMe 固æ€ç¡¬ç›˜å¯èƒ½ä¼šæ¶ˆè€—更多电力并更易过热。 - - - sunnypilot 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. - sunnypilot 无法识别您的车辆。您的车辆å¯èƒ½æœªè¢«æ”¯æŒï¼Œæˆ–是其电控å•å…ƒ (ECU) 未被识别。请æäº¤ä¸€ä¸ª Pull Request 为您的车辆添加正确的固件版本。需è¦å¸®åŠ©å—?请加入 discord.comma.ai。 - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot 检测到设备的安装ä½ç½®å‘生å˜åŒ–。请确ä¿è®¾å¤‡å®Œå…¨å®‰è£…åœ¨æ”¯æž¶ä¸Šï¼Œå¹¶ç¡®ä¿æ”¯æž¶ç‰¢å›ºåœ°å›ºå®šåœ¨æŒ¡é£ŽçŽ»ç’ƒä¸Šã€‚ - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 设备温度过高。系统正在冷å´ä¸­ï¼Œç­‰å†·å´å®Œæ¯•åŽæ‰ä¼šå¯åŠ¨ã€‚ç›®å‰å†…部组件温度:%1 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + 设备未能注册到 comma.ai åŽç«¯ã€‚该设备将无法连接或上传数æ®åˆ° comma.ai æœåŠ¡å™¨ï¼Œä¹Ÿæ— æ³•èŽ·å¾— comma.ai 的支æŒã€‚如果该设备是在 comma.ai/shop 购买的,请访问 https://comma.ai/support æäº¤å·¥å•。 + + + Acknowledge Excessive Actuation + + + + Snooze Update + æš‚åœæ›´æ–° + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + + + + sunnypilot 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. + + + + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + + + + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1029,11 +1222,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - 无法使用 sunnypilot + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY ç«‹å³æŽ¥ç®¡ @@ -1050,6 +1246,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive 系统无å“应 + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + 查看 + + + Country + + + + SELECT + 选择 + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + æ›´æ–° + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1136,7 +1467,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - 确认 + Cancel @@ -1219,10 +1550,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1572,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now 现在 - - - Reset - Reset failed. Reboot to try again. - é‡ç½®å¤±è´¥ã€‚ 釿–°å¯åЍ以é‡è¯•。 - - - Are you sure you want to reset your device? - 您确定è¦é‡ç½®æ‚¨çš„设备å—? - - - System Reset - æ¢å¤å‡ºåŽ‚è®¾ç½® - - - Cancel - å–æ¶ˆ - - - Reboot - é‡å¯ - - - Confirm - 确认 - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - 无法挂载数æ®åˆ†åŒºã€‚分区å¯èƒ½å·²ç»æŸå。请确认是å¦è¦åˆ é™¤å¹¶é‡æ–°è®¾ç½®ã€‚ - - - Resetting device... -This may take up to a minute. - 设备é‡ç½®ä¸­â€¦ -è¿™å¯èƒ½éœ€è¦ä¸€åˆ†é’Ÿçš„æ—¶é—´ã€‚ - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 系统é‡ç½®å·²è§¦å‘。按下“确认â€ä»¥æ¸…é™¤æ‰€æœ‰å†…å®¹å’Œè®¾ç½®ï¼ŒæŒ‰ä¸‹â€œå–æ¶ˆâ€ä»¥ç»§ç»­å¯åŠ¨ã€‚ + sunnypilot + @@ -1315,7 +1605,7 @@ This may take up to a minute. Firehose - 训练上传 + Firehose @@ -1345,29 +1635,13 @@ This may take up to a minute. 软件 - Trips + Models - - Vehicle - - - - Developer - å¼€å‘人员 - - - Firehose - 训练上传 - Steering - - Models - - Cruise @@ -1376,100 +1650,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - 警告:低电压 + OSM + - Power your device in a car with a harness or proceed at your own risk. - 请使用car harness线æŸä¸ºæ‚¨çš„设备供电,或自行承担风险。 + Trips + - Power off - 关机 + Vehicle + - Continue - ç»§ç»­ + Firehose + Firehose - Getting Started - 开始设置 - - - Before we get on the road, let’s finish installation and cover some details. - 开始旅程之å‰ï¼Œè®©æˆ‘们完æˆå®‰è£…并介ç»ä¸€äº›ç»†èŠ‚ã€‚ - - - Connect to Wi-Fi - 连接到WiFi - - - Back - 返回 - - - Continue without Wi-Fi - ä¸è¿žæŽ¥WiFiå¹¶ç»§ç»­ - - - Waiting for internet - 等待网络连接 - - - Enter URL - è¾“å…¥ç½‘å€ - - - for Custom Software - 以下载自定义软件 - - - Downloading... - 正在下载…… - - - Download Failed - 下载失败 - - - Ensure the entered URL is valid, and the device’s internet connection is good. - 请确ä¿äº’è”网连接良好且输入的URL有效。 - - - Reboot device - é‡å¯è®¾å¤‡ - - - Start over - 釿¥ - - - No custom software found at this URL. - åœ¨æ­¤ç½‘å€æ‰¾ä¸åˆ°è‡ªå®šä¹‰è½¯ä»¶ã€‚ - - - Something went wrong. Reboot the device. - å‘ç”Ÿäº†ä¸€äº›é”™è¯¯ã€‚è¯·é‡æ–°å¯åŠ¨æ‚¨çš„è®¾å¤‡ã€‚ - - - Select a language - 选择语言 - - - Choose Software to Install - 选择è¦å®‰è£…的软件 - - - sunnypilot - sunnypilot - - - Custom Software - 定制软件 + Developer + å¼€å‘人员 @@ -1674,6 +1873,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1889,22 @@ This may take up to a minute. Select a branch 选择分支 + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + é‡å¯ + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1739,22 +1962,6 @@ This may take up to a minute. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - N/A - Sponsor Status @@ -1779,24 +1986,20 @@ This may take up to a minute. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + N/A + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1851,6 +2054,50 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1903,11 +2150,11 @@ This may take up to a minute. Welcome to sunnypilot - 欢迎使用 sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 您必须接å—ã€Šæ¡æ¬¾ä¸Žæ¡ä»¶ã€‹æ‰èƒ½ä½¿ç”¨ sunnypilot。在继续之å‰ï¼Œè¯·å…ˆé˜…è¯»æœ€æ–°æ¡æ¬¾ï¼š<span style='color: #465BEA;'>https://comma.ai/terms</span>。 + @@ -1940,22 +2187,10 @@ This may take up to a minute. Disengage on Accelerator Pedal è¸©æ²¹é—¨æ—¶å–æ¶ˆæŽ§åˆ¶ - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - å¯ç”¨åŽï¼Œè¸©ä¸‹æ²¹é—¨è¸æ¿å°†å–消sunnypilot。 - Experimental Mode æµ‹è¯•æ¨¡å¼ - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot 默认 <b>è½»æ¾æ¨¡å¼</b>驾驶车辆。试验模å¼å¯ç”¨ä¸€äº›è½»æ¾æ¨¡å¼ä¹‹å¤–çš„ <b>试验性功能</b>。试验性功能包括: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - å…许驾驶模型控制加速和制动,sunnypilot将模仿人类驾驶车辆,包括在红ç¯å’Œåœè½¦è®©è¡Œæ ‡è¯†å‰åœè½¦ã€‚鉴于驾驶模型确定行驶车速,所设定的车速仅作为上é™ã€‚此功能尚处于早期测试状æ€ï¼Œæœ‰å¯èƒ½ä¼šå‡ºçްæ“作错误。 - New Driving Visualization 新驾驶视角 @@ -1964,6 +2199,10 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. 由于此车辆使用自带的ACCçºµå‘æŽ§åˆ¶ï¼Œå½“å‰æ— æ³•使用试验模å¼ã€‚ + + openpilot longitudinal control may come in a future update. + openpilotçºµå‘æŽ§åˆ¶å¯èƒ½ä¼šåœ¨æœªæ¥çš„æ›´æ–°ä¸­æä¾›ã€‚ + Aggressive ç§¯æž @@ -1980,22 +2219,10 @@ This may take up to a minute. Driving Personality 驾驶风格 - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - 在正å¼ï¼ˆrelease)版本以外的分支上,å¯ä»¥æµ‹è¯• sunnypilot çºµå‘æŽ§åˆ¶çš„ Alpha 版本以åŠå®žéªŒæ¨¡å¼ã€‚ - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - å¯ç”¨ sunnypilot çºµå‘æŽ§åˆ¶ï¼ˆalpha)开关以å…许实验模å¼ã€‚ - End-to-End Longitudinal Control ç«¯åˆ°ç«¯çºµå‘æŽ§åˆ¶ - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 推è使用标准模å¼ã€‚åœ¨ç§¯æžæ¨¡å¼ä¸‹ï¼Œsunnypilot 会更é è¿‘剿–¹è½¦è¾†ï¼Œå¹¶åœ¨æ²¹é—¨å’Œåˆ¹è½¦æ–¹é¢æ›´åŠ æ¿€è¿›ã€‚åœ¨æ”¾æ¾æ¨¡å¼ä¸‹ï¼Œsunnypilot ä¼šä¸Žå‰æ–¹è½¦è¾†ä¿æŒæ›´è¿œè·ç¦»ã€‚在支æŒçš„车型上,你å¯ä»¥ä½¿ç”¨æ–¹å‘盘上的è·ç¦»æŒ‰é’®æ¥å¾ªçŽ¯åˆ‡æ¢è¿™äº›é©¾é©¶é£Žæ ¼ã€‚ - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. 在低速时,驾驶å¯è§†åŒ–将转æ¢ä¸ºé“è·¯æœå‘的广角摄åƒå¤´ï¼Œä»¥æ›´å¥½åœ°å±•示æŸäº›è½¬å¼¯ã€‚æµ‹è¯•æ¨¡å¼æ ‡å¿—也将显示在å³ä¸Šè§’。 @@ -2005,8 +2232,24 @@ This may take up to a minute. 驾驶员监控常开 - Enable driver monitoring even when sunnypilot is not engaged. - å³ä½¿åœ¨sunnypilot未激活时也å¯ç”¨é©¾é©¶å‘˜ç›‘控。 + Changing this setting will restart openpilot if the car is powered on. + å¦‚æžœè½¦è¾†å·²é€šç”µï¼Œæ›´æ”¹æ­¤è®¾ç½®å°†ä¼šé‡æ–°å¯åЍ openpilot。 + + + Record and Upload Microphone Audio + 录制并上传麦克风音频 + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 在驾驶时录制并存储麦克风音频。该音频将会包å«åœ¨ comma connect 的行车记录仪视频中。 + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2017,55 +2260,86 @@ This may take up to a minute. - Enable sunnypilot - å¯ç”¨sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Updater + TreeOptionDialog - Update Required - éœ€è¦æ›´æ–° + Select + 选择 - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - æ“ä½œç³»ç»Ÿéœ€è¦æ›´æ–°ã€‚请将您的设备连接到WiFiä»¥èŽ·å–æ›´å¿«çš„æ›´æ–°ä½“验。下载大å°çº¦ä¸º1GB。 + Cancel + å–æ¶ˆ + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - 连接到WiFi + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - 安装 + Changing this setting will restart openpilot if the car is powered on. + å¦‚æžœè½¦è¾†å·²é€šç”µï¼Œæ›´æ”¹æ­¤è®¾ç½®å°†ä¼šé‡æ–°å¯åЍ openpilot。 - Back - 返回 + Off + - Loading... - 正在加载…… + Distance + - Reboot - é‡å¯ + Speed + - Update failed - 更新失败 + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2075,12 +2349,12 @@ This may take up to a minute. å¼€å¯ - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> 训练数æ®ä¸Šä¼ æ¨¡å¼ <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + 最大化您的训练数æ®ä¸Šä¼ ï¼Œä»¥æ”¹å–„ openpilot 的驾驶模型。 - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose æ¨¡å¼ <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/translations/main_zh-CHT.ts b/selfdrive/ui/translations/main_zh-CHT.ts index c46157e78e..1a8784846c 100644 --- a/selfdrive/ui/translations/main_zh-CHT.ts +++ b/selfdrive/ui/translations/main_zh-CHT.ts @@ -7,10 +7,6 @@ Close 關閉 - - Snooze Update - æš«åœæ›´æ–° - Reboot and Update é‡å•Ÿä¸¦æ›´æ–° @@ -60,7 +56,7 @@ Cellular Metered - 行動網路 + 計費的行動網路 Hidden Network @@ -84,27 +80,27 @@ Prevent large data uploads when on a metered cellular connection - + åœ¨ä½¿ç”¨è¨ˆè²»è¡Œå‹•ç¶²è·¯æ™‚ï¼Œé˜²æ­¢ä¸Šå‚³å¤§é‡æ•¸æ“š default - + é è¨­ metered - + 計費 unmetered - + éžè¨ˆè²» Wi-Fi Network Metered - + 計費 Wi-Fi 網路 Prevent large data uploads when on a metered Wi-Fi connection - + 在使用計費 Wi-Fi ç¶²è·¯æ™‚ï¼Œé˜²æ­¢ä¸Šå‚³å¤§é‡æ•¸æ“š @@ -135,6 +131,25 @@ Please use caution when using this feature. Only use the blinker when traffic an + + Brightness + + Brightness + + + + Overrides the brightness of the device. + + + + Auto (Dark) + + + + Auto + + + ConfirmationDialog @@ -148,10 +163,6 @@ Please use caution when using this feature. Only use the blinker when traffic an DeclinePage - - You must accept the Terms and Conditions in order to use sunnypilot. - æ‚¨å¿…é ˆå…ˆæŽ¥å—æ¢æ¬¾å’Œæ¢ä»¶æ‰èƒ½ä½¿ç”¨ sunnypilot。 - Back å›žä¸Šé  @@ -160,6 +171,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Decline, uninstall %1 æ‹’çµ•ä¸¦è§£é™¤å®‰è£ %1 + + You must accept the Terms and Conditions in order to use sunnypilot. + + DeveloperPanel @@ -171,6 +186,14 @@ Please use caution when using this feature. Only use the blinker when traffic an Longitudinal Maneuver Mode ç¸±å‘æ“æŽ§æ¸¬è©¦æ¨¡å¼ + + openpilot Longitudinal Control (Alpha) + openpilot ç¸±å‘æŽ§åˆ¶ (Alpha 版) + + + WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). + 警告:此車輛的 openpilot ç¸±å‘æŽ§åˆ¶åŠŸèƒ½ç›®å‰è™•æ–¼ Alpha 版本,使用此功能將會åœç”¨è‡ªå‹•緊急煞車(AEB)功能。 + Enable ADB 啟用 ADB @@ -179,6 +202,22 @@ Please use caution when using this feature. Only use the blinker when traffic an ADB (Android Debug Bridge) allows connecting to your device over USB or over the network. See https://docs.comma.ai/how-to/connect-to-comma for more info. ADB(Android 調試橋接)å…è¨±é€šéŽ USB 或網絡連接到您的設備。更多信æ¯è«‹åƒè¦‹ [https://docs.comma.ai/how-to/connect-to-comma](https://docs.comma.ai/how-to/connect-to-comma)。 + + On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + + + + + DeveloperPanelSP + + Show Advanced Controls + + + + Toggle visibility of advanced sunnypilot controls. +This only toggles the visibility of the controls; it does not toggle the actual control enabled/disabled state. + + Enable GitHub runner service @@ -187,6 +226,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Enables or disables the github runner service. + + Enable Quickboot Mode + + Error Log @@ -200,15 +243,11 @@ Please use caution when using this feature. Only use the blinker when traffic an - openpilot Longitudinal Control (Alpha) + When toggled on, this creates a prebuilt file to allow accelerated boot times. When toggled off, it immediately removes the prebuilt file so compilation of locally edited cpp files can be made. <br><br><b>To edit C++ files locally on device, you MUST first turn off this toggle so the changes can recompile.</b> - WARNING: openpilot longitudinal control is in alpha for this car and will disable Automatic Emergency Braking (AEB). - - - - On this car, sunnypilot defaults to the car's built-in ACC instead of openpilot's longitudinal control. Enable this to switch to openpilot longitudinal control. Enabling Experimental mode is recommended when enabling openpilot longitudinal control alpha. + Quickboot mode requires updates to be disabled.<br>Enable 'Disable Updates' in the Software panel first. @@ -258,10 +297,6 @@ Please use caution when using this feature. Only use the blinker when traffic an REVIEW 觀看 - - Review the rules, features, and limitations of sunnypilot - 觀看 sunnypilot 的使用è¦å‰‡ã€åŠŸèƒ½å’Œé™åˆ¶ - Are you sure you want to review the training guide? 您確定è¦è§€çœ‹ä½¿ç”¨æ•™å­¸å—Žï¼Ÿ @@ -294,10 +329,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Power Off 關機 - - sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. sunnypilot is continuously calibrating, resetting is rarely required. - sunnypilot 需è¦å°‡è£ç½®å›ºå®šåœ¨å·¦å³åå·® 4° 以內,æœä¸Šåå·® 5° 以內或æœä¸‹åå·® 9° 以內。é¡é ­åœ¨å¾Œå°æœƒæŒçºŒè‡ªå‹•校準,很少有需è¦é‡ç½®çš„æƒ…æ³ã€‚ - Your device is pointed %1° %2 and %3° %4. ä½ çš„è£ç½®ç›®å‰æœ%2 %1° ä»¥åŠæœ%4 %3° 。 @@ -356,15 +387,51 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Reset Calibration + 解除以é‡è¨­æ ¡æº– + + + openpilot is continuously calibrating, resetting is rarely required. Resetting calibration will restart openpilot if the car is powered on. + openpilot 會æŒçºŒé€²è¡Œæ ¡æº–,因此很少需è¦é‡è¨­ã€‚若車輛電æºé–‹å•Ÿï¼Œé‡è¨­æ ¡æº–å°‡æœƒé‡æ–°å•Ÿå‹• openpilot。 + + + + +Steering lag calibration is %1% complete. + + +轉å‘延鲿 ¡æº–å·²å®Œæˆ %1%。 + + + + +Steering lag calibration is complete. + + +轉å‘延鲿 ¡æº–已完æˆã€‚ + + + Steering torque response calibration is %1% complete. + è½‰å‘æ‰­çŸ©åæ‡‰æ ¡æº–å·²å®Œæˆ %1%。 + + + Steering torque response calibration is complete. + è½‰å‘æ‰­çŸ©å應校準已完æˆã€‚ + + + Review the rules, features, and limitations of sunnypilot - Resetting calibration will restart openpilot if the car is powered on. + sunnypilot requires the device to be mounted within 4° left or right and within 5° up or 9° down. DevicePanelSP + + Quiet Mode + + Driver Camera Preview @@ -381,6 +448,10 @@ Please use caution when using this feature. Only use the blinker when traffic an Language + + Reset Settings + + Are you sure you want to review the training guide? 您確定è¦è§€çœ‹ä½¿ç”¨æ•™å­¸å—Žï¼Ÿ @@ -393,6 +464,19 @@ Please use caution when using this feature. Only use the blinker when traffic an Select a language 鏿“‡èªžè¨€ + + Wake-Up Behavior + + + + Interactivity Timeout + + + + Apply a custom timeout for settings UI. +This is the time after which settings UI closes automatically if user is not interacting with the screen. + + Reboot 釿–°å•Ÿå‹• @@ -411,7 +495,7 @@ Please use caution when using this feature. Only use the blinker when traffic an Confirm - ç¢ºèª + Are you sure you want to enter Always Offroad mode? @@ -421,22 +505,6 @@ Please use caution when using this feature. Only use the blinker when traffic an Disengage to Enter Always Offroad Mode - - Exit Always Offroad - - - - Always Offroad - - - - Quiet Mode - - - - Reset Settings - - Are you sure you want to reset all sunnypilot settings to default? Once the settings are reset, there is no going back. @@ -449,6 +517,26 @@ Please use caution when using this feature. Only use the blinker when traffic an The reset cannot be undone. You have been warned. + + Exit Always Offroad + + + + Always Offroad + + + + â Default: Device will boot/wake-up normally & will be ready to engage. + + + + â Offroad: Device will be in Always Offroad mode after boot/wake-up. + + + + Controls state of the device after boot/sleep. + + DriveStats @@ -484,6 +572,21 @@ Please use caution when using this feature. Only use the blinker when traffic an 開啟相機中 + + ExitOffroadButton + + Are you sure you want to exit Always Offroad mode? + + + + Confirm + + + + EXIT ALWAYS OFFROAD MODE + + + ExperimentalModeButton @@ -497,28 +600,28 @@ Please use caution when using this feature. Only use the blinker when traffic an FirehosePanel - - 🔥 Firehose Mode 🔥 - 🔥 è¨“ç·´è³‡æ–™ä¸Šå‚³æ¨¡å¼ ðŸ”¥ - Firehose Mode: ACTIVE - 訓練資料上傳模å¼ï¼šå•Ÿç”¨ä¸­ + Firehose 模å¼ï¼šå•Ÿç”¨ä¸­ ACTIVE 啟用中 - - For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. - 為了é”到最佳效果,請æ¯é€±å°‡æ‚¨çš„è£ç½®å¸¶å›žå®¤å…§ï¼Œä¸¦é€£æŽ¥è‡³å„ªè³ªçš„ USB-C 充電器與 Wi-Fi。<br><br>訓練資料上傳模å¼åœ¨è¡Œé§›æ™‚也能é‹ä½œï¼Œä½†éœ€é€£æŽ¥è‡³è¡Œå‹•熱點或使用ä¸é™æµé‡çš„ SIM å¡ã€‚<br><br><br><b>常見å•題</b><br><br><i>æˆ‘é–‹è»Šçš„æ–¹å¼æˆ–地點有影響嗎?</i> 䏿œƒï¼Œè«‹åƒå¹³å¸¸ä¸€æ¨£é§•é§›å³å¯ã€‚<br><br><i>è¨“ç·´è³‡æ–™ä¸Šå‚³æ¨¡å¼æœƒä¸Šå‚³æ‰€æœ‰çš„駕駛片段嗎?</i>䏿œƒï¼Œæˆ‘å€‘æœƒé¸æ“‡æ€§åœ°ä¸Šå‚³éƒ¨åˆ†ç‰‡æ®µã€‚<br><br><i>什麼是好的 USB-C 充電器?</i>任何快速手機或筆電充電器都應該é©ç”¨ã€‚<br><br><i>我使用的軟體版本有影響嗎?</i>æœ‰çš„ï¼Œåªæœ‰å®˜æ–¹ sunnypilot(以åŠç‰¹å®šçš„分支)å¯ä»¥ç”¨æ–¼è¨“練。 - <b>%n segment(s)</b> of your driving is in the training dataset so far. <b>ç›®å‰å·²æœ‰ %n 段</b> 您的駕駛數據被ç´å…¥è¨“練資料集。 + + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network + <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>閒置中</span>ï¼šè«‹é€£æŽ¥åˆ°ä¸æŒ‰æµé‡è¨ˆè²»çš„網絡 + + + Firehose Mode + Firehose æ¨¡å¼ + sunnypilot learns to drive by watching humans, like you, drive. @@ -526,8 +629,8 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>INACTIVE</span>: connect to an unmetered network - <span stylesheet='font-size: 60px; font-weight: bold; color: #e74c3c;'>閒置中</span>ï¼šè«‹é€£æŽ¥åˆ°ä¸æŒ‰æµé‡è¨ˆè²»çš„網絡 + For maximum effectiveness, bring your device inside and connect to a good USB-C adapter and Wi-Fi weekly.<br><br>Firehose Mode can also work while you're driving if connected to a hotspot or unlimited SIM card.<br><br><br><b>Frequently Asked Questions</b><br><br><i>Does it matter how or where I drive?</i> Nope, just drive as you normally would.<br><br><i>Do all of my segments get pulled in Firehose Mode?</i> No, we selectively pull a subset of your segments.<br><br><i>What's a good USB-C adapter?</i> Any fast phone or laptop charger should be fine.<br><br><i>Does it matter which software I run?</i> Yes, only upstream sunnypilot (and particular forks) are able to be used for training. + @@ -634,6 +737,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Customize Lane Change + + Pause Lateral Control with Blinker + + + + Pause lateral control with blinker when traveling below the desired speed selected. + + Enables independent engagements of Automatic Lane Centering (ALC) and Adaptive Cruise Control (ACC). @@ -651,6 +762,29 @@ Firehose Mode allows you to maximize your training data uploads to improve openp + + LongitudinalPanel + + Custom ACC Speed Increments + + + + Enable custom Short & Long press increments for cruise speed increase/decrease. + + + + This feature can only be used with openpilot longitudinal control enabled. + + + + This feature is not supported on this platform due to vehicle limitations. + + + + Start the vehicle to check vehicle compatibility. + + + MadsSettings @@ -658,11 +792,15 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. + Unified Engagement Mode (UEM) - Unified Engagement Mode (UEM) + Steering Mode on Brake Pedal + + + + Note: For vehicles without LFA/LKAS button, disabling this will prevent lateral control engagement. @@ -673,18 +811,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Note: Once lateral control is engaged via UEM, it will remain engaged until it is manually disabled via the MADS button or car shut off. - - Remain Active - - - - Disengage - - - - Steering Mode on Brake Pedal - - Start the vehicle to check vehicle compatibility. @@ -701,6 +827,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp This platform only supports Disengage mode due to vehicle limitations. + + Remain Active + + Remain Active: ALC will remain active when the brake pedal is pressed. @@ -713,6 +843,10 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Pause: ALC will pause when the brake pedal is pressed. + + Disengage + + Disengage: ALC will disengage when the brake pedal is pressed. @@ -760,31 +894,44 @@ Firehose Mode allows you to maximize your training data uploads to improve openp é¸å– - No custom model selected! + Clear Model Cache - Driving + CLEAR - Navigation + Driving Model - Vision + Navigation Model - Policy + Vision Model - Downloading %1 model [%2]... (%3%) + Policy Model - %1 model [%2] %3 + Live Learning Steer Delay + + + + Adjust Software Delay + + + + Adjust the software delay when Live Learning Steer Delay is toggled off. +The default software delay value is 0.2 + + + + %1 - %2 @@ -800,21 +947,17 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - %1 model [%2] download failed + download failed - %1 - %1 model [%2] pending... + pending - %1 Fetching models... - - Use Default - - Select a Model @@ -843,6 +986,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Driving Model Selector + + This will delete ALL downloaded models from the cache<br/><u>except the currently active model</u>.<br/><br/>Are you sure you want to continue? + + + + Clear Cache + + Warning: You are on a metered connection! @@ -859,6 +1010,38 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Cancel å–æ¶ˆ + + Refresh Model List + + + + REFRESH + + + + Fetching Latest Models + + + + Enable this for the car to learn and adapt its steering response time. Disable to use a fixed steering response time. Keeping this on provides the stock openpilot experience. + + + + Live Steer Delay: + + + + Actuator Delay: + + + + Software Delay: + + + + Total Delay: + + MultiOptionDialog @@ -962,14 +1145,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp OffroadAlert - - Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 - è«‹ç«‹å³é€£æŽ¥ç¶²è·¯æª¢æŸ¥æ›´æ–°ã€‚如果ä¸é€£æŽ¥ç¶²è·¯ï¼Œsunnypilot 將在 %1 後便無法使用 - - - Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. - è«‹é€£æŽ¥è‡³ç¶²éš›ç¶²è·¯ä»¥æª¢æŸ¥æ›´æ–°ã€‚åœ¨é€£æŽ¥è‡³ç¶²éš›ç¶²è·¯ä¸¦å®Œæˆæ›´æ–°æª¢æŸ¥ä¹‹å‰ï¼Œsunnypilot 將䏿œƒè‡ªå‹•啟動。 - Unable to download updates %1 @@ -984,32 +1159,50 @@ 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. 一個有關æ“作系統的更新正在後å°ä¸‹è¼‰ä¸­ã€‚ç•¶æ›´æ–°æº–å‚™å¥½å®‰è£æ™‚,您將收到æç¤ºé€²è¡Œæ›´æ–°ã€‚ - - Device failed to register. It will not connect to or upload to comma.ai servers, and receives no support from comma.ai. If this is an official device, visit https://comma.ai/support. - è£ç½®è¨»å†Šå¤±æ•—。它將無法連接或上傳至 comma.ai 伺æœå™¨ï¼Œä¸¦ä¸”無法ç²å¾— comma.ai 的支æ´ã€‚如果這是一個官方è£ç½®ï¼Œè«‹è¨ªå• https://comma.ai/support 。 - NVMe drive not mounted. NVMe 固態硬碟未被掛載。 - - Unsupported NVMe drive detected. Device may draw significantly more power and overheat due to the unsupported NVMe. - æª¢æ¸¬åˆ°ä¸æ”¯æ´çš„ NVMe 固態硬碟。您的è£ç½®å› ç‚ºä½¿ç”¨äº†ä¸æ”¯æ´çš„ NVMe 固態硬碟å¯èƒ½æœƒæ¶ˆè€—更多電力並更易éŽç†±ã€‚ - - - sunnypilot 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. - sunnypilot 無法識別您的車輛。您的車輛å¯èƒ½æœªè¢«æ”¯æ´ï¼Œæˆ–是其電控單元 (ECU) 未被識別。請æäº¤ä¸€å€‹ Pull Request 為您的車輛添加正確的韌體版本。需è¦å¹«åŠ©å—Žï¼Ÿè«‹åŠ å…¥ discord.comma.ai 。 - - - sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. - sunnypilot 嵿¸¬åˆ°è£ç½®çš„安è£ä½ç½®ç™¼ç”Ÿè®ŠåŒ–。請確ä¿è£ç½®å®Œå…¨å®‰è£åœ¨æ”¯æž¶ä¸Šï¼Œä¸¦ç¢ºä¿æ”¯æž¶ç‰¢å›ºåœ°å›ºå®šåœ¨æ“‹é¢¨çŽ»ç’ƒä¸Šã€‚ - Device temperature too high. System cooling down before starting. Current internal component temperature: %1 è£ç½®æº«åº¦éŽé«˜ã€‚系統正在冷å»ä¸­ï¼Œç­‰å†·å»å®Œç•¢å¾Œæ‰æœƒå•Ÿå‹•。目å‰å…§éƒ¨çµ„件溫度:%1 - sunnypilot is now in Always Offroad mode. sunnypilot won't start until Always Offroad mode is disabled. Go to "Settings" -> "Device" to exit Always Offroad mode. + 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. + è£ç½®è¨»å†Š comma.ai 後端失敗。此è£ç½®å°‡ç„¡æ³•連線或上傳資料至 comma.ai 伺æœå™¨ï¼Œä¹Ÿç„¡æ³•ç²å¾— comma.ai 的支æ´ã€‚若此è£ç½®è³¼è‡ª comma.ai/shop,請至 https://comma.ai/support 建立支æ´è«‹æ±‚。 + + + Acknowledge Excessive Actuation + + + + Snooze Update + æš«åœæ›´æ–° + + + 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. + + + + Immediately connect to the internet to check for updates. If you do not connect to the internet, sunnypilot won't engage in %1 + + + + Connect to internet to check for updates. sunnypilot won't automatically start until it connects to internet to check for updates. + + + + sunnypilot 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. + + + + sunnypilot detected a change in the device's mounting position. Ensure the device is fully seated in the mount and the mount is firmly secured to the windshield. + + + + OpenStreetMap database is out of date. New maps must be downloaded if you wish to continue using OpenStreetMap data for Enhanced Speed Control and road name display. + +%1 @@ -1029,11 +1222,14 @@ Firehose Mode allows you to maximize your training data uploads to improve openp - OnroadAlerts + OffroadHomeSP - sunnypilot Unavailable - 無法使用 sunnypilot + ALWAYS OFFROAD ACTIVE + + + + OnroadAlerts TAKE CONTROL IMMEDIATELY ç«‹å³æŽ¥ç®¡ @@ -1050,6 +1246,141 @@ Firehose Mode allows you to maximize your training data uploads to improve openp System Unresponsive 系統無回應 + + sunnypilot Unavailable + + + + + OsmPanel + + Mapd Version + + + + Offline Maps ETA + + + + Time Elapsed + + + + Downloaded Maps + + + + DELETE + + + + This will delete ALL downloaded maps + +Are you sure you want to delete all the maps? + + + + Yes, delete all the maps. + + + + Database Update + + + + CHECK + 檢查 + + + Country + + + + SELECT + é¸å– + + + Fetching Country list... + + + + State + + + + Fetching State list... + + + + All + + + + REFRESH + + + + UPDATE + æ›´æ–° + + + Download starting... + + + + Error: Invalid download. Retry. + + + + Download complete! + + + + + +Warning: You are on a metered connection! + + + + This will start the download process and it might take a while to complete. + + + + Continue on Metered + + + + Start Download + + + + m + + + + s + + + + Calculating... + + + + Downloaded + + + + Calculating ETA... + + + + Ready + + + + Time remaining: + + PairingPopup @@ -1136,7 +1467,7 @@ Firehose Mode allows you to maximize your training data uploads to improve openp Confirm - ç¢ºèª + Cancel @@ -1219,10 +1550,6 @@ Firehose Mode allows you to maximize your training data uploads to improve openp QObject - - sunnypilot - sunnypilot - %n minute(s) ago @@ -1245,46 +1572,9 @@ Firehose Mode allows you to maximize your training data uploads to improve openp now ç¾åœ¨ - - - Reset - Reset failed. Reboot to try again. - é‡è¨­å¤±æ•—ã€‚è«‹é‡æ–°å•Ÿå‹•後å†è©¦ã€‚ - - - Are you sure you want to reset your device? - 您確定è¦é‡è¨­ä½ çš„è£ç½®å—Žï¼Ÿ - - - System Reset - 系統é‡è¨­ - - - Cancel - å–æ¶ˆ - - - Reboot - 釿–°å•Ÿå‹• - - - Confirm - ç¢ºèª - - - Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device. - 無法掛載資料分割å€ã€‚分割å€å¯èƒ½å·²ç¶“毀æã€‚è«‹ç¢ºèªæ˜¯å¦è¦åˆªé™¤ä¸¦é‡ç½®ã€‚ - - - Resetting device... -This may take up to a minute. - é‡ç½®ä¸­â€¦ -這å¯èƒ½éœ€è¦ä¸€åˆ†é˜çš„æ™‚間。 - - - System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot. - 系統é‡è¨­å·²å•Ÿå‹•。按下「確èªã€ä»¥æ¸…é™¤æ‰€æœ‰å…§å®¹å’Œè¨­å®šï¼Œæˆ–æŒ‰ä¸‹ã€Œå–æ¶ˆã€ä»¥ç¹¼çºŒé–‹æ©Ÿã€‚ + sunnypilot + @@ -1315,7 +1605,7 @@ This may take up to a minute. Firehose - 訓練上傳 + Firehose @@ -1345,29 +1635,13 @@ This may take up to a minute. 軟體 - Trips + Models - - Vehicle - - - - Developer - 開發人員 - - - Firehose - 訓練上傳 - Steering - - Models - - Cruise @@ -1376,100 +1650,25 @@ This may take up to a minute. Visuals - - - Setup - WARNING: Low Voltage - 警告:電壓éŽä½Ž + OSM + - Power your device in a car with a harness or proceed at your own risk. - 請使用車上 harness æä¾›çš„é›»æºï¼Œè‹¥ç¹¼çºŒçš„話您需è¦è‡ªæ“”風險。 + Trips + - Power off - 關機 + Vehicle + - Continue - 繼續 + Firehose + Firehose - Getting Started - 入門 - - - Before we get on the road, let’s finish installation and cover some details. - 在我們上路之å‰ï¼Œè®“我們完æˆå®‰è£ä¸¦ä»‹ç´¹ä¸€äº›ç´°ç¯€ã€‚ - - - Connect to Wi-Fi - 連接到無線網路 - - - Back - å›žä¸Šé  - - - Continue without Wi-Fi - 在沒有 Wi-Fi 的情æ³ä¸‹ç¹¼çºŒ - - - Waiting for internet - 連接至網路中 - - - Enter URL - è¼¸å…¥ç¶²å€ - - - for Custom Software - 訂製的軟體 - - - Downloading... - 下載中… - - - Download Failed - 下載失敗 - - - Ensure the entered URL is valid, and the device’s internet connection is good. - 請確定您輸入的是有效的安è£ç¶²å€ï¼Œä¸¦ä¸”確定è£ç½®çš„網路連線狀態良好。 - - - Reboot device - 釿–°å•Ÿå‹• - - - Start over - 釿–°é–‹å§‹ - - - No custom software found at this URL. - åœ¨æ­¤ç¶²å€æ‰¾ä¸åˆ°è‡ªè¨‚軟體。 - - - Something went wrong. Reboot the device. - ç™¼ç”Ÿäº†ä¸€äº›éŒ¯èª¤ã€‚è«‹é‡æ–°å•Ÿå‹•您的è£ç½®ã€‚ - - - Select a language - 鏿“‡èªžè¨€ - - - Choose Software to Install - 鏿“‡è¦å®‰è£çš„軟體 - - - sunnypilot - sunnypilot - - - Custom Software - 自訂軟體 + Developer + 開發人員 @@ -1674,6 +1873,14 @@ This may take up to a minute. Enter search keywords, or leave blank to list all branches. + + Disable Updates + + + + When enabled, software updates will be disabled. <b>This requires a reboot to take effect.</b> + + No branches found for keywords: %1 @@ -1682,6 +1889,22 @@ This may take up to a minute. Select a branch é¸å–一個分支 + + %1 updates requires a reboot.<br>Reboot now? + + + + Reboot + 釿–°å•Ÿå‹• + + + When enabled, software updates will be disabled.<br><b>This requires a reboot to take effect.</b> + + + + Please enable always offroad mode or turn off vehicle to adjust these toggles + + SshControl @@ -1739,22 +1962,6 @@ This may take up to a minute. Enable sunnylink - - 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - - - - 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. - - - - Device ID - - - - N/A - 無法使用 - Sponsor Status @@ -1779,24 +1986,20 @@ This may take up to a minute. Pair your GitHub account to grant your device sponsor benefits, including API access on sunnylink. + + N/A + 無法使用 + sunnylink Dongle ID not found. This may be due to weak internet connection or sunnylink registration issue. Please reboot and try again. - Not Sponsor + 🎉Welcome back! We're excited to see you've enabled sunnylink again! 🚀 - Paired - - - - Not Paired - - - - THANKS ♥ + 👋Not going to lie, it's sad to see you disabled sunnylink 😢, but we'll be here when you're ready to come back 🎉. @@ -1851,6 +2054,50 @@ This may take up to a minute. Settings restored. Confirm to restart the interface. + + Device ID + + + + THANKS ♥ + + + + Not Sponsor + + + + Paired + + + + Not Paired + + + + Enable sunnylink uploader to allow sunnypilot to upload your driving data to sunnypilot servers. (only for highest tiers, and does NOT bring ANY benefit to you. We are just testing data volume.) + + + + [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 @@ -1903,11 +2150,11 @@ This may take up to a minute. Welcome to sunnypilot - 歡迎使用 sunnypilot + You must accept the Terms and Conditions to use sunnypilot. Read the latest terms at <span style='color: #465BEA;'>https://comma.ai/terms</span> before continuing. - 您必須接å—ã€Šæ¢æ¬¾èˆ‡æ¢ä»¶ã€‹æ‰èƒ½ä½¿ç”¨ sunnypilot。在繼續之å‰ï¼Œè«‹å…ˆé–±è®€æœ€æ–°æ¢æ¬¾ï¼š<span style='color: #465BEA;'>https://comma.ai/terms</span>。 + @@ -1940,22 +2187,10 @@ This may take up to a minute. Disengage on Accelerator Pedal æ²¹é–€å–æ¶ˆæŽ§è»Š - - When enabled, pressing the accelerator pedal will disengage sunnypilot. - å•Ÿç”¨å¾Œï¼Œè¸©è¸æ²¹é–€å°‡æœƒå–消 sunnypilot 控制。 - Experimental Mode å¯¦é©—æ¨¡å¼ - - sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: - sunnypilot é è¨­ä»¥ <b>輕鬆模å¼</b> 駕駛。 實驗模å¼å•Ÿç”¨äº†å°šæœªæº–備好進入輕鬆模å¼çš„ <b>alpha 級功能</b>。實驗功能如下: - - - Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. - 讓駕駛模型來控制油門åŠç…žè»Šã€‚sunnypilot將會模擬人類的駕駛行為,包å«åœ¨çœ‹è¦‹ç´…燈åŠåœæ­¢æ¨™ç¤ºæ™‚åœè»Šã€‚由於車速將由駕駛模型決定,因此您設定的時速將æˆç‚ºé€Ÿåº¦ä¸Šé™ã€‚本功能ä»åœ¨æ—©æœŸå¯¦é©—éšŽæ®µï¼Œè«‹é æœŸæ¨¡åž‹æœ‰çŠ¯éŒ¯çš„å¯èƒ½æ€§ã€‚ - New Driving Visualization æ–°çš„é§•é§›è¦–è¦ºä»‹é¢ @@ -1964,6 +2199,10 @@ This may take up to a minute. Experimental mode is currently unavailable on this car since the car's stock ACC is used for longitudinal control. 因車輛使用內建ACC系統,無法在本車輛上啟動實驗模å¼ã€‚ + + openpilot longitudinal control may come in a future update. + openpilot ç¸±å‘æŽ§åˆ¶å¯èƒ½æœƒåœ¨æœªä¾†çš„æ›´æ–°ä¸­æä¾›ã€‚ + Aggressive ç©æ¥µ @@ -1980,22 +2219,10 @@ This may take up to a minute. Driving Personality 駕駛風格 - - An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. - åœ¨æ­£å¼ (release) 版以外的分支上å¯ä»¥æ¸¬è©¦ sunnypilot ç¸±å‘æŽ§åˆ¶çš„ Alpha 版本以åŠå¯¦é©—模å¼ã€‚ - - - Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - 啟用 sunnypilot ç¸±å‘æŽ§åˆ¶ï¼ˆalpha)切æ›ä»¥å…許實驗模å¼ã€‚ - End-to-End Longitudinal Control ç«¯åˆ°ç«¯ç¸±å‘æŽ§åˆ¶ - - Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. - 建議使用標準模å¼ã€‚åœ¨ç©æ¥µæ¨¡å¼ä¸‹ï¼Œsunnypilot 會更接近å‰è»Šä¸¦æ›´ç©æ¥µåœ°ä½¿ç”¨æ²¹é–€å’Œå‰Žè»Šã€‚在輕鬆模å¼ä¸‹ï¼Œsunnypilot 會與å‰è»Šä¿æŒè¼ƒé è·é›¢ã€‚å°æ–¼æ”¯æ´çš„æ±½è»Šï¼Œæ‚¨å¯ä»¥ä½¿ç”¨æ–¹å‘盤上的è·é›¢æŒ‰éˆ•來切æ›é€™äº›é§•駛風格。 - The driving visualization will transition to the road-facing wide-angle camera at low speeds to better show some turns. The Experimental mode logo will also be shown in the top right corner. 在低速時,駕駛å¯è¦–化將切æ›è‡³é“è·¯æœå‘的廣角æ”影機,以更好地顯示æŸäº›å½Žé“。在å³ä¸Šè§’還會顯示「實驗模å¼ã€çš„æ¨™èªŒã€‚ @@ -2005,8 +2232,24 @@ This may take up to a minute. 駕駛監控常開 - Enable driver monitoring even when sunnypilot is not engaged. - å³ä½¿åœ¨sunnypilot未激活時也啟用駕駛監控。 + Changing this setting will restart openpilot if the car is powered on. + 若車輛電æºç‚ºé–‹å•Ÿç‹€æ…‹ï¼Œè®Šæ›´æ­¤è¨­å®šå°‡æœƒé‡æ–°å•Ÿå‹• openpilot。 + + + Record and Upload Microphone Audio + 錄製並上傳麥克風音訊 + + + Record and store microphone audio while driving. The audio will be included in the dashcam video in comma connect. + 在駕駛時錄製並儲存麥克風音訊。此音訊將會收錄在 comma connect 的行車記錄器影片中。 + + + Enable sunnypilot + + + + Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + Enable Dynamic Experimental Control @@ -2017,55 +2260,86 @@ This may take up to a minute. - Enable sunnypilot - 啟用 sunnypilot - - - Use the sunnypilot system for adaptive cruise control and lane keep driver assistance. Your attention is required at all times to use this feature. + When enabled, pressing the accelerator pedal will disengage sunnypilot. - Changing this setting will restart openpilot if the car is powered on. + Enable driver monitoring even when sunnypilot is not engaged. - openpilot longitudinal control may come in a future update. + Standard is recommended. In aggressive mode, sunnypilot will follow lead cars closer and be more aggressive with the gas and brake. In relaxed mode sunnypilot will stay further away from lead cars. On supported cars, you can cycle through these personalities with your steering wheel distance button. + + + + sunnypilot defaults to driving in <b>chill mode</b>. Experimental mode enables <b>alpha-level features</b> that aren't ready for chill mode. Experimental features are listed below: + + + + Let the driving model control the gas and brakes. sunnypilot will drive as it thinks a human would, including stopping for red lights and stop signs. Since the driving model decides the speed to drive, the set speed will only act as an upper bound. This is an alpha quality feature; mistakes should be expected. + + + + An alpha version of sunnypilot longitudinal control can be tested, along with Experimental mode, on non-release branches. + + + + Enable the sunnypilot longitudinal control (alpha) toggle to allow Experimental mode. - Updater + TreeOptionDialog - Update Required - 系統更新 + Select + 鏿“‡ - An operating system update is required. Connect your device to Wi-Fi for the fastest update experience. The download size is approximately 1GB. - 需è¦é€²è¡Œä½œæ¥­ç³»çµ±æ›´æ–°ã€‚建議將您的è£ç½®é€£æŽ¥ä¸Š Wi-Fi ç²å¾—更快的更新下載。下載大å°ç´„為 1GB。 + Cancel + å–æ¶ˆ + + + + VisualsPanel + + Show Blind Spot Warnings + - Connect to Wi-Fi - 連接到無線網路 + Enabling this will display warnings when a vehicle is detected in your blind spot as long as your car has BSM supported. + - Install - å®‰è£ + Changing this setting will restart openpilot if the car is powered on. + 若車輛電æºç‚ºé–‹å•Ÿç‹€æ…‹ï¼Œè®Šæ›´æ­¤è¨­å®šå°‡æœƒé‡æ–°å•Ÿå‹• openpilot。 - Back - å›žä¸Šé  + Off + - Loading... - 載入中… + Distance + - Reboot - 釿–°å•Ÿå‹• + Speed + - Update failed - 更新失敗 + Time + + + + All + + + + Display Metrics Below Chevron + + + + Display useful metrics below the chevron that tracks the lead car (only applicable to cars with openpilot longitudinal control). + @@ -2075,12 +2349,12 @@ This may take up to a minute. 開啟 - <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> - <span style='font-family: "Noto Color Emoji";'>🔥</span> è¨“ç·´è³‡æ–™ä¸Šå‚³æ¨¡å¼ <span style='font-family: Noto Color Emoji;'>🔥</span> + Maximize your training data uploads to improve openpilot's driving models. + 最大化您的訓練數據上傳,以改善 openpilot 的駕駛模型。 - Maximize your training data uploads to improve openpilot's driving models. - + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose Mode <span style='font-family: Noto Color Emoji;'>🔥</span> + <span style='font-family: "Noto Color Emoji";'>🔥</span> Firehose æ¨¡å¼ <span style='font-family: Noto Color Emoji;'>🔥</span> diff --git a/selfdrive/ui/ui.cc b/selfdrive/ui/ui.cc index c9dc70cdff..4b8f79785b 100644 --- a/selfdrive/ui/ui.cc +++ b/selfdrive/ui/ui.cc @@ -61,6 +61,9 @@ void update_state(UIState *s) { scene.light_sensor = -1; } scene.started = sm["deviceState"].getDeviceState().getStarted() && scene.ignition; + + auto params = Params(); + scene.recording_audio = params.getBool("RecordAudio") && scene.started; } void ui_update_params(UIState *s) { @@ -164,8 +167,9 @@ void Device::setAwake(bool on) { } void Device::resetInteractiveTimeout(int timeout) { + int customTimeout = QString::fromStdString(Params().get("InteractivityTimeout")).toInt(); if (timeout == -1) { - timeout = (ignition_on ? 10 : 30); + timeout = customTimeout == 0 ? (ignition_on ? 10 : 30) : customTimeout; } interactive_timeout = timeout * UI_FREQ; } diff --git a/selfdrive/ui/ui.h b/selfdrive/ui/ui.h index 4ead8aecb9..e78b573b66 100644 --- a/selfdrive/ui/ui.h +++ b/selfdrive/ui/ui.h @@ -62,7 +62,7 @@ typedef struct UIScene { cereal::LongitudinalPersonality personality; float light_sensor = -1; - bool started, ignition, is_metric; + bool started, ignition, is_metric, recording_audio; uint64_t started_frame; } UIScene; diff --git a/selfdrive/ui/ui_state.py b/selfdrive/ui/ui_state.py index 3738ea6d5d..c2bb5f77a4 100644 --- a/selfdrive/ui/ui_state.py +++ b/selfdrive/ui/ui_state.py @@ -1,12 +1,20 @@ import pyray as rl +import numpy as np import time +import threading from collections.abc import Callable from enum import Enum from cereal import messaging, log +from openpilot.common.filter_simple import FirstOrderFilter from openpilot.common.params import Params, UnknownKeyName +from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.ui.lib.prime_state import PrimeState +from openpilot.system.ui.lib.application import DEFAULT_FPS +from openpilot.system.hardware import HARDWARE +from openpilot.system.ui.lib.application import gui_app UI_BORDER_SIZE = 30 +BACKLIGHT_OFFROAD = 50 class UIStatus(Enum): @@ -139,10 +147,15 @@ class UIState: class Device: def __init__(self): self._ignition = False - self._interaction_time: float = 0.0 + self._interaction_time: float = -1 self._interactive_timeout_callbacks: list[Callable] = [] self._prev_timed_out = False - self.reset_interactive_timeout() + self._awake = False + + self._offroad_brightness: int = BACKLIGHT_OFFROAD + self._last_brightness: int = 0 + self._brightness_filter = FirstOrderFilter(BACKLIGHT_OFFROAD, 10.00, 1 / DEFAULT_FPS) + self._brightness_thread: threading.Thread | None = None def reset_interactive_timeout(self, timeout: int = -1) -> None: if timeout == -1: @@ -153,18 +166,64 @@ class Device: self._interactive_timeout_callbacks.append(callback) def update(self): + # do initial reset + if self._interaction_time <= 0: + self.reset_interactive_timeout() + + self._update_brightness() + self._update_wakefulness() + + def set_offroad_brightness(self, brightness: int): + # TODO: not yet used, should be used in prime widget for QR code, etc. + self._offroad_brightness = min(max(brightness, 0), 100) + + def _update_brightness(self): + clipped_brightness = self._offroad_brightness + + if ui_state.started and ui_state.light_sensor >= 0: + clipped_brightness = ui_state.light_sensor + + # CIE 1931 - https://www.photonstophotos.net/GeneralTopics/Exposure/Psychometric_Lightness_and_Gamma.htm + if clipped_brightness <= 8: + clipped_brightness = clipped_brightness / 903.3 + else: + clipped_brightness = ((clipped_brightness + 16.0) / 116.0) ** 3.0 + + clipped_brightness = float(np.clip(100 * clipped_brightness, 10, 100)) + + brightness = round(self._brightness_filter.update(clipped_brightness)) + if not self._awake: + brightness = 0 + + if brightness != self._last_brightness: + if self._brightness_thread is None or not self._brightness_thread.is_alive(): + cloudlog.debug(f"setting display brightness {brightness}") + self._brightness_thread = threading.Thread(target=HARDWARE.set_screen_brightness, args=(brightness,)) + self._brightness_thread.start() + self._last_brightness = brightness + + def _update_wakefulness(self): # Handle interactive timeout ignition_just_turned_off = not ui_state.ignition and self._ignition self._ignition = ui_state.ignition - interaction_timeout = time.monotonic() > self._interaction_time - if ignition_just_turned_off or rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): + if ignition_just_turned_off or any(ev.left_down for ev in gui_app.mouse_events): self.reset_interactive_timeout() - elif interaction_timeout and not self._prev_timed_out: + + interaction_timeout = time.monotonic() > self._interaction_time + if interaction_timeout and not self._prev_timed_out: for callback in self._interactive_timeout_callbacks: callback() self._prev_timed_out = interaction_timeout + self._set_awake(ui_state.ignition or not interaction_timeout) + + def _set_awake(self, on: bool): + if on != self._awake: + self._awake = on + cloudlog.debug(f"setting display power {int(on)}") + HARDWARE.set_display_power(on) + # Global instance ui_state = UIState() diff --git a/selfdrive/ui/watch3.cc b/selfdrive/ui/watch3.cc deleted file mode 100644 index 258e2a7bd6..0000000000 --- a/selfdrive/ui/watch3.cc +++ /dev/null @@ -1,33 +0,0 @@ -#include -#include - -#include "selfdrive/ui/qt/qt_window.h" -#include "selfdrive/ui/qt/util.h" -#include "selfdrive/ui/qt/widgets/cameraview.h" - -int main(int argc, char *argv[]) { - initApp(argc, argv); - - QApplication a(argc, argv); - QWidget w; - setMainWindow(&w); - - QVBoxLayout *layout = new QVBoxLayout(&w); - layout->setMargin(0); - layout->setSpacing(0); - - { - QHBoxLayout *hlayout = new QHBoxLayout(); - layout->addLayout(hlayout); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_ROAD)); - } - - { - QHBoxLayout *hlayout = new QHBoxLayout(); - layout->addLayout(hlayout); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_DRIVER)); - hlayout->addWidget(new CameraWidget("camerad", VISION_STREAM_WIDE_ROAD)); - } - - return a.exec(); -} diff --git a/selfdrive/ui/watch3.py b/selfdrive/ui/watch3.py new file mode 100755 index 0000000000..bb64cdc4d5 --- /dev/null +++ b/selfdrive/ui/watch3.py @@ -0,0 +1,17 @@ +#!/usr/bin/env python3 +import pyray as rl + +from msgq.visionipc import VisionStreamType +from openpilot.system.ui.lib.application import gui_app +from openpilot.selfdrive.ui.onroad.cameraview import CameraView + + +if __name__ == "__main__": + gui_app.init_window("watch3") + road = CameraView("camerad", VisionStreamType.VISION_STREAM_ROAD) + driver = CameraView("camerad", VisionStreamType.VISION_STREAM_DRIVER) + wide = CameraView("camerad", VisionStreamType.VISION_STREAM_WIDE_ROAD) + for _ in gui_app.render(): + road.render(rl.Rectangle(gui_app.width // 4, 0, gui_app.width // 2, gui_app.height // 2)) + driver.render(rl.Rectangle(0, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) + wide.render(rl.Rectangle(gui_app.width // 2, gui_app.height // 2, gui_app.width // 2, gui_app.height // 2)) diff --git a/selfdrive/ui/widgets/exp_mode_button.py b/selfdrive/ui/widgets/exp_mode_button.py index 83caf413d1..9618768957 100644 --- a/selfdrive/ui/widgets/exp_mode_button.py +++ b/selfdrive/ui/widgets/exp_mode_button.py @@ -1,7 +1,7 @@ import pyray as rl from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget class ExperimentalModeButton(Widget): @@ -14,7 +14,6 @@ class ExperimentalModeButton(Widget): self.params = Params() self.experimental_mode = self.params.get_bool("ExperimentalMode") - self.is_pressed = False self.chill_pixmap = gui_app.texture("icons/couch.png", self.img_width, self.img_width) self.experimental_pixmap = gui_app.texture("icons/experimental_grey.png", self.img_width, self.img_width) @@ -32,20 +31,13 @@ class ExperimentalModeButton(Widget): rl.draw_rectangle_gradient_h(int(rect.x), int(rect.y), int(rect.width), int(rect.height), start_color, end_color) - def _handle_interaction(self, rect): - mouse_pos = rl.get_mouse_position() - mouse_in_rect = rl.check_collision_point_rec(mouse_pos, rect) - - self.is_pressed = mouse_in_rect and rl.is_mouse_button_down(rl.MOUSE_BUTTON_LEFT) - return mouse_in_rect and rl.is_mouse_button_released(rl.MOUSE_BUTTON_LEFT) + def _handle_mouse_release(self, mouse_pos): + self.experimental_mode = not self.experimental_mode + # TODO: Opening settings for ExperimentalMode + self.params.put_bool("ExperimentalMode", self.experimental_mode) def _render(self, rect): - if self._handle_interaction(rect): - self.experimental_mode = not self.experimental_mode - # TODO: Opening settings for ExperimentalMode - self.params.put_bool("ExperimentalMode", self.experimental_mode) - - rl.draw_rectangle_rounded(rect, 0.08, 20, rl.Color(255, 255, 255, 255)) + rl.draw_rectangle_rounded(rect, 0.08, 20, rl.WHITE) rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(rect.height)) self._draw_gradient_background(rect) @@ -61,7 +53,7 @@ class ExperimentalModeButton(Widget): text_x = rect.x + self.horizontal_padding text_y = rect.y + rect.height / 2 - 45 // 2 # Center vertically - rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.Color(0, 0, 0, 255)) + rl.draw_text_ex(gui_app.font(FontWeight.NORMAL), text, rl.Vector2(int(text_x), int(text_y)), 45, 0, rl.BLACK) # Draw icon (right aligned) icon_x = rect.x + rect.width - self.horizontal_padding - self.img_width @@ -71,4 +63,4 @@ class ExperimentalModeButton(Widget): # Draw current mode icon current_icon = self.experimental_pixmap if self.experimental_mode else self.chill_pixmap source_rect = rl.Rectangle(0, 0, current_icon.width, current_icon.height) - rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.Color(255, 255, 255, 255)) + rl.draw_texture_pro(current_icon, source_rect, icon_rect, rl.Vector2(0, 0), 0, rl.WHITE) diff --git a/selfdrive/ui/widgets/offroad_alerts.py b/selfdrive/ui/widgets/offroad_alerts.py index aba07139db..f54bd9116c 100644 --- a/selfdrive/ui/widgets/offroad_alerts.py +++ b/selfdrive/ui/widgets/offroad_alerts.py @@ -5,11 +5,11 @@ from collections.abc import Callable from dataclasses import dataclass from openpilot.common.params import Params from openpilot.system.hardware import HARDWARE -from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.text_measure import measure_text_cached from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget class AlertColors: @@ -190,14 +190,10 @@ class OffroadAlert(AbstractAlert): for alert_data in self.sorted_alerts: text = "" - bytes_data = self.params.get(alert_data.key) + alert_json = self.params.get(alert_data.key) - if bytes_data: - try: - alert_json = json.loads(bytes_data) - text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) - except json.JSONDecodeError: - text = "" + if alert_json: + text = alert_json.get("text", "").replace("{}", alert_json.get("extra", "")) alert_data.text = text alert_data.visible = bool(text) @@ -296,7 +292,7 @@ class UpdateAlert(AbstractAlert): def refresh(self) -> bool: update_available: bool = self.params.get_bool("UpdateAvailable") if update_available: - self.release_notes = self.params.get("UpdaterNewReleaseNotes", encoding='utf-8') + self.release_notes = self.params.get("UpdaterNewReleaseNotes") self._cached_content_height = 0 return update_available diff --git a/selfdrive/ui/widgets/pairing_dialog.py b/selfdrive/ui/widgets/pairing_dialog.py index 2cb1499499..63298914ea 100644 --- a/selfdrive/ui/widgets/pairing_dialog.py +++ b/selfdrive/ui/widgets/pairing_dialog.py @@ -23,7 +23,7 @@ class PairingDialog: def _get_pairing_url(self) -> str: try: - dongle_id = self.params.get("DongleId", encoding='utf8') or "" + dongle_id = self.params.get("DongleId") or "" token = Api(dongle_id).get_token() except Exception as e: cloudlog.warning(f"Failed to get pairing token: {e}") @@ -55,7 +55,7 @@ class PairingDialog: self.qr_texture = None def _check_qr_refresh(self) -> None: - current_time = time.time() + current_time = time.monotonic() if current_time - self.last_qr_generation >= self.QR_REFRESH_INTERVAL: self._generate_qr_code() self.last_qr_generation = current_time diff --git a/selfdrive/ui/widgets/prime.py b/selfdrive/ui/widgets/prime.py index afdfa33e7a..6b601f6dff 100644 --- a/selfdrive/ui/widgets/prime.py +++ b/selfdrive/ui/widgets/prime.py @@ -2,10 +2,10 @@ import pyray as rl from openpilot.selfdrive.ui.ui_state import ui_state from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.label import gui_label -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.label import gui_label class PrimeWidget(Widget): @@ -36,7 +36,7 @@ class PrimeWidget(Widget): font = gui_app.font(FontWeight.LIGHT) wrapped_text = "\n".join(wrap_text(font, "Become a comma prime member at connect.comma.ai", 56, int(w))) text_size = measure_text_cached(font, wrapped_text, 56) - rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.Color(255, 255, 255, 255)) + rl.draw_text_ex(font, wrapped_text, rl.Vector2(x, desc_y), 56, 0, rl.WHITE) # Features section features_y = desc_y + text_size.y + 50 diff --git a/selfdrive/ui/widgets/setup.py b/selfdrive/ui/widgets/setup.py index 4fd56aa522..35a7c4101c 100644 --- a/selfdrive/ui/widgets/setup.py +++ b/selfdrive/ui/widgets/setup.py @@ -1,11 +1,11 @@ import pyray as rl from openpilot.selfdrive.ui.lib.prime_state import PrimeType -from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.selfdrive.ui.ui_state import ui_state +from openpilot.selfdrive.ui.widgets.pairing_dialog import PairingDialog from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle from openpilot.system.ui.lib.wrap_text import wrap_text -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle class SetupWidget(Widget): diff --git a/selfdrive/ui/widgets/ssh_key.py b/selfdrive/ui/widgets/ssh_key.py index 418867c86f..4f4a8dcdff 100644 --- a/selfdrive/ui/widgets/ssh_key.py +++ b/selfdrive/ui/widgets/ssh_key.py @@ -6,8 +6,12 @@ from enum import Enum from openpilot.common.params import Params from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.list_view import ( +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.confirm_dialog import alert_dialog +from openpilot.system.ui.widgets.keyboard import Keyboard +from openpilot.system.ui.widgets.list_view import ( ItemAction, ListItem, BUTTON_HEIGHT, @@ -15,10 +19,6 @@ from openpilot.system.ui.lib.list_view import ( BUTTON_FONT_SIZE, BUTTON_WIDTH, ) -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import DialogResult -from openpilot.system.ui.widgets.confirm_dialog import alert_dialog -from openpilot.system.ui.widgets.keyboard import Keyboard class SshKeyActionState(Enum): @@ -42,7 +42,7 @@ class SshKeyAction(ItemAction): self._refresh_state() def _refresh_state(self): - self._username = self._params.get("GithubUsername", "") + self._username = self._params.get("GithubUsername") self._state = SshKeyActionState.REMOVE if self._params.get("GithubSshKeys") else SshKeyActionState.ADD def _render(self, rect: rl.Rectangle) -> bool: diff --git a/selfdrive/controls/lib/tests/__init__.py b/sunnypilot/livedelay/__init__.py similarity index 100% rename from selfdrive/controls/lib/tests/__init__.py rename to sunnypilot/livedelay/__init__.py diff --git a/sunnypilot/livedelay/helpers.py b/sunnypilot/livedelay/helpers.py new file mode 100644 index 0000000000..0f7437ceea --- /dev/null +++ b/sunnypilot/livedelay/helpers.py @@ -0,0 +1,14 @@ +""" +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 openpilot.common.params import Params + + +def get_lat_delay(params: Params, stock_lat_delay: float) -> float: + if params.get_bool("LagdToggle"): + return float(params.get("LagdValueCache", return_default=True)) + + return stock_lat_delay diff --git a/sunnypilot/livedelay/lagd_toggle.py b/sunnypilot/livedelay/lagd_toggle.py new file mode 100644 index 0000000000..8495dcee0a --- /dev/null +++ b/sunnypilot/livedelay/lagd_toggle.py @@ -0,0 +1,38 @@ +""" +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 log + +from opendbc.car import structs +from openpilot.common.params import Params + + +class LagdToggle: + def __init__(self, CP: structs.CarParams): + self.CP = CP + self.params = Params() + self.lag = 0.0 + + self.lagd_toggle = self.params.get_bool("LagdToggle") + self.software_delay = self.params.get("LagdToggleDelay", return_default=True) + + def read_params(self) -> None: + self.lagd_toggle = self.params.get_bool("LagdToggle") + self.software_delay = self.params.get("LagdToggleDelay", return_default=True) + + def update(self, lag_msg: log.LiveDelayData) -> None: + self.read_params() + + if not self.lagd_toggle: + steer_actuator_delay = self.CP.steerActuatorDelay + delay = self.software_delay + self.lag = (steer_actuator_delay + delay) + self.params.put_nonblocking("LagdValueCache", self.lag) + return + + lateral_delay = lag_msg.liveDelay.lateralDelay + self.lag = lateral_delay + self.params.put_nonblocking("LagdValueCache", self.lag) diff --git a/sunnypilot/mads/helpers.py b/sunnypilot/mads/helpers.py index 4b1095c63e..70b590a8b2 100644 --- a/sunnypilot/mads/helpers.py +++ b/sunnypilot/mads/helpers.py @@ -25,10 +25,7 @@ def read_steering_mode_param(CP: structs.CarParams, params: Params): if get_mads_limited_brands(CP): return MadsSteeringModeOnBrake.DISENGAGE - try: - return int(params.get("MadsSteeringMode")) - except (ValueError, TypeError): - return MadsSteeringModeOnBrake.REMAIN_ACTIVE + return params.get("MadsSteeringMode", return_default=True) def set_alternative_experience(CP: structs.CarParams, params: Params): @@ -59,6 +56,6 @@ def set_car_specific_params(CP: structs.CarParams, CP_SP: structs.CarParamsSP, p # TODO-SP: To enable MADS full support for Rivian/Tesla, identify consistent signals for MADS toggling mads_partial_support = get_mads_limited_brands(CP) if mads_partial_support: - params.put("MadsSteeringMode", "2") + params.put("MadsSteeringMode", 2) params.put_bool("MadsUnifiedEngagementMode", True) params.remove("MadsMainCruiseAllowed") diff --git a/sunnypilot/mapd/__init__.py b/sunnypilot/mapd/__init__.py index e69de29bb2..7ad6f74149 100644 --- a/sunnypilot/mapd/__init__.py +++ b/sunnypilot/mapd/__init__.py @@ -0,0 +1,5 @@ +import os +from openpilot.common.basedir import BASEDIR + +MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj') +MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd') diff --git a/sunnypilot/mapd/live_map_data/osm_map_data.py b/sunnypilot/mapd/live_map_data/osm_map_data.py index 5f74a5d011..1eaf76dc8d 100644 --- a/sunnypilot/mapd/live_map_data/osm_map_data.py +++ b/sunnypilot/mapd/live_map_data/osm_map_data.py @@ -31,14 +31,14 @@ class OsmMapData(BaseMapData): self.mem_params.put("LastGPSPosition", json.dumps(params)) def get_current_speed_limit(self) -> float: - return float(self.mem_params.get("MapSpeedLimit", encoding='utf8') or 0.0) + return float(self.mem_params.get("MapSpeedLimit") or 0.0) def get_current_road_name(self) -> str: - return self.mem_params.get("RoadName", encoding='utf8') or "" + return str(self.mem_params.get("RoadName") or "") def get_next_speed_limit_and_distance(self) -> tuple[float, float]: - next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit", encoding='utf8') - next_speed_limit_section = json.loads(next_speed_limit_section_str) if next_speed_limit_section_str else {} + next_speed_limit_section_str = self.mem_params.get("NextMapSpeedLimit") + next_speed_limit_section = next_speed_limit_section_str if next_speed_limit_section_str else {} next_speed_limit = next_speed_limit_section.get('speedlimit', 0.0) next_speed_limit_latitude = next_speed_limit_section.get('latitude') next_speed_limit_longitude = next_speed_limit_section.get('longitude') diff --git a/sunnypilot/mapd/mapd_installer.py b/sunnypilot/mapd/mapd_installer.py index a151dd4807..9f7a8c2a5c 100755 --- a/sunnypilot/mapd/mapd_installer.py +++ b/sunnypilot/mapd/mapd_installer.py @@ -16,32 +16,39 @@ from urllib.request import urlopen from cereal import messaging from openpilot.common.params import Params -from openpilot.sunnypilot.mapd.mapd_manager import MAPD_PATH, MAPD_BIN_DIR from openpilot.system.hardware.hw import Paths from openpilot.common.spinner import Spinner from openpilot.system.version import is_prebuilt +from openpilot.sunnypilot.mapd import MAPD_PATH, MAPD_BIN_DIR import openpilot.system.sentry as sentry -VERSION = 'v1.9.0' +VERSION = 'v1.10.0' URL = f"https://github.com/pfeiferj/openpilot-mapd/releases/download/{VERSION}/mapd" +def update_installed_version(version: str, params: Params = None) -> None: + if params is None: + params = Params() + + params.put("MapdVersion", version) + + class MapdInstallManager: def __init__(self, spinner_ref: Spinner): self._spinner = spinner_ref + self._params = Params() def download(self) -> None: self.ensure_directories_exist() self._download_file() - self.update_installed_version(VERSION) + update_installed_version(VERSION, self._params) def check_and_download(self) -> None: if self.download_needed(): self.download() - @staticmethod - def download_needed() -> bool: - return not os.path.exists(MAPD_PATH) or MapdInstallManager.get_installed_version() != VERSION + def download_needed(self) -> bool: + return not os.path.exists(MAPD_PATH) or self.get_installed_version() != VERSION @staticmethod def ensure_directories_exist() -> None: @@ -82,13 +89,8 @@ class MapdInstallManager: temp_file.unlink() logging.error("Failed to download file after all retries") - @staticmethod - def update_installed_version(version: str) -> None: - Params().put("MapdVersion", version) - - @staticmethod - def get_installed_version() -> str: - return Params().get("MapdVersion", encoding="utf-8") or "" + def get_installed_version(self) -> str: + return str(self._params.get("MapdVersion") or "") def wait_for_internet_connection(self, return_on_failure: bool = False) -> bool: max_retries = 10 @@ -122,7 +124,7 @@ class MapdInstallManager: return if self.wait_for_internet_connection(return_on_failure=True): - self._spinner.update(f"Downloading pfeiferj's mapd [{install_manager.get_installed_version()}] => [{VERSION}].") + self._spinner.update(f"Downloading pfeiferj's mapd [{self.get_installed_version()}] => [{VERSION}].") time.sleep(0.1) self.check_and_download() self._spinner.close() @@ -146,7 +148,7 @@ if __name__ == "__main__": if is_prebuilt(): debug_msg = f"[DEBUG] This is prebuilt, no mapd install required. VERSION: [{VERSION}], Param [{install_manager.get_installed_version()}]" spinner.update(debug_msg) - install_manager.update_installed_version(VERSION) + update_installed_version(VERSION) else: spinner.update(f"Checking if mapd is installed and valid. Prebuilt [{is_prebuilt()}]") install_manager.non_prebuilt_install() diff --git a/sunnypilot/mapd/mapd_manager.py b/sunnypilot/mapd/mapd_manager.py index 342ba3fe7b..1211c1ecc6 100755 --- a/sunnypilot/mapd/mapd_manager.py +++ b/sunnypilot/mapd/mapd_manager.py @@ -6,28 +6,26 @@ 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.basedir import BASEDIR from openpilot.common.params import Params from openpilot.common.realtime import Ratekeeper, config_realtime_process from openpilot.common.swaglog import cloudlog from openpilot.selfdrive.selfdrived.alertmanager import set_offroad_alert from openpilot.sunnypilot.mapd.live_map_data.osm_map_data import OsmMapData from openpilot.system.hardware.hw import Paths +from openpilot.sunnypilot.mapd import MAPD_PATH +from openpilot.sunnypilot.mapd.mapd_installer import VERSION, update_installed_version # PFEIFER - MAPD {{ params = Params() mem_params = Params("/dev/shm/params") if platform.system() != "Darwin" else params # }} PFEIFER - MAPD -MAPD_BIN_DIR = os.path.join(BASEDIR, 'third_party/mapd_pfeiferj') -MAPD_PATH = os.path.join(MAPD_BIN_DIR, 'mapd') - def get_files_for_cleanup() -> list[str]: paths = [ @@ -58,15 +56,20 @@ 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.time())) + params.put("OsmDownloadedDate", str(datetime.now().timestamp())) params.put_bool("OsmDbUpdatesCheck", False) - osm_download_locations = json.dumps({ + osm_download_locations = { + "nations": nations, + "states": states or [] + } + + osm_download_locations_dump = json.dumps({ "nations": nations, "states": states or [] }) - print(f"Downloading maps for {osm_download_locations}") + print(f"Downloading maps for {osm_download_locations_dump}") mem_params.put("OSMDownloadLocations", osm_download_locations) @@ -100,12 +103,12 @@ def filter_nations_and_states(nations: list[str], states: list[str] = None) -> t def update_osm_db() -> None: - # last_downloaded_date = float(params.get('OsmDownloadedDate', encoding='utf-8') or 0.0) - # if params.get_bool("OsmDbUpdatesCheck") or time.time() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60 + # last_downloaded_date = params.get("OsmDownloadedDate", return_default=True) + # if params.get_bool("OsmDbUpdatesCheck") or time.monotonic() - last_downloaded_date >= 604800: # 7 days * 24 hours/day * 60 if params.get_bool("OsmDbUpdatesCheck"): cleanup_old_osm_data(get_files_for_cleanup()) - country = params.get('OsmLocationName', encoding='utf-8') - state = params.get('OsmStateName', encoding='utf-8') or "All" + country = params.get("OsmLocationName", return_default=True) + state = params.get("OsmStateName", return_default=True) filtered_nations, filtered_states = filter_nations_and_states([country], [state]) request_refresh_osm_location_data(filtered_nations, filtered_states) @@ -117,6 +120,7 @@ def update_osm_db() -> None: def main_thread(): + update_installed_version(VERSION, params) config_realtime_process([0, 1, 2, 3], 5) rk = Ratekeeper(1, print_delay_threshold=None) diff --git a/sunnypilot/modeld/fill_model_msg.py b/sunnypilot/modeld/fill_model_msg.py index 608f24424d..dadffc8433 100644 --- a/sunnypilot/modeld/fill_model_msg.py +++ b/sunnypilot/modeld/fill_model_msg.py @@ -103,7 +103,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(orientation_rate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) # temporal pose - temporal_pose = modelV2.temporalPose + temporal_pose = modelV2.temporalPoseDEPRECATED temporal_pose.trans = net_output_data['plan'][0,0,Plan.VELOCITY].tolist() temporal_pose.transStd = net_output_data['plan_stds'][0,0,Plan.VELOCITY].tolist() temporal_pose.rot = net_output_data['plan'][0,0,Plan.ORIENTATION_RATE].tolist() diff --git a/sunnypilot/modeld/modeld.py b/sunnypilot/modeld/modeld.py index d4bd96ff66..3d11ed23f4 100755 --- a/sunnypilot/modeld/modeld.py +++ b/sunnypilot/modeld/modeld.py @@ -20,13 +20,14 @@ from openpilot.system import sentry from openpilot.selfdrive.controls.lib.desire_helper import DesireHelper from openpilot.selfdrive.controls.lib.drive_helpers import get_accel_from_plan, smooth_value +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay from openpilot.sunnypilot.modeld.runners import ModelRunner, Runtime from openpilot.sunnypilot.modeld.parse_model_outputs import Parser from openpilot.sunnypilot.modeld.fill_model_msg import fill_model_msg, fill_pose_msg, PublishState from openpilot.sunnypilot.modeld.constants import ModelConstants, Plan from openpilot.sunnypilot.models.helpers import get_active_bundle, get_model_path, load_metadata, prepare_inputs, load_meta_constants -from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd from openpilot.sunnypilot.modeld.models.commonmodel_pyx import ModelFrame, CLContext +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase PROCESS_NAME = "selfdrive.modeld.modeld_snpe" @@ -48,7 +49,7 @@ 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 ModelState: +class ModelState(ModelStateBase): frame: ModelFrame wide_frame: ModelFrame inputs: dict[str, np.ndarray] @@ -57,12 +58,13 @@ class ModelState: model: ModelRunner def __init__(self, context: CLContext): + ModelStateBase.__init__(self) self.frame = ModelFrame(context) self.wide_frame = ModelFrame(context) self.prev_desire = np.zeros(ModelConstants.DESIRE_LEN, dtype=np.float32) bundle = get_active_bundle() overrides = {override.key: override.value for override in bundle.overrides} - self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2")) + self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) model_paths = get_model_path() @@ -175,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() @@ -202,8 +204,6 @@ def main(demo=False): cloudlog.info("modeld got CarParams: %s", CP.brand) - modeld_lagd = ModeldLagd() - # Enable lagd support for sunnypilot modeld long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -248,8 +248,9 @@ def main(demo=False): v_ego = sm["carState"].vEgo is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId - steer_delay = modeld_lagd.lagd_main(CP, sm, model) - + if sm.frame % 60 == 0: + model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) + lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -283,7 +284,7 @@ def main(demo=False): } if "lateral_control_params" in model.inputs.keys(): - inputs['lateral_control_params'] = np.array([max(v_ego, 0.), steer_delay], dtype=np.float32) + inputs['lateral_control_params'] = np.array([max(v_ego, 0.), lat_delay], dtype=np.float32) if "driving_style" in model.inputs.keys(): inputs['driving_style'] = np.array([1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0], dtype=np.float32) @@ -303,10 +304,11 @@ 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, - v_ego, steer_delay, model.meta) + v_ego, lat_delay, model.meta) desire_state = modelv2_send.modelV2.meta.desireState l_lane_change_prob = desire_state[log.Desire.laneChangeLeft] @@ -315,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 @@ -322,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/modeld_base.py b/sunnypilot/modeld/modeld_base.py new file mode 100644 index 0000000000..ba57659b09 --- /dev/null +++ b/sunnypilot/modeld/modeld_base.py @@ -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. +""" +from openpilot.common.params import Params + + +class ModelStateBase: + def __init__(self): + self.lat_delay = Params().get("LagdValueCache", return_default=True) diff --git a/sunnypilot/modeld_v2/fill_model_msg.py b/sunnypilot/modeld_v2/fill_model_msg.py index 4d04d6e5ec..c7de698f61 100644 --- a/sunnypilot/modeld_v2/fill_model_msg.py +++ b/sunnypilot/modeld_v2/fill_model_msg.py @@ -10,8 +10,8 @@ SEND_RAW_PRED = os.getenv('SEND_RAW_PRED') ConfidenceClass = log.ModelDataV2.ConfidenceClass -def get_curvature_from_output(output, vego, lat_action_t, current_generation=None): - if current_generation != 11: +def get_curvature_from_output(output, vego, lat_action_t, mlsim): + if not mlsim: if desired_curv := output.get('desired_curvature'): # If the model outputs the desired curvature, use that directly return float(desired_curv[0, 0]) @@ -100,7 +100,7 @@ def fill_model_msg(base_msg: capnp._DynamicStructBuilder, extended_msg: capnp._D fill_xyzt(modelV2.orientationRate, ModelConstants.T_IDXS, *net_output_data['plan'][0,:,Plan.ORIENTATION_RATE].T) # temporal pose - temporal_pose = modelV2.temporalPose + temporal_pose = modelV2.temporalPoseDEPRECATED if 'sim_pose' in net_output_data: temporal_pose.trans = net_output_data['sim_pose'][0,:ModelConstants.POSE_WIDTH//2].tolist() temporal_pose.transStd = net_output_data['sim_pose_stds'][0,:ModelConstants.POSE_WIDTH//2].tolist() diff --git a/sunnypilot/modeld_v2/modeld.py b/sunnypilot/modeld_v2/modeld.py index 5b9c5ef9e5..0fd45940d8 100755 --- a/sunnypilot/modeld_v2/modeld.py +++ b/sunnypilot/modeld_v2/modeld.py @@ -22,11 +22,12 @@ from openpilot.sunnypilot.modeld_v2.constants import Plan from openpilot.sunnypilot.modeld_v2.models.commonmodel_pyx import DrivingModelFrame, CLContext from openpilot.sunnypilot.modeld_v2.meta_helper import load_meta_constants +from openpilot.sunnypilot.livedelay.helpers import get_lat_delay +from openpilot.sunnypilot.modeld.modeld_base import ModelStateBase from openpilot.sunnypilot.models.helpers import get_active_bundle -from openpilot.sunnypilot.models.modeld_lagd import ModeldLagd from openpilot.sunnypilot.models.runners.helpers import get_model_runner -PROCESS_NAME = "selfdrive.modeld.modeld" +PROCESS_NAME = "selfdrive.modeld.modeld_tinygrad" class FrameMeta: @@ -39,13 +40,14 @@ class FrameMeta: self.frame_id, self.timestamp_sof, self.timestamp_eof = vipc.frame_id, vipc.timestamp_sof, vipc.timestamp_eof -class ModelState: +class ModelState(ModelStateBase): frames: dict[str, DrivingModelFrame] inputs: dict[str, np.ndarray] prev_desire: np.ndarray # for tracking the rising edge of the pulse temporal_idxs: slice | np.ndarray def __init__(self, context: CLContext): + ModelStateBase.__init__(self) try: self.model_runner = get_model_runner() self.constants = self.model_runner.constants @@ -54,10 +56,10 @@ class ModelState: raise model_bundle = get_active_bundle() - self.generation = model_bundle.generation + self.generation = model_bundle.generation if model_bundle is not None else None overrides = {override.key: override.value for override in model_bundle.overrides} - self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".2")) + self.LAT_SMOOTH_SECONDS = float(overrides.get('lat', ".0")) self.LONG_SMOOTH_SECONDS = float(overrides.get('long', ".0")) self.MIN_LAT_CONTROL_SPEED = 0.3 @@ -67,47 +69,55 @@ class ModelState: # img buffers are managed in openCL transform code self.numpy_inputs = {} + self.temporal_buffers = {} + self.temporal_idxs_map = {} for key, shape in self.model_runner.input_shapes.items(): if key not in self.frames: # Managed by opencl 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 = shape[1] * 4 if shape[1] < 99 else shape[1] # Allow for higher history buffers in the future + feature_len = shape[2] + 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] >= 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) - if self.model_runner.is_20hz_3d: # split models - self.full_features_buffer = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.FEATURE_LEN), dtype=np.float32) - self.full_desire = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.DESIRE_LEN), dtype=np.float32) - self.full_prev_desired_curv = np.zeros((1, self.constants.FULL_HISTORY_BUFFER_LEN, self.constants.PREV_DESIRED_CURV_LEN), dtype=np.float32) - self.temporal_idxs = slice(-1-(self.constants.TEMPORAL_SKIP*(self.constants.INPUT_HISTORY_BUFFER_LEN-1)), None, self.constants.TEMPORAL_SKIP) - elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: - self.full_features_buffer = np.zeros((self.constants.FULL_HISTORY_BUFFER_LEN + 1, self.constants.FEATURE_LEN), dtype=np.float32) - self.full_desire = np.zeros((self.constants.FULL_HISTORY_BUFFER_LEN + 1, self.constants.DESIRE_LEN), dtype=np.float32) - num_elements = self.numpy_inputs['features_buffer'].shape[1] - step_size = int(-100 / num_elements) - self.temporal_idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] - self.desire_reshape_dims = (self.numpy_inputs['desire'].shape[0], self.numpy_inputs['desire'].shape[1], -1, - self.numpy_inputs['desire'].shape[2]) + @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'] + 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 - if self.model_runner.is_20hz_3d: # split models - 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, self.constants.INPUT_HISTORY_BUFFER_LEN, self.constants.TEMPORAL_SKIP, -1)).max(axis=2) - elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: # 20hz supercombo - self.full_desire[:-1] = self.full_desire[1:] - self.full_desire[-1] = new_desire - self.numpy_inputs['desire'][:] = self.full_desire.reshape(self.desire_reshape_dims).max(axis=2) - else: # not 20hz - length = inputs['desire'].shape[0] - self.numpy_inputs['desire'][0, :-1] = self.numpy_inputs['desire'][0, 1:] - self.numpy_inputs['desire'][0, -1, :length] = new_desire[:length] + # Roll buffer and assign based on desire.shape[1] value + 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[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} @@ -121,42 +131,27 @@ class ModelState: # Run model inference outputs = self.model_runner.run_model() - if self.model_runner.is_20hz_3d: # split models - self.full_features_buffer[0, :-1] = self.full_features_buffer[0, 1:] - self.full_features_buffer[0, -1] = outputs['hidden_state'][0, :] - self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[0, self.temporal_idxs] - elif self.model_runner.is_20hz and not self.model_runner.is_20hz_3d: # 20hz supercombo - self.full_features_buffer[:-1] = self.full_features_buffer[1:] - self.full_features_buffer[-1] = outputs['hidden_state'][0, :] - self.numpy_inputs['features_buffer'][:] = self.full_features_buffer[self.temporal_idxs] - else: # not 20hz - feature_len = outputs['hidden_state'].shape[1] - self.numpy_inputs['features_buffer'][0, :-1] = self.numpy_inputs['features_buffer'][0, 1:] - self.numpy_inputs['features_buffer'][0, -1, :feature_len] = outputs['hidden_state'][0, :feature_len] + # Update features_buffer + self.temporal_buffers['features_buffer'][0, :-1] = self.temporal_buffers['features_buffer'][0, 1:] + self.temporal_buffers['features_buffer'][0, -1] = outputs['hidden_state'][0, :] + self.numpy_inputs['features_buffer'][:] = self.temporal_buffers['features_buffer'][0, self.temporal_idxs_map['features_buffer']] if "desired_curvature" in outputs: input_name_prev = None - if "prev_desired_curvs" in self.numpy_inputs.keys(): input_name_prev = 'prev_desired_curvs' elif "prev_desired_curv" in self.numpy_inputs.keys(): input_name_prev = 'prev_desired_curv' - - if input_name_prev is not None: + if input_name_prev and input_name_prev in self.temporal_buffers: self.process_desired_curvature(outputs, input_name_prev) return outputs def process_desired_curvature(self, outputs, input_name_prev): - if self.model_runner.is_20hz_3d: # split models - self.full_prev_desired_curv[0,:-1] = self.full_prev_desired_curv[0,1:] - self.full_prev_desired_curv[0,-1,:] = outputs['desired_curvature'][0, :] - self.numpy_inputs[input_name_prev][:] = self.full_prev_desired_curv[0, self.temporal_idxs] - if self.generation == 11: - self.numpy_inputs[input_name_prev][:] = 0*self.full_prev_desired_curv[0, self.temporal_idxs] - else: - length = outputs['desired_curvature'][0].size - self.numpy_inputs[input_name_prev][0, :-length, 0] = self.numpy_inputs[input_name_prev][0, length:, 0] - self.numpy_inputs[input_name_prev][0, -length:, 0] = outputs['desired_curvature'][0] + self.temporal_buffers[input_name_prev][0,:-1] = self.temporal_buffers[input_name_prev][0,1:] + self.temporal_buffers[input_name_prev][0,-1,:] = outputs['desired_curvature'][0, :] + self.numpy_inputs[input_name_prev][:] = self.temporal_buffers[input_name_prev][0, self.temporal_idxs_map[input_name_prev]] + if self.mlsim: + self.numpy_inputs[input_name_prev][:] = 0*self.temporal_buffers[input_name_prev][0, self.temporal_idxs_map[input_name_prev]] def get_action_from_model(self, model_output: dict[str, np.ndarray], prev_action: log.ModelDataV2.Action, lat_action_t: float, long_action_t: float, v_ego: float) -> log.ModelDataV2.Action: @@ -165,11 +160,12 @@ class ModelState: action_t=long_action_t) 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.generation) - 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 + desired_curvature = get_curvature_from_output(model_output, v_ego, lat_action_t, self.mlsim) + 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)) @@ -212,7 +208,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() @@ -238,9 +234,6 @@ def main(demo=False): CP = messaging.log_from_bytes(params.get("CarParams", block=True), car.CarParams) cloudlog.info("modeld got CarParams: %s", CP.brand) - - modeld_lagd = ModeldLagd() - # TODO Move smooth seconds to action function long_delay = CP.longitudinalActuatorDelay + model.LONG_SMOOTH_SECONDS prev_action = log.ModelDataV2.Action() @@ -285,7 +278,9 @@ def main(demo=False): is_rhd = sm["driverMonitoringState"].isRHD frame_id = sm["roadCameraState"].frameId v_ego = max(sm["carState"].vEgo, 0.) - steer_delay = modeld_lagd.lagd_main(CP, sm, model) + if sm.frame % 60 == 0: + model.lat_delay = get_lat_delay(params, sm["liveDelay"].lateralDelay) + lat_delay = model.lat_delay + model.LAT_SMOOTH_SECONDS if sm.updated["liveCalibration"] and sm.seen['roadCameraState'] and sm.seen['deviceState']: device_from_calib_euler = np.array(sm["liveCalibration"].rpyCalib, dtype=np.float32) dc = DEVICE_CAMERAS[(str(sm['deviceState'].deviceType), str(sm['roadCameraState'].sensor))] @@ -317,12 +312,12 @@ 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, } if "lateral_control_params" in model.numpy_inputs.keys(): - inputs['lateral_control_params'] = np.array([v_ego, steer_delay], dtype=np.float32) + inputs['lateral_control_params'] = np.array([v_ego, lat_delay], dtype=np.float32) mt1 = time.perf_counter() model_output = model.run(bufs, transforms, inputs, prepare_only) @@ -333,8 +328,9 @@ 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, steer_delay + DT_MDL, long_delay + DT_MDL, v_ego) + action = model.get_action_from_model(model_output, prev_action, lat_delay + DT_MDL, long_delay + DT_MDL, v_ego) prev_action = action fill_model_msg(drivingdata_send, modelv2_send, model_output, action, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, @@ -347,6 +343,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 +351,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/parse_model_outputs_split.py b/sunnypilot/modeld_v2/parse_model_outputs_split.py index 7fab66e038..9cf321a1b6 100644 --- a/sunnypilot/modeld_v2/parse_model_outputs_split.py +++ b/sunnypilot/modeld_v2/parse_model_outputs_split.py @@ -88,37 +88,62 @@ class Parser: outs[name] = pred_mu_final.reshape(final_shape) outs[name + '_stds'] = pred_std_final.reshape(final_shape) + def is_mhp(self, outs, name, shape): + if self.check_missing(outs, name): + return False + if outs[name].shape[1] == 2 * shape: + return False + return True + + def parse_dynamic_outputs(self, outs: dict[str, np.ndarray]) -> None: + if 'lead' in outs: + lead_mhp = self.is_mhp(outs, 'lead', + SplitModelConstants.LEAD_MHP_SELECTION * SplitModelConstants.LEAD_TRAJ_LEN * SplitModelConstants.LEAD_WIDTH) + lead_in_N, lead_out_N = (SplitModelConstants.LEAD_MHP_N, SplitModelConstants.LEAD_MHP_SELECTION) if lead_mhp else (0, 0) + lead_out_shape = (SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH) if lead_mhp else \ + (SplitModelConstants.LEAD_MHP_SELECTION, SplitModelConstants.LEAD_TRAJ_LEN, SplitModelConstants.LEAD_WIDTH) + self.parse_mdn('lead', outs, in_N=lead_in_N, out_N=lead_out_N, out_shape=lead_out_shape) + if 'plan' in outs: + plan_mhp = self.is_mhp(outs, 'plan', SplitModelConstants.IDX_N * SplitModelConstants.PLAN_WIDTH) + plan_in_N, plan_out_N = (SplitModelConstants.PLAN_MHP_N, SplitModelConstants.PLAN_MHP_SELECTION) if plan_mhp else (0, 0) + self.parse_mdn('plan', outs, in_N=plan_in_N, out_N=plan_out_N, + out_shape=(SplitModelConstants.IDX_N, SplitModelConstants.PLAN_WIDTH)) + def split_outputs(self, outs: dict[str, np.ndarray]) -> None: + if 'desired_curvature' in outs: + self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,)) + if 'desire_pred' in outs: + self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH)) + if 'desire_state' in outs: + self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,)) if 'lane_lines' in outs: self.parse_mdn('lane_lines', outs, in_N=0, out_N=0, - out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + out_shape=(SplitModelConstants.NUM_LANE_LINES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + if 'lane_lines_prob' in outs: + self.parse_binary_crossentropy('lane_lines_prob', outs) + if 'lead_prob' in outs: + self.parse_binary_crossentropy('lead_prob', outs) + if 'lat_planner_solution' in outs: + self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) + if 'meta' in outs: + self.parse_binary_crossentropy('meta', outs) + if 'road_edges' in outs: self.parse_mdn('road_edges', outs, in_N=0, out_N=0, - out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) - self.parse_mdn('lead', outs, in_N=SplitModelConstants.LEAD_MHP_N, out_N=SplitModelConstants.LEAD_MHP_SELECTION, - out_shape=(SplitModelConstants.LEAD_TRAJ_LEN,SplitModelConstants.LEAD_WIDTH)) - if 'sim_pose' in outs: - self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) - for k in ['lead_prob', 'lane_lines_prob']: - self.parse_binary_crossentropy(k, outs) + out_shape=(SplitModelConstants.NUM_ROAD_EDGES,SplitModelConstants.IDX_N,SplitModelConstants.LANE_LINES_WIDTH)) + if 'sim_pose' in outs: + self.parse_mdn('sim_pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) def parse_vision_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: self.parse_mdn('pose', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) self.parse_mdn('wide_from_device_euler', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.WIDE_FROM_DEVICE_WIDTH,)) self.parse_mdn('road_transform', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.POSE_WIDTH,)) + self.parse_dynamic_outputs(outs) self.split_outputs(outs) - self.parse_categorical_crossentropy('desire_pred', outs, out_shape=(SplitModelConstants.DESIRE_PRED_LEN,SplitModelConstants.DESIRE_PRED_WIDTH)) - self.parse_binary_crossentropy('meta', outs) return outs def parse_policy_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: - self.parse_mdn('plan', outs, in_N=SplitModelConstants.PLAN_MHP_N, out_N=SplitModelConstants.PLAN_MHP_SELECTION, - out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.PLAN_WIDTH)) + self.parse_dynamic_outputs(outs) self.split_outputs(outs) - if 'lat_planner_solution' in outs: - self.parse_mdn('lat_planner_solution', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.IDX_N,SplitModelConstants.LAT_PLANNER_SOLUTION_WIDTH)) - if 'desired_curvature' in outs: - self.parse_mdn('desired_curvature', outs, in_N=0, out_N=0, out_shape=(SplitModelConstants.DESIRED_CURV_WIDTH,)) - self.parse_categorical_crossentropy('desire_state', outs, out_shape=(SplitModelConstants.DESIRE_PRED_WIDTH,)) return outs def parse_outputs(self, outs: dict[str, np.ndarray]) -> dict[str, np.ndarray]: diff --git a/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py b/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py new file mode 100644 index 0000000000..f664db31b3 --- /dev/null +++ b/sunnypilot/modeld_v2/tests/test_buffer_logic_inspect.py @@ -0,0 +1,263 @@ +import numpy as np +import pytest +from typing import Any + +import openpilot.sunnypilot.models.helpers as helpers +import openpilot.sunnypilot.models.runners.helpers as runner_helpers +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, 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 +] + + +# This creates a dummy runner, override, and bundle instance for the tests to run, without actually trying to load a physical model. +class DummyOverride: + def __init__(self, key: str, value: str) -> None: + self.key = key + self.value = value + + +class DummyBundle: + def __init__(self) -> None: + self.overrides = [DummyOverride('lat', '.1'), DummyOverride('long', '.3')] + self.generation = 10 # default to non-mlsim for buffer-update tests, as raising to 11 here will zero curvature buffer + + +class DummyModelRunner: + def __init__(self, input_shapes: dict[str, tuple[int, int, int]], constants: Any = None) -> None: + self.input_shapes = input_shapes + self.constants = constants or type('C', (), { + 'FULL_HISTORY_BUFFER_LEN': 100, + 'FEATURE_LEN': 512, + 'DESIRE_LEN': 8, + 'PREV_DESIRED_CURV_LEN': 1, + 'INPUT_HISTORY_BUFFER_LEN': 25, + 'TEMPORAL_SKIP': 4, + })() + self.vision_input_names: list[str] = [] + shape = input_shapes.get('desire', (1, 0, 0)) # [batch, history, features] + if shape[1] == 25: + self.is_20hz = True + else: + self.is_20hz = False + + # Minimal prepare/run methods so ModelState can be run without actually running the model + def prepare_inputs(self, imgs_cl, numpy_inputs, frames): + return None + + def run_model(self): + return { + 'hidden_state': np.zeros((1, self.constants.FEATURE_LEN), dtype=np.float32), + 'desired_curvature': np.zeros((1, 1), dtype=np.float32), + } + + +@pytest.fixture +def shapes(request): + return request.param + + +@pytest.fixture +def bundle() -> DummyBundle: + return DummyBundle() + + +@pytest.fixture +def runner(shapes) -> DummyModelRunner: + return DummyModelRunner(shapes) + + +@pytest.fixture +def apply_patches(monkeypatch: pytest.MonkeyPatch, bundle: DummyBundle, runner: DummyModelRunner): + monkeypatch.setattr(helpers, 'get_active_bundle', lambda params=None: bundle, raising=False) + monkeypatch.setattr(runner_helpers, 'get_model_runner', lambda: runner, raising=False) + monkeypatch.setattr(modeld_module, 'get_model_runner', lambda: runner, raising=False) + monkeypatch.setattr(modeld_module, 'get_active_bundle', lambda params=None: bundle, raising=False) + + +# These are expected shapes and indices based on the time the model was presented +def get_expected_indices(shape, constants, mode, key=None): + if mode == 'split': + start = -1 - (constants.TEMPORAL_SKIP * (constants.INPUT_HISTORY_BUFFER_LEN - 1)) + arr = np.arange(constants.FULL_HISTORY_BUFFER_LEN) + idxs = arr[start::constants.TEMPORAL_SKIP] + return idxs + elif mode == '20hz': + num_elements = shape[1] + step_size = int(-100 / num_elements) + idxs = np.arange(step_size, step_size * (num_elements + 1), step_size)[::-1] + return idxs + elif mode == 'non20hz': + return np.arange(shape[1]) + return None + + +@pytest.mark.parametrize("shapes,mode", SHAPE_MODE_PARAMS, indirect=["shapes"]) +def test_buffer_shapes_and_indices(shapes, mode, apply_patches): + state = ModelState(None) + constants = DummyModelRunner(shapes).constants + 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]) + expected_idxs = get_expected_indices(shapes[key], constants, 'split', key) + elif mode == '20hz': + expected_shape = (1, constants.FULL_HISTORY_BUFFER_LEN, shapes[key][2]) + expected_idxs = get_expected_indices(shapes[key], constants, '20hz', key) + elif mode == 'non20hz': + 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" + assert buf.shape == expected_shape, f"{key}: buffer shape {buf.shape} != expected {expected_shape}" + if expected_idxs is not None: + assert np.all(idxs == expected_idxs), f"{key}: buffer idxs {idxs} != expected {expected_idxs}" + else: + 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, 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' 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) + elif key == 'features_buffer': + buf[0,:-1] = buf[0,1:] + buf[0,-1] = new_val + return buf[0, idxs] + elif key == 'prev_desired_curv': + buf[0,:-1] = buf[0,1:] + buf[0,-1,:] = new_val + return buf[0, idxs] + elif mode == '20hz': + if key == 'desire': + buf[:-1] = buf[1:] + buf[-1] = new_val + reshape_dims = (1, buf.shape[1], -1, buf.shape[2]) + reshaped = buf.reshape(reshape_dims).max(axis=2) + # Slice to last shape[1] elements to match model input shape + input_len = reshaped.shape[1] + model_input_len = 25 # For 20hz mode, desire shape[1] is 25 + if input_len > model_input_len: + reshaped = reshaped[:, -model_input_len:, :] + return reshaped + elif key == 'features_buffer': + buffer_history_len = buf.shape[1] + legacy_buf = np.zeros((buffer_history_len, buf.shape[2]), dtype=np.float32) + legacy_buf[:] = buf[0] + legacy_buf[:-1] = legacy_buf[1:] + legacy_buf[-1] = new_val + return legacy_buf[idxs] + elif key == 'prev_desired_curv': + buffer_history_len = buf.shape[1] + legacy_buf = np.zeros((buffer_history_len, buf.shape[2]), dtype=np.float32) + legacy_buf[:] = buf[0] + legacy_buf[:-1] = legacy_buf[1:] + legacy_buf[-1,:] = new_val + return legacy_buf[idxs] + elif mode == 'non20hz': + if key == 'desire': + 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': + 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] + buf[0,-length:,0] = new_val[:length] + return buf[0] + return None + + +def dynamic_buffer_update(state, key, new_val, mode): + 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), + } + state.model_runner.run_model = run_model_stub + state.run({}, {}, inputs, prepare_only=False) + 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), + 'desired_curvature': np.asarray(new_val, dtype=np.float32).reshape(1, -1), + } + state.model_runner.run_model = run_model_stub + state.run({}, {}, inputs, prepare_only=False) + return state.numpy_inputs['prev_desired_curv'][0] + return None + + +@pytest.mark.parametrize("shapes,mode", SHAPE_MODE_PARAMS, indirect=["shapes"]) +@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(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, 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} {actual_key}: dynamic buffer update does not match legacy logic" diff --git a/sunnypilot/models/README.md b/sunnypilot/models/README.md new file mode 100644 index 0000000000..bf4fb72a98 --- /dev/null +++ b/sunnypilot/models/README.md @@ -0,0 +1,63 @@ +# Model Selector Version Compatibility + +This document explains the version compatibility mechanism used by the Model Selector system, and the rationale behind certain version constraints and JSON file management strategies. + +## Overview + +The Model Selector is responsible for selecting and validating model bundles based on their metadata and version constraints. Each model bundle is distributed via a JSON file and includes a `minimumSelectorVersion` field indicating the minimum selector version required to load it. + +To ensure robust compatibility and prevent mismatches between model expectations and selector capabilities, the selector enforces two version boundaries: + +* **`REQUIRED_MIN_SELECTOR_VERSION`**: the oldest selector version we support. +* **`CURRENT_SELECTOR_VERSION`**: the current version of the selector logic. + +## Version Compatibility Check + +A model bundle is considered compatible if: + +```python +REQUIRED_MIN_SELECTOR_VERSION <= bundle["minimumSelectorVersion"] <= CURRENT_SELECTOR_VERSION +``` + +This ensures: + +* **Old bundles are rejected** if they rely on deprecated selector behavior. +* **Future bundles are ignored** if they expect logic that the current selector doesn't yet implement. + +## Handling Breaking Changes + +When a deep change in selector behavior requires *all* models to be recompiled (e.g., due to a major architectural update), we: + +1. **Create a new JSON file** (e.g., from `models_v4.json` to `models_v5.json`). +2. **Assign updated `minimumSelectorVersion` values** in the new bundles. + +This allows older selector versions to continue using the previous JSON file, while newer versions point to the new one, preventing cross-contamination. + +## Why `REQUIRED_MIN_SELECTOR_VERSION` Still Matters + +Despite using new JSON files to isolate breaking changes, `REQUIRED_MIN_SELECTOR_VERSION` plays a critical role: + +### 1. **Cached Bundle Validation** + +Model bundles are cached locally (e.g., in-memory or on disk). A user might have previously loaded a now-invalid bundle from an older JSON file. + +`REQUIRED_MIN_SELECTOR_VERSION` prevents the selector from reloading or trusting that stale cached bundle, even if the original JSON is gone. + +### 2. **Explicit Deprecation Boundary** + +By raising `REQUIRED_MIN_SELECTOR_VERSION`, we declare older bundles officially unsupported, even if they technically still exist in a legacy JSON file. + +### 3. **Avoiding Race Conditions** + +Some clients may have intermittent access to updated JSONs. The runtime check ensures version compatibility is enforced independently of external file state. + +## Summary + +| Component | Purpose | +| ------------------------------- | --------------------------------------------------------------------- | +| `minimumSelectorVersion` | Declares the minimum selector version required to load a model bundle | +| `REQUIRED_MIN_SELECTOR_VERSION` | Prevents loading bundles that are too old (e.g., from stale cache) | +| `CURRENT_SELECTOR_VERSION` | Prevents loading bundles that are too new or forward-incompatible | +| JSON file renaming | Isolates bundles by selector generation to handle full recompiles | + +This layered strategy ensures safe evolution of the model selection system while maintaining backward compatibility and runtime protection against stale or incompatible bundles. diff --git a/sunnypilot/models/fetcher.py b/sunnypilot/models/fetcher.py index 02ae52d77e..3be6e0b46c 100644 --- a/sunnypilot/models/fetcher.py +++ b/sunnypilot/models/fetcher.py @@ -5,10 +5,10 @@ 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 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 @@ -67,6 +67,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 @@ -88,8 +89,8 @@ class ModelCache: def _is_expired(self) -> bool: """Checks if the cache has expired""" current_time = int(time.monotonic() * 1e9) - last_sync = int(self.params.get(self._LAST_SYNC_KEY, encoding="utf-8") or 0) - return last_sync == 0 or (current_time - last_sync) >= self.cache_timeout + last_sync = self.params.get(self._LAST_SYNC_KEY) or 0 + return bool(last_sync == 0) or (current_time - last_sync) >= self.cache_timeout def get(self) -> tuple[dict, bool]: """ @@ -98,43 +99,60 @@ class ModelCache: If no cached data exists or on error, returns an empty dict """ try: - cached_data = self.params.get(self._CACHE_KEY, encoding="utf-8") + cached_data = self.params.get(self._CACHE_KEY) if not cached_data: cloudlog.warning("No cached model data available") return {}, True - return json.loads(cached_data), self._is_expired() + return cached_data, self._is_expired() except Exception as e: cloudlog.exception(f"Error retrieving cached model data: {str(e)}") return {}, True def set(self, data: dict) -> None: """Updates the cache with new model data""" - self.params.put(self._CACHE_KEY, json.dumps(data)) - self.params.put(self._LAST_SYNC_KEY, str(int(time.monotonic() * 1e9))) + self.params.put(self._CACHE_KEY, data) + self.params.put(self._LAST_SYNC_KEY, int(time.monotonic() * 1e9)) class ModelFetcher: """Handles fetching and caching of model data from remote source""" - MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v4.json" + MODEL_URL = "https://docs.sunnypilot.ai/driving_models_v7.json" def __init__(self, params: Params): self.params = params 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.warning(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) diff --git a/sunnypilot/models/helpers.py b/sunnypilot/models/helpers.py index a614447ceb..ecf0a39b72 100644 --- a/sunnypilot/models/helpers.py +++ b/sunnypilot/models/helpers.py @@ -9,7 +9,6 @@ import hashlib import os import pickle import numpy as np -import json from openpilot.common.params import Params from cereal import custom @@ -19,8 +18,9 @@ from openpilot.system.hardware import PC from openpilot.system.hardware.hw import Paths from pathlib import Path -CURRENT_SELECTOR_VERSION = 6 -REQUIRED_MIN_SELECTOR_VERSION = 5 +# see the README.md for more details on the model selector versioning +CURRENT_SELECTOR_VERSION = 10 +REQUIRED_MIN_SELECTOR_VERSION = 9 USE_ONNX = os.getenv('USE_ONNX', PC) @@ -63,13 +63,14 @@ def is_bundle_version_compatible(bundle: dict) -> bool: """ return bool(REQUIRED_MIN_SELECTOR_VERSION <= bundle.get("minimumSelectorVersion", 0) <= CURRENT_SELECTOR_VERSION) + def get_active_bundle(params: Params = None) -> custom.ModelManagerSP.ModelBundle: """Gets the active model bundle from cache""" if params is None: params = Params() try: - if (active_bundle := json.loads(params.get("ModelManager_ActiveBundle") or "{}")) and is_bundle_version_compatible(active_bundle): + if (active_bundle := params.get("ModelManager_ActiveBundle") or {}) and is_bundle_version_compatible(active_bundle): return custom.ModelManagerSP.ModelBundle(**active_bundle) except Exception: pass @@ -110,7 +111,7 @@ def get_active_model_runner(params: Params = None, force_check=False) -> custom. runner_type = active_bundle.runner.raw if cached_runner_type != runner_type: - params.put("ModelRunnerTypeCache", str(int(runner_type))) + params.put("ModelRunnerTypeCache", int(runner_type)) return runner_type diff --git a/sunnypilot/models/manager.py b/sunnypilot/models/manager.py index 630d6a8d77..a3ce046a33 100644 --- a/sunnypilot/models/manager.py +++ b/sunnypilot/models/manager.py @@ -8,7 +8,6 @@ See the LICENSE.md file in the root directory for more details. import asyncio import os import time -import json import aiohttp from openpilot.common.params import Params @@ -146,7 +145,7 @@ class ModelManagerSP: await asyncio.gather(*tasks) self.active_bundle = self.selected_bundle self.active_bundle.status = custom.ModelManagerSP.DownloadStatus.downloaded - self.params.put("ModelManager_ActiveBundle", json.dumps(self.active_bundle.to_dict())) + self.params.put("ModelManager_ActiveBundle", self.active_bundle.to_dict()) self.selected_bundle = None except Exception: @@ -169,14 +168,18 @@ class ModelManagerSP: self.available_models = self.model_fetcher.get_available_bundles() self.active_bundle = get_active_bundle(self.params) - if index_to_download := self.params.get("ModelManager_DownloadIndex", block=False, encoding="utf-8"): - if model_to_download := next((model for model in self.available_models if model.index == int(index_to_download)), None): + if index_to_download := self.params.get("ModelManager_DownloadIndex"): + if model_to_download := next((model for model in self.available_models if model.index == index_to_download), None): try: self.download(model_to_download, Paths.model_root()) except Exception as e: cloudlog.exception(e) finally: - self.params.put("ModelManager_DownloadIndex", "") + self.params.remove("ModelManager_DownloadIndex") + + if self.params.get("ModelManager_ClearCache"): + self.clear_model_cache() + self.params.remove("ModelManager_ClearCache") self._report_status() rk.keep_time() @@ -185,6 +188,31 @@ class ModelManagerSP: cloudlog.exception(f"Error in main thread: {str(e)}") rk.keep_time() + def clear_model_cache(self) -> None: + """ + Clears the model cache directory of all files except those in the active model bundle. + """ + + # Get list of files used by active model bundle + active_files = [] + if self.active_bundle is not None: # When the default model is active + for model in self.active_bundle.models: + if hasattr(model, 'artifact') and model.artifact.fileName: + active_files.append(model.artifact.fileName) + if hasattr(model, 'metadata') and model.metadata.fileName: + active_files.append(model.metadata.fileName) + + # Remove all files except active ones + model_dir = Paths.model_root() + try: + for filename in os.listdir(model_dir): + if filename not in active_files: + file_path = os.path.join(model_dir, filename) + if os.path.isfile(file_path): + os.remove(file_path) + cloudlog.info("Model cache cleared, keeping active model files") + except Exception as e: + cloudlog.exception(f"Error clearing model cache: {str(e)}") def main(): ModelManagerSP().main_thread() diff --git a/sunnypilot/models/modeld_lagd.py b/sunnypilot/models/modeld_lagd.py deleted file mode 100644 index 9e538123a8..0000000000 --- a/sunnypilot/models/modeld_lagd.py +++ /dev/null @@ -1,26 +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 openpilot.common.params import Params -from openpilot.common.swaglog import cloudlog - - -class ModeldLagd: - def __init__(self): - self.params = Params() - - def lagd_main(self, CP, sm, model): - if self.params.get_bool("LagdToggle"): - lateral_delay = sm["liveDelay"].lateralDelay - lat_smooth = model.LAT_SMOOTH_SECONDS - result = lateral_delay + lat_smooth - cloudlog.debug(f"LAGD USING LIVE DELAY: {lateral_delay:.3f} + {lat_smooth:.3f} = {result:.3f}") - return result - steer_actuator_delay = CP.steerActuatorDelay - lat_smooth = model.LAT_SMOOTH_SECONDS - result = (steer_actuator_delay + 0.2) + lat_smooth - cloudlog.debug(f"LAGD USING STEER ACTUATOR: {steer_actuator_delay:.3f} + 0.2 + {lat_smooth:.3f} = {result:.3f}") - return result diff --git a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py index 270890cace..2800179fb2 100644 --- a/sunnypilot/models/runners/tinygrad/tinygrad_runner.py +++ b/sunnypilot/models/runners/tinygrad/tinygrad_runner.py @@ -83,7 +83,7 @@ class TinygradRunner(ModelRunner, SupercomboTinygrad, PolicyTinygrad, VisionTiny def _run_model(self) -> NumpyDict: """Runs the Tinygrad model inference and parses the outputs.""" - outputs = self.model_run(**self.inputs).numpy().flatten() + outputs = self.model_run(**self.inputs).contiguous().realize().uop.base.buffer.numpy() return self._parse_outputs(outputs) def _parse_outputs(self, model_outputs: np.ndarray) -> NumpyDict: diff --git a/sunnypilot/models/tests/model_hash b/sunnypilot/models/tests/model_hash index f65c628164..f66baa7e71 100644 --- a/sunnypilot/models/tests/model_hash +++ b/sunnypilot/models/tests/model_hash @@ -1 +1 @@ -71979b29c4bab3007de1a4265442d79f44c0eaef066af66086dddfc432709b94 \ No newline at end of file +2ff2f49176a13bc7f856645d785b3b838a5c7ecf7f6cb37699fa0459ebf12453 \ No newline at end of file diff --git a/sunnypilot/models/tests/test_tinygrad_ref.py b/sunnypilot/models/tests/test_tinygrad_ref.py new file mode 100644 index 0000000000..c4f98a2cb6 --- /dev/null +++ b/sunnypilot/models/tests/test_tinygrad_ref.py @@ -0,0 +1,23 @@ +import requests + +from sunnypilot.models.tinygrad_ref import get_tinygrad_ref +from sunnypilot.models.fetcher import ModelFetcher + + +def fetch_tinygrad_ref(): + response = requests.get(ModelFetcher.MODEL_URL, timeout=10) + response.raise_for_status() + json_data = response.json() + return json_data.get("tinygrad_ref") + + +def test_tinygrad_ref(): + current_ref = get_tinygrad_ref() + remote_ref = fetch_tinygrad_ref() + assert remote_ref == current_ref, ( + f"""tinygrad_repo ref does not match remote tinygrad_ref of current compiled driving models json. + Current: {current_ref} + Remote: {remote_ref} + Please run build-all workflow to update models.""" + ) + print("tinygrad_repo ref matches current compiled driving models json ref.") diff --git a/sunnypilot/models/tinygrad_ref.py b/sunnypilot/models/tinygrad_ref.py new file mode 100644 index 0000000000..4dd333e323 --- /dev/null +++ b/sunnypilot/models/tinygrad_ref.py @@ -0,0 +1,36 @@ +import os + +from openpilot.common.basedir import BASEDIR + + +def get_tinygrad_ref(): + repo_path = os.path.join(BASEDIR, "tinygrad_repo") + git_path = os.path.join(repo_path, ".git") + try: + if os.path.isdir(git_path): + git_dir = git_path + else: + with open(git_path) as f: + line = f.read().strip() + git_dir = os.path.join(repo_path, line[8:]) + with open(os.path.join(git_dir, "HEAD")) as f: + ref = f.read().strip() + if ref.startswith("ref:"): + with open(os.path.join(git_dir, ref.split(" ", 1)[1])) as f: + return f.read().strip() + return ref + except Exception as e: + print(f"Error getting tinygrad_repo ref: {e}") + return None + + +def main(): + current_ref = get_tinygrad_ref() + if current_ref: + print(current_ref) + else: + print("") + + +if __name__ == "__main__": + main() diff --git a/sunnypilot/navd/helpers.py b/sunnypilot/navd/helpers.py index 85894fb2ab..c57706d32a 100644 --- a/sunnypilot/navd/helpers.py +++ b/sunnypilot/navd/helpers.py @@ -5,7 +5,7 @@ import math import numpy as np from typing import Any, cast -from openpilot.common.conversions import Conversions +from openpilot.common.constants import CV from openpilot.common.params import Params DIRECTIONS = ('left', 'right', 'straight') @@ -13,8 +13,8 @@ MODIFIABLE_DIRECTIONS = ('left', 'right') EARTH_MEAN_RADIUS = 6371007.2 SPEED_CONVERSIONS = { - 'km/h': Conversions.KPH_TO_MS, - 'mph': Conversions.MPH_TO_MS, + 'km/h': CV.KPH_TO_MS, + 'mph': CV.MPH_TO_MS, } @@ -133,7 +133,7 @@ def string_to_direction(direction: str) -> str: def maxspeed_to_ms(maxspeed: dict[str, str | float]) -> float: unit = cast(str, maxspeed['unit']) speed = cast(float, maxspeed['speed']) - return SPEED_CONVERSIONS[unit] * speed + return float(SPEED_CONVERSIONS[unit] * speed) def field_valid(dat: dict, field: str) -> bool: diff --git a/sunnypilot/neural_network_data b/sunnypilot/neural_network_data index b59ab483c8..03cac2d30e 160000 --- a/sunnypilot/neural_network_data +++ b/sunnypilot/neural_network_data @@ -1 +1 @@ -Subproject commit b59ab483c86cd70d04de91ad878024cd66fe86b6 +Subproject commit 03cac2d30e111e0689c0429cb8c1fe6cb5a905af 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 diff --git a/sunnypilot/selfdrive/car/cruise_ext.py b/sunnypilot/selfdrive/car/cruise_ext.py index 6f0419ed14..f443aeee0e 100644 --- a/sunnypilot/selfdrive/car/cruise_ext.py +++ b/sunnypilot/selfdrive/car/cruise_ext.py @@ -17,19 +17,13 @@ class VCruiseHelperSP: self.params = Params() self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") - self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1) - self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5) - - def read_int_param(self, key: str, default: int = 0) -> int: - try: - return int(self.params.get(key, encoding='utf8')) - except (ValueError, TypeError): - return default + self.short_increment = self.params.get("CustomAccShortPressIncrement", return_default=True) + self.long_increment = self.params.get("CustomAccLongPressIncrement", return_default=True) def read_custom_set_speed_params(self) -> None: self.custom_acc_enabled = self.params.get_bool("CustomAccIncrementsEnabled") - self.short_increment = self.read_int_param("CustomAccShortPressIncrement", 1) - self.long_increment = self.read_int_param("CustomAccLongPressIncrement", 5) + self.short_increment = self.params.get("CustomAccShortPressIncrement", return_default=True) + self.long_increment = self.params.get("CustomAccLongPressIncrement", return_default=True) def update_v_cruise_delta(self, long_press: bool, v_cruise_delta: float) -> tuple[bool, float]: if not self.custom_acc_enabled: diff --git a/sunnypilot/selfdrive/car/interfaces.py b/sunnypilot/selfdrive/car/interfaces.py index f936de5934..9e11ecdaac 100644 --- a/sunnypilot/selfdrive/car/interfaces.py +++ b/sunnypilot/selfdrive/car/interfaces.py @@ -4,6 +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. """ +from typing import Any from opendbc.car import structs from opendbc.car.interfaces import CarInterfaceBase @@ -49,16 +50,17 @@ def setup_interfaces(CI: CarInterfaceBase, params: Params = None) -> None: _initialize_neural_network_lateral_control(CI, CP, CP_SP, params) -def get_init_params(params) -> list[dict[str, str]]: - keys: list = [ +def initialize_params(params) -> list[dict[str, Any]]: + keys: list = [] + + # hyundai + keys.extend([ "HyundaiLongitudinalTuning", "LongTuningCustomToggle", - "LongTuningAccelMin", - "LongTuningVEgoStopping", - "LongTuningStoppingDecelRate", + "LongTuningAccelMax", "LongTuningMinUpperJerk", "LongTuningMinLowerJerk", "LongTuningJerkLimits", - ] + ]) - return [{k: params.get(k, encoding='utf8') or "0"} for k in keys] + return [{k: params.get(k, return_default=True)} for k in keys] diff --git a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py index 2f9d976847..3bed8534ef 100644 --- a/sunnypilot/selfdrive/car/tests/test_custom_cruise.py +++ b/sunnypilot/selfdrive/car/tests/test_custom_cruise.py @@ -2,7 +2,7 @@ import pytest from parameterized import parameterized_class from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params from openpilot.selfdrive.car.cruise import V_CRUISE_INITIAL from openpilot.selfdrive.car.tests.test_cruise_speed import TestVCruiseHelper @@ -21,8 +21,8 @@ class TestCustomAccIncrements(TestVCruiseHelper): def reset_custom_params(self) -> None: """Reset to default custom ACC parameters""" self.params.put_bool("CustomAccIncrementsEnabled", False) - self.params.put("CustomAccShortPressIncrement", "1") - self.params.put("CustomAccLongPressIncrement", "5") + self.params.put("CustomAccShortPressIncrement", 1) + self.params.put("CustomAccLongPressIncrement", 5) self.v_cruise_helper.read_custom_set_speed_params() def press_button_short(self, button_type: car.CarState.ButtonEvent.Type) -> None: @@ -51,8 +51,8 @@ class TestCustomAccIncrements(TestVCruiseHelper): def set_custom_increments(self, enabled: bool, short_inc: int, long_inc: int) -> None: """Set custom ACC increment parameters""" self.params.put_bool("CustomAccIncrementsEnabled", enabled) - self.params.put("CustomAccShortPressIncrement", str(short_inc)) - self.params.put("CustomAccLongPressIncrement", str(long_inc)) + self.params.put("CustomAccShortPressIncrement", short_inc) + self.params.put("CustomAccLongPressIncrement", long_inc) self.v_cruise_helper.read_custom_set_speed_params() def test_default_behavior_when_disabled(self): diff --git a/sunnypilot/selfdrive/controls/controlsd_ext.py b/sunnypilot/selfdrive/controls/controlsd_ext.py index 9a7f0cb5fc..7e06ac77c1 100644 --- a/sunnypilot/selfdrive/controls/controlsd_ext.py +++ b/sunnypilot/selfdrive/controls/controlsd_ext.py @@ -5,7 +5,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. """ import cereal.messaging as messaging -from cereal import custom +from cereal import log, custom from opendbc.car import structs from openpilot.common.params import Params @@ -26,7 +26,7 @@ class ControlsExt: self.CP_SP = messaging.log_from_bytes(params.get("CarParamsSP", block=True), custom.CarParamsSP) cloudlog.info("controlsd_ext got CarParamsSP") - self.sm_services_ext = ['selfdriveStateSP'] + self.sm_services_ext = ['radarState', 'selfdriveStateSP'] self.pm_services_ext = ['carControlSP'] def get_params_sp(self) -> None: @@ -44,9 +44,32 @@ class ControlsExt: # MADS not available, use stock state to engage return bool(sm['selfdriveState'].active) + @staticmethod + def get_lead_data(ld: log.RadarState.LeadData) -> dict: + return { + "dRel": ld.dRel, + "yRel": ld.yRel, + "vRel": ld.vRel, + "aRel": ld.aRel, + "vLead": ld.vLead, + "dPath": ld.dPath, + "vLat": ld.vLat, + "vLeadK": ld.vLeadK, + "aLeadK": ld.aLeadK, + "fcw": ld.fcw, + "status": ld.status, + "aLeadTau": ld.aLeadTau, + "modelProb": ld.modelProb, + "radar": ld.radar, + "radarTrackId": ld.radarTrackId, + } + def state_control_ext(self, sm: messaging.SubMaster) -> custom.CarControlSP: CC_SP = custom.CarControlSP.new_message() + CC_SP.leadOne = self.get_lead_data(sm['radarState'].leadOne) + CC_SP.leadTwo = self.get_lead_data(sm['radarState'].leadTwo) + # MADS state CC_SP.mads = sm['selfdriveStateSP'].mads diff --git a/sunnypilot/selfdrive/controls/lib/auto_lane_change.py b/sunnypilot/selfdrive/controls/lib/auto_lane_change.py index 29056d5ac5..cf8de2a1b2 100644 --- a/sunnypilot/selfdrive/controls/lib/auto_lane_change.py +++ b/sunnypilot/selfdrive/controls/lib/auto_lane_change.py @@ -42,7 +42,7 @@ class AutoLaneChangeController: self.param_read_counter = 0 self.lane_change_delay = 0.0 - self.lane_change_set_timer = AutoLaneChangeMode.NUDGE + self.lane_change_set_timer = self.params.get("AutoLaneChangeTimer", return_default=True) self.lane_change_bsm_delay = False self.prev_brake_pressed = False @@ -61,10 +61,7 @@ class AutoLaneChangeController: def read_params(self) -> None: self.lane_change_bsm_delay = self.params.get_bool("AutoLaneChangeBsmDelay") - try: - self.lane_change_set_timer = int(self.params.get("AutoLaneChangeTimer", encoding="utf8")) - except (ValueError, TypeError): - self.lane_change_set_timer = AutoLaneChangeMode.NUDGE + self.lane_change_set_timer = self.params.get("AutoLaneChangeTimer", return_default=True) def update_params(self) -> None: if self.param_read_counter % 50 == 0: diff --git a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py index fb86280358..2f27e7e42f 100644 --- a/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/blinker_pause_lateral.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.params import Params @@ -21,7 +21,7 @@ class BlinkerPauseLateral: def get_params(self) -> None: self.enabled = self.params.get_bool("BlinkerPauseLateralControl") self.is_metric = self.params.get_bool("IsMetric") - self.min_speed = int(self.params.get("BlinkerMinLateralControlSpeed", encoding='utf8')) + self.min_speed = self.params.get("BlinkerMinLateralControlSpeed") def update(self, CS: car.CarState) -> bool: if not self.enabled: diff --git a/sunnypilot/selfdrive/controls/lib/dec/constants.py b/sunnypilot/selfdrive/controls/lib/dec/constants.py index 48f4203e93..4586afbc9f 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/constants.py +++ b/sunnypilot/selfdrive/controls/lib/dec/constants.py @@ -1,25 +1,17 @@ class WMACConstants: - LEAD_WINDOW_SIZE = 5 - LEAD_PROB = 0.5 + # Lead detection parameters + LEAD_WINDOW_SIZE = 6 # Stable detection window + LEAD_PROB = 0.45 # Balanced threshold for lead detection - SLOW_DOWN_WINDOW_SIZE = 5 - SLOW_DOWN_PROB = 0.6 + # Slow down detection parameters + SLOW_DOWN_WINDOW_SIZE = 5 # Responsive but stable + SLOW_DOWN_PROB = 0.3 # Balanced threshold for slow down scenarios + # Optimized slow down distance curve - smooth and progressive SLOW_DOWN_BP = [0., 10., 20., 30., 40., 50., 55., 60.] - SLOW_DOWN_DIST = [25., 38., 55., 75., 95., 115., 130., 150.] + SLOW_DOWN_DIST = [32., 46., 64., 86., 108., 130., 145., 165.] - SLOWNESS_WINDOW_SIZE = 12 - SLOWNESS_PROB = 0.5 - SLOWNESS_CRUISE_OFFSET = 1.05 - - DANGEROUS_TTC_WINDOW_SIZE = 3 - DANGEROUS_TTC = 2.3 - - MPC_FCW_WINDOW_SIZE = 10 - MPC_FCW_PROB = 0.5 - - -class SNG_State: - off = 0 - stopped = 1 - going = 2 + # Slowness detection parameters + SLOWNESS_WINDOW_SIZE = 10 # Stable slowness detection + SLOWNESS_PROB = 0.55 # Clear threshold for slowness + SLOWNESS_CRUISE_OFFSET = 1.025 # Conservative cruise speed offset diff --git a/sunnypilot/selfdrive/controls/lib/dec/dec.py b/sunnypilot/selfdrive/controls/lib/dec/dec.py index 812e5cc15f..46cad1b048 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/dec.py +++ b/sunnypilot/selfdrive/controls/lib/dec/dec.py @@ -1,88 +1,133 @@ -# The MIT License -# -# Copyright (c) 2019-, Rick Lan, dragonpilot community, and a number of other of contributors. -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# Version = 2025-1-18 +""" +Copyright (c) 2021-, rav4kumar, Haibin Wen, sunnypilot, and a number of other contributors. -import numpy as np +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. +""" +# Version = 2025-6-30 from cereal import messaging from opendbc.car import structs from numpy import interp from openpilot.common.params import Params from openpilot.common.realtime import DT_MDL -from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State +from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants +from typing import Literal # d-e2e, from modeldata.h TRAJECTORY_SIZE = 33 +SET_MODE_TIMEOUT = 15 -HIGHWAY_CRUISE_KPH = 70 - -STOP_AND_GO_FRAME = 60 - -SET_MODE_TIMEOUT = 10 - -V_ACC_MIN = 9.72 +# Define the valid mode types +ModeType = Literal['acc', 'blended'] -class GenericMovingAverageCalculator: - def __init__(self, window_size): - self.window_size = window_size - self.data = [] - self.total = 0 +class SmoothKalmanFilter: + """Enhanced Kalman filter with smoothing for stable decision making.""" - def add_data(self, value: float) -> None: - if len(self.data) == self.window_size: - self.total -= self.data.pop(0) - self.data.append(value) - self.total += value + def __init__(self, initial_value=0, measurement_noise=0.1, process_noise=0.01, + alpha=1.0, smoothing_factor=0.85): + self.x = initial_value + self.P = 1.0 + self.R = measurement_noise + self.Q = process_noise + self.alpha = alpha + self.smoothing_factor = smoothing_factor + self.initialized = False + self.history = [] + self.max_history = 10 + self.confidence = 0.0 - def get_moving_average(self) -> float | None: - return None if len(self.data) == 0 else self.total / len(self.data) + def add_data(self, measurement): + if len(self.history) >= self.max_history: + self.history.pop(0) + self.history.append(measurement) - def reset_data(self) -> None: - self.data = [] - self.total = 0 + if not self.initialized: + self.x = measurement + self.initialized = True + self.confidence = 0.1 + return + + self.P = self.alpha * self.P + self.Q + + K = self.P / (self.P + self.R) + effective_K = K * (1.0 - self.smoothing_factor) + self.smoothing_factor * 0.1 + + innovation = measurement - self.x + self.x = self.x + effective_K * innovation + self.P = (1 - effective_K) * self.P + + if abs(innovation) < 0.1: + self.confidence = min(1.0, self.confidence + 0.05) + else: + self.confidence = max(0.1, self.confidence - 0.02) + + def get_value(self): + return self.x if self.initialized else None + + def get_confidence(self): + return self.confidence + + def reset_data(self): + self.initialized = False + self.history = [] + self.confidence = 0.0 -class WeightedMovingAverageCalculator: - def __init__(self, window_size): - self.window_size = window_size - self.data = [] - self.weights = np.linspace(1, 3, window_size) # Linear weights, adjust as needed +class ModeTransitionManager: + """Manages smooth transitions between driving modes with hysteresis.""" - def add_data(self, value: float) -> None: - if len(self.data) == self.window_size: - self.data.pop(0) - self.data.append(value) + def __init__(self): + self.current_mode: ModeType = 'acc' + self.mode_confidence = {'acc': 1.0, 'blended': 0.0} + self.transition_timeout = 0 + self.min_mode_duration = 10 + self.mode_duration = 0 + self.emergency_override = False - def get_weighted_average(self) -> float | None: - if len(self.data) == 0: - return None - weighted_sum: float = float(np.dot(self.data, self.weights[-len(self.data):])) - weight_total: float = float(np.sum(self.weights[-len(self.data):])) - return weighted_sum / weight_total + def request_mode(self, mode: ModeType, confidence: float = 1.0, emergency: bool = False): + # Emergency override for critical situations (stops, collisions) + if emergency: + self.emergency_override = True + self.current_mode = mode + self.transition_timeout = SET_MODE_TIMEOUT + self.mode_duration = 0 + return - def reset_data(self) -> None: - self.data = [] + self.mode_confidence[mode] = min(1.0, self.mode_confidence[mode] + 0.1 * confidence) + for m in self.mode_confidence: + if m != mode: + self.mode_confidence[m] = max(0.0, self.mode_confidence[m] - 0.05) + + # Require minimum duration in current mode (unless emergency) + if self.mode_duration < self.min_mode_duration and not self.emergency_override: + return + + # Hysteresis: higher threshold for mode changes + confidence_threshold = 0.6 if mode != self.current_mode else 0.3 # Lower threshold for faster response + + if self.mode_confidence[mode] > confidence_threshold: + if mode != self.current_mode and self.transition_timeout == 0: + self.transition_timeout = SET_MODE_TIMEOUT + self.current_mode = mode + self.mode_duration = 0 + + def update(self): + if self.transition_timeout > 0: + self.transition_timeout -= 1 + self.mode_duration += 1 + + # Reset emergency override after some time + if self.emergency_override and self.mode_duration > 20: + self.emergency_override = False + + # Gradual confidence decay + for mode in self.mode_confidence: + self.mode_confidence[mode] *= 0.98 + + def get_mode(self) -> ModeType: + return self.current_mode class DynamicExperimentalController: @@ -92,51 +137,59 @@ class DynamicExperimentalController: self._params = params or Params() self._enabled: bool = self._params.get_bool("DynamicExperimentalControl") self._active: bool = False - self._mode: str = 'acc' self._frame: int = 0 + self._urgency = 0.0 - # Use weighted moving average for filtering leads - self._lead_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.LEAD_WINDOW_SIZE) + self._mode_manager = ModeTransitionManager() + + # Smooth filters for stable decision making with faster response for critical scenarios + self._lead_filter = SmoothKalmanFilter( + measurement_noise=0.15, + process_noise=0.05, + alpha=1.02, + smoothing_factor=0.8 + ) + + self._slow_down_filter = SmoothKalmanFilter( + measurement_noise=0.1, + process_noise=0.1, + alpha=1.05, + smoothing_factor=0.7 + ) + + self._slowness_filter = SmoothKalmanFilter( + measurement_noise=0.1, + process_noise=0.06, + alpha=1.015, + smoothing_factor=0.92 + ) + + self._mpc_fcw_filter = SmoothKalmanFilter( + measurement_noise=0.2, + process_noise=0.1, + alpha=1.1, + smoothing_factor=0.5 + ) self._has_lead_filtered = False - self._has_lead_filtered_prev = False - - self._slow_down_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOW_DOWN_WINDOW_SIZE) - self._has_slow_down: bool = False - self._slow_down_confidence: float = 0.0 - - self._has_blinkers = False - - self._slowness_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.SLOWNESS_WINDOW_SIZE) - self._has_slowness: bool = False - - self._has_nav_instruction = False - - self._dangerous_ttc_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.DANGEROUS_TTC_WINDOW_SIZE) - self._has_dangerous_ttc: bool = False - - self._v_ego_kph = 0. - self._v_cruise_kph = 0. - - self._has_lead = False - + self._has_slow_down = False + self._has_slowness = False + self._has_mpc_fcw = False + self._v_ego_kph = 0.0 + self._v_cruise_kph = 0.0 self._has_standstill = False - self._has_standstill_prev = False - - self._sng_transit_frame = 0 - self._sng_state = SNG_State.off - - self._mpc_fcw_gmac = WeightedMovingAverageCalculator(window_size=WMACConstants.MPC_FCW_WINDOW_SIZE) - self._has_mpc_fcw: bool = False self._mpc_fcw_crash_cnt = 0 - - self._set_mode_timeout = 0 + self._standstill_count = 0 + # debug + self._endpoint_x = float('inf') + self._expected_distance = 0.0 + self._trajectory_valid = False def _read_params(self) -> None: if self._frame % int(1. / DT_MDL) == 0: self._enabled = self._params.get_bool("DynamicExperimentalControl") def mode(self) -> str: - return str(self._mode) + return self._mode_manager.get_mode() def enabled(self) -> bool: return self._enabled @@ -144,46 +197,9 @@ class DynamicExperimentalController: def active(self) -> bool: return self._active - @staticmethod - def _anomaly_detection(recent_data: list[float], threshold: float = 2.0, context_check: bool = True) -> bool: - """ - Basic anomaly detection using standard deviation. - """ - if len(recent_data) < 5: - return False - mean: float = float(np.mean(recent_data)) - std_dev: float = float(np.std(recent_data)) - anomaly: bool = bool(recent_data[-1] > mean + threshold * std_dev) - - # Context check to ensure repeated anomaly - if context_check: - return np.count_nonzero(np.array(recent_data) > mean + threshold * std_dev) > 1 - return anomaly - - def _adaptive_slowdown_threshold(self) -> float: - """ - Adapts the slow-down threshold based on vehicle speed and recent behavior. - """ - slowdown_scaling_factor: float = (1.0 + 0.03 * np.log(1 + len(self._slow_down_gmac.data))) - adaptive_threshold: float = float( - interp(self._v_ego_kph, WMACConstants.SLOW_DOWN_BP, WMACConstants.SLOW_DOWN_DIST) * slowdown_scaling_factor - ) - return adaptive_threshold - - def _smoothed_lead_detection(self, lead_prob: float, smoothing_factor: float = 0.2): - """ - Smoothing the lead detection to avoid erratic behavior. - """ - lead_filtering: float = (1 - smoothing_factor) * self._has_lead_filtered + smoothing_factor * lead_prob - return lead_filtering > WMACConstants.LEAD_PROB - - def _adaptive_lead_prob_threshold(self) -> float: - """ - Adapts lead probability threshold based on driving conditions. - """ - if self._v_ego_kph > HIGHWAY_CRUISE_KPH: - return float(WMACConstants.LEAD_PROB + 0.1) # Increase the threshold on highways - return float(WMACConstants.LEAD_PROB) + def set_mpc_fcw_crash_cnt(self) -> None: + """Set MPC FCW crash count""" + self._mpc_fcw_crash_cnt = self._mpc.crash_cnt def _update_calculations(self, sm: messaging.SubMaster) -> None: car_state = sm['carState'] @@ -192,186 +208,168 @@ class DynamicExperimentalController: self._v_ego_kph = car_state.vEgo * 3.6 self._v_cruise_kph = car_state.vCruise - self._has_lead = lead_one.status self._has_standstill = car_state.standstill - # fcw detection - self._mpc_fcw_gmac.add_data(self._mpc_fcw_crash_cnt > 0) - if _mpc_fcw_weighted_average := self._mpc_fcw_gmac.get_weighted_average(): - self._has_mpc_fcw = _mpc_fcw_weighted_average > WMACConstants.MPC_FCW_PROB - else: - self._has_mpc_fcw = False - - # nav enable detection - # self._has_nav_instruction = md.navEnabledDEPRECATED and maneuver_distance / max(car_state.vEgo, 1) < 13 - - # lead detection with smoothing - self._lead_gmac.add_data(lead_one.status) - self._has_lead_filtered = (self._lead_gmac.get_weighted_average() or -1.) > WMACConstants.LEAD_PROB - #lead_prob = self._lead_gmac.get_weighted_average() or 0 - #self._has_lead_filtered = self._smoothed_lead_detection(lead_prob) - - # adaptive slow down detection - adaptive_threshold = self._adaptive_slowdown_threshold() - slow_down_trigger = len(md.orientation.x) == len(md.position.x) == TRAJECTORY_SIZE and md.position.x[TRAJECTORY_SIZE - 1] < adaptive_threshold - self._slow_down_gmac.add_data(slow_down_trigger) - if _has_slow_down_weighted_average := self._slow_down_gmac.get_weighted_average(): - self._has_slow_down = _has_slow_down_weighted_average > WMACConstants.SLOW_DOWN_PROB - self._slow_down_confidence = _has_slow_down_weighted_average # Store confidence level - else: - self._has_slow_down = False - self._slow_down_confidence = 0.0 # No confidence if no slowdown - - # anomaly detection for slow down events - if self._anomaly_detection(self._slow_down_gmac.data): - self._slow_down_confidence *= 0.85 # Reduce confidence - self._has_slow_down = self._slow_down_confidence > WMACConstants.SLOW_DOWN_PROB - - # blinker detection - self._has_blinkers = car_state.leftBlinker or car_state.rightBlinker - - # sng detection + # standstill detection if self._has_standstill: - self._sng_state = SNG_State.stopped - self._sng_transit_frame = 0 + self._standstill_count = min(20, self._standstill_count + 1) else: - if self._sng_transit_frame == 0: - if self._sng_state == SNG_State.stopped: - self._sng_state = SNG_State.going - self._sng_transit_frame = STOP_AND_GO_FRAME - elif self._sng_state == SNG_State.going: - self._sng_state = SNG_State.off - elif self._sng_transit_frame > 0: - self._sng_transit_frame -= 1 + self._standstill_count = max(0, self._standstill_count - 1) - # slowness detection - if not self._has_standstill: - self._slowness_gmac.add_data(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET)) - if _slowness_weighted_average := self._slowness_gmac.get_weighted_average(): - self._has_slowness = _slowness_weighted_average > WMACConstants.SLOWNESS_PROB - else: - self._has_slowness = False + # Lead detection + self._lead_filter.add_data(float(lead_one.status)) + lead_value = self._lead_filter.get_value() or 0.0 + self._has_lead_filtered = lead_value > WMACConstants.LEAD_PROB - # dangerous TTC detection - if not self._has_lead_filtered and self._has_lead_filtered_prev: - self._dangerous_ttc_gmac.reset_data() - self._has_dangerous_ttc = False + # MPC FCW detection + fcw_filtered_value = self._mpc_fcw_filter.get_value() or 0.0 + self._mpc_fcw_filter.add_data(float(self._mpc_fcw_crash_cnt > 0)) + self._has_mpc_fcw = fcw_filtered_value > 0.5 - if self._has_lead and car_state.vEgo >= 0.01: - self._dangerous_ttc_gmac.add_data(lead_one.dRel / car_state.vEgo) + # Slow down detection + self._calculate_slow_down(md) - if _dangerous_ttc_weighted_average := self._dangerous_ttc_gmac.get_weighted_average(): - self._has_dangerous_ttc = _dangerous_ttc_weighted_average <= WMACConstants.DANGEROUS_TTC - else: - self._has_dangerous_ttc = False + # Slowness detection + if not (self._standstill_count > 5) and not self._has_slow_down: + current_slowness = float(self._v_ego_kph <= (self._v_cruise_kph * WMACConstants.SLOWNESS_CRUISE_OFFSET)) + self._slowness_filter.add_data(current_slowness) + slowness_value = self._slowness_filter.get_value() or 0.0 - # keep prev values - self._has_standstill_prev = self._has_standstill - self._has_lead_filtered_prev = self._has_lead_filtered + # Hysteresis for slowness + threshold = WMACConstants.SLOWNESS_PROB * (0.8 if self._has_slowness else 1.1) + self._has_slowness = slowness_value > threshold + + def _calculate_slow_down(self, md): + """Calculate urgency based on trajectory endpoint vs expected distance.""" + + # Reset to safe defaults + urgency = 0.0 + self._endpoint_x = float('inf') + self._trajectory_valid = False + + #Require exact trajectory size + position_valid = len(md.position.x) == TRAJECTORY_SIZE + orientation_valid = len(md.orientation.x) == TRAJECTORY_SIZE + + if not (position_valid and orientation_valid): + # Invalid trajectory - this itself might indicate a stop scenario + # Apply moderate urgency for incomplete trajectories at speed + if self._v_ego_kph > 20.0: + urgency = 0.3 + + self._slow_down_filter.add_data(urgency) + urgency_filtered = self._slow_down_filter.get_value() or 0.0 + self._has_slow_down = urgency_filtered > WMACConstants.SLOW_DOWN_PROB + self._urgency = urgency_filtered + return + + # We have a valid full trajectory + self._trajectory_valid = True + + # Use the exact endpoint (33rd point, index 32) + endpoint_x = md.position.x[TRAJECTORY_SIZE - 1] + self._endpoint_x = endpoint_x + + # Get expected distance based on current speed using tuned constants + expected_distance = interp(self._v_ego_kph, + WMACConstants.SLOW_DOWN_BP, + WMACConstants.SLOW_DOWN_DIST) + self._expected_distance = expected_distance + + # Calculate urgency based on trajectory shortage + if endpoint_x < expected_distance: + shortage = expected_distance - endpoint_x + shortage_ratio = shortage / expected_distance + + # Base urgency on shortage ratio + urgency = min(1.0, shortage_ratio * 2.0) + + # Increase urgency for very short trajectories (imminent stops) + critical_distance = expected_distance * 0.3 + if endpoint_x < critical_distance: + urgency = min(1.0, urgency * 2.0) + + # Speed-based urgency adjustment + if self._v_ego_kph > 25.0: + speed_factor = 1.0 + (self._v_ego_kph - 25.0) / 80.0 + urgency = min(1.0, urgency * speed_factor) + + # Apply filtering but with less smoothing for stops + self._slow_down_filter.add_data(urgency) + urgency_filtered = self._slow_down_filter.get_value() or 0.0 + + # Update state with lower threshold for better stop detection + self._has_slow_down = urgency_filtered > (WMACConstants.SLOW_DOWN_PROB * 0.8) + self._urgency = urgency_filtered def _radarless_mode(self) -> None: - # when mpc fcw crash prob is high - # use blended to slow down quickly + """Radarless mode decision logic with emergency handling.""" + + # EMERGENCY: MPC FCW - immediate blended mode if self._has_mpc_fcw: - self._set_mode('blended') + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) return - # Nav enabled and distance to upcoming turning is 300 or below - # if self._has_nav_instruction: - # self._set_mode('blended') - # return - - # when blinker is on and speed is driving below V_ACC_MIN: blended - # we don't want it to switch mode at higher speed, blended may trigger hard brake - # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: - # self._set_mode('blended') - # return - - # when at highway cruise and SNG: blended - # ensuring blended mode is used because acc is bad at catching SNG lead car - # especially those who accel very fast and then brake very hard. - # if self._sng_state == SNG_State.going and self._v_cruise_kph >= V_ACC_MIN: - # self._set_mode('blended') - # return - - # when standstill: blended - # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. - if self._has_standstill: - self._set_mode('blended') + # Standstill: use blended + if self._standstill_count > 3: + self._mode_manager.request_mode('blended', confidence=0.9) return - # when detecting slow down scenario: blended - # e.g. traffic light, curve, stop sign etc. + # Slow down scenarios: emergency for high urgency, normal for lower urgency if self._has_slow_down: - self._set_mode('blended') + if self._urgency > 0.7: + # Emergency: immediate blended mode for high urgency stops + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) + else: + # Normal: blended with urgency-based confidence + confidence = min(1.0, self._urgency * 1.5) + self._mode_manager.request_mode('blended', confidence=confidence) return - # when detecting lead slow down: blended - # use blended for higher braking capability - if self._has_dangerous_ttc: - self._set_mode('blended') + # Driving slow: use ACC (but not if actively slowing down) + if self._has_slowness and not self._has_slow_down: + self._mode_manager.request_mode('acc', confidence=0.8) return - # car driving at speed lower than set speed: acc - if self._has_slowness: - self._set_mode('acc') - return - - self._set_mode('acc') + # Default: ACC + self._mode_manager.request_mode('acc', confidence=0.7) def _radar_mode(self) -> None: - # when mpc fcw crash prob is high - # use blended to slow down quickly + """Radar mode with emergency handling.""" + + # EMERGENCY: MPC FCW - immediate blended mode if self._has_mpc_fcw: - self._set_mode('blended') + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) return - # If there is a filtered lead, the vehicle is not in standstill, and the lead vehicle's yRel meets the condition, - if self._has_lead_filtered and not self._has_standstill: - self._set_mode('acc') + # If lead detected and not in standstill: always use ACC + if self._has_lead_filtered and not (self._standstill_count > 3): + self._mode_manager.request_mode('acc', confidence=1.0) return - # when blinker is on and speed is driving below V_ACC_MIN: blended - # we don't want it to switch mode at higher speed, blended may trigger hard brake - # if self._has_blinkers and self._v_ego_kph < V_ACC_MIN: - # self._set_mode('blended') - # return - - # when standstill: blended - # in case of lead car suddenly move away under traffic light, acc mode won't brake at traffic light. - if self._has_standstill: - self._set_mode('blended') - return - - # when detecting slow down scenario: blended - # e.g. traffic light, curve, stop sign etc. + # Slow down scenarios: emergency for high urgency, normal for lower urgency if self._has_slow_down: - self._set_mode('blended') + if self._urgency > 0.7: + # Emergency: immediate blended mode for high urgency stops + self._mode_manager.request_mode('blended', confidence=1.0, emergency=True) + else: + # Normal: blended with urgency-based confidence + confidence = min(1.0, self._urgency * 1.3) + self._mode_manager.request_mode('blended', confidence=confidence) return - # car driving at speed lower than set speed: acc - if self._has_slowness: - self._set_mode('acc') + # Standstill: use blended + if self._standstill_count > 3: + self._mode_manager.request_mode('blended', confidence=0.9) return - # Nav enabled and distance to upcoming turning is 300 or below - # if self._has_nav_instruction: - # self._set_mode('blended') - # return + # Driving slow: use ACC (but not if actively slowing down) + if self._has_slowness and not self._has_slow_down: + self._mode_manager.request_mode('acc', confidence=0.8) + return - self._set_mode('acc') - - def set_mpc_fcw_crash_cnt(self) -> None: - self._mpc_fcw_crash_cnt = self._mpc.crash_cnt - - def _set_mode(self, mode: str) -> None: - if self._set_mode_timeout == 0: - self._mode = mode - if mode == 'blended': - self._set_mode_timeout = SET_MODE_TIMEOUT - - if self._set_mode_timeout > 0: - self._set_mode_timeout -= 1 + # Default: ACC + self._mode_manager.request_mode('acc', confidence=0.7) def update(self, sm: messaging.SubMaster) -> None: self._read_params() @@ -385,6 +383,6 @@ class DynamicExperimentalController: else: self._radar_mode() + self._mode_manager.update() self._active = sm['selfdriveState'].experimentalMode and self._enabled - self._frame += 1 diff --git a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py index d15d88aefe..f9da39c03b 100644 --- a/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py +++ b/sunnypilot/selfdrive/controls/lib/dec/tests/pytest_dynamic_controller.py @@ -1,248 +1,94 @@ import pytest -import numpy as np -from openpilot.common.params import Params -from openpilot.sunnypilot.selfdrive.controls.lib.dec.constants import WMACConstants, SNG_State -from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController, TRAJECTORY_SIZE, STOP_AND_GO_FRAME - -class MockInterp: - def __call__(self, x, xp, fp): - return np.interp(x, xp, fp) - -class MockCarState: - def __init__(self, v_ego=0., standstill=False, left_blinker=False, right_blinker=False): - self.vEgo = v_ego - self.standstill = standstill - self.leftBlinker = left_blinker - self.rightBlinker = right_blinker +from openpilot.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController class MockLeadOne: - def __init__(self, status=False, d_rel=0): + def __init__(self, status=0.0): self.status = status - self.dRel = d_rel + +class MockRadarState: + def __init__(self, status=0.0): + self.leadOne = MockLeadOne(status=status) + +class MockCarState: + def __init__(self, vEgo=0.0, vCruise=0.0, standstill=False): + self.vEgo = vEgo + self.vCruise = vCruise + self.standstill = standstill class MockModelData: - def __init__(self, x_vals=None, positions=None): - self.orientation = type('Orientation', (), {'x': x_vals})() - self.position = type('Position', (), {'x': positions})() + def __init__(self, valid=True): + size = 33 if valid else 10 # incomplete if invalid + self.position = type("Pos", (), {"x": [0.0] * size})() + self.orientation = type("Ori", (), {"x": [0.0] * size})() -class MockControlState: - def __init__(self, v_cruise=0): - self.vCruise = v_cruise +class MockSelfDriveState: + def __init__(self, experimentalMode=False): + self.experimentalMode = experimentalMode + +class MockParams: + def get_bool(self, name): + return True @pytest.fixture -def interp(monkeypatch): - mock_interp = MockInterp() - monkeypatch.setattr('numpy.interp', mock_interp) - return mock_interp +def default_sm(): + sm = { + 'carState': MockCarState(vEgo=10.0, vCruise=20.0), + 'radarState': MockRadarState(status=1.0), + 'modelV2': MockModelData(valid=True), + 'selfdriveState': MockSelfDriveState(experimentalMode=True), + } + return sm @pytest.fixture -def controller(interp): - params = Params() - params.put_bool("DynamicExperimentalControl", True) - return DynamicExperimentalController() +def mock_cp(): + class CP: + radarUnavailable = False + return CP() -def test_initial_state(controller): - """Test initial state of the controller""" - assert controller._mode == 'acc' - assert not controller._has_lead - assert not controller._has_standstill - assert controller._sng_state == SNG_State.off - assert not controller._has_lead_filtered - assert not controller._has_slow_down - assert not controller._has_dangerous_ttc - assert not controller._has_mpc_fcw +@pytest.fixture +def mock_mpc(): + class MPC: + crash_cnt = 0 + return MPC() -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_standstill_detection(controller, has_radar): - """Test standstill detection and state transitions""" - car_state = MockCarState(standstill=True) - lead_one = MockLeadOne() - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState() +# Fake Kalman Filter that always returns a given value +class FakeKalman: + def __init__(self, value=1.0): + self.value = value + def add_data(self, v): pass + def get_value(self): return self.value + def get_confidence(self): return 1.0 + def reset_data(self): pass - # Test transition to standstill - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.stopped - assert controller.get_mpc_mode() == 'blended' +def test_initial_mode_is_acc(mock_cp, mock_mpc): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + assert controller.mode() == "acc" - # Test transition from standstill to moving - car_state.standstill = False - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.going +def test_standstill_triggers_blended(mock_cp, mock_mpc, default_sm): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + default_sm['carState'].standstill = True + for _ in range(10): + controller.update(default_sm) + assert controller.mode() == "blended" - # Test complete transition to normal driving - for _ in range(STOP_AND_GO_FRAME + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller._sng_state == SNG_State.off +def test_emergency_blended_on_fcw(mock_cp, mock_mpc, default_sm): + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) + mock_mpc.crash_cnt = 1 # simulate FCW + for _ in range(2): + controller.update(default_sm) + assert controller.mode() == "blended" -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_lead_detection(controller, has_radar): - """Test lead vehicle detection and filtering""" - car_state = MockCarState(v_ego=20) # 72 kph - lead_one = MockLeadOne(status=True, d_rel=50) # Safe distance - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=72) +def test_radarless_slowdown_triggers_blended(mock_cp, mock_mpc, default_sm): + mock_cp.radarUnavailable = True + controller = DynamicExperimentalController(mock_cp, mock_mpc, params=MockParams()) - # Let moving average stabilize - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) + # Force conditions to simulate slowdown + controller._slow_down_filter = FakeKalman(value=1.0) # Ensure urgency triggers slowdown + controller._v_ego_kph = 35.0 + default_sm['modelV2'] = MockModelData(valid=False) # Incomplete trajectory - assert controller._has_lead_filtered - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode + for _ in range(3): + controller.update(default_sm) - # Test lead loss detection - lead_one.status = False - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_lead_filtered - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_slow_down_detection(controller, has_radar): - """Test slow down detection based on trajectory""" - car_state = MockCarState(v_ego=10/3.6) # 10 kph - lead_one = MockLeadOne() - x_vals = [0] * TRAJECTORY_SIZE - positions = [20] * TRAJECTORY_SIZE # Position within slow down threshold - md = MockModelData(x_vals=x_vals, positions=positions) - controls_state = MockControlState(v_cruise=30) - - # Test slow down detection - for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_slow_down - assert controller.get_mpc_mode() == 'blended' - - # Test slow down recovery - positions = [200] * TRAJECTORY_SIZE # Position outside slow down threshold - md = MockModelData(x_vals=x_vals, positions=positions) - for _ in range(WMACConstants.SLOW_DOWN_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_slow_down - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_dangerous_ttc_detection(controller, has_radar): - """Test Time-To-Collision detection and handling""" - car_state = MockCarState(v_ego=10) # 36 kph - lead_one = MockLeadOne(status=True) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=36) - - # First establish normal conditions with lead - lead_one.dRel = 100 # Safe distance - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): # First establish lead detection - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_lead_filtered # Verify lead is detected - - # Now test dangerous TTC detection - lead_one.dRel = 10 # 10m distance - should trigger dangerous TTC - # TTC = dRel/vEgo = 10/10 = 1s (which is less than DANGEROUS_TTC = 2.3s) - - # Need to update multiple times to allow the weighted average to stabilize - for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_dangerous_ttc, "TTC of 1s should be considered dangerous" - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_mode_transitions(controller, has_radar): - """Test comprehensive mode transitions under different conditions""" - # Initialize with normal driving conditions - car_state = MockCarState(v_ego=25) # 90 kph - lead_one = MockLeadOne(status=False) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[200] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=100) - - def stabilize_filters(): - """Helper to let all moving averages stabilize""" - for _ in range(max(WMACConstants.LEAD_WINDOW_SIZE, WMACConstants.SLOW_DOWN_WINDOW_SIZE, - WMACConstants.DANGEROUS_TTC_WINDOW_SIZE, WMACConstants.MPC_FCW_WINDOW_SIZE) + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - # Test 1: Normal driving -> ACC mode - stabilize_filters() - assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode under normal driving conditions" - - # Test 2: Standstill -> Blended mode - car_state.standstill = True - controller.update(not has_radar, car_state, lead_one, md, controls_state) - assert controller.get_mpc_mode() == 'blended', "Should be in blended mode during standstill" - - # Test 3: Lead car appears -> ACC mode - car_state = MockCarState(v_ego=20) # Reset car state - lead_one.status = True - lead_one.dRel = 50 # Safe distance - stabilize_filters() - assert not controller._has_dangerous_ttc, "Should not have dangerous TTC" - assert controller.get_mpc_mode() == 'acc', "Should be in ACC mode with safe lead distance" - - # Test 4: Dangerous TTC -> Blended mode - car_state = MockCarState(v_ego=20) # 72 kph - lead_one.status = True - lead_one.dRel = 50 # First establish normal lead detection - - # First establish lead detection - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_lead_filtered # Verify lead is detected - - # Now create dangerous TTC condition - lead_one.dRel = 20 # This creates a TTC of 1s, well below DANGEROUS_TTC - - for _ in range(WMACConstants.DANGEROUS_TTC_WINDOW_SIZE * 2): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_dangerous_ttc, "Should detect dangerous TTC condition" - expected_mode = 'acc' if has_radar else 'blended' - assert controller.get_mpc_mode() == expected_mode, f"Should be in [{expected_mode}] mode with dangerous TTC" - -@pytest.mark.parametrize("has_radar", [True, False], ids=["with_radar", "without_radar"]) -def test_mpc_fcw_handling(controller, has_radar): - """Test MPC FCW crash count handling and mode transitions""" - car_state = MockCarState(v_ego=20) - lead_one = MockLeadOne() - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=72) - - # Test FCW activation - controller.set_mpc_fcw_crash_cnt(5) - for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert controller._has_mpc_fcw - assert controller.get_mpc_mode() == 'blended' - - # Test FCW recovery - controller.set_mpc_fcw_crash_cnt(0) - for _ in range(WMACConstants.MPC_FCW_WINDOW_SIZE + 1): - controller.update(not has_radar, car_state, lead_one, md, controls_state) - - assert not controller._has_mpc_fcw - -def test_radar_unavailable_handling(controller): - """Test behavior transitions between radar available and unavailable states""" - car_state = MockCarState(v_ego=27.78) # 100 kph - lead_one = MockLeadOne(status=True, d_rel=50) - md = MockModelData(x_vals=[0] * TRAJECTORY_SIZE, positions=[150] * TRAJECTORY_SIZE) - controls_state = MockControlState(v_cruise=100) - - # Test with radar available - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(False, car_state, lead_one, md, controls_state) - radar_mode = controller.get_mpc_mode() - - # Test with radar unavailable - for _ in range(WMACConstants.LEAD_WINDOW_SIZE + 1): - controller.update(True, car_state, lead_one, md, controls_state) - radarless_mode = controller.get_mpc_mode() - - assert radar_mode is not None - assert radarless_mode is not None + assert controller.mode() == "blended" 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/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 8ccd81d8ff..1965d50b51 100644 --- a/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py +++ b/sunnypilot/selfdrive/controls/lib/latcontrol_torque_ext_base.py @@ -7,11 +7,13 @@ 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 LAT_PLAN_MIN_IDX = 5 -LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan +LATERAL_LAG_MOD = 0.0 # seconds, modifies how far in the future we look ahead for the lateral plan + def get_predicted_lateral_jerk(lat_accels, t_diffs): # compute finite difference between subsequent model_v2.acceleration.y values @@ -42,28 +44,33 @@ 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.use_steering_angle = lac_torque.use_steering_angle + self.lac_torque = lac_torque + self.torque_params = lac_torque.torque_params self.actual_lateral_jerk: float = 0.0 self.lateral_jerk_setpoint: float = 0.0 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_params = lac_torque.torque_params + 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, @@ -106,9 +113,8 @@ class LatControlTorqueExtBase: self.lateral_jerk_measurement = 0.0 self.lookahead_lateral_jerk = 0.0 - if self.use_steering_angle: - actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0) - self.actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2 + actual_curvature_rate = -VM.calc_curvature(math.radians(CS.steeringRateDeg), CS.vEgo, 0.0) + self.actual_lateral_jerk = actual_curvature_rate * CS.vEgo ** 2 if self.model_valid: # prepare "look-ahead" desired lateral jerk @@ -118,8 +124,7 @@ class LatControlTorqueExtBase: desired_lateral_jerk = (np.interp(self.desired_lat_jerk_time, ModelConstants.T_IDXS, self.model_v2.acceleration.y) - desired_lateral_accel) / self.desired_lat_jerk_time self.lookahead_lateral_jerk = get_lookahead_value(predicted_lateral_jerk[LAT_PLAN_MIN_IDX:friction_upper_idx], desired_lateral_jerk) - if not self.use_steering_angle or self.lookahead_lateral_jerk == 0.0: - self.lookahead_lateral_jerk = 0.0 + if self.lookahead_lateral_jerk == 0.0: self.actual_lateral_jerk = 0.0 self.lat_accel_friction_factor = 1.0 self.lateral_jerk_setpoint = self.lat_jerk_friction_factor * self.lookahead_lateral_jerk diff --git a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py index 56f32373d5..6952a97f1f 100644 --- a/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py +++ b/sunnypilot/selfdrive/controls/lib/longitudinal_planner.py @@ -8,6 +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.sunnypilot.selfdrive.controls.lib.dec.dec import DynamicExperimentalController +from openpilot.sunnypilot.models.helpers import get_active_bundle DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimentalControlState @@ -15,6 +16,12 @@ DecState = custom.LongitudinalPlanSP.DynamicExperimentalControl.DynamicExperimen class LongitudinalPlannerSP: def __init__(self, CP: structs.CarParams, mpc): self.dec = DynamicExperimentalController(CP, mpc) + self.generation = int(model_bundle.generation) if (model_bundle := get_active_bundle()) else None + + @property + def mlsim(self) -> bool: + # 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 get_mpc_mode(self) -> str | None: if not self.dec.active(): diff --git a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py index 2f89594c8f..1738a11e49 100644 --- a/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py +++ b/sunnypilot/selfdrive/controls/lib/nnlc/nnlc.py @@ -8,7 +8,9 @@ from collections import deque import math import numpy as np -from opendbc.car.interfaces import LatControlInputs +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 @@ -127,7 +159,6 @@ class NeuralNetworkLateralControl(LatControlTorqueExtBase): # apply friction override for cars with low NN friction response if self.model.friction_override: - self._pid_log.error += self.torque_from_lateral_accel(LatControlInputs(0.0, 0.0, CS.vEgo, CS.aEgo), self.torque_params, - friction_input, - self._lateral_accel_deadzone, friction_compensation=True, - gravity_adjusted=False) + 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 diff --git a/sunnypilot/selfdrive/controls/lib/param_store.py b/sunnypilot/selfdrive/controls/lib/param_store.py index 74cd35620e..49ef94cbdb 100644 --- a/sunnypilot/selfdrive/controls/lib/param_store.py +++ b/sunnypilot/selfdrive/controls/lib/param_store.py @@ -24,9 +24,7 @@ class ParamStore: brand_params.extend([ "HyundaiLongitudinalTuning", "LongTuningCustomToggle", - "LongTuningAccelMin", - "LongTuningVEgoStopping", - "LongTuningStoppingDecelRate", + "LongTuningAccelMax", "LongTuningMinUpperJerk", "LongTuningMinLowerJerk", "LongTuningJerkLimits", @@ -34,14 +32,21 @@ class ParamStore: self.keys = universal_params + brand_params self.values = {} + self.cached_params_list: list[capnp.lib.capnp._DynamicStructBuilder] | None = None + + self.frame = 0 def update(self, params: Params) -> None: - self.values = {k: params.get(k, encoding='utf8') or "0" for k in self.keys} + 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 def publish(self) -> list[capnp.lib.capnp._DynamicStructBuilder]: - params_list: list[capnp.lib.capnp._DynamicStructBuilder] = [] - - for k in self.keys: - params_list.append(custom.CarControlSP.Param(key=k, value=self.values[k])) - - return params_list + 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 diff --git a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py index 6ba1dcef41..ca8f16fe74 100644 --- a/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py +++ b/sunnypilot/selfdrive/controls/lib/tests/test_blinker_pause_lateral.py @@ -6,7 +6,7 @@ See the LICENSE.md file in the root directory for more details. """ from cereal import car -from openpilot.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.sunnypilot.selfdrive.controls.lib.blinker_pause_lateral import BlinkerPauseLateral 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.), + } } diff --git a/sunnypilot/sunnylink/api.py b/sunnypilot/sunnylink/api.py index eb9343af9a..b7838a40b0 100644 --- a/sunnypilot/sunnylink/api.py +++ b/sunnypilot/sunnylink/api.py @@ -46,8 +46,8 @@ class SunnylinkApi(BaseApi): time.sleep(0.5) def _resolve_dongle_ids(self): - sunnylink_dongle_id = self.params.get("SunnylinkDongleId", encoding='utf-8') - comma_dongle_id = self.dongle_id or self.params.get("DongleId", encoding='utf-8') + sunnylink_dongle_id = self.params.get("SunnylinkDongleId") + comma_dongle_id = self.dongle_id or self.params.get("DongleId") return sunnylink_dongle_id, comma_dongle_id def _resolve_imeis(self): @@ -63,7 +63,7 @@ class SunnylinkApi(BaseApi): return imei1, imei2 def _resolve_serial(self): - return (self.params.get("HardwareSerial", encoding='utf8') + return (self.params.get("HardwareSerial") or HARDWARE.get_serial()) def register_device(self, spinner=None, timeout=60, verbose=False): @@ -90,7 +90,7 @@ class SunnylinkApi(BaseApi): sunnylink_dongle_id = UNREGISTERED_SUNNYLINK_DONGLE_ID self._status_update("Public key not found, setting dongle ID to unregistered.") else: - Params().put("LastSunnylinkPingTime", "0") # Reset the last ping time to 0 if we are trying to register + Params().put("LastSunnylinkPingTime", 0) # Reset the last ping time to 0 if we are trying to register with pubkey_path.open() as f1, privkey_path.open() as f2: public_key = f1.read() private_key = f2.read() @@ -148,7 +148,7 @@ class SunnylinkApi(BaseApi): # Set the last ping time to the current time since we were just talking to the API last_ping = int(time.monotonic() * 1e9) if successful_registration else start_time - Params().put("LastSunnylinkPingTime", str(last_ping)) + Params().put("LastSunnylinkPingTime", last_ping) # Disable sunnylink if registration was not successful if not successful_registration: diff --git a/sunnypilot/sunnylink/athena/sunnylinkd.py b/sunnypilot/sunnylink/athena/sunnylinkd.py index a675820628..363fa1defc 100755 --- a/sunnypilot/sunnylink/athena/sunnylinkd.py +++ b/sunnypilot/sunnylink/athena/sunnylinkd.py @@ -3,7 +3,9 @@ from __future__ import annotations import base64 +import errno import gzip +import json import os import ssl import threading @@ -15,13 +17,13 @@ from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity 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 + 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 -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")) @@ -31,7 +33,7 @@ SUNNYLINK_RECONNECT_TIMEOUT_S = 70 # FYI changing this will also would require DISALLOW_LOG_UPLOAD = threading.Event() params = Params() -sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') +sunnylink_dongle_id = params.get("SunnylinkDongleId") sunnylink_api = SunnylinkApi(sunnylink_dongle_id) @@ -47,7 +49,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: threading.Thread(target=ws_send, args=(ws, end_event), name='ws_send'), threading.Thread(target=ws_ping, args=(ws, end_event), name='ws_ping'), threading.Thread(target=ws_queue, args=(end_event,), name='ws_queue'), - # threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'), + threading.Thread(target=upload_handler, args=(end_event,), name='upload_handler'), # threading.Thread(target=sunny_log_handler, args=(end_event, comma_prime_cellular_end_event), name='log_handler'), # threading.Thread(target=stat_handler, args=(end_event,), name='stat_handler'), ] + [ @@ -68,7 +70,7 @@ def handle_long_poll(ws: WebSocket, exit_event: threading.Event | None) -> None: end_event.set() comma_prime_cellular_end_event.set() - prime_type = params.get("PrimeType", encoding='utf-8') or 0 + prime_type = params.get("PrimeType") or 0 metered = sm['deviceState'].networkMetered if DISALLOW_LOG_UPLOAD.is_set() and not comma_prime_cellular_end_event.is_set(): @@ -103,14 +105,17 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: elif opcode in (ABNF.OPCODE_PING, ABNF.OPCODE_PONG): cloudlog.debug("sunnylinkd.ws_recv.pong") last_ping = int(time.monotonic() * 1e9) - Params().put("LastSunnylinkPingTime", str(last_ping)) + Params().put("LastSunnylinkPingTime", last_ping) 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 +142,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") @@ -171,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) @@ -252,14 +267,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)) diff --git a/sunnypilot/sunnylink/backups/manager.py b/sunnypilot/sunnylink/backups/manager.py index d6408d7ba7..315300c73c 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 +from openpilot.common.params import Params, ParamKeyType, ParamKeyFlag from openpilot.common.realtime import Ratekeeper from openpilot.common.swaglog import cloudlog from openpilot.system.version import get_version @@ -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): @@ -32,7 +33,7 @@ class BackupManagerSP: def __init__(self): self.params = Params() - self.device_id = self.params.get("SunnylinkDongleId", encoding="utf8") + self.device_id = self.params.get("SunnylinkDongleId") self.api = SunnylinkApi(self.device_id) self.pm = messaging.PubMaster(["backupManagerSP"]) @@ -72,9 +73,9 @@ class BackupManagerSP: def _collect_config_data(self) -> dict[str, Any]: """Collects configuration data to be backed up.""" config_data = {} - params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)] + params_to_backup = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyFlag.BACKUP)] for param in params_to_backup: - value = self.params.get(param) + 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) @@ -185,25 +189,42 @@ class BackupManagerSP: def _apply_config(self, config_data: dict[str, str], all_values_encoded: bool = False) -> None: """Applies configuration data from a backup, but only for parameters marked as backupable.""" - # Get the current list of parameters that can be backed up - backupable_params = [k.decode('utf-8') for k in self.params.all_keys(ParamKeyType.BACKUP)] + 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} - # Count for logging/reporting restored_count = 0 skipped_count = 0 for param, encoded_value in config_data.items(): - try: - # Only restore parameters that are currently marked as backupable - if param in backupable_params: + 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 - self.params.put(param, 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) restored_count += 1 - else: - skipped_count += 1 - cloudlog.info(f"Skipped restoring param {param}: not marked for backup in current version") - except Exception as e: - cloudlog.error(f"Failed to restore param {param}: {str(e)}") + except Exception as e: + cloudlog.error(f"Failed to restore param {param}: {str(e)}") + else: + skipped_count += 1 + cloudlog.info(f"Skipped restoring param {param}: not marked for backup in current version") cloudlog.info(f"Restore complete: {restored_count} params restored, {skipped_count} params skipped") @@ -247,13 +268,13 @@ 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") # Check for restore command - restore_version = self.params.get("BackupManager_RestoreVersion", encoding="utf8") + restore_version = self.params.get("BackupManager_RestoreVersion") if restore_version: try: version = int(restore_version) if restore_version.isdigit() else None diff --git a/sunnypilot/sunnylink/uploader.py b/sunnypilot/sunnylink/uploader.py index 69ef2cfd8a..117fbdc159 100755 --- a/sunnypilot/sunnylink/uploader.py +++ b/sunnypilot/sunnylink/uploader.py @@ -1,40 +1,38 @@ #!/usr/bin/env python3 -import bz2 -import datetime -import io import json import os import random +import requests import threading import time import traceback +import datetime from collections.abc import Iterator -from typing import BinaryIO -import requests +from cereal import log +import cereal.messaging as messaging +from sunnypilot.sunnylink.api import SunnylinkApi +from openpilot.common.file_helpers import get_upload_stream from openpilot.common.params import Params from openpilot.common.realtime import set_core_affinity -from openpilot.common.swaglog import cloudlog from openpilot.system.hardware.hw import Paths from openpilot.system.loggerd.xattr_cache import getxattr, setxattr - -import cereal.messaging as messaging -from cereal import log -from sunnypilot.sunnylink.api import SunnylinkApi +from openpilot.common.swaglog import cloudlog NetworkType = log.DeviceState.NetworkType UPLOAD_ATTR_NAME = 'user.sunny.upload' - UPLOAD_ATTR_VALUE = b'1' -UPLOAD_QLOG_QCAM_MAX_SIZE = 5 * 1e6 # MB +MAX_UPLOAD_SIZES = { + "qlog": 25*1e6, # can't be too restrictive here since we use qlogs to find + # bugs, including ones that can cause massive log sizes + "qcam": 5*1e6, +} allow_sleep = bool(os.getenv("UPLOADER_SLEEP", "1")) force_wifi = os.getenv("FORCEWIFI") is not None fake_upload = os.getenv("FAKEUPLOAD") is not None -OFFROAD_TRANSITION_TIMEOUT = 900. # wait until offroad for 15 minutes before allowing uploads - class FakeRequest: def __init__(self): @@ -52,7 +50,6 @@ def get_directory_sort(d: str) -> list[str]: o = ["0", ] if d.startswith("2024-") else ["1", ] return o + [s.rjust(10, '0') for s in d.rsplit('--', 1)] - def listdir_by_creation(d: str) -> list[str]: if not os.path.isdir(d): return [] @@ -65,7 +62,6 @@ def listdir_by_creation(d: str) -> list[str]: cloudlog.exception("listdir_by_creation failed") return [] - def clear_locks(root: str) -> None: for logdir in os.listdir(root): path = os.path.join(root, logdir) @@ -89,11 +85,11 @@ class Uploader: self.last_filename = "" self.immediate_folders = ["crash/", "boot/"] - self.immediate_priority = {"qlog": 0, "qlog.bz2": 0, "qcamera.ts": 1} + self.immediate_priority = {"qlog": 0, "qlog.zst": 0, "qcamera.ts": 1} def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]: - r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8") - requested_routes = [] if r is None else r.split(",") + r = self.params.get("AthenadRecentlyViewedRoutes") + requested_routes = [] if r is None else [route for route in r.split(",") if route] for logdir in listdir_by_creation(self.root): path = os.path.join(self.root, logdir) @@ -137,11 +133,11 @@ class Uploader: if any(f in fn for f in self.immediate_folders): return name, key, fn - return next( - ((name, key, fn) - for name, key, fn in upload_files if name in self.immediate_priority), - None, - ) + for name, key, fn in upload_files: + if name in self.immediate_priority: + return name, key, fn + + return None def do_upload(self, key: str, fn: str): url_resp = self.api.get( @@ -161,15 +157,15 @@ class Uploader: if fake_upload: return FakeResponse() - with open(fn, "rb") as f: - data: BinaryIO - if key.endswith('.bz2') and not fn.endswith('.bz2'): - compressed = bz2.compress(f.read()) - data = io.BytesIO(compressed) - else: - data = f - - return requests.put(url, data=data, headers=headers, timeout=10) + stream = None + try: + compress = key.endswith('.zst') and not fn.endswith('.zst') + stream, _ = get_upload_stream(fn, compress) + response = requests.put(url, data=stream, headers=headers, timeout=10) + return response + finally: + if stream: + stream.close() def upload(self, name: str, key: str, fn: str, network_type: int, metered: bool) -> bool: try: @@ -183,7 +179,7 @@ class Uploader: if sz == 0: # tag files of 0 size as uploaded success = True - elif name in self.immediate_priority and sz > UPLOAD_QLOG_QCAM_MAX_SIZE: + elif name in MAX_UPLOAD_SIZES and sz > MAX_UPLOAD_SIZES[name]: cloudlog.event("uploader_too_large", key=key, fn=fn, sz=sz) success = True else: @@ -224,6 +220,7 @@ class Uploader: return success + def step(self, network_type: int, metered: bool) -> bool | None: d = self.next_file_to_upload(metered) if d is None: @@ -232,8 +229,8 @@ class Uploader: name, key, fn = d # qlogs and bootlogs need to be compressed before uploading - if key.endswith(('qlog', 'rlog')) or (key.startswith('boot/') and not key.endswith('.bz2')): - key += ".bz2" + if key.endswith(('qlog', 'rlog')) or (key.startswith('boot/') and not key.endswith('.zst')): + key += ".zst" return self.upload(name, key, fn, network_type, metered) @@ -250,10 +247,7 @@ def main(exit_event: threading.Event = None) -> None: clear_locks(Paths.log_root()) params = Params() - dongle_id = params.get("SunnylinkDongleId", encoding='utf8') - - offroad_transition_prev = 0. - offroad_last = False + dongle_id = params.get("SunnylinkDongleId") if dongle_id is None: cloudlog.info("uploader missing dongle_id") @@ -265,35 +259,13 @@ def main(exit_event: threading.Event = None) -> None: backoff = 0.1 while not exit_event.is_set(): sm.update(0) - offroad = params.get_bool("IsOffroad") - t = time.monotonic() - if offroad and not offroad_last and t > 300.: - offroad_transition_prev = time.monotonic() - offroad_last = offroad - - network_type = NetworkType.wifi if force_wifi else sm['deviceState'].networkType + network_type = sm['deviceState'].networkType if not force_wifi else NetworkType.wifi if network_type == NetworkType.none: if allow_sleep: time.sleep(60 if offroad else 5) continue - if params.get_bool("DisableOnroadUploads"): - if not offroad or (offroad_transition_prev > 0. and t - offroad_transition_prev < OFFROAD_TRANSITION_TIMEOUT): - if not offroad: - cloudlog.info("not uploading: onroad uploads disabled") - else: - wait_minutes = int(OFFROAD_TRANSITION_TIMEOUT / 60) - time_left = OFFROAD_TRANSITION_TIMEOUT - (t - offroad_transition_prev) - if time_left > 2.0 * 60.0: - time_left_str = f"{int(time_left / 60)} minute(s)" - else: - time_left_str = f"{int(time_left)} seconds(s)" - cloudlog.info(f"not uploading: waiting until offroad for {wait_minutes} minutes; {time_left_str} left") - if allow_sleep: - time.sleep(60) - continue - success = uploader.step(sm['deviceState'].networkType.raw, sm['deviceState'].networkMetered) if success is None: backoff = 60 if offroad else 5 @@ -301,7 +273,7 @@ def main(exit_event: threading.Event = None) -> None: backoff = 0.1 else: cloudlog.info("upload backoff %r", backoff) - backoff = min(backoff * 2, 120) + backoff = min(backoff*2, 120) if allow_sleep: time.sleep(backoff + random.uniform(0, backoff)) diff --git a/sunnypilot/sunnylink/utils.py b/sunnypilot/sunnylink/utils.py index 55b1f6f285..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 @@ -7,7 +8,7 @@ def get_sunnylink_status(params=None) -> tuple[bool, bool]: """Get the status of Sunnylink on the device. Returns a tuple of (is_sunnylink_enabled, is_registered).""" params = params or Params() is_sunnylink_enabled = params.get_bool("SunnylinkEnabled") - is_registered = params.get("SunnylinkDongleId", encoding='utf-8') not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID) + is_registered = params.get("SunnylinkDongleId") not in (None, UNREGISTERED_SUNNYLINK_DONGLE_ID) return is_sunnylink_enabled, is_registered @@ -51,7 +52,19 @@ def register_sunnylink(): def get_api_token(): """Get the API token for the device.""" params = Params() - sunnylink_dongle_id = params.get("SunnylinkDongleId", encoding='utf-8') + sunnylink_dongle_id = params.get("SunnylinkDongleId") 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') 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 diff --git a/system/athena/athenad.py b/system/athena/athenad.py index 018fa0a0b5..42c9cf8a1c 100755 --- a/system/athena/athenad.py +++ b/system/athena/athenad.py @@ -23,6 +23,7 @@ from typing import cast from collections.abc import Callable import requests +from requests.adapters import HTTPAdapter, DEFAULT_POOLBLOCK from jsonrpc import JSONRPCResponseManager, dispatcher from websocket import (ABNF, WebSocket, WebSocketException, WebSocketTimeoutException, create_connection) @@ -56,6 +57,11 @@ WS_FRAME_SIZE = 4096 DEVICE_STATE_UPDATE_INTERVAL = 1.0 # in seconds DEFAULT_UPLOAD_PRIORITY = 99 # higher number = lower priority +# https://bytesolutions.com/dscp-tos-cos-precedence-conversion-chart, +# https://en.wikipedia.org/wiki/Differentiated_services +UPLOAD_TOS = 0x20 # CS1, low priority background traffic +SSH_TOS = 0x90 # AF42, DSCP of 36/HDD_LINUX_AC_VI with the minimum delay flag + NetworkType = log.DeviceState.NetworkType UploadFileDict = dict[str, str | int | float | bool] @@ -64,6 +70,17 @@ UploadItemDict = dict[str, str | bool | int | float | dict[str, str]] UploadFilesToUrlResponse = dict[str, int | list[UploadItemDict] | list[str]] +class UploadTOSAdapter(HTTPAdapter): + def init_poolmanager(self, connections, maxsize, block=DEFAULT_POOLBLOCK, **pool_kwargs): + pool_kwargs["socket_options"] = [(socket.IPPROTO_IP, socket.IP_TOS, UPLOAD_TOS)] + super().init_poolmanager(connections, maxsize, block, **pool_kwargs) + + +UPLOAD_SESS = requests.Session() +UPLOAD_SESS.mount("http://", UploadTOSAdapter()) +UPLOAD_SESS.mount("https://", UploadTOSAdapter()) + + @dataclass class UploadFile: fn: str @@ -136,7 +153,7 @@ class UploadQueueCache: try: upload_queue_json = Params().get("AthenadUploadQueue") if upload_queue_json is not None: - for item in json.loads(upload_queue_json): + for item in upload_queue_json: upload_queue.put(UploadItem.from_dict(item)) except Exception: cloudlog.exception("athena.UploadQueueCache.initialize.exception") @@ -146,7 +163,7 @@ class UploadQueueCache: try: queue: list[UploadItem | None] = list(upload_queue.queue) items = [asdict(i) for i in queue if i is not None and (i.id not in cancelled_uploads)] - Params().put("AthenadUploadQueue", json.dumps(items)) + Params().put("AthenadUploadQueue", items) except Exception: cloudlog.exception("athena.UploadQueueCache.cache.exception") @@ -311,10 +328,10 @@ def _do_upload(upload_item: UploadItem, callback: Callable = None) -> requests.R stream = None try: stream, content_length = get_upload_stream(path, compress) - response = requests.put(upload_item.url, - data=CallbackReader(stream, callback, content_length) if callback else stream, - headers={**upload_item.headers, 'Content-Length': str(content_length)}, - timeout=30) + response = UPLOAD_SESS.put(upload_item.url, + data=CallbackReader(stream, callback, content_length) if callback else stream, + headers={**upload_item.headers, 'Content-Length': str(content_length)}, + timeout=30) return response finally: if stream: @@ -364,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) @@ -385,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 @@ -410,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 @@ -424,7 +453,7 @@ def uploadFilesToUrls(files_data: list[UploadFileDict]) -> UploadFilesToUrlRespo path=path, url=file.url, headers=file.headers, - created_at=int(time.time() * 1000), + created_at=int(time.time() * 1000), # noqa: TID251 id=None, allow_cellular=file.allow_cellular, priority=file.priority, @@ -468,7 +497,7 @@ def setRouteViewed(route: str) -> dict[str, int | str]: # maintain a list of the last 10 routes viewed in connect params = Params() - r = params.get("AthenadRecentlyViewedRoutes", encoding="utf8") + r = params.get("AthenadRecentlyViewedRoutes") routes = [] if r is None else r.split(",") routes.append(route) @@ -481,7 +510,7 @@ def setRouteViewed(route: str) -> dict[str, int | str]: def startLocalProxy(global_end_event: threading.Event, remote_ws_uri: str, local_port: int) -> dict[str, int]: cloudlog.debug("athena.startLocalProxy.starting") - dongle_id = Params().get("DongleId").decode('utf8') + dongle_id = Params().get("DongleId") identity_token = Api(dongle_id).get_token() ws = create_connection(remote_ws_uri, cookie="jwt=" + identity_token, enable_multithread=True) @@ -501,8 +530,7 @@ def start_local_proxy_shim(global_end_event: threading.Event, local_port: int, w raise Exception("Requested local port not whitelisted") # Set TOS to keep connection responsive while under load. - # DSCP of 36/HDD_LINUX_AC_VI with the minimum delay flag - ws.sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, 0x90) + ws.sock.setsockopt(socket.IPPROTO_IP, socket.IP_TOS, SSH_TOS) ssock, csock = socket.socketpair() local_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -535,12 +563,12 @@ def getPublicKey() -> str | None: @dispatcher.add_method def getSshAuthorizedKeys() -> str: - return Params().get("GithubSshKeys", encoding='utf8') or '' + return cast(str, Params().get("GithubSshKeys") or "") @dispatcher.add_method def getGithubUsername() -> str: - return Params().get("GithubUsername", encoding='utf8') or '' + return cast(str, Params().get("GithubUsername") or "") @dispatcher.add_method def getSimInfo(): @@ -583,7 +611,7 @@ def takeSnapshot() -> str | dict[str, str] | None: def get_logs_to_send_sorted(log_attr_name=LOG_ATTR_NAME) -> list[str]: # TODO: scan once then use inotify to detect file creation/deletion - curr_time = int(time.time()) + curr_time = int(time.time()) # noqa: TID251 logs = [] for log_entry in os.listdir(Paths.swaglog_root()): log_path = os.path.join(Paths.swaglog_root(), log_entry) @@ -681,7 +709,7 @@ def log_handler(end_event: threading.Event, log_attr_name=LOG_ATTR_NAME) -> None log_entry = log_files.pop() # newest log file cloudlog.debug(f"athena.log_handler.forward_request {log_entry}") try: - curr_time = int(time.time()) + curr_time = int(time.time()) # noqa: TID251 log_path = os.path.join(Paths.swaglog_root(), log_entry) setxattr(log_path, log_attr_name, int.to_bytes(curr_time, 4, sys.byteorder)) @@ -804,7 +832,7 @@ def ws_recv(ws: WebSocket, end_event: threading.Event) -> None: recv_queue.put_nowait(data) elif opcode == ABNF.OPCODE_PING: last_ping = int(time.monotonic() * 1e9) - Params().put("LastAthenaPingTime", str(last_ping)) + Params().put("LastAthenaPingTime", last_ping) except WebSocketTimeoutException: ns_since_last_ping = int(time.monotonic() * 1e9) - last_ping if ns_since_last_ping > RECONNECT_TIMEOUT_S * 1e9: @@ -869,7 +897,7 @@ def main(exit_event: threading.Event = None): cloudlog.exception("failed to set core affinity") params = Params() - dongle_id = params.get("DongleId", encoding='utf-8') + dongle_id = params.get("DongleId") UploadQueueCache.initialize(upload_queue) ws_uri = ATHENA_HOST + "/ws/v2/" + dongle_id diff --git a/system/athena/manage_athenad.py b/system/athena/manage_athenad.py index 7158cd9220..98557db06a 100755 --- a/system/athena/manage_athenad.py +++ b/system/athena/manage_athenad.py @@ -18,7 +18,7 @@ def main(): def manage_athenad(dongle_id_param, pid_param, process_name, target): params = Params() - dongle_id = params.get(dongle_id_param, encoding='utf-8') + dongle_id = params.get(dongle_id_param) build_metadata = get_build_metadata() cloudlog.bind_global(dongle_id=dongle_id, diff --git a/system/athena/registration.py b/system/athena/registration.py index fce984c03a..2c099c4bce 100755 --- a/system/athena/registration.py +++ b/system/athena/registration.py @@ -17,7 +17,7 @@ from openpilot.common.swaglog import cloudlog UNREGISTERED_DONGLE_ID = "UnregisteredDevice" def is_registered_device() -> bool: - dongle = Params().get("DongleId", encoding='utf-8') + dongle = Params().get("DongleId") return dongle not in (None, UNREGISTERED_DONGLE_ID) @@ -33,7 +33,7 @@ def register(show_spinner=False) -> str | None: """ params = Params() - dongle_id: str | None = params.get("DongleId", encoding='utf8') + dongle_id: str | None = params.get("DongleId") if dongle_id is None and Path(Paths.persist_root()+"/comma/dongle_id").is_file(): # not all devices will have this; added early in comma 3X production (2/28/24) with open(Paths.persist_root()+"/comma/dongle_id") as f: @@ -98,7 +98,7 @@ def register(show_spinner=False) -> str | None: if dongle_id: params.put("DongleId", dongle_id) - set_offroad_alert("Offroad_UnofficialHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC) + set_offroad_alert("Offroad_UnregisteredHardware", (dongle_id == UNREGISTERED_DONGLE_ID) and not PC) return dongle_id diff --git a/system/athena/tests/test_athenad.py b/system/athena/tests/test_athenad.py index dd82325815..99ac3b1c6b 100644 --- a/system/athena/tests/test_athenad.py +++ b/system/athena/tests/test_athenad.py @@ -19,7 +19,7 @@ from cereal import messaging from openpilot.common.params import Params from openpilot.common.timeout import Timeout from openpilot.system.athena import athenad -from openpilot.system.athena.athenad import MAX_RETRY_COUNT, dispatcher +from openpilot.system.athena.athenad import MAX_RETRY_COUNT, UPLOAD_SESS, dispatcher from openpilot.system.athena.tests.helpers import HTTPRequestHandler, MockWebsocket, MockApi, EchoSocket from openpilot.selfdrive.test.helpers import http_server_context from openpilot.system.hardware.hw import Paths @@ -29,7 +29,7 @@ def seed_athena_server(host, port): with Timeout(2, 'HTTP Server seeding failed'): while True: try: - requests.put(f'http://{host}:{port}/qlog.zst', data='', timeout=10) + UPLOAD_SESS.put(f'http://{host}:{port}/qlog.zst', data='', timeout=10) break except requests.exceptions.ConnectionError: time.sleep(0.1) @@ -66,9 +66,9 @@ class TestAthenadMethods: def setup_method(self): self.default_params = { "DongleId": "0000000000000000", - "GithubSshKeys": b"ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC307aE+nuHzTAgaJhzSf5v7ZZQW9gaperjhCmyPyl4PzY7T1mDGenTlVTN7yoVFZ9UfO9oMQqo0n1OwDIiqbIFxqnhrHU0cYfj88rI85m5BEKlNu5RdaVTj1tcbaPpQc5kZEolaI1nDDjzV0lwS7jo5VYDHseiJHlik3HH1SgtdtsuamGR2T80q1SyW+5rHoMOJG73IH2553NnWuikKiuikGHUYBd00K1ilVAK2xSiMWJp55tQfZ0ecr9QjEsJ+J/efL4HqGNXhffxvypCXvbUYAFSddOwXUPo5BTKevpxMtH+2YrkpSjocWA04VnTYFiPG6U4ItKmbLOTFZtPzoez private", # noqa: E501 - "GithubUsername": b"commaci", - "AthenadUploadQueue": '[]', + "GithubSshKeys": "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC307aE+nuHzTAgaJhzSf5v7ZZQW9gaperjhCmyPyl4PzY7T1mDGenTlVTN7yoVFZ9UfO9oMQqo0n1OwDIiqbIFxqnhrHU0cYfj88rI85m5BEKlNu5RdaVTj1tcbaPpQc5kZEolaI1nDDjzV0lwS7jo5VYDHseiJHlik3HH1SgtdtsuamGR2T80q1SyW+5rHoMOJG73IH2553NnWuikKiuikGHUYBd00K1ilVAK2xSiMWJp55tQfZ0ecr9QjEsJ+J/efL4HqGNXhffxvypCXvbUYAFSddOwXUPo5BTKevpxMtH+2YrkpSjocWA04VnTYFiPG6U4ItKmbLOTFZtPzoez private", # noqa: E501 + "GithubUsername": "commaci", + "AthenadUploadQueue": [], } self.params = Params() @@ -91,8 +91,8 @@ class TestAthenadMethods: @staticmethod def _wait_for_upload(): - now = time.time() - while time.time() - now < 5: + now = time.monotonic() + while time.monotonic() - now < 5: if athenad.upload_queue.qsize() == 0: break @@ -190,11 +190,11 @@ class TestAthenadMethods: fn = self._create_file('qlog', data=os.urandom(10000 * 1024)) upload_fn = fn + ('.zst' if compress else '') - item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='') + item = athenad.UploadItem(path=upload_fn, url="http://localhost:1238", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251 with pytest.raises(requests.exceptions.ConnectionError): athenad._do_upload(item) - item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='') + item = athenad.UploadItem(path=upload_fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='') # noqa: TID251 resp = athenad._do_upload(item) assert resp.status_code == 201 @@ -226,7 +226,7 @@ class TestAthenadMethods: @with_upload_handler def test_upload_handler(self, host): fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) self._wait_for_upload() @@ -239,10 +239,10 @@ class TestAthenadMethods: @pytest.mark.parametrize("status,retry", [(500,True), (412,False)]) @with_upload_handler def test_upload_handler_retry(self, mocker, host, status, retry): - mock_put = mocker.patch('requests.put') + mock_put = mocker.patch('openpilot.system.athena.athenad.UPLOAD_SESS.put') mock_put.return_value.__enter__.return_value.status_code = status fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) self._wait_for_upload() @@ -257,7 +257,7 @@ class TestAthenadMethods: def test_upload_handler_timeout(self): """When an upload times out or fails to connect it should be placed back in the queue""" fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url="http://localhost:44444/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 item_no_retry = replace(item, retry_count=MAX_RETRY_COUNT) athenad.upload_queue.put_nowait(item_no_retry) @@ -278,7 +278,7 @@ class TestAthenadMethods: @with_upload_handler def test_cancel_upload(self): item = athenad.UploadItem(path="qlog.zst", url="http://localhost:44444/qlog.zst", headers={}, - created_at=int(time.time()*1000), id='id', allow_cellular=True) + created_at=int(time.time()*1000), id='id', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) dispatcher["cancelUpload"](item.id) @@ -312,7 +312,7 @@ class TestAthenadMethods: @with_upload_handler def test_list_upload_queue_current(self, host: str): fn = self._create_file('qlog.zst') - item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) + item = athenad.UploadItem(path=fn, url=f"{host}/qlog.zst", headers={}, created_at=int(time.time()*1000), id='', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) self._wait_for_upload() @@ -331,7 +331,7 @@ class TestAthenadMethods: path=fp, url=f"http://localhost:44444/{fn}", headers={}, - created_at=int(time.time()*1000), + created_at=int(time.time()*1000), # noqa: TID251 id='', allow_cellular=True, priority=i @@ -343,7 +343,7 @@ class TestAthenadMethods: def test_list_upload_queue(self): item = athenad.UploadItem(path="qlog.zst", url="http://localhost:44444/qlog.zst", headers={}, - created_at=int(time.time()*1000), id='id', allow_cellular=True) + created_at=int(time.time()*1000), id='id', allow_cellular=True) # noqa: TID251 athenad.upload_queue.put_nowait(item) items = dispatcher["listUploadQueue"]() @@ -356,8 +356,8 @@ class TestAthenadMethods: assert len(items) == 0 def test_upload_queue_persistence(self): - item1 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id1') - item2 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id2') + item1 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id1') # noqa: TID251 + item2 = athenad.UploadItem(path="_", url="_", headers={}, created_at=int(time.time()), id='id2') # noqa: TID251 athenad.upload_queue.put_nowait(item1) athenad.upload_queue.put_nowait(item2) @@ -400,11 +400,11 @@ class TestAthenadMethods: def test_get_ssh_authorized_keys(self): keys = dispatcher["getSshAuthorizedKeys"]() - assert keys == self.default_params["GithubSshKeys"].decode('utf-8') + assert keys == self.default_params["GithubSshKeys"] def test_get_github_username(self): keys = dispatcher["getGithubUsername"]() - assert keys == self.default_params["GithubUsername"].decode('utf-8') + assert keys == self.default_params["GithubUsername"] def test_get_version(self): resp = dispatcher["getVersion"]() diff --git a/system/athena/tests/test_athenad_ping.py b/system/athena/tests/test_athenad_ping.py index 025df7d614..8ff1e37a5d 100644 --- a/system/athena/tests/test_athenad_ping.py +++ b/system/athena/tests/test_athenad_ping.py @@ -28,7 +28,7 @@ class TestAthenadPing: exit_event: threading.Event def _get_ping_time(self) -> str | None: - return cast(str | None, self.params.get("LastAthenaPingTime", encoding="utf-8")) + return cast(str | None, self.params.get("LastAthenaPingTime")) def _clear_ping_time(self) -> None: self.params.remove("LastAthenaPingTime") @@ -42,7 +42,7 @@ class TestAthenadPing: def setup_method(self) -> None: self.params = Params() - self.dongle_id = self.params.get("DongleId", encoding="utf-8") + self.dongle_id = self.params.get("DongleId") wifi_radio(True) self._clear_ping_time() diff --git a/system/athena/tests/test_registration.py b/system/athena/tests/test_registration.py index c1dabb78ec..c20722659a 100644 --- a/system/athena/tests/test_registration.py +++ b/system/athena/tests/test_registration.py @@ -49,7 +49,7 @@ class TestRegistration: dongle = register() assert m.call_count == 0 assert dongle == UNREGISTERED_DONGLE_ID - assert self.params.get("DongleId", encoding='utf-8') == dongle + assert self.params.get("DongleId") == dongle def test_missing_cache(self, mocker): # keys exist but no dongle id @@ -63,7 +63,7 @@ class TestRegistration: # call again, shouldn't hit the API this time assert register() == dongle assert m.call_count == 1 - assert self.params.get("DongleId", encoding='utf-8') == dongle + assert self.params.get("DongleId") == dongle def test_unregistered(self, mocker): # keys exist, but unregistered @@ -73,4 +73,4 @@ class TestRegistration: dongle = register() assert m.call_count == 1 assert dongle == UNREGISTERED_DONGLE_ID - assert self.params.get("DongleId", encoding='utf-8') == dongle + assert self.params.get("DongleId") == dongle diff --git a/system/camerad/sensors/ar0231_cl.h b/system/camerad/sensors/ar0231_cl.h deleted file mode 100644 index c79242543b..0000000000 --- a/system/camerad/sensors/ar0231_cl.h +++ /dev/null @@ -1,34 +0,0 @@ -#if SENSOR_ID == 1 - -#define VIGNETTE_PROFILE_8DT0MM - -#define BIT_DEPTH 12 -#define PV_MAX 4096 -#define BLACK_LVL 168 - -float4 normalize_pv(int4 parsed, float vignette_factor) { - float4 pv = (convert_float4(parsed) - BLACK_LVL) / (PV_MAX - BLACK_LVL); - return clamp(pv*vignette_factor, 0.0, 1.0); -} - -float3 color_correct(float3 rgb) { - float3 corrected = rgb.x * (float3)(1.82717181, -0.31231438, 0.07307673); - corrected += rgb.y * (float3)(-0.5743977, 1.36858544, -0.53183455); - corrected += rgb.z * (float3)(-0.25277411, -0.05627105, 1.45875782); - return corrected; -} - -float3 apply_gamma(float3 rgb, int expo_time) { - // tone mapping params - 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 - return (rgb > mp) ? - ((rk * (rgb-mp) * (1-(gamma_k*mp+gamma_b)) * (1+1/(rk*(1-mp))) / (1+rk*(rgb-mp))) + gamma_k*mp + gamma_b) : - ((rk * (rgb-mp) * (gamma_k*mp+gamma_b) * (1+1/(rk*mp)) / (1-rk*(rgb-mp))) + gamma_k*mp + gamma_b); -} - -#endif diff --git a/system/camerad/sensors/os04c10_cl.h b/system/camerad/sensors/os04c10_cl.h deleted file mode 100644 index 3b5cf88839..0000000000 --- a/system/camerad/sensors/os04c10_cl.h +++ /dev/null @@ -1,58 +0,0 @@ -#if SENSOR_ID == 3 - -#define BGGR -#define VIGNETTE_PROFILE_4DT6MM - -#define BIT_DEPTH 12 -#define PV_MAX10 1023 -#define PV_MAX12 4095 -#define PV_MAX16 65536 // gamma curve is calibrated to 16bit -#define BLACK_LVL 48 - -float combine_dual_pvs(float lv, float sv, int expo_time) { - float svc = fmax(sv * expo_time, (float)(64 * (PV_MAX10 - BLACK_LVL))); - float svd = sv * fmin(expo_time, 8.0) / 8; - - if (expo_time > 64) { - if (lv < PV_MAX10 - BLACK_LVL) { - return lv / (PV_MAX16 - BLACK_LVL); - } else { - return (svc / 64) / (PV_MAX16 - BLACK_LVL); - } - } else { - if (lv > 32) { - return (lv * 64 / fmax(expo_time, 8.0)) / (PV_MAX16 - BLACK_LVL); - } else { - return svd / (PV_MAX16 - BLACK_LVL); - } - } -} - -float4 normalize_pv_hdr(int4 parsed, int4 short_parsed, float vignette_factor, int expo_time) { - float4 pl = convert_float4(parsed - BLACK_LVL); - float4 ps = convert_float4(short_parsed - BLACK_LVL); - float4 pv; - pv.s0 = combine_dual_pvs(pl.s0, ps.s0, expo_time); - pv.s1 = combine_dual_pvs(pl.s1, ps.s1, expo_time); - pv.s2 = combine_dual_pvs(pl.s2, ps.s2, expo_time); - pv.s3 = combine_dual_pvs(pl.s3, ps.s3, expo_time); - return clamp(pv*vignette_factor, 0.0, 1.0); -} - -float4 normalize_pv(int4 parsed, float vignette_factor) { - float4 pv = (convert_float4(parsed) - BLACK_LVL) / (PV_MAX12 - BLACK_LVL); - return clamp(pv*vignette_factor, 0.0, 1.0); -} - -float3 color_correct(float3 rgb) { - float3 corrected = rgb.x * (float3)(1.55361989, -0.268894615, -0.000593219); - corrected += rgb.y * (float3)(-0.421217301, 1.51883144, -0.69760146); - corrected += rgb.z * (float3)(-0.132402589, -0.249936825, 1.69819468); - return corrected; -} - -float3 apply_gamma(float3 rgb, int expo_time) { - return (10 * rgb) / (1 + 9 * rgb); -} - -#endif diff --git a/system/camerad/sensors/ox03c10_cl.h b/system/camerad/sensors/ox03c10_cl.h deleted file mode 100644 index c8cec7cf8a..0000000000 --- a/system/camerad/sensors/ox03c10_cl.h +++ /dev/null @@ -1,47 +0,0 @@ -#if SENSOR_ID == 2 - -#define VIGNETTE_PROFILE_8DT0MM - -#define BIT_DEPTH 12 -#define BLACK_LVL 64 - -float ox_lut_func(int x) { - if (x < 512) { - return x * 5.94873e-8; - } else if (512 <= x && x < 768) { - return 3.0458e-05 + (x-512) * 1.19913e-7; - } else if (768 <= x && x < 1536) { - return 6.1154e-05 + (x-768) * 2.38493e-7; - } else if (1536 <= x && x < 1792) { - return 0.0002448 + (x-1536) * 9.56930e-7; - } else if (1792 <= x && x < 2048) { - return 0.00048977 + (x-1792) * 1.91441e-6; - } else if (2048 <= x && x < 2304) { - return 0.00097984 + (x-2048) * 3.82937e-6; - } else if (2304 <= x && x < 2560) { - return 0.0019601 + (x-2304) * 7.659055e-6; - } else if (2560 <= x && x < 2816) { - return 0.0039207 + (x-2560) * 1.525e-5; - } else { - return 0.0078421 + (exp((x-2816)/273.0) - 1) * 0.0092421; - } -} - -float4 normalize_pv(int4 parsed, float vignette_factor) { - // PWL - float4 pv = {ox_lut_func(parsed.s0), ox_lut_func(parsed.s1), ox_lut_func(parsed.s2), ox_lut_func(parsed.s3)}; - return clamp(pv*vignette_factor*256.0, 0.0, 1.0); -} - -float3 color_correct(float3 rgb) { - float3 corrected = rgb.x * (float3)(1.5664815, -0.29808738, -0.03973474); - corrected += rgb.y * (float3)(-0.48672447, 1.41914433, -0.40295248); - corrected += rgb.z * (float3)(-0.07975703, -0.12105695, 1.44268722); - return corrected; -} - -float3 apply_gamma(float3 rgb, int expo_time) { - return -0.507089*exp(-12.54124638*rgb) + 0.9655*powr(rgb, 0.5) - 0.472597*rgb + 0.507089; -} - -#endif diff --git a/system/camerad/test/test_camerad.py b/system/camerad/test/test_camerad.py index ab76985972..1f3f97b082 100644 --- a/system/camerad/test/test_camerad.py +++ b/system/camerad/test/test_camerad.py @@ -84,7 +84,10 @@ class TestCamerad: # logMonoTime > SOF assert np.all((ts[c]['t'] - ts[c]['timestampSof']/1e9) > 1e-7) - assert np.all((ts[c]['t'] - ts[c]['timestampEof']/1e9) > 1e-7) + + # logMonoTime > EOF, needs some tolerance since EOF is (SOF + readout time) but there is noise in the SOF timestamping (done via IRQ) + assert np.mean((ts[c]['t'] - ts[c]['timestampEof']/1e9) > 1e-7) > 0.7 # should be mostly logMonoTime > EOF + assert np.all((ts[c]['t'] - ts[c]['timestampEof']/1e9) > -0.10) # when EOF > logMonoTime, it should never be more than two frames def test_stress_test(self): os.environ['SPECTRA_ERROR_PROB'] = '0.008' diff --git a/system/camerad/test/test_exposure.py b/system/camerad/test/test_exposure.py index f431c03410..b853b0f2f2 100644 --- a/system/camerad/test/test_exposure.py +++ b/system/camerad/test/test_exposure.py @@ -33,8 +33,8 @@ class TestCamerad: @with_processes(['camerad']) def test_camera_operation(self): passed = 0 - start = time.time() - while time.time() - start < TEST_TIME and passed < REPEAT: + start = time.monotonic() + while time.monotonic() - start < TEST_TIME and passed < REPEAT: rpic, dpic = get_snapshots(frame="roadCameraState", front_frame="driverCameraState") wpic, _ = get_snapshots(frame="wideRoadCameraState") diff --git a/system/hardware/base.h b/system/hardware/base.h index baf0f3c3da..df9700a017 100644 --- a/system/hardware/base.h +++ b/system/hardware/base.h @@ -10,9 +10,6 @@ // no-op base hw class class HardwareNone { public: - static constexpr float MAX_VOLUME = 0.7; - static constexpr float MIN_VOLUME = 0.2; - static std::string get_os_version() { return ""; } static std::string get_name() { return ""; } static cereal::InitData::DeviceType get_device_type() { return cereal::InitData::DeviceType::UNKNOWN; } diff --git a/system/hardware/base.py b/system/hardware/base.py index 3506be5145..b457ea4e17 100644 --- a/system/hardware/base.py +++ b/system/hardware/base.py @@ -199,9 +199,6 @@ class HardwareBase(ABC): def get_modem_temperatures(self): pass - @abstractmethod - def get_nvme_temperatures(self): - pass @abstractmethod def initialize_hardware(self): diff --git a/system/hardware/fan_controller.py b/system/hardware/fan_controller.py index 7d5bec0509..4c7adc0a3e 100755 --- a/system/hardware/fan_controller.py +++ b/system/hardware/fan_controller.py @@ -21,16 +21,16 @@ class TiciFanController(BaseFanController): self.controller = PIDController(k_p=0, k_i=4e-3, k_f=1, rate=(1 / DT_HW)) def update(self, cur_temp: float, ignition: bool) -> int: - self.controller.neg_limit = -(100 if ignition else 30) - self.controller.pos_limit = -(30 if ignition else 0) + self.controller.pos_limit = 100 if ignition else 30 + self.controller.neg_limit = 30 if ignition else 0 if ignition != self.last_ignition: self.controller.reset() - error = 75 - cur_temp - fan_pwr_out = -int(self.controller.update( + error = cur_temp - 75 + fan_pwr_out = int(self.controller.update( error=error, - feedforward=np.interp(cur_temp, [60.0, 100.0], [0, -100]) + feedforward=np.interp(cur_temp, [60.0, 100.0], [0, 100]) )) self.last_ignition = ignition diff --git a/system/hardware/hardwared.py b/system/hardware/hardwared.py index 223faeaafd..d702334fa8 100755 --- a/system/hardware/hardwared.py +++ b/system/hardware/hardwared.py @@ -1,7 +1,6 @@ #!/usr/bin/env python3 import fcntl import os -import json import queue import struct import threading @@ -38,7 +37,7 @@ ONROAD_CYCLE_TIME = 1 # seconds to wait offroad after requesting an onroad cycl ThermalBand = namedtuple("ThermalBand", ['min_temp', 'max_temp']) HardwareState = namedtuple("HardwareState", ['network_type', 'network_info', 'network_strength', 'network_stats', - 'network_metered', 'nvme_temps', 'modem_temps']) + 'network_metered', 'modem_temps']) # List of thermal bands. We will stay within this region as long as we are within the bounds. # When exiting the bounds, we'll jump to the lower or higher band. Bands are ordered in the dict. @@ -141,7 +140,6 @@ def hw_state_thread(end_event, hw_queue): network_strength=HARDWARE.get_network_strength(network_type), network_stats={'wwanTx': tx, 'wwanRx': rx}, network_metered=HARDWARE.get_network_metered(network_type), - nvme_temps=HARDWARE.get_nvme_temperatures(), modem_temps=modem_temps, ) @@ -172,6 +170,7 @@ def hardware_thread(end_event, hw_queue) -> None: onroad_conditions: dict[str, bool] = { "ignition": False, "not_onroad_cycle": True, + "device_temp_good": True, } startup_conditions: dict[str, bool] = {} startup_conditions_prev: dict[str, bool] = {} @@ -188,7 +187,6 @@ def hardware_thread(end_event, hw_queue) -> None: network_metered=False, network_strength=NetworkStrength.unknown, network_stats={'wwanTx': -1, 'wwanRx': -1}, - nvme_temps=[], modem_temps=[], ) @@ -202,6 +200,10 @@ def hardware_thread(end_event, hw_queue) -> None: params = Params() power_monitor = PowerMonitoring() + uptime_offroad: float = params.get("UptimeOffroad", return_default=True) + uptime_onroad: float = params.get("UptimeOnroad", return_default=True) + last_uptime_ts: float = time.monotonic() + HARDWARE.initialize_hardware() thermal_config = HARDWARE.get_thermal_config() @@ -267,7 +269,6 @@ def hardware_thread(end_event, hw_queue) -> None: if last_hw_state.network_info is not None: msg.deviceState.networkInfo = last_hw_state.network_info - msg.deviceState.nvmeTempC = last_hw_state.nvme_temps msg.deviceState.modemTempC = last_hw_state.modem_temps msg.deviceState.screenBrightnessPercent = HARDWARE.get_screen_brightness() @@ -304,6 +305,7 @@ def hardware_thread(end_event, hw_queue) -> None: # **** starting logic **** startup_conditions["up_to_date"] = params.get("Offroad_ConnectivityNeeded") is None or params.get_bool("DisableUpdates") or params.get_bool("SnoozeUpdate") + startup_conditions["no_excessive_actuation"] = params.get("Offroad_ExcessiveActuation") is None startup_conditions["not_uninstalling"] = not params.get_bool("DoUninstall") startup_conditions["accepted_terms"] = params.get("HasAcceptedTerms") == terms_version @@ -335,16 +337,6 @@ def hardware_thread(end_event, hw_queue) -> None: 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) - else: - # check for bad NVMe - try: - with open("/sys/block/nvme0n1/device/model") as f: - model = f.read().strip() - if not model.startswith("Samsung SSD 980") and params.get("Offroad_BadNvme") is None: - set_offroad_alert_if_changed("Offroad_BadNvme", True) - cloudlog.event("Unsupported NVMe", model=model, error=True) - except Exception: - pass # Handle offroad/onroad transition should_start = all(onroad_conditions.values()) @@ -390,6 +382,11 @@ def hardware_thread(end_event, hw_queue) -> None: # Offroad power monitoring voltage = None if peripheralState.pandaType == log.PandaState.PandaType.unknown else peripheralState.voltage + + # GitHub runner auto off: 9V is used as the threshold because most desktop runners + # will rarely exceed 5V so 9V is set as our buffer between desk use and car use. + params.put_bool_nonblocking("GithubRunnerSufficientVoltage", ((voltage or 0) and voltage > 9000)) + power_monitor.calculate(voltage, onroad_conditions["ignition"]) msg.deviceState.offroadPowerUsageUwh = power_monitor.get_power_used() msg.deviceState.carBatteryCapacityUwh = max(0, power_monitor.get_car_battery_capacity()) @@ -411,7 +408,7 @@ def hardware_thread(end_event, hw_queue) -> None: last_ping = params.get("LastAthenaPingTime") if last_ping is not None: - msg.deviceState.lastAthenaPingTime = int(last_ping) + msg.deviceState.lastAthenaPingTime = last_ping msg.deviceState.thermalStatus = thermal_status pm.send("deviceState", msg) @@ -429,8 +426,6 @@ def hardware_thread(end_event, hw_queue) -> None: statlog.gauge("memory_temperature", msg.deviceState.memoryTempC) for i, temp in enumerate(msg.deviceState.pmicTempC): statlog.gauge(f"pmic{i}_temperature", temp) - for i, temp in enumerate(last_hw_state.nvme_temps): - statlog.gauge(f"nvme_temperature{i}", temp) for i, temp in enumerate(last_hw_state.modem_temps): statlog.gauge(f"modem_temperature{i}", temp) statlog.gauge("fan_speed_percent_desired", msg.deviceState.fanSpeedPercentDesired) @@ -451,12 +446,23 @@ def hardware_thread(end_event, hw_queue) -> None: # save last one before going onroad if rising_edge_started: try: - params.put("LastOffroadStatusPacket", json.dumps(dat)) + params.put("LastOffroadStatusPacket", dat) except Exception: cloudlog.exception("failed to save offroad status") params.put_bool_nonblocking("NetworkMetered", msg.deviceState.networkMetered) + now_ts = time.monotonic() + if off_ts: + uptime_offroad += now_ts - max(last_uptime_ts, off_ts) + elif started_ts: + uptime_onroad += now_ts - max(last_uptime_ts, started_ts) + last_uptime_ts = now_ts + + if (count % int(60. / DT_HW)) == 0: + params.put("UptimeOffroad", uptime_offroad) + params.put("UptimeOnroad", uptime_onroad) + count += 1 should_start_prev = should_start diff --git a/system/hardware/hw.h b/system/hardware/hw.h index d2083a5985..bc9d17dd81 100644 --- a/system/hardware/hw.h +++ b/system/hardware/hw.h @@ -55,4 +55,8 @@ namespace Path { return "/dev/shm"; #endif } + + inline std::string model_root() { + return Hardware::PC() ? Path::comma_home() + "/media/0/models" : "/data/media/0/models"; + } } // namespace Path 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/hardware/pc/hardware.py b/system/hardware/pc/hardware.py index 9a80f10bed..6c11896390 100644 --- a/system/hardware/pc/hardware.py +++ b/system/hardware/pc/hardware.py @@ -70,8 +70,6 @@ class Pc(HardwareBase): def get_modem_temperatures(self): return [] - def get_nvme_temperatures(self): - return [] def initialize_hardware(self): pass diff --git a/system/hardware/power_monitoring.py b/system/hardware/power_monitoring.py index 952aebd9d7..d0a1e623a7 100644 --- a/system/hardware/power_monitoring.py +++ b/system/hardware/power_monitoring.py @@ -29,12 +29,10 @@ class PowerMonitoring: self.car_voltage_instant_mV = 12e3 # Last value of peripheralState voltage self.integration_lock = threading.Lock() - car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") - if car_battery_capacity_uWh is None: - car_battery_capacity_uWh = 0 + car_battery_capacity_uWh = self.params.get("CarBatteryCapacity") or 0 # Reset capacity if it's low - self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), int(car_battery_capacity_uWh)) + self.car_battery_capacity_uWh = max((CAR_BATTERY_CAPACITY_uWh / 10), car_battery_capacity_uWh) # Calculation tick def calculate(self, voltage: int | None, ignition: bool): @@ -58,7 +56,7 @@ class PowerMonitoring: self.car_battery_capacity_uWh = max(self.car_battery_capacity_uWh, 0) self.car_battery_capacity_uWh = min(self.car_battery_capacity_uWh, CAR_BATTERY_CAPACITY_uWh) if now - self.last_save_time >= 10: - self.params.put_nonblocking("CarBatteryCapacity", str(int(self.car_battery_capacity_uWh))) + self.params.put_nonblocking("CarBatteryCapacity", int(self.car_battery_capacity_uWh)) self.last_save_time = now # First measurement, set integration time @@ -114,15 +112,14 @@ class PowerMonitoring: :return: True if the max time offroad has been exceeded, False otherwise """ try: - param = self.params.get("MaxTimeOffroad", encoding="utf8") - sp_max_time_val_s = int(param) * 60 if param is not None and int(param) >= 0 else MAX_TIME_OFFROAD_S + param = self.params.get("MaxTimeOffroad") + sp_max_time_val_s = param * 60 if param is not None and param >= 0 else MAX_TIME_OFFROAD_S except Exception: sp_max_time_val_s = MAX_TIME_OFFROAD_S - return sp_max_time_val_s > 0 and offroad_time >= sp_max_time_val_s + return 0 < sp_max_time_val_s <= offroad_time - -# See if we need to shutdown + # See if we need to shutdown def should_shutdown(self, ignition: bool, in_car: bool, offroad_timestamp: float | None, started_seen: bool): if offroad_timestamp is None: return False diff --git a/system/hardware/tests/test_power_monitoring.py b/system/hardware/tests/test_power_monitoring.py index 3eec13dc4c..451efcd735 100644 --- a/system/hardware/tests/test_power_monitoring.py +++ b/system/hardware/tests/test_power_monitoring.py @@ -206,18 +206,18 @@ class TestPowerMonitoring: (None, MAX_TIME_OFFROAD_S + 1, True), # exceeds 30h (1800+ mins) # Valid max time values (in minutes) - ("60", 59, False), # under limit - ("60", 120, True), # over limit - ("10", 8, False), # under limit - ("10", 11, True), # over limit + (60, 59, False), # under limit + (60, 120, True), # over limit + (10, 8, False), # under limit + (10, 11, True), # over limit # Edge case: max time is zero → no limit enforced - ("0", 0, False), - ("0", 400, False), + (0, 0, False), + (0, 400, False), # Invalid max time formats or negative values → fallback to 30 hours - ("invalid", 100, False), # should fallback to 30h - ("-1", MAX_TIME_OFFROAD_S + 1, True), # should fallback to 30h, and exceed it + (-100, 100, False), # should fallback to 30h + (-1, MAX_TIME_OFFROAD_S + 1, True), # should fallback to 30h, and exceed it ] ) def test_max_time_offroad_exceeded(self, max_time_offroad, offroad_time_min, expected_result): diff --git a/system/hardware/tici/agnos.json b/system/hardware/tici/agnos.json index ec2557422c..941a4956bf 100644 --- a/system/hardware/tici/agnos.json +++ b/system/hardware/tici/agnos.json @@ -1,25 +1,25 @@ [ { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz", - "hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", - "hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "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": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089" + "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz", - "hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", - "hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "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": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091" + "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" }, { "name": "abl", @@ -56,28 +56,28 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", - "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "size": 18479104, + "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": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" + "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", - "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", - "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "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": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", + "ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7", "alt": { - "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", + "hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", + "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img", "size": 5368709120 } } diff --git a/system/hardware/tici/all-partitions.json b/system/hardware/tici/all-partitions.json index d28b48128b..5891e2748a 100644 --- a/system/hardware/tici/all-partitions.json +++ b/system/hardware/tici/all-partitions.json @@ -97,14 +97,14 @@ }, { "name": "persist", - "url": "https://commadist.azureedge.net/agnosupdate/persist-9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d.img.xz", - "hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d", - "hash_raw": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d", - "size": 33554432, + "url": "https://commadist.azureedge.net/agnosupdate/persist-d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786.img.xz", + "hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786", + "hash_raw": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786", + "size": 4096, "sparse": false, "full_check": true, "has_ab": false, - "ondevice_hash": "9814b07851292f510f3794b767489f38ab379a99f0ea75dc620ad2d3a496d54d" + "ondevice_hash": "d6af4ec18df180c7417353b52a9e05e43a6480b29425f087874136436cefe786" }, { "name": "systemrw", @@ -130,25 +130,25 @@ }, { "name": "xbl", - "url": "https://commadist.azureedge.net/agnosupdate/xbl-6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1.img.xz", - "hash": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", - "hash_raw": "6710967ca9701f205d7ab19c3a9b0dd2f547e65b3d96048b7c2b03755aafa0f1", + "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": "003a17ab1be68a696f7efe4c9938e8be511d4aacfc2f3211fc896bdc1681d089" + "ondevice_hash": "ed61a650bea0c56652dd0fc68465d8fc722a4e6489dc8f257630c42c6adcdc89" }, { "name": "xbl_config", - "url": "https://commadist.azureedge.net/agnosupdate/xbl_config-63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535.img.xz", - "hash": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", - "hash_raw": "63922cfbfdf4ab87986c4ba8f3a4df5bf28414b3f71a29ec5947336722215535", + "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": "2a855dd636cc94718b64bea83a44d0a31741ecaa8f72a63613ff348ec7404091" + "ondevice_hash": "b12801ffaa81e58e3cef914488d3b447e35483ba549b28c6cd9deb4814c3265f" }, { "name": "abl", @@ -339,62 +339,62 @@ }, { "name": "boot", - "url": "https://commadist.azureedge.net/agnosupdate/boot-4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef.img.xz", - "hash": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "hash_raw": "4de8f892dbac3fa3fee1efe68ca76e23e75812e81a6577d00d52e2da1ef624ef", - "size": 18479104, + "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": "8d7094d774faa4e801e36b403a31b53b913b31d086f4dc682d2f64710c557e8a" + "ondevice_hash": "492ae27f569e8db457c79d0e358a7a6297d1a1c685c2b1ae6deba7315d3a6cb0" }, { "name": "system", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img.xz", - "hash": "cccd7073d067027396f2afd49874729757db0bbbc79853a0bf2938bd356fe164", - "hash_raw": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", + "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": "c7707f16ce7d977748677cc354e250943b4ff6c21b9a19a492053d32397cf9ec", + "ondevice_hash": "242aa5adad1c04e1398e00e2440d1babf962022eb12b89adf2e60ee3068946e7", "alt": { - "hash": "4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39", - "url": "https://commadist.azureedge.net/agnosupdate/system-4bc3951f4aa3f70c53837dc2542d8b0666d37103b353fd81417cc7de1bbebe39.img", + "hash": "e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087", + "url": "https://commadist.azureedge.net/agnosupdate/system-e0007afa5d1026671c1943d44bb7f7ad26259f673392dd00a03073a2870df087.img", "size": 5368709120 } }, { "name": "userdata_90", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21.img.xz", - "hash": "3d8a007bae088c5959eb9b82454013f91868946d78380fecea2b1afdfb575c02", - "hash_raw": "f0c675e0fae420870c9ba8979fa246b170f4f1a7a04b49609b55b6bdfa8c1b21", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_90-602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a.img.xz", + "hash": "6a11d448bac50467791809339051eed2894aae971c37bf6284b3b972a99ba3ac", + "hash_raw": "602d5103cba97e1b07f76508d5febb47cfc4463a7e31bd20e461b55c801feb0a", "size": 96636764160, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "5bfbabb8ff96b149056aa75d5b7e66a7cdd9cb4bcefe23b922c292f7f3a43462" + "ondevice_hash": "e014d92940a696bf8582807259820ab73948b950656ed83a45da738f26083705" }, { "name": "userdata_89", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf.img.xz", - "hash": "443f136484294b210318842d09fb618d5411c8bdbab9f7421d8c89eb291a8d3f", - "hash_raw": "06fc52be37b42690ed7b4f8c66c4611309a2dea9fca37dd9d27d1eff302eb1bf", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_89-4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48.img.xz", + "hash": "748e31a5fc01fc256c012e359c3382d1f98cce98feafe8ecc0fca3e47caef116", + "hash_raw": "4d7f6d12a5557eb6e3cbff9a4cd595677456fdfddcc879eddcea96a43a9d8b48", "size": 95563022336, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "67db02b29a7e4435951c64cc962a474d048ed444aa912f3494391417cd51a074" + "ondevice_hash": "c181b93050787adcfef730c086bcb780f28508d84e6376d9b80d37e5dc02b55e" }, { "name": "userdata_30", - "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6.img.xz", - "hash": "875b580cb786f290a842e9187fd945657561886123eb3075a26f7995a18068f6", - "hash_raw": "06679488f0c5c3fcfd5f351133050751cd189f705e478a979c45fc4a166d18a6", + "url": "https://commadist.azureedge.net/agnosupdate/userdata_30-80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668.img.xz", + "hash": "09ff390e639e4373d772e1688d05a5ac77a573463ed1deeff86390686fa686f9", + "hash_raw": "80a76c8e56bbd7536fd5e87e8daa12984e2960db4edeb1f83229b2baeecc4668", "size": 32212254720, "sparse": true, "full_check": true, "has_ab": false, - "ondevice_hash": "16e27ba3c5cf9f0394ce6235ba6021b8a2de293fdb08399f8ca832fa5e4d0b9d" + "ondevice_hash": "2c01ab470c02121c721ff6afc25582437e821686207f3afef659387afb69c507" } ] \ No newline at end of file diff --git a/system/hardware/tici/hardware.h b/system/hardware/tici/hardware.h index 179ef54a9b..ed8a7e7d17 100644 --- a/system/hardware/tici/hardware.h +++ b/system/hardware/tici/hardware.h @@ -13,8 +13,6 @@ class HardwareTici : public HardwareNone { public: - static constexpr float MAX_VOLUME = 0.9; - static constexpr float MIN_VOLUME = 0.1; static bool TICI() { return true; } static bool AGNOS() { return true; } static std::string get_os_version() { @@ -75,7 +73,7 @@ public: return; } - int value = util::map_val(std::clamp(percent, 0, 100), 0, 100, 0, 255); + int value = util::map_val(std::clamp(percent, 0, 100), 0, 100, 0, 300); std::ofstream("/sys/class/leds/led:switch_2/brightness") << 0 << "\n"; std::ofstream("/sys/class/leds/led:torch_2/brightness") << value << "\n"; std::ofstream("/sys/class/leds/led:switch_2/brightness") << value << "\n"; diff --git a/system/hardware/tici/hardware.py b/system/hardware/tici/hardware.py index 8e4ba722c4..35f9916c31 100644 --- a/system/hardware/tici/hardware.py +++ b/system/hardware/tici/hardware.py @@ -1,4 +1,3 @@ -import json import math import os import subprocess @@ -291,15 +290,6 @@ class Tici(HardwareBase): except Exception: return [] - def get_nvme_temperatures(self): - ret = [] - try: - out = subprocess.check_output("sudo smartctl -aj /dev/nvme0", shell=True) - dat = json.loads(out) - ret = list(map(int, dat["nvme_smart_health_information_log"]["temperature_sensors"])) - except Exception: - pass - return ret def get_current_power_draw(self): return (self.read_param_file("/sys/class/hwmon/hwmon1/power1_input", int) / 1e6) @@ -415,8 +405,8 @@ class Tici(HardwareBase): # *** GPU config *** # https://github.com/commaai/agnos-kernel-sdm845/blob/master/arch/arm64/boot/dts/qcom/sdm845-gpu.dtsi#L216 - sudo_write("0", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") - sudo_write("0", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/min_pwrlevel") + sudo_write("1", "/sys/class/kgsl/kgsl-3d0/max_pwrlevel") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_bus_on") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_clk_on") sudo_write("1", "/sys/class/kgsl/kgsl-3d0/force_rail_on") diff --git a/system/loggerd/bootlog.cc b/system/loggerd/bootlog.cc index a7b1ae8244..1b87ce394f 100644 --- a/system/loggerd/bootlog.cc +++ b/system/loggerd/bootlog.cc @@ -31,9 +31,6 @@ static kj::Array build_boot_log() { "[ -x \"$(command -v journalctl)\" ] && journalctl -o short-monotonic", }; - if (Hardware::TICI()) { - bootlog_commands.push_back("[ -e /dev/nvme0 ] && sudo nvme smart-log --output-format=json /dev/nvme0"); - } auto commands = boot.initCommands().initEntries(bootlog_commands.size()); for (int j = 0; j < bootlog_commands.size(); j++) { 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) diff --git a/system/loggerd/encoder/encoder.cc b/system/loggerd/encoder/encoder.cc index b8f2f274d2..06922b0cd0 100644 --- a/system/loggerd/encoder/encoder.cc +++ b/system/loggerd/encoder/encoder.cc @@ -21,7 +21,7 @@ void VideoEncoder::publisher_publish(int segment_num, uint32_t idx, VisionIpcBuf edata.setFrameId(extra.frame_id); edata.setTimestampSof(extra.timestamp_sof); edata.setTimestampEof(extra.timestamp_eof); - edata.setType(encoder_info.encode_type); + edata.setType(encoder_info.get_settings(in_width).encode_type); edata.setEncodeId(cnt++); edata.setSegmentNum(segment_num); edata.setSegmentId(idx); diff --git a/system/loggerd/encoder/ffmpeg_encoder.cc b/system/loggerd/encoder/ffmpeg_encoder.cc index 4e6946363f..4d6be47182 100644 --- a/system/loggerd/encoder/ffmpeg_encoder.cc +++ b/system/loggerd/encoder/ffmpeg_encoder.cc @@ -1,5 +1,3 @@ -#pragma clang diagnostic ignored "-Wdeprecated-declarations" - #include "system/loggerd/encoder/ffmpeg_encoder.h" #include @@ -48,7 +46,7 @@ FfmpegEncoder::~FfmpegEncoder() { } void FfmpegEncoder::encoder_open() { - auto codec_id = encoder_info.encode_type == cereal::EncodeIndex::Type::QCAMERA_H264 + auto codec_id = encoder_info.get_settings(in_width).encode_type == cereal::EncodeIndex::Type::QCAMERA_H264 ? AV_CODEC_ID_H264 : AV_CODEC_ID_FFVHUFF; const AVCodec *codec = avcodec_find_encoder(codec_id); @@ -119,8 +117,7 @@ int FfmpegEncoder::encode_frame(VisionBuf* buf, VisionIpcBufExtra *extra) { ret = -1; } - AVPacket pkt; - av_init_packet(&pkt); + AVPacket pkt = {}; pkt.data = NULL; pkt.size = 0; while (ret >= 0) { diff --git a/system/loggerd/encoder/v4l_encoder.cc b/system/loggerd/encoder/v4l_encoder.cc index ac1da09693..6ee3af13b0 100644 --- a/system/loggerd/encoder/v4l_encoder.cc +++ b/system/loggerd/encoder/v4l_encoder.cc @@ -24,14 +24,6 @@ */ const int env_debug_encoder = (getenv("DEBUG_ENCODER") != NULL) ? atoi(getenv("DEBUG_ENCODER")) : 0; -static void checked_ioctl(int fd, unsigned long request, void *argp) { - int ret = util::safe_ioctl(fd, request, argp); - if (ret != 0) { - LOGE("checked_ioctl failed with error %d (%d %lx %p)", errno, fd, request, argp); - assert(0); - } -} - static void dequeue_buffer(int fd, v4l2_buf_type buf_type, unsigned int *index=NULL, unsigned int *bytesused=NULL, unsigned int *flags=NULL, struct timeval *timestamp=NULL) { v4l2_plane plane = {0}; v4l2_buffer v4l_buf = { @@ -40,7 +32,7 @@ static void dequeue_buffer(int fd, v4l2_buf_type buf_type, unsigned int *index=N .m = { .planes = &plane, }, .length = 1, }; - checked_ioctl(fd, VIDIOC_DQBUF, &v4l_buf); + util::safe_ioctl(fd, VIDIOC_DQBUF, &v4l_buf, "VIDIOC_DQBUF failed"); if (index) *index = v4l_buf.index; if (bytesused) *bytesused = v4l_buf.m.planes[0].bytesused; @@ -66,8 +58,7 @@ static void queue_buffer(int fd, v4l2_buf_type buf_type, unsigned int index, Vis .flags = V4L2_BUF_FLAG_TIMESTAMP_COPY, .timestamp = timestamp }; - - checked_ioctl(fd, VIDIOC_QBUF, &v4l_buf); + util::safe_ioctl(fd, VIDIOC_QBUF, &v4l_buf, "VIDIOC_QBUF failed"); } static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) { @@ -76,7 +67,7 @@ static void request_buffers(int fd, v4l2_buf_type buf_type, unsigned int count) .memory = V4L2_MEMORY_USERPTR, .count = count }; - checked_ioctl(fd, VIDIOC_REQBUFS, &reqbuf); + util::safe_ioctl(fd, VIDIOC_REQBUFS, &reqbuf, "VIDIOC_REQBUFS failed"); } void V4LEncoder::dequeue_handler(V4LEncoder *e) { @@ -159,11 +150,14 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei assert(fd >= 0); struct v4l2_capability cap; - checked_ioctl(fd, VIDIOC_QUERYCAP, &cap); + util::safe_ioctl(fd, VIDIOC_QUERYCAP, &cap, "VIDIOC_QUERYCAP failed"); LOGD("opened encoder device %s %s = %d", cap.driver, cap.card, fd); assert(strcmp((const char *)cap.driver, "msm_vidc_driver") == 0); assert(strcmp((const char *)cap.card, "msm_vidc_venc") == 0); + EncoderSettings encoder_settings = encoder_info.get_settings(in_width); + bool is_h265 = encoder_settings.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C; + struct v4l2_format fmt_out = { .type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, .fmt = { @@ -171,13 +165,13 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // downscales are free with v4l .width = (unsigned int)(out_width), .height = (unsigned int)(out_height), - .pixelformat = (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264, + .pixelformat = is_h265 ? V4L2_PIX_FMT_HEVC : V4L2_PIX_FMT_H264, .field = V4L2_FIELD_ANY, .colorspace = V4L2_COLORSPACE_DEFAULT, } } }; - checked_ioctl(fd, VIDIOC_S_FMT, &fmt_out); + util::safe_ioctl(fd, VIDIOC_S_FMT, &fmt_out, "VIDIOC_S_FMT failed"); v4l2_streamparm streamparm = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, @@ -191,7 +185,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei } } }; - checked_ioctl(fd, VIDIOC_S_PARM, &streamparm); + util::safe_ioctl(fd, VIDIOC_S_PARM, &streamparm, "VIDIOC_S_PARM failed"); struct v4l2_format fmt_in = { .type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, @@ -205,7 +199,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei } } }; - checked_ioctl(fd, VIDIOC_S_FMT, &fmt_in); + util::safe_ioctl(fd, VIDIOC_S_FMT, &fmt_in, "VIDIOC_S_FMT failed"); LOGD("in buffer size %d, out buffer size %d", fmt_in.fmt.pix_mp.plane_fmt[0].sizeimage, @@ -214,34 +208,32 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // shared ctrls { struct v4l2_control ctrls[] = { + { .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_settings.bitrate}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = encoder_settings.gop_size - encoder_settings.b_frames - 1}, + { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = encoder_settings.b_frames}, { .id = V4L2_CID_MPEG_VIDEO_HEADER_MODE, .value = V4L2_MPEG_VIDEO_HEADER_MODE_SEPARATE}, - { .id = V4L2_CID_MPEG_VIDEO_BITRATE, .value = encoder_info.bitrate}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL, .value = V4L2_CID_MPEG_VIDC_VIDEO_RATE_CONTROL_VBR_CFR}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_PRIORITY, .value = V4L2_MPEG_VIDC_VIDEO_PRIORITY_REALTIME_DISABLE}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_IDR_PERIOD, .value = 1}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } - if (encoder_info.encode_type == cereal::EncodeIndex::Type::FULL_H_E_V_C) { + if (is_h265) { struct v4l2_control ctrls[] = { { .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_PROFILE, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_PROFILE_MAIN}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_HEVC_TIER_LEVEL, .value = V4L2_MPEG_VIDC_VIDEO_HEVC_LEVEL_HIGH_TIER_LEVEL_5}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_VUI_TIMING_INFO, .value = V4L2_MPEG_VIDC_VIDEO_VUI_TIMING_INFO_ENABLED}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 29}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } else { struct v4l2_control ctrls[] = { { .id = V4L2_CID_MPEG_VIDEO_H264_PROFILE, .value = V4L2_MPEG_VIDEO_H264_PROFILE_HIGH}, { .id = V4L2_CID_MPEG_VIDEO_H264_LEVEL, .value = V4L2_MPEG_VIDEO_H264_LEVEL_UNKNOWN}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_P_FRAMES, .value = 14}, - { .id = V4L2_CID_MPEG_VIDC_VIDEO_NUM_B_FRAMES, .value = 0}, { .id = V4L2_CID_MPEG_VIDEO_H264_ENTROPY_MODE, .value = V4L2_MPEG_VIDEO_H264_ENTROPY_MODE_CABAC}, { .id = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL, .value = V4L2_CID_MPEG_VIDC_VIDEO_H264_CABAC_MODEL_0}, { .id = V4L2_CID_MPEG_VIDEO_H264_LOOP_FILTER_MODE, .value = 0}, @@ -250,7 +242,7 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei { .id = V4L2_CID_MPEG_VIDEO_MULTI_SLICE_MODE, .value = 0}, }; for (auto ctrl : ctrls) { - checked_ioctl(fd, VIDIOC_S_CTRL, &ctrl); + util::safe_ioctl(fd, VIDIOC_S_CTRL, &ctrl, "VIDIOC_S_CTRL failed"); } } @@ -260,9 +252,9 @@ V4LEncoder::V4LEncoder(const EncoderInfo &encoder_info, int in_width, int in_hei // start encoder v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMON, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMON, &buf_type, "VIDIOC_STREAMON failed"); buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMON, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMON, &buf_type, "VIDIOC_STREAMON failed"); // queue up output buffers for (unsigned int i = 0; i < BUF_OUT_COUNT; i++) { @@ -305,7 +297,7 @@ void V4LEncoder::encoder_close() { for (int i = 0; i < BUF_IN_COUNT; i++) free_buf_in.push(i); // no frames, stop the encoder struct v4l2_encoder_cmd encoder_cmd = { .cmd = V4L2_ENC_CMD_STOP }; - checked_ioctl(fd, VIDIOC_ENCODER_CMD, &encoder_cmd); + util::safe_ioctl(fd, VIDIOC_ENCODER_CMD, &encoder_cmd, "VIDIOC_ENCODER_CMD failed"); // join waits for V4L2_QCOM_BUF_FLAG_EOS dequeue_handler_thread.join(); assert(extras.empty()); @@ -316,10 +308,10 @@ void V4LEncoder::encoder_close() { V4LEncoder::~V4LEncoder() { encoder_close(); v4l2_buf_type buf_type = V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMOFF, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMOFF, &buf_type, "VIDIOC_STREAMOFF failed"); request_buffers(fd, V4L2_BUF_TYPE_VIDEO_OUTPUT_MPLANE, 0); buf_type = V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE; - checked_ioctl(fd, VIDIOC_STREAMOFF, &buf_type); + util::safe_ioctl(fd, VIDIOC_STREAMOFF, &buf_type, "VIDIOC_STREAMOFF failed"); request_buffers(fd, V4L2_BUF_TYPE_VIDEO_CAPTURE_MPLANE, 0); close(fd); diff --git a/system/loggerd/logger.cc b/system/loggerd/logger.cc index f07aee1596..0ebe323939 100644 --- a/system/loggerd/logger.cc +++ b/system/loggerd/logger.cc @@ -59,7 +59,7 @@ kj::Array logger_build_init_data() { for (auto& [key, value] : params_map) { auto lentry = lparams[j]; lentry.setKey(key); - if ( !(params.getKeyType(key) & DONT_LOG) ) { + if ( !(params.getKeyFlag(key) & DONT_LOG) ) { lentry.setValue(capnp::Data::Reader((const kj::byte*)value.data(), value.size())); } j++; diff --git a/system/loggerd/loggerd.cc b/system/loggerd/loggerd.cc index 2d2d4640ed..21de1ff33f 100644 --- a/system/loggerd/loggerd.cc +++ b/system/loggerd/loggerd.cc @@ -62,6 +62,7 @@ struct RemoteEncoder { bool recording = false; bool marked_ready_to_rotate = false; bool seen_first_packet = false; + bool audio_initialized = false; }; size_t write_encode_data(LoggerdState *s, cereal::Event::Reader event, RemoteEncoder &re, const EncoderInfo &encoder_info) { @@ -78,12 +79,7 @@ size_t write_encode_data(LoggerdState *s, cereal::Event::Reader event, RemoteEnc LOGW("%s: dropped %d non iframe packets before init", encoder_info.publish_name, re.dropped_frames); re.dropped_frames = 0; } - // if we aren't actually recording, don't create the writer if (encoder_info.record) { - assert(encoder_info.filename != NULL); - re.writer.reset(new VideoWriter(s->logger.segmentPath().c_str(), - encoder_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C, - edata.getWidth(), edata.getHeight(), encoder_info.fps, idx.getType())); // write the header auto header = edata.getHeader(); re.writer->write((uint8_t *)header.begin(), header.size(), idx.getTimestampEof() / 1000, true, false); @@ -138,12 +134,19 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct // if this is a new segment, we close any possible old segments, move to the new, and process any queued packets if (re.current_segment != s->logger.segment()) { - if (re.recording) { - re.writer.reset(); + // if we aren't actually recording, don't create the writer + if (encoder_info.record) { + assert(encoder_info.filename != NULL); + re.writer.reset(new VideoWriter(s->logger.segmentPath().c_str(), + encoder_info.filename, idx.getType() != cereal::EncodeIndex::Type::FULL_H_E_V_C, + edata.getWidth(), edata.getHeight(), encoder_info.fps, idx.getType())); re.recording = false; + re.audio_initialized = false; } re.current_segment = s->logger.segment(); re.marked_ready_to_rotate = false; + } + if (re.audio_initialized || !encoder_info.include_audio) { // we are in this segment now, process any queued messages before this one if (!re.q.empty()) { for (auto qmsg : re.q) { @@ -153,9 +156,14 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct } re.q.clear(); } + bytes_count += write_encode_data(s, event, re, encoder_info); + delete msg; + } else if (re.q.size() > MAIN_FPS*10) { + LOGE_100("%s: dropping frame waiting for audio initialization, queue is too large", name.c_str()); + delete msg; + } else { + re.q.push_back(msg); // queue up all the new segment messages, they go in after audio is initialized } - bytes_count += write_encode_data(s, event, re, encoder_info); - delete msg; } else if (offset_segment_num > s->logger.segment()) { // encoderd packet has a newer segment, this means encoderd has rolled over if (!re.marked_ready_to_rotate) { @@ -186,7 +194,7 @@ int handle_encoder_msg(LoggerdState *s, Message *msg, std::string &name, struct return bytes_count; } -void handle_user_flag(LoggerdState *s) { +void handle_preserve_segment(LoggerdState *s) { static int prev_segment = -1; if (s->logger.segment() == prev_segment) return; @@ -214,7 +222,7 @@ void loggerd_thread() { typedef struct ServiceState { std::string name; int counter, freq; - bool encoder, user_flag; + bool encoder, preserve_segment, record_audio; } ServiceState; std::unordered_map service_state; std::unordered_map remote_encoders; @@ -226,19 +234,22 @@ void loggerd_thread() { for (const auto& [_, it] : services) { const bool encoder = util::ends_with(it.name, "EncodeData"); const bool livestream_encoder = util::starts_with(it.name, "livestream"); - if (!it.should_log && (!encoder || livestream_encoder)) continue; - LOGD("logging %s", it.name.c_str()); + const bool record_audio = (it.name == "rawAudioData") && Params().getBool("RecordAudio"); + if (it.should_log || (encoder && !livestream_encoder) || record_audio) { + LOGD("logging %s", it.name.c_str()); - SubSocket * sock = SubSocket::create(ctx.get(), it.name); - assert(sock != NULL); - poller->registerSocket(sock); - service_state[sock] = { - .name = it.name, - .counter = 0, - .freq = it.decimation, - .encoder = encoder, - .user_flag = it.name == "userFlag", - }; + SubSocket * sock = SubSocket::create(ctx.get(), it.name); + assert(sock != NULL); + poller->registerSocket(sock); + service_state[sock] = { + .name = it.name, + .counter = 0, + .freq = it.decimation, + .encoder = encoder, + .preserve_segment = (it.name == "userBookmark") || (it.name == "audioFeedback"), + .record_audio = record_audio, + }; + } } LoggerdState s; @@ -247,6 +258,7 @@ void loggerd_thread() { Params().put("CurrentRoute", s.logger.routeName()); std::map encoder_infos_dict; + std::vector encoders_with_audio; for (const auto &cam : cameras_logged) { for (const auto &encoder_info : cam.encoder_infos) { encoder_infos_dict[encoder_info.publish_name] = encoder_info; @@ -254,6 +266,13 @@ void loggerd_thread() { } } + for (auto &[sock, service] : service_state) { + auto it = encoder_infos_dict.find(service.name); + if (it != encoder_infos_dict.end() && it->second.include_audio) { + encoders_with_audio.push_back(&remote_encoders[sock]); + } + } + uint64_t msg_count = 0, bytes_count = 0; double start_ts = millis_since_boot(); while (!do_exit) { @@ -262,8 +281,8 @@ void loggerd_thread() { if (do_exit) break; ServiceState &service = service_state[sock]; - if (service.user_flag) { - handle_user_flag(&s); + if (service.preserve_segment) { + handle_preserve_segment(&s); } // drain socket @@ -271,6 +290,20 @@ void loggerd_thread() { Message *msg = nullptr; while (!do_exit && (msg = sock->receive(true))) { const bool in_qlog = service.freq != -1 && (service.counter++ % service.freq == 0); + + if (service.record_audio) { + capnp::FlatArrayMessageReader cmsg(kj::ArrayPtr((capnp::word *)msg->getData(), msg->getSize() / sizeof(capnp::word))); + auto event = cmsg.getRoot(); + auto audio_data = event.getRawAudioData().getData(); + auto sample_rate = event.getRawAudioData().getSampleRate(); + for (auto* encoder : encoders_with_audio) { + if (encoder && encoder->writer) { + encoder->writer->write_audio((uint8_t*)audio_data.begin(), audio_data.size(), event.getLogMonoTime() / 1000, sample_rate); + encoder->audio_initialized = true; + } + } + } + if (service.encoder) { s.last_camera_seen_tms = millis_since_boot(); bytes_count += handle_encoder_msg(&s, msg, service.name, remote_encoders[sock], encoder_infos_dict[service.name]); diff --git a/system/loggerd/loggerd.h b/system/loggerd/loggerd.h index 27d2d37fc4..967caec867 100644 --- a/system/loggerd/loggerd.h +++ b/system/loggerd/loggerd.h @@ -13,10 +13,7 @@ #include "system/loggerd/logger.h" constexpr int MAIN_FPS = 20; -const int MAIN_BITRATE = 1e7; -const int LIVESTREAM_BITRATE = 1e6; -const int QCAM_BITRATE = 256000; - +const auto MAIN_ENCODE_TYPE = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS : cereal::EncodeIndex::Type::FULL_H_E_V_C; #define NO_CAMERA_PATIENCE 500 // fall back to time-based rotation if all cameras are dead #define INIT_ENCODE_FUNCTIONS(encode_type) \ @@ -29,18 +26,42 @@ const int SEGMENT_LENGTH = LOGGERD_TEST ? atoi(getenv("LOGGERD_SEGMENT_LENGTH")) constexpr char PRESERVE_ATTR_NAME[] = "user.preserve"; constexpr char PRESERVE_ATTR_VALUE = '1'; + +struct EncoderSettings { + cereal::EncodeIndex::Type encode_type; + int bitrate; + int gop_size; + int b_frames = 0; // we don't use b frames + + static EncoderSettings MainEncoderSettings(int in_width) { + if (in_width <= 1344) { + return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 5'000'000, .gop_size = 20}; + } else { + return EncoderSettings{.encode_type = MAIN_ENCODE_TYPE, .bitrate = 10'000'000, .gop_size = 30}; + } + } + + static EncoderSettings QcamEncoderSettings() { + return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 256'000, .gop_size = 15}; + } + + static EncoderSettings StreamEncoderSettings() { + return EncoderSettings{.encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .bitrate = 1'000'000, .gop_size = 15}; + } +}; + class EncoderInfo { public: const char *publish_name; const char *thumbnail_name = NULL; const char *filename = NULL; bool record = true; + bool include_audio = false; int frame_width = -1; int frame_height = -1; int fps = MAIN_FPS; - int bitrate = MAIN_BITRATE; - cereal::EncodeIndex::Type encode_type = Hardware::PC() ? cereal::EncodeIndex::Type::BIG_BOX_LOSSLESS - : cereal::EncodeIndex::Type::FULL_H_E_V_C; + std::function get_settings; + ::cereal::EncodeData::Reader (cereal::Event::Reader::*get_encode_data_func)() const; void (cereal::Event::Builder::*set_encode_idx_func)(::cereal::EncodeIndex::Reader); cereal::EncodeData::Builder (cereal::Event::Builder::*init_encode_data_func)(); @@ -58,12 +79,14 @@ const EncoderInfo main_road_encoder_info = { .publish_name = "roadEncodeData", .thumbnail_name = "thumbnail", .filename = "fcamera.hevc", + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(RoadEncode), }; const EncoderInfo main_wide_road_encoder_info = { .publish_name = "wideRoadEncodeData", .filename = "ecamera.hevc", + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(WideRoadEncode), }; @@ -71,41 +94,39 @@ const EncoderInfo main_driver_encoder_info = { .publish_name = "driverEncodeData", .filename = "dcamera.hevc", .record = Params().getBool("RecordFront"), + .get_settings = [](int in_width){return EncoderSettings::MainEncoderSettings(in_width);}, INIT_ENCODE_FUNCTIONS(DriverEncode), }; const EncoderInfo stream_road_encoder_info = { .publish_name = "livestreamRoadEncodeData", //.thumbnail_name = "thumbnail", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamRoadEncode), }; const EncoderInfo stream_wide_road_encoder_info = { .publish_name = "livestreamWideRoadEncodeData", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamWideRoadEncode), }; const EncoderInfo stream_driver_encoder_info = { .publish_name = "livestreamDriverEncodeData", - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, .record = false, - .bitrate = LIVESTREAM_BITRATE, + .get_settings = [](int){return EncoderSettings::StreamEncoderSettings();}, INIT_ENCODE_FUNCTIONS(LivestreamDriverEncode), }; const EncoderInfo qcam_encoder_info = { .publish_name = "qRoadEncodeData", .filename = "qcamera.ts", - .bitrate = QCAM_BITRATE, - .encode_type = cereal::EncodeIndex::Type::QCAMERA_H264, + .get_settings = [](int){return EncoderSettings::QcamEncoderSettings();}, .frame_width = 526, .frame_height = 330, + .include_audio = Params().getBool("RecordAudio"), INIT_ENCODE_FUNCTIONS(QRoadEncode), }; diff --git a/system/loggerd/tests/loggerd_tests_common.py b/system/loggerd/tests/loggerd_tests_common.py index 15fd997eb8..87c3da65c2 100644 --- a/system/loggerd/tests/loggerd_tests_common.py +++ b/system/loggerd/tests/loggerd_tests_common.py @@ -76,7 +76,7 @@ class UploaderTestCase: self.seg_dir = self.seg_format.format(self.seg_num) self.params = Params() - self.params.put("IsOffroad", "1") + self.params.put("IsOffroad", True) self.params.put("DongleId", "0000000000000000") def make_file_with_data(self, f_dir: str, fn: str, size_mb: float = .1, lock: bool = False, diff --git a/system/loggerd/tests/test_encoder.py b/system/loggerd/tests/test_encoder.py index b24bfbe168..e4dabd3df9 100644 --- a/system/loggerd/tests/test_encoder.py +++ b/system/loggerd/tests/test_encoder.py @@ -19,11 +19,12 @@ from openpilot.system.hardware.hw import Paths SEGMENT_LENGTH = 2 FULL_SIZE = 2507572 +def hevc_size(w): return FULL_SIZE // 2 if w <= 1344 else FULL_SIZE CAMERAS = [ - ("fcamera.hevc", 20, FULL_SIZE, "roadEncodeIdx"), - ("dcamera.hevc", 20, FULL_SIZE, "driverEncodeIdx"), - ("ecamera.hevc", 20, FULL_SIZE, "wideRoadEncodeIdx"), - ("qcamera.ts", 20, 130000, None), + ("fcamera.hevc", 20, hevc_size, "roadEncodeIdx"), + ("dcamera.hevc", 20, hevc_size, "driverEncodeIdx"), + ("ecamera.hevc", 20, hevc_size, "wideRoadEncodeIdx"), + ("qcamera.ts", 20, lambda x: 130000, None), ] # we check frame count, so we don't have to be too strict on size @@ -76,7 +77,7 @@ class TestEncoder: # check each camera file size counts = [] first_frames = [] - for camera, fps, size, encode_idx_name in CAMERAS: + for camera, fps, size_lambda, encode_idx_name in CAMERAS: if not record_front and "dcamera" in camera: continue @@ -86,14 +87,14 @@ class TestEncoder: assert os.path.exists(file_path), f"segment #{i}: '{file_path}' missing" # TODO: this ffprobe call is really slow - # check frame count - cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets -of csv=p=0 {file_path}" + # get width and check frame count + cmd = f"ffprobe -v error -select_streams v:0 -count_packets -show_entries stream=nb_read_packets,width -of csv=p=0 {file_path}" if TICI: cmd = "LD_LIBRARY_PATH=/usr/local/lib " + cmd expected_frames = fps * SEGMENT_LENGTH - probe = subprocess.check_output(cmd, shell=True, encoding='utf8') - frame_count = int(probe.split('\n')[0].strip()) + probe = subprocess.check_output(cmd, shell=True, encoding='utf8').split('\n')[0].strip().split(',') + frame_width, frame_count = int(probe[0]), int(probe[1]) counts.append(frame_count) assert frame_count == expected_frames, \ @@ -101,8 +102,9 @@ class TestEncoder: # sanity check file size file_size = os.path.getsize(file_path) - assert math.isclose(file_size, size, rel_tol=FILE_SIZE_TOLERANCE), \ - f"{file_path} size {file_size} isn't close to target size {size}" + target_size = size_lambda(frame_width) + assert math.isclose(file_size, target_size, rel_tol=FILE_SIZE_TOLERANCE), \ + f"{file_path} size {file_size} isn't close to target size {target_size}" # Check encodeIdx if encode_idx_name is not None: diff --git a/system/loggerd/tests/test_loggerd.py b/system/loggerd/tests/test_loggerd.py index 179e237a65..c6a4b12e63 100644 --- a/system/loggerd/tests/test_loggerd.py +++ b/system/loggerd/tests/test_loggerd.py @@ -97,6 +97,50 @@ class TestLoggerd: return sent_msgs + def _publish_camera_and_audio_messages(self, num_segs=1, segment_length=5): + d = DEVICE_CAMERAS[("tici", "ar0231")] + streams = [ + (VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048 * 2346, 2048, 2048 * 1216), "roadCameraState"), + (VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048 * 2346, 2048, 2048 * 1216), "driverCameraState"), + (VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048 * 2346, 2048, 2048 * 1216), "wideRoadCameraState"), + ] + + pm = messaging.PubMaster([s for _, _, s in streams] + ["rawAudioData"]) + vipc_server = VisionIpcServer("camerad") + for stream_type, frame_spec, _ in streams: + vipc_server.create_buffers_with_sizes(stream_type, 40, *(frame_spec)) + vipc_server.start_listener() + + os.environ["LOGGERD_TEST"] = "1" + os.environ["LOGGERD_SEGMENT_LENGTH"] = str(segment_length) + managed_processes["loggerd"].start() + managed_processes["encoderd"].start() + assert pm.wait_for_readers_to_update("roadCameraState", timeout=5) + + fps = 20 + for n in range(1, int(num_segs * segment_length * fps) + 1): + # send video + for stream_type, frame_spec, state in streams: + dat = np.empty(frame_spec[2], dtype=np.uint8) + vipc_server.send(stream_type, dat[:].flatten().tobytes(), n, n / fps, n / fps) + + camera_state = messaging.new_message(state) + frame = getattr(camera_state, state) + frame.frameId = n + pm.send(state, camera_state) + + # send audio + msg = messaging.new_message('rawAudioData') + msg.rawAudioData.data = bytes(800 * 2) # 800 samples of int16 + msg.rawAudioData.sampleRate = 16000 + pm.send('rawAudioData', msg) + + for _, _, state in streams: + assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001) + + managed_processes["loggerd"].stop() + managed_processes["encoderd"].stop() + def test_init_data_values(self): os.environ["CLEAN"] = random.choice(["0", "1"]) @@ -136,53 +180,23 @@ class TestLoggerd: assert getattr(initData, initData_key) == v assert logged_params[param_key].decode() == v - @pytest.mark.skip("FIXME: encoderd sometimes crashes in CI when running with pytest-xdist") + @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing def test_rotation(self): - os.environ["LOGGERD_TEST"] = "1" - Params().put("RecordFront", "1") + Params().put("RecordFront", True) - d = DEVICE_CAMERAS[("tici", "ar0231")] expected_files = {"rlog.zst", "qlog.zst", "qcamera.ts", "fcamera.hevc", "dcamera.hevc", "ecamera.hevc"} - streams = [(VisionStreamType.VISION_STREAM_ROAD, (d.fcam.width, d.fcam.height, 2048*2346, 2048, 2048*1216), "roadCameraState"), - (VisionStreamType.VISION_STREAM_DRIVER, (d.dcam.width, d.dcam.height, 2048*2346, 2048, 2048*1216), "driverCameraState"), - (VisionStreamType.VISION_STREAM_WIDE_ROAD, (d.ecam.width, d.ecam.height, 2048*2346, 2048, 2048*1216), "wideRoadCameraState")] - pm = messaging.PubMaster(["roadCameraState", "driverCameraState", "wideRoadCameraState"]) - vipc_server = VisionIpcServer("camerad") - for stream_type, frame_spec, _ in streams: - vipc_server.create_buffers_with_sizes(stream_type, 40, *(frame_spec)) - vipc_server.start_listener() + num_segs = random.randint(2, 3) + length = random.randint(4, 5) # H264 encoder uses 40 lookahead frames and does B-frame reordering, so minimum 3 seconds before qcam output - num_segs = random.randint(2, 5) - length = random.randint(1, 3) - os.environ["LOGGERD_SEGMENT_LENGTH"] = str(length) - managed_processes["loggerd"].start() - managed_processes["encoderd"].start() - assert pm.wait_for_readers_to_update("roadCameraState", timeout=5) - - fps = 20.0 - for n in range(1, int(num_segs*length*fps)+1): - for stream_type, frame_spec, state in streams: - dat = np.empty(frame_spec[2], dtype=np.uint8) - vipc_server.send(stream_type, dat[:].flatten().tobytes(), n, n/fps, n/fps) - - camera_state = messaging.new_message(state) - frame = getattr(camera_state, state) - frame.frameId = n - pm.send(state, camera_state) - - for _, _, state in streams: - assert pm.wait_for_readers_to_update(state, timeout=5, dt=0.001) - - managed_processes["loggerd"].stop() - managed_processes["encoderd"].stop() + self._publish_camera_and_audio_messages(num_segs=num_segs, segment_length=length) route_path = str(self._get_latest_log_dir()).rsplit("--", 1)[0] for n in range(num_segs): p = Path(f"{route_path}--{n}") logged = {f.name for f in p.iterdir() if f.is_file()} diff = logged ^ expected_files - assert len(diff) == 0, f"didn't get all expected files. run={_} seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}" + assert len(diff) == 0, f"didn't get all expected files. seg={n} {route_path=}, {diff=}\n{logged=} {expected_files=}" def test_bootlog(self): # generate bootlog with fake launch log @@ -268,16 +282,43 @@ class TestLoggerd: sent.clear_write_flag() assert sent.to_bytes() == m.as_builder().to_bytes() - def test_preserving_flagged_segments(self): - services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userFlag"} + def test_preserving_bookmarked_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) | {"userBookmark"} self._publish_random_messages(services) segment_dir = self._get_latest_log_dir() assert getxattr(segment_dir, PRESERVE_ATTR_NAME) == PRESERVE_ATTR_VALUE - def test_not_preserving_unflagged_segments(self): - services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userFlag"} + def test_not_preserving_nonbookmarked_segments(self): + services = set(random.sample(CEREAL_SERVICES, random.randint(5, 10))) - {"userBookmark", "audioFeedback"} self._publish_random_messages(services) segment_dir = self._get_latest_log_dir() assert getxattr(segment_dir, PRESERVE_ATTR_NAME) is None + + @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing + @pytest.mark.parametrize("record_front", [True, False]) + def test_record_front(self, record_front): + params = Params() + params.put_bool("RecordFront", record_front) + + self._publish_camera_and_audio_messages() + + dcamera_hevc_exists = os.path.exists(os.path.join(self._get_latest_log_dir(), 'dcamera.hevc')) + assert dcamera_hevc_exists == record_front + + @pytest.mark.xdist_group("camera_encoder_tests") # setting xdist group ensures tests are run in same worker, prevents encoderd from crashing + @pytest.mark.parametrize("record_audio", [True, False]) + def test_record_audio(self, record_audio): + params = Params() + params.put_bool("RecordAudio", record_audio) + + self._publish_camera_and_audio_messages() + + qcamera_ts_path = os.path.join(self._get_latest_log_dir(), 'qcamera.ts') + ffprobe_cmd = f"ffprobe -i {qcamera_ts_path} -show_streams -select_streams a -loglevel error" + has_audio_stream = subprocess.run(ffprobe_cmd, shell=True, capture_output=True).stdout.strip() != b'' + assert has_audio_stream == record_audio + + raw_audio_in_rlog = any(m.which() == 'rawAudioData' for m in LogReader(os.path.join(self._get_latest_log_dir(), 'rlog.zst'))) + assert raw_audio_in_rlog == record_audio diff --git a/system/loggerd/uploader.py b/system/loggerd/uploader.py index 6e6df6114e..38fc0e9209 100755 --- a/system/loggerd/uploader.py +++ b/system/loggerd/uploader.py @@ -88,7 +88,7 @@ class Uploader: self.immediate_priority = {"qlog": 0, "qlog.zst": 0, "qcamera.ts": 1} def list_upload_files(self, metered: bool) -> Iterator[tuple[str, str, str]]: - r = self.params.get("AthenadRecentlyViewedRoutes", encoding="utf8") + r = self.params.get("AthenadRecentlyViewedRoutes") requested_routes = [] if r is None else [route for route in r.split(",") if route] for logdir in listdir_by_creation(self.root): @@ -238,7 +238,7 @@ def main(exit_event: threading.Event = None) -> None: clear_locks(Paths.log_root()) params = Params() - dongle_id = params.get("DongleId", encoding='utf8') + dongle_id = params.get("DongleId") if dongle_id is None: cloudlog.info("uploader missing dongle_id") diff --git a/system/loggerd/video_writer.cc b/system/loggerd/video_writer.cc index 90b5f1af3d..1b47a8fceb 100644 --- a/system/loggerd/video_writer.cc +++ b/system/loggerd/video_writer.cc @@ -1,4 +1,3 @@ -#pragma clang diagnostic ignored "-Wdeprecated-declarations" #include #include "system/loggerd/video_writer.h" @@ -50,6 +49,45 @@ VideoWriter::VideoWriter(const char *path, const char *filename, bool remuxing, } } +void VideoWriter::initialize_audio(int sample_rate) { + assert(this->ofmt_ctx->oformat->audio_codec != AV_CODEC_ID_NONE); // check output format supports audio streams + const AVCodec *audio_avcodec = avcodec_find_encoder(AV_CODEC_ID_AAC); + assert(audio_avcodec); + this->audio_codec_ctx = avcodec_alloc_context3(audio_avcodec); + assert(this->audio_codec_ctx); + this->audio_codec_ctx->sample_fmt = AV_SAMPLE_FMT_FLTP; + this->audio_codec_ctx->sample_rate = sample_rate; + #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1+ + av_channel_layout_default(&this->audio_codec_ctx->ch_layout, 1); + #else + this->audio_codec_ctx->channel_layout = AV_CH_LAYOUT_MONO; + #endif + this->audio_codec_ctx->bit_rate = 32000; + this->audio_codec_ctx->flags |= AV_CODEC_FLAG_GLOBAL_HEADER; + this->audio_codec_ctx->time_base = (AVRational){1, audio_codec_ctx->sample_rate}; + int err = avcodec_open2(this->audio_codec_ctx, audio_avcodec, NULL); + assert(err >= 0); + av_log_set_level(AV_LOG_WARNING); // hide "QAvg" info msgs at the end of every segment + + this->audio_stream = avformat_new_stream(this->ofmt_ctx, NULL); + assert(this->audio_stream); + err = avcodec_parameters_from_context(this->audio_stream->codecpar, this->audio_codec_ctx); + assert(err >= 0); + + this->audio_frame = av_frame_alloc(); + assert(this->audio_frame); + this->audio_frame->format = this->audio_codec_ctx->sample_fmt; + #if LIBAVUTIL_VERSION_INT >= AV_VERSION_INT(57, 28, 100) // FFmpeg 5.1+ + av_channel_layout_copy(&this->audio_frame->ch_layout, &this->audio_codec_ctx->ch_layout); + #else + this->audio_frame->channel_layout = this->audio_codec_ctx->channel_layout; + #endif + this->audio_frame->sample_rate = this->audio_codec_ctx->sample_rate; + this->audio_frame->nb_samples = this->audio_codec_ctx->frame_size; + err = av_frame_get_buffer(this->audio_frame, 0); + assert(err >= 0); +} + void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe) { if (of && data) { size_t written = util::safe_fwrite(data, 1, len, of); @@ -67,16 +105,18 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc } int err = avcodec_parameters_from_context(out_stream->codecpar, codec_ctx); assert(err >= 0); + // if there is an audio stream, it must be initialized before this point err = avformat_write_header(ofmt_ctx, NULL); assert(err >= 0); + header_written = true; } else { // input timestamps are in microseconds AVRational in_timebase = {1, 1000000}; - AVPacket pkt; - av_init_packet(&pkt); + AVPacket pkt = {}; pkt.data = data; pkt.size = len; + pkt.stream_index = this->out_stream->index; enum AVRounding rnd = static_cast(AV_ROUND_NEAR_INF|AV_ROUND_PASS_MINMAX); pkt.pts = pkt.dts = av_rescale_q_rnd(timestamp, in_timebase, ofmt_ctx->streams[0]->time_base, rnd); @@ -95,11 +135,93 @@ void VideoWriter::write(uint8_t *data, int len, long long timestamp, bool codecc } } +void VideoWriter::write_audio(uint8_t *data, int len, long long timestamp, int sample_rate) { + if (!remuxing) return; + if (!audio_initialized) { + initialize_audio(sample_rate); + audio_initialized = true; + } + if (!audio_codec_ctx) return; + // sync logMonoTime of first audio packet with the timestampEof of first video packet + if (audio_pts == 0) { + audio_pts = (timestamp * audio_codec_ctx->sample_rate) / 1000000ULL; + } + + // convert s16le samples to fltp and add to buffer + const int16_t *raw_samples = reinterpret_cast(data); + int sample_count = len / sizeof(int16_t); + constexpr float normalizer = 1.0f / 32768.0f; + + const size_t max_buffer_size = sample_rate * 10; // 10 seconds + if (audio_buffer.size() + sample_count > max_buffer_size) { + size_t samples_to_drop = (audio_buffer.size() + sample_count) - max_buffer_size; + LOGE("Audio buffer overflow, dropping %zu oldest samples", samples_to_drop); + audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + samples_to_drop); + audio_pts += samples_to_drop; + } + + // Add new samples to the buffer + const size_t original_size = audio_buffer.size(); + audio_buffer.resize(original_size + sample_count); + std::transform(raw_samples, raw_samples + sample_count, audio_buffer.begin() + original_size, + [](int16_t sample) { return sample * normalizer; }); + + if (!header_written) return; // header not written yet, process audio frame after header is written + while (audio_buffer.size() >= audio_codec_ctx->frame_size) { + audio_frame->pts = audio_pts; + float *f_samples = reinterpret_cast(audio_frame->data[0]); + std::copy(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size, f_samples); + audio_buffer.erase(audio_buffer.begin(), audio_buffer.begin() + audio_codec_ctx->frame_size); + encode_and_write_audio_frame(audio_frame); + } +} + +void VideoWriter::encode_and_write_audio_frame(AVFrame* frame) { + if (!remuxing || !audio_codec_ctx) return; + int send_result = avcodec_send_frame(audio_codec_ctx, frame); // encode frame + if (send_result >= 0) { + AVPacket *pkt = av_packet_alloc(); + while (avcodec_receive_packet(audio_codec_ctx, pkt) == 0) { + av_packet_rescale_ts(pkt, audio_codec_ctx->time_base, audio_stream->time_base); + pkt->stream_index = audio_stream->index; + + int err = av_interleaved_write_frame(ofmt_ctx, pkt); // write encoded frame + if (err < 0) { + LOGW("AUDIO: Write frame failed - error: %d", err); + } + av_packet_unref(pkt); + } + av_packet_free(&pkt); + } else { + LOGW("AUDIO: Failed to send audio frame to encoder: %d", send_result); + } + audio_pts += audio_codec_ctx->frame_size; +} + +void VideoWriter::process_remaining_audio() { + // Process remaining audio samples by padding with silence + if (audio_buffer.size() > 0 && audio_buffer.size() < audio_codec_ctx->frame_size) { + audio_buffer.resize(audio_codec_ctx->frame_size, 0.0f); + + // Encode final frame + audio_frame->pts = audio_pts; + float *f_samples = reinterpret_cast(audio_frame->data[0]); + std::copy(audio_buffer.begin(), audio_buffer.end(), f_samples); + encode_and_write_audio_frame(audio_frame); + } +} + VideoWriter::~VideoWriter() { if (this->remuxing) { + if (this->audio_codec_ctx) { + process_remaining_audio(); + encode_and_write_audio_frame(NULL); // flush encoder + avcodec_free_context(&this->audio_codec_ctx); + } int err = av_write_trailer(this->ofmt_ctx); if (err != 0) LOGE("av_write_trailer failed %d", err); avcodec_free_context(&this->codec_ctx); + if (this->audio_frame) av_frame_free(&this->audio_frame); err = avio_closep(&this->ofmt_ctx->pb); if (err != 0) LOGE("avio_closep failed %d", err); avformat_free_context(this->ofmt_ctx); diff --git a/system/loggerd/video_writer.h b/system/loggerd/video_writer.h index 1aa758b42b..e973c5d811 100644 --- a/system/loggerd/video_writer.h +++ b/system/loggerd/video_writer.h @@ -1,6 +1,7 @@ #pragma once #include +#include extern "C" { #include @@ -13,13 +14,29 @@ class VideoWriter { public: VideoWriter(const char *path, const char *filename, bool remuxing, int width, int height, int fps, cereal::EncodeIndex::Type codec); void write(uint8_t *data, int len, long long timestamp, bool codecconfig, bool keyframe); + void write_audio(uint8_t *data, int len, long long timestamp, int sample_rate); + ~VideoWriter(); + private: + void initialize_audio(int sample_rate); + void encode_and_write_audio_frame(AVFrame* frame); + void process_remaining_audio(); + std::string vid_path, lock_path; FILE *of = nullptr; AVCodecContext *codec_ctx; AVFormatContext *ofmt_ctx; AVStream *out_stream; + + bool audio_initialized = false; + bool header_written = false; + AVStream *audio_stream = nullptr; + AVCodecContext *audio_codec_ctx = nullptr; + AVFrame *audio_frame = nullptr; + uint64_t audio_pts = 0; + std::deque audio_buffer; + bool remuxing; }; diff --git a/system/manager/manager.py b/system/manager/manager.py index 61cca2afcc..1186c25114 100755 --- a/system/manager/manager.py +++ b/system/manager/manager.py @@ -3,12 +3,13 @@ import datetime import os import signal import sys +import time import traceback from cereal import log import cereal.messaging as messaging import openpilot.system.sentry as sentry -from openpilot.common.params import Params, ParamKeyType +from openpilot.common.params import Params, ParamKeyFlag from openpilot.common.text_window import TextWindow from openpilot.system.hardware import HARDWARE from openpilot.system.manager.helpers import unblock_stdout, write_onroad_params, save_bootlog @@ -19,8 +20,6 @@ from openpilot.common.swaglog import cloudlog, add_file_handler from openpilot.system.version import get_build_metadata, terms_version, training_version from openpilot.system.hardware.hw import Paths -from openpilot.sunnypilot.mapd.mapd_installer import VERSION - def manager_init() -> None: save_bootlog() @@ -28,74 +27,25 @@ def manager_init() -> None: build_metadata = get_build_metadata() params = Params() - params.clear_all(ParamKeyType.CLEAR_ON_MANAGER_START) - params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) - params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_MANAGER_START) + params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON) if build_metadata.release_channel: - params.clear_all(ParamKeyType.DEVELOPMENT_ONLY) - - default_params: list[tuple[str, str | bytes]] = [ - ("CompletedTrainingVersion", "0"), - ("DisengageOnAccelerator", "0"), - ("GsmMetered", "1"), - ("HasAcceptedTerms", "0"), - ("LanguageSetting", "main_en"), - ("OpenpilotEnabledToggle", "1"), - ("LongitudinalPersonality", str(log.LongitudinalPersonality.standard)), - ] - - sunnypilot_default_params: list[tuple[str, str | bytes]] = [ - ("AutoLaneChangeTimer", "0"), - ("AutoLaneChangeBsmDelay", "0"), - ("BlindSpot", "0"), - ("BlinkerMinLateralControlSpeed", "20"), # MPH or km/h - ("BlinkerPauseLateralControl", "0"), - ("CustomAccIncrementsEnabled", "0"), - ("CustomAccLongPressIncrement", "5"), - ("CustomAccShortPressIncrement", "1"), - ("DeviceBootMode", "0"), - ("DynamicExperimentalControl", "0"), - ("HyundaiLongitudinalTuning", "0"), - ("LagdToggle", "1"), - ("Mads", "1"), - ("MadsMainCruiseAllowed", "1"), - ("MadsSteeringMode", "0"), - ("MadsUnifiedEngagementMode", "1"), - ("MapdVersion", f"{VERSION}"), - ("MaxTimeOffroad", "1800"), - ("Brightness", "0"), - ("ModelManager_LastSyncTime", "0"), - ("ModelManager_ModelsCache", ""), - ("NeuralNetworkLateralControl", "0"), - ("QuietMode", "0"), - - # Longitudinal Tuning Live Parameters - ("LongTuningCustomToggle", "0"), - ("LongTuningVEgoStopping", "0.25"), - ("LongTuningVEgoStarting", "0.10"), - ("LongTuningStoppingDecelRate", "0.40"), - ("LongTuningLookaheadJerkBp", "2.0,5.0,20.0"), - ("LongTuningLookaheadJerkUpperV", "0.25,0.5,1.0"), - ("LongTuningLookaheadJerkLowerV", "0.05,0.10,0.3"), - ("LongTuningLongitudinalActuatorDelay", "0.45"), - ("LongTuningJerkLimits", "4.0"), - ("LongTuningUpperJerkV", "2.0,2.0,1.2"), - ("LongTuningLowerJerkV", "3.0,3.0,2.5"), - ("LongTuningMinUpperJerk", "0.5"), - ("LongTuningMinLowerJerk", "0.5"), - ] + params.clear_all(ParamKeyFlag.DEVELOPMENT_ONLY) # device boot mode - if params.get("DeviceBootMode") == b"1": # start in always offroad mode + if params.get("DeviceBootMode") == 1: # start in Always Offroad mode params.put_bool("OffroadMode", True) if params.get_bool("RecordFrontLock"): params.put_bool("RecordFront", True) - # set unset params - for k, v in (default_params + sunnypilot_default_params): - if params.get(k) is None: - params.put(k, v) + # set unset params to their default value + for k in params.all_keys(): + default_value = params.get_default_value(k) + if default_value is not None and params.get(k) is None: + params.put(k, default_value) # Create folders needed for msgq try: @@ -114,6 +64,7 @@ def manager_init() -> None: params.put("GitCommitDate", build_metadata.openpilot.git_commit_date) params.put("GitBranch", build_metadata.channel) params.put("GitRemote", build_metadata.openpilot.git_origin) + params.put_bool("IsDevelopmentBranch", build_metadata.development_channel) params.put_bool("IsTestedBranch", build_metadata.tested_channel) params.put_bool("IsReleaseBranch", build_metadata.release_channel) params.put("HardwareSerial", serial) @@ -167,19 +118,20 @@ def manager_thread() -> None: params = Params() ignore: list[str] = [] - if params.get("DongleId", encoding='utf8') in (None, UNREGISTERED_DONGLE_ID): + if params.get("DongleId") in (None, UNREGISTERED_DONGLE_ID): ignore += ["manage_athenad", "uploader"] if os.getenv("NOBOARD") is not None: ignore.append("pandad") ignore += [x for x in os.getenv("BLOCK", "").split(",") if len(x) > 0] - sm = messaging.SubMaster(['deviceState', 'carParams'], poll='deviceState') + sm = messaging.SubMaster(['deviceState', 'carParams', 'pandaStates'], poll='deviceState') pm = messaging.PubMaster(['managerState']) write_onroad_params(False, params) ensure_running(managed_processes.values(), False, params=params, CP=sm['carParams'], not_run=ignore) started_prev = False + ignition_prev = False while True: sm.update(1000) @@ -187,15 +139,20 @@ def manager_thread() -> None: started = sm['deviceState'].started if started and not started_prev: - params.clear_all(ParamKeyType.CLEAR_ON_ONROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_ONROAD_TRANSITION) elif not started and started_prev: - params.clear_all(ParamKeyType.CLEAR_ON_OFFROAD_TRANSITION) + params.clear_all(ParamKeyFlag.CLEAR_ON_OFFROAD_TRANSITION) + + ignition = any(ps.ignitionLine or ps.ignitionCan for ps in sm['pandaStates'] if ps.pandaType != log.PandaState.PandaType.unknown) + if ignition and not ignition_prev: + params.clear_all(ParamKeyFlag.CLEAR_ON_IGNITION_ON) # update onroad params, which drives pandad's safety setter thread if started != started_prev: write_onroad_params(started, params) started_prev = started + ignition_prev = ignition ensure_running(managed_processes.values(), started, params=params, CP=sm['carParams'], not_run=ignore) @@ -209,6 +166,14 @@ def manager_thread() -> None: msg.managerState.processes = [p.get_process_state_msg() for p in managed_processes.values()] pm.send('managerState', msg) + # kick AGNOS power monitoring watchdog + try: + if sm.all_checks(['deviceState']): + with open("/var/tmp/power_watchdog", "w") as f: + f.write(str(time.monotonic())) + except Exception: + pass + # Exit main loop when uninstall/shutdown/reboot is needed shutdown = False for param in ("DoUninstall", "DoShutdown", "DoReboot"): diff --git a/system/manager/process.py b/system/manager/process.py index 0ebddae049..5e86e87c76 100644 --- a/system/manager/process.py +++ b/system/manager/process.py @@ -251,7 +251,7 @@ class DaemonProcess(ManagerProcess): if self.params is None: self.params = Params() - pid = self.params.get(self.param_name, encoding='utf-8') + pid = self.params.get(self.param_name) if pid is not None: try: os.kill(int(pid), 0) @@ -270,7 +270,7 @@ class DaemonProcess(ManagerProcess): stderr=open('/dev/null', 'w'), preexec_fn=os.setpgrp) - self.params.put(self.param_name, str(proc.pid)) + self.params.put(self.param_name, proc.pid) def stop(self, retry=True, block=True, sig=None) -> None: pass diff --git a/system/manager/process_config.py b/system/manager/process_config.py index 2e3f754a76..6130916887 100644 --- a/system/manager/process_config.py +++ b/system/manager/process_config.py @@ -62,7 +62,11 @@ def only_offroad(started: bool, params: Params, CP: car.CarParams) -> bool: return not started def use_github_runner(started, params, CP: car.CarParams) -> bool: - return not PC and params.get_bool("EnableGithubRunner") and not params.get_bool("NetworkMetered") + 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.""" @@ -88,6 +92,9 @@ def is_stock_model(started, params, CP: car.CarParams) -> bool: """Check if the active model runner is stock.""" return bool(get_active_model_runner(params, not started) == custom.ModelManagerSP.Runner.stock) +def mapd_ready(started: bool, params: Params, CP: car.CarParams) -> bool: + return bool(os.path.exists(Paths.mapd_root())) + def or_(*fns): return lambda *args: operator.or_(*(fn(*args) for fn in fns)) @@ -114,6 +121,7 @@ procs = [ PythonProcess("sensord", "system.sensord.sensord", only_onroad, enabled=not PC), NativeProcess("ui", "selfdrive/ui", ["./ui"], always_run, watchdog_max_dt=(5 if not PC else None)), + PythonProcess("raylib_ui", "selfdrive.ui.ui", always_run, enabled=False, watchdog_max_dt=(5 if not PC else None)), PythonProcess("soundd", "selfdrive.ui.soundd", only_onroad), PythonProcess("locationd", "selfdrive.locationd.locationd", only_onroad), NativeProcess("_pandad", "selfdrive/pandad", ["./pandad"], always_run, enabled=False), @@ -139,6 +147,7 @@ procs = [ PythonProcess("updated", "system.updated.updated", only_offroad, enabled=not PC), PythonProcess("uploader", "system.loggerd.uploader", always_run), PythonProcess("statsd", "system.statsd", always_run), + PythonProcess("feedbackd", "selfdrive.ui.feedback.feedbackd", only_onroad), # debug procs NativeProcess("bridge", "cereal/messaging", ["./bridge"], notcar), @@ -162,14 +171,25 @@ procs += [ PythonProcess("backup_manager", "sunnypilot.sunnylink.backups.manager", and_(only_offroad, sunnylink_ready_shim)), # mapd - NativeProcess("mapd", Paths.mapd_root(), [MAPD_PATH], always_run), + NativeProcess("mapd", Paths.mapd_root(), ["bash", "-c", f"{MAPD_PATH} > /dev/null 2>&1"], mapd_ready), PythonProcess("mapd_manager", "sunnypilot.mapd.mapd_manager", always_run), ] if os.path.exists("./github_runner.sh"): procs += [NativeProcess("github_runner_start", "system/manager", ["./github_runner.sh", "start"], and_(only_offroad, use_github_runner), sigkill=False)] -if os.path.exists("../sunnypilot/sunnylink/uploader.py"): +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/system/manager/test/test_manager.py b/system/manager/test/test_manager.py index 9c0b664006..34d07c6724 100644 --- a/system/manager/test/test_manager.py +++ b/system/manager/test/test_manager.py @@ -38,6 +38,19 @@ class TestManager: # TODO: ensure there are blacklisted procs until we have a dedicated test assert len(BLACKLIST_PROCS), "No blacklisted procs to test not_run" + def test_set_params_with_default_value(self): + params = Params() + params.clear_all() + + os.environ['PREPAREONLY'] = '1' + manager.main() + for k in params.all_keys(): + default_value = params.get_default_value(k) + if default_value is not None: + assert params.get(k) == default_value + assert params.get("OpenpilotEnabledToggle") + assert params.get("RouteCount") == 0 + @pytest.mark.skip("this test is flaky the way it's currently written, should be moved to test_onroad") def test_clean_exit(self, subtests): """ diff --git a/system/micd.py b/system/micd.py index 38f3225f55..02ef82390b 100755 --- a/system/micd.py +++ b/system/micd.py @@ -9,10 +9,10 @@ from openpilot.common.retry import retry from openpilot.common.swaglog import cloudlog RATE = 10 -FFT_SAMPLES = 4096 +FFT_SAMPLES = 1600 # 100ms REFERENCE_SPL = 2e-5 # newtons/m^2 -SAMPLE_RATE = 44100 -SAMPLE_BUFFER = 4096 # approx 100ms +SAMPLE_RATE = 16000 +SAMPLE_BUFFER = 800 # 50ms @cache @@ -45,7 +45,7 @@ def apply_a_weighting(measurements: np.ndarray) -> np.ndarray: class Mic: def __init__(self): self.rk = Ratekeeper(RATE) - self.pm = messaging.PubMaster(['microphone']) + self.pm = messaging.PubMaster(['soundPressure', 'rawAudioData']) self.measurements = np.empty(0) @@ -61,12 +61,12 @@ class Mic: sound_pressure_weighted = self.sound_pressure_weighted sound_pressure_level_weighted = self.sound_pressure_level_weighted - msg = messaging.new_message('microphone', valid=True) - msg.microphone.soundPressure = float(sound_pressure) - msg.microphone.soundPressureWeighted = float(sound_pressure_weighted) - msg.microphone.soundPressureWeightedDb = float(sound_pressure_level_weighted) + msg = messaging.new_message('soundPressure', valid=True) + msg.soundPressure.soundPressure = float(sound_pressure) + msg.soundPressure.soundPressureWeighted = float(sound_pressure_weighted) + msg.soundPressure.soundPressureWeightedDb = float(sound_pressure_level_weighted) - self.pm.send('microphone', msg) + self.pm.send('soundPressure', msg) self.rk.keep_time() def callback(self, indata, frames, time, status): @@ -76,6 +76,12 @@ class Mic: Logged A-weighted equivalents are rough approximations of the human-perceived loudness. """ + msg = messaging.new_message('rawAudioData', valid=True) + audio_data_int_16 = (indata[:, 0] * 32767).astype(np.int16) + msg.rawAudioData.data = audio_data_int_16.tobytes() + msg.rawAudioData.sampleRate = SAMPLE_RATE + self.pm.send('rawAudioData', msg) + with self.lock: self.measurements = np.concatenate((self.measurements, indata[:, 0])) diff --git a/system/qcomgpsd/qcomgpsd.py b/system/qcomgpsd/qcomgpsd.py index 3a9e3585ba..819b17f113 100755 --- a/system/qcomgpsd/qcomgpsd.py +++ b/system/qcomgpsd/qcomgpsd.py @@ -286,7 +286,7 @@ def main() -> NoReturn: continue if DEBUG: - print(f"{time.time():.4f}: got log: {log_type} len {len(log_payload)}") + print(f"{time.time():.4f}: got log: {log_type} len {len(log_payload)}") # noqa: TID251 if log_type == LOG_GNSS_OEMDRE_MEASUREMENT_REPORT: msg = messaging.new_message('qcomGnss', valid=True) diff --git a/system/sensord/sensord.py b/system/sensord/sensord.py index ed9d0ed415..2b6467fa78 100755 --- a/system/sensord/sensord.py +++ b/system/sensord/sensord.py @@ -98,6 +98,13 @@ def main() -> None: (MMC5603NJ_Magn(I2C_BUS_IMU), "magnetometer", False), ] + # Reset sensors + for sensor, _, _ in sensors_cfg: + try: + sensor.reset() + except Exception: + cloudlog.exception(f"Error initializing {sensor} sensor") + # Initialize sensors exit_event = threading.Event() threads = [ diff --git a/system/sensord/sensors/i2c_sensor.py b/system/sensord/sensors/i2c_sensor.py index 0e15a6622b..336ebb1fd3 100644 --- a/system/sensord/sensors/i2c_sensor.py +++ b/system/sensord/sensors/i2c_sensor.py @@ -40,6 +40,11 @@ class Sensor: def device_address(self) -> int: raise NotImplementedError + def reset(self) -> None: + # optional. + # not part of init due to shared registers + pass + def init(self) -> None: raise NotImplementedError diff --git a/system/sensord/sensors/lsm6ds3_accel.py b/system/sensord/sensors/lsm6ds3_accel.py index 2d788fcbe2..43863daa93 100644 --- a/system/sensord/sensors/lsm6ds3_accel.py +++ b/system/sensord/sensors/lsm6ds3_accel.py @@ -31,6 +31,10 @@ class LSM6DS3_Accel(Sensor): def device_address(self) -> int: return 0x6A + def reset(self): + self.write(0x12, 0x1) + time.sleep(0.1) + def init(self): chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) if chip_id == 0x6A: diff --git a/system/sensord/sensors/lsm6ds3_gyro.py b/system/sensord/sensors/lsm6ds3_gyro.py index 68fd267df2..60de2bbe02 100644 --- a/system/sensord/sensors/lsm6ds3_gyro.py +++ b/system/sensord/sensors/lsm6ds3_gyro.py @@ -29,6 +29,10 @@ class LSM6DS3_Gyro(Sensor): def device_address(self) -> int: return 0x6A + def reset(self): + self.write(0x12, 0x1) + time.sleep(0.1) + def init(self): chip_id = self.verify_chip_id(0x0F, [0x69, 0x6A]) if chip_id == 0x6A: diff --git a/system/sensord/sensors/lsm6ds3_temp.py b/system/sensord/sensors/lsm6ds3_temp.py index 7d7b1c2ce1..b9bb9fe3da 100644 --- a/system/sensord/sensors/lsm6ds3_temp.py +++ b/system/sensord/sensors/lsm6ds3_temp.py @@ -7,10 +7,10 @@ from openpilot.system.sensord.sensors.i2c_sensor import Sensor class LSM6DS3_Temp(Sensor): @property def device_address(self) -> int: - return 0x6A # Default I2C address for LSM6DS3 + return 0x6A def _read_temperature(self) -> float: - scale = 16.0 if log.SensorEventData.SensorSource.lsm6ds3 else 256.0 + scale = 16.0 if self.source == log.SensorEventData.SensorSource.lsm6ds3 else 256.0 data = self.read(0x20, 2) return 25 + (self.parse_16bit(data[0], data[1]) / scale) diff --git a/system/sentry.py b/system/sentry.py index 2f918ebc49..a1fb604dea 100644 --- a/system/sentry.py +++ b/system/sentry.py @@ -106,10 +106,10 @@ def set_user() -> None: def get_properties() -> tuple[str, str, str]: params = Params() - hardware_serial: str = params.get("HardwareSerial", encoding='utf-8') or "" - git_username: str = params.get("GithubUsername", encoding='utf-8') or "" - dongle_id: str = params.get("DongleId", encoding='utf-8') or f"{UNREGISTERED_DONGLE_ID}-{hardware_serial}" - sunnylink_dongle_id: str = params.get("SunnylinkDongleId", encoding='utf-8') or UNREGISTERED_SUNNYLINK_DONGLE_ID + hardware_serial: str = params.get("HardwareSerial") or "" + git_username: str = params.get("GithubUsername") or "" + dongle_id: str = params.get("DongleId") or f"{UNREGISTERED_DONGLE_ID}-{hardware_serial}" + sunnylink_dongle_id: str = params.get("SunnylinkDongleId") or UNREGISTERED_SUNNYLINK_DONGLE_ID return dongle_id, git_username, sunnylink_dongle_id diff --git a/system/statsd.py b/system/statsd.py index b8a7f2c661..d60064fc91 100755 --- a/system/statsd.py +++ b/system/statsd.py @@ -61,7 +61,7 @@ class StatLog: def main() -> NoReturn: - dongle_id = Params().get("DongleId", encoding='utf-8') + dongle_id = Params().get("DongleId") def get_influxdb_line(measurement: str, value: float | dict[str, float], timestamp: datetime, tags: dict) -> str: res = f"{measurement}" for k, v in tags.items(): diff --git a/system/ubloxd/pigeond.py b/system/ubloxd/pigeond.py index 2194a4b9d2..e458a9d65f 100755 --- a/system/ubloxd/pigeond.py +++ b/system/ubloxd/pigeond.py @@ -41,7 +41,7 @@ def add_ubx_checksum(msg: bytes) -> bytes: B = (B + A) % 256 return msg + bytes([A, B]) -def get_assistnow_messages(token: bytes) -> list[bytes]: +def get_assistnow_messages(token: str) -> list[bytes]: # make request # TODO: implement adding the last known location r = requests.get("https://online-live2.services.u-blox.com/GetOnlineData.ashx", params=urllib.parse.urlencode({ @@ -136,6 +136,17 @@ class TTYPigeon: return True return False +def save_almanac(pigeon: TTYPigeon) -> None: + # store almanac in flash + pigeon.send(b"\xB5\x62\x09\x14\x04\x00\x00\x00\x00\x00\x21\xEC") + try: + if pigeon.wait_for_ack(ack=UBLOX_SOS_ACK, nack=UBLOX_SOS_NACK): + cloudlog.info("Done storing almanac") + else: + cloudlog.error("Error storing almanac") + except TimeoutError: + pass + def init_baudrate(pigeon: TTYPigeon): # ublox default setting on startup is 9600 baudrate pigeon.set_baud(9600) @@ -146,7 +157,7 @@ def init_baudrate(pigeon: TTYPigeon): pigeon.set_baud(460800) -def initialize_pigeon(pigeon: TTYPigeon) -> bool: +def init_pigeon(pigeon: TTYPigeon) -> bool: # try initializing a few times for _ in range(10): try: @@ -239,32 +250,17 @@ def initialize_pigeon(pigeon: TTYPigeon) -> bool: return True def deinitialize_and_exit(pigeon: TTYPigeon | None): - cloudlog.warning("Storing almanac in ublox flash") - if pigeon is not None: # controlled GNSS stop pigeon.send(b"\xB5\x62\x06\x04\x04\x00\x00\x00\x08\x00\x16\x74") - # store almanac in flash - pigeon.send(b"\xB5\x62\x09\x14\x04\x00\x00\x00\x00\x00\x21\xEC") - try: - if pigeon.wait_for_ack(ack=UBLOX_SOS_ACK, nack=UBLOX_SOS_NACK): - cloudlog.warning("Done storing almanac") - else: - cloudlog.error("Error storing almanac") - except TimeoutError: - pass - # turn off power and exit cleanly set_power(False) sys.exit(0) -def create_pigeon() -> tuple[TTYPigeon, messaging.PubMaster]: - pigeon = None - +def init(pigeon: TTYPigeon) -> None: # register exit handler signal.signal(signal.SIGINT, lambda sig, frame: deinitialize_and_exit(pigeon)) - pm = messaging.PubMaster(['ubloxRaw']) # power cycle ublox set_power(False) @@ -272,28 +268,34 @@ def create_pigeon() -> tuple[TTYPigeon, messaging.PubMaster]: set_power(True) time.sleep(0.5) - pigeon = TTYPigeon() - return pigeon, pm + init_baudrate(pigeon) + init_pigeon(pigeon) -def run_receiving(pigeon: TTYPigeon, pm: messaging.PubMaster, duration: int = 0): +def run_receiving(duration: int = 0): + pm = messaging.PubMaster(['ubloxRaw']) + + pigeon = TTYPigeon() + init(pigeon) start_time = time.monotonic() - def end_condition(): - return True if duration == 0 else time.monotonic() - start_time < duration - - while end_condition(): + last_almanac_save = time.monotonic() + while (duration == 0) or (time.monotonic() - start_time < duration): dat = pigeon.receive() if len(dat) > 0: if dat[0] == 0x00: cloudlog.warning("received invalid data from ublox, re-initing!") - init_baudrate(pigeon) - initialize_pigeon(pigeon) + init(pigeon) continue # send out to socket msg = messaging.new_message('ubloxRaw', len(dat), valid=True) msg.ubloxRaw = dat[:] pm.send('ubloxRaw', msg) + + # save almanac every 5 minutes + if (time.monotonic() - last_almanac_save) > 60*5: + save_almanac(pigeon) + last_almanac_save = time.monotonic() else: # prevent locking up a CPU core if ublox disconnects time.sleep(0.001) @@ -301,13 +303,7 @@ def run_receiving(pigeon: TTYPigeon, pm: messaging.PubMaster, duration: int = 0) def main(): assert TICI, "unsupported hardware for pigeond" - - pigeon, pm = create_pigeon() - init_baudrate(pigeon) - initialize_pigeon(pigeon) - - # start receiving data - run_receiving(pigeon, pm) + run_receiving() if __name__ == "__main__": main() diff --git a/system/ui/README.md b/system/ui/README.md index 85d32bfd6c..b124ae4d85 100644 --- a/system/ui/README.md +++ b/system/ui/README.md @@ -8,3 +8,8 @@ Quick start: * set `SCALE=1.5` to scale the entire UI by 1.5x * https://www.raylib.com/cheatsheet/cheatsheet.html * https://electronstudio.github.io/raylib-python-cffi/README.html#quickstart + +Style guide: +* All graphical elements should subclass [`Widget`](/system/ui/widgets/__init__.py). + * Prefer a stateful widget over a function for easy migration from QT +* All internal class variables and functions should be prefixed with `_` diff --git a/system/ui/lib/application.py b/system/ui/lib/application.py index 805fdcc71f..3f433e1fcb 100644 --- a/system/ui/lib/application.py +++ b/system/ui/lib/application.py @@ -3,21 +3,28 @@ import cffi import os import time import pyray as rl +import threading from collections.abc import Callable +from collections import deque from dataclasses import dataclass -from enum import IntEnum +from enum import StrEnum +from typing import NamedTuple from importlib.resources import as_file, files from openpilot.common.swaglog import cloudlog -from openpilot.system.hardware import HARDWARE +from openpilot.system.hardware import HARDWARE, PC +from openpilot.common.realtime import Ratekeeper -DEFAULT_FPS = 60 +DEFAULT_FPS = int(os.getenv("FPS", "60")) FPS_LOG_INTERVAL = 5 # Seconds between logging FPS drops FPS_DROP_THRESHOLD = 0.9 # FPS drop threshold for triggering a warning FPS_CRITICAL_THRESHOLD = 0.5 # Critical threshold for triggering strict actions +MOUSE_THREAD_RATE = 140 # touch controller runs at 140Hz +MAX_TOUCH_SLOTS = 2 -ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "1") == "1" -SHOW_FPS = os.getenv("SHOW_FPS") == '1' -STRICT_MODE = os.getenv("STRICT_MODE") == '1' +ENABLE_VSYNC = os.getenv("ENABLE_VSYNC", "0") == "1" +SHOW_FPS = os.getenv("SHOW_FPS") == "1" +SHOW_TOUCHES = os.getenv("SHOW_TOUCHES") == "1" +STRICT_MODE = os.getenv("STRICT_MODE") == "1" SCALE = float(os.getenv("SCALE", "1.0")) DEFAULT_TEXT_SIZE = 60 @@ -27,16 +34,16 @@ ASSETS_DIR = files("openpilot.selfdrive").joinpath("assets") FONT_DIR = ASSETS_DIR.joinpath("fonts") -class FontWeight(IntEnum): - THIN = 0 - EXTRA_LIGHT = 1 - LIGHT = 2 - NORMAL = 3 - MEDIUM = 4 - SEMI_BOLD = 5 - BOLD = 6 - EXTRA_BOLD = 7 - BLACK = 8 +class FontWeight(StrEnum): + THIN = "Inter-Thin.ttf" + EXTRA_LIGHT = "Inter-ExtraLight.ttf" + LIGHT = "Inter-Light.ttf" + NORMAL = "Inter-Regular.ttf" + MEDIUM = "Inter-Medium.ttf" + SEMI_BOLD = "Inter-SemiBold.ttf" + BOLD = "Inter-Bold.ttf" + EXTRA_BOLD = "Inter-ExtraBold.ttf" + BLACK = "Inter-Black.ttf" @dataclass @@ -45,6 +52,74 @@ class ModalOverlay: callback: Callable | None = None +class MousePos(NamedTuple): + x: float + y: float + + +class MouseEvent(NamedTuple): + pos: MousePos + slot: int + left_pressed: bool + left_released: bool + left_down: bool + t: float + + +class MouseState: + def __init__(self, scale: float = 1.0): + self._scale = scale + self._events: deque[MouseEvent] = deque(maxlen=MOUSE_THREAD_RATE) # bound event list + self._prev_mouse_event: list[MouseEvent | None] = [None] * MAX_TOUCH_SLOTS + + self._rk = Ratekeeper(MOUSE_THREAD_RATE) + self._lock = threading.Lock() + self._exit_event = threading.Event() + self._thread = None + + def get_events(self) -> list[MouseEvent]: + with self._lock: + events = list(self._events) + self._events.clear() + return events + + def start(self): + self._exit_event.clear() + if self._thread is None or not self._thread.is_alive(): + self._thread = threading.Thread(target=self._run_thread, daemon=True) + self._thread.start() + + def stop(self): + self._exit_event.set() + if self._thread is not None and self._thread.is_alive(): + self._thread.join() + + def _run_thread(self): + while not self._exit_event.is_set(): + rl.poll_input_events() + self._handle_mouse_event() + self._rk.keep_time() + + def _handle_mouse_event(self): + for slot in range(MAX_TOUCH_SLOTS): + mouse_pos = rl.get_touch_position(slot) + x = mouse_pos.x / self._scale if self._scale != 1.0 else mouse_pos.x + y = mouse_pos.y / self._scale if self._scale != 1.0 else mouse_pos.y + ev = MouseEvent( + MousePos(x, y), + slot, + rl.is_mouse_button_pressed(slot), + rl.is_mouse_button_released(slot), + rl.is_mouse_button_down(slot), + time.monotonic(), + ) + # Only add changes + if self._prev_mouse_event[slot] is None or ev[:-1] != self._prev_mouse_event[slot][:-1]: + with self._lock: + self._events.append(ev) + self._prev_mouse_event[slot] = ev + + class GuiApplication: def __init__(self, width: int, height: int): self._fonts: dict[FontWeight, rl.Font] = {} @@ -61,6 +136,12 @@ class GuiApplication: self._trace_log_callback = None self._modal_overlay = ModalOverlay() + self._mouse = MouseState(self._scale) + self._mouse_events: list[MouseEvent] = [] + + # Debug variables + self._mouse_history: deque[MousePos] = deque(maxlen=MOUSE_THREAD_RATE) + def request_close(self): self._window_close_requested = True @@ -89,6 +170,9 @@ class GuiApplication: self._set_styles() self._load_fonts() + if not PC: + self._mouse.start() + def set_modal_overlay(self, overlay, callback: Callable | None = None): self._modal_overlay = ModalOverlay(overlay=overlay, callback=callback) @@ -149,11 +233,25 @@ class GuiApplication: rl.unload_render_texture(self._render_texture) self._render_texture = None + if not PC: + self._mouse.stop() + rl.close_window() + @property + def mouse_events(self) -> list[MouseEvent]: + return self._mouse_events + def render(self): try: while not (self._window_close_requested or rl.window_should_close()): + if PC: + # Thread is not used on PC, need to manually add mouse events + self._mouse._handle_mouse_event() + + # Store all mouse events for the current frame + self._mouse_events = self._mouse.get_events() + if self._render_texture: rl.begin_texture_mode(self._render_texture) rl.clear_background(rl.BLACK) @@ -190,6 +288,20 @@ class GuiApplication: if SHOW_FPS: rl.draw_fps(10, 10) + if SHOW_TOUCHES: + for mouse_event in self._mouse_events: + if mouse_event.left_pressed: + self._mouse_history.clear() + self._mouse_history.append(mouse_event.pos) + + if self._mouse_history: + mouse_pos = self._mouse_history[-1] + rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 15, rl.RED) + for idx, mouse_pos in enumerate(self._mouse_history): + perc = idx / len(self._mouse_history) + color = rl.Color(min(int(255 * (1.5 - perc)), 255), int(min(255 * (perc + 0.5), 255)), 50, 255) + rl.draw_circle(int(mouse_pos.x), int(mouse_pos.y), 5, color) + rl.end_drawing() self._monitor_fps() except KeyboardInterrupt: @@ -207,34 +319,23 @@ class GuiApplication: return self._height def _load_fonts(self): - font_files = ( - "Inter-Thin.ttf", - "Inter-ExtraLight.ttf", - "Inter-Light.ttf", - "Inter-Regular.ttf", - "Inter-Medium.ttf", - "Inter-SemiBold.ttf", - "Inter-Bold.ttf", - "Inter-ExtraBold.ttf", - "Inter-Black.ttf", - ) - # Create a character set from our keyboard layouts from openpilot.system.ui.widgets.keyboard import KEYBOARD_LAYOUTS + all_chars = set() for layout in KEYBOARD_LAYOUTS.values(): all_chars.update(key for row in layout for key in row) all_chars = "".join(all_chars) - all_chars += "–✓°" + all_chars += "–✓×°" codepoint_count = rl.ffi.new("int *", 1) codepoints = rl.load_codepoints(all_chars, codepoint_count) - for index, font_file in enumerate(font_files): - with as_file(FONT_DIR.joinpath(font_file)) as fspath: + for font_weight_file in FontWeight: + with as_file(FONT_DIR.joinpath(font_weight_file)) as fspath: font = rl.load_font_ex(fspath.as_posix(), 200, codepoints, codepoint_count[0]) rl.set_texture_filter(font.texture, rl.TextureFilter.TEXTURE_FILTER_BILINEAR) - self._fonts[index] = font + self._fonts[font_weight_file] = font rl.unload_codepoints(codepoints) rl.gui_set_font(self._fonts[FontWeight.NORMAL]) diff --git a/system/ui/lib/emoji.py b/system/ui/lib/emoji.py new file mode 100644 index 0000000000..28139158a1 --- /dev/null +++ b/system/ui/lib/emoji.py @@ -0,0 +1,47 @@ +import io +import re + +from PIL import Image, ImageDraw, ImageFont +import pyray as rl + +_cache: dict[str, rl.Texture] = {} + +EMOJI_REGEX = re.compile( +"""[\U0001F600-\U0001F64F +\U0001F300-\U0001F5FF +\U0001F680-\U0001F6FF +\U0001F1E0-\U0001F1FF +\U00002700-\U000027BF +\U0001F900-\U0001F9FF +\U00002600-\U000026FF +\U00002300-\U000023FF +\U00002B00-\U00002BFF +\U0001FA70-\U0001FAFF +\U0001F700-\U0001F77F +\u2640-\u2642 +\u2600-\u2B55 +\u200d +\u23cf +\u23e9 +\u231a +\ufe0f +\u3030 +]+""", + flags=re.UNICODE +) + +def find_emoji(text): + return [(m.start(), m.end(), m.group()) for m in EMOJI_REGEX.finditer(text)] + +def emoji_tex(emoji): + if emoji not in _cache: + img = Image.new("RGBA", (128, 128), (0, 0, 0, 0)) + draw = ImageDraw.Draw(img) + font = ImageFont.truetype("NotoColorEmoji", 109) + draw.text((0, 0), emoji, font=font, embedded_color=True) + buffer = io.BytesIO() + img.save(buffer, format="PNG") + l = buffer.tell() + buffer.seek(0) + _cache[emoji] = rl.load_texture_from_image(rl.load_image_from_memory(".png", buffer.getvalue(), l)) + return _cache[emoji] diff --git a/system/ui/lib/label.py b/system/ui/lib/label.py deleted file mode 100644 index c3d0e0303a..0000000000 --- a/system/ui/lib/label.py +++ /dev/null @@ -1,78 +0,0 @@ -import pyray as rl -from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.utils import GuiStyleContext - - -def gui_label( - rect: rl.Rectangle, - text: str, - font_size: int = DEFAULT_TEXT_SIZE, - color: rl.Color = DEFAULT_TEXT_COLOR, - font_weight: FontWeight = FontWeight.NORMAL, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, - elide_right: bool = True -): - font = gui_app.font(font_weight) - text_size = measure_text_cached(font, text, font_size) - display_text = text - - # Elide text to fit within the rectangle - if elide_right and text_size.x > rect.width: - ellipsis = "..." - left, right = 0, len(text) - while left < right: - mid = (left + right) // 2 - candidate = text[:mid] + ellipsis - candidate_size = measure_text_cached(font, candidate, font_size) - if candidate_size.x <= rect.width: - left = mid + 1 - else: - right = mid - display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis - text_size = measure_text_cached(font, display_text, font_size) - - # Calculate horizontal position based on alignment - text_x = rect.x + { - rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, - rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, - rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, - }.get(alignment, 0) - - # Calculate vertical position based on alignment - text_y = rect.y + { - rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, - rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, - }.get(alignment_vertical, 0) - - # Draw the text in the specified rectangle - rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) - - -def gui_text_box( - rect: rl.Rectangle, - text: str, - font_size: int = DEFAULT_TEXT_SIZE, - color: rl.Color = DEFAULT_TEXT_COLOR, - alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, - alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, - font_weight: FontWeight = FontWeight.NORMAL, -): - styles = [ - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, font_size), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, font_size), - (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), - (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) - ] - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(font_weight)) - - with GuiStyleContext(styles): - rl.gui_label(rect, text) - - if font_weight != FontWeight.NORMAL: - rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) diff --git a/system/ui/lib/scroll_panel.py b/system/ui/lib/scroll_panel.py index 32f3b7b575..e2296fd5ed 100644 --- a/system/ui/lib/scroll_panel.py +++ b/system/ui/lib/scroll_panel.py @@ -1,6 +1,8 @@ +import time import pyray as rl from collections import deque from enum import IntEnum +from openpilot.system.ui.lib.application import gui_app, MouseEvent, MousePos # Scroll constants for smooth scrolling behavior MOUSE_WHEEL_SCROLL_SPEED = 30 @@ -38,51 +40,55 @@ class GuiScrollPanel: self._bounds_rect: rl.Rectangle | None = None def handle_scroll(self, bounds: rl.Rectangle, content: rl.Rectangle) -> rl.Vector2: + # TODO: HACK: this class is driven by mouse events, so we need to ensure we have at least one event to process + for mouse_event in gui_app.mouse_events or [MouseEvent(MousePos(0, 0), 0, False, False, False, time.monotonic())]: + if mouse_event.slot == 0: + self._handle_mouse_event(mouse_event, bounds, content) + return self._offset + + def _handle_mouse_event(self, mouse_event: MouseEvent, bounds: rl.Rectangle, content: rl.Rectangle): # Store rectangles for reference self._content_rect = content self._bounds_rect = bounds - # Calculate time delta - current_time = rl.get_time() - - mouse_pos = rl.get_mouse_position() max_scroll_y = max(content.height - bounds.height, 0) # Start dragging on mouse press - if rl.check_collision_point_rec(mouse_pos, bounds) and rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): + if rl.check_collision_point_rec(mouse_event.pos, bounds) and mouse_event.left_pressed: if self._scroll_state == ScrollState.IDLE or self._scroll_state == ScrollState.BOUNCING: self._scroll_state = ScrollState.DRAGGING_CONTENT if self._show_vertical_scroll_bar: scrollbar_width = rl.gui_get_style(rl.GuiControl.LISTVIEW, rl.GuiListViewProperty.SCROLLBAR_WIDTH) scrollbar_x = bounds.x + bounds.width - scrollbar_width - if mouse_pos.x >= scrollbar_x: + if mouse_event.pos.x >= scrollbar_x: self._scroll_state = ScrollState.DRAGGING_SCROLLBAR # TODO: hacky # when clicking while moving, go straight into dragging self._is_dragging = abs(self._velocity_y) > MIN_VELOCITY - self._last_mouse_y = mouse_pos.y - self._start_mouse_y = mouse_pos.y - self._last_drag_time = current_time + self._last_mouse_y = mouse_event.pos.y + self._start_mouse_y = mouse_event.pos.y + self._last_drag_time = mouse_event.t self._velocity_history.clear() self._velocity_y = 0.0 self._bounce_offset = 0.0 # Handle active dragging if self._scroll_state == ScrollState.DRAGGING_CONTENT or self._scroll_state == ScrollState.DRAGGING_SCROLLBAR: - if rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): - delta_y = mouse_pos.y - self._last_mouse_y + if mouse_event.left_down: + delta_y = mouse_event.pos.y - self._last_mouse_y # Track velocity for inertia - time_since_last_drag = current_time - self._last_drag_time + time_since_last_drag = mouse_event.t - self._last_drag_time if time_since_last_drag > 0: - drag_velocity = delta_y / time_since_last_drag / 60.0 + # TODO: HACK: /2 since we usually get two touch events per frame + drag_velocity = delta_y / time_since_last_drag / 60.0 / 2 # TODO: shouldn't be hardcoded self._velocity_history.append(drag_velocity) - self._last_drag_time = current_time + self._last_drag_time = mouse_event.t # Detect actual dragging - total_drag = abs(mouse_pos.y - self._start_mouse_y) + total_drag = abs(mouse_event.pos.y - self._start_mouse_y) if total_drag > DRAG_THRESHOLD: self._is_dragging = True @@ -96,9 +102,9 @@ class GuiScrollPanel: scroll_ratio = content.height / bounds.height self._offset.y -= delta_y * scroll_ratio - self._last_mouse_y = mouse_pos.y + self._last_mouse_y = mouse_event.pos.y - elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): + elif mouse_event.left_released: # Calculate flick velocity if self._velocity_history: total_weight = 0 @@ -167,8 +173,6 @@ class GuiScrollPanel: elif self._offset.y < -(max_scroll_y + MAX_BOUNCE_DISTANCE): self._offset.y = -(max_scroll_y + MAX_BOUNCE_DISTANCE) - return self._offset - def is_touch_valid(self): return not self._is_dragging diff --git a/system/ui/lib/shader_polygon.py b/system/ui/lib/shader_polygon.py index cfde81c554..39bc0d5aa4 100644 --- a/system/ui/lib/shader_polygon.py +++ b/system/ui/lib/shader_polygon.py @@ -39,6 +39,10 @@ vec4 getGradientColor(vec2 pos) { float t = clamp(dot(pos - gradientStart, normalizedDir) / gradientLength, 0.0, 1.0); if (gradientColorCount <= 1) return gradientColors[0]; + + // handle t before first / after last stop + if (t <= gradientStops[0]) return gradientColors[0]; + if (t >= gradientStops[gradientColorCount-1]) return gradientColors[gradientColorCount-1]; for (int i = 0; i < gradientColorCount - 1; i++) { if (t >= gradientStops[i] && t <= gradientStops[i+1]) { float segmentT = (t - gradientStops[i]) / (gradientStops[i+1] - gradientStops[i]); diff --git a/system/ui/lib/wifi_manager.py b/system/ui/lib/wifi_manager.py index dd08b66373..4cb741bc95 100644 --- a/system/ui/lib/wifi_manager.py +++ b/system/ui/lib/wifi_manager.py @@ -37,7 +37,7 @@ NM_DEVICE_IFACE = "org.freedesktop.NetworkManager.Device" NM_DEVICE_STATE_REASON_SUPPLICANT_DISCONNECT = 8 TETHERING_IP_ADDRESS = "192.168.43.1" -DEFAULT_TETHERING_PASSWORD = "12345678" +DEFAULT_TETHERING_PASSWORD = "swagswagcomma" # NetworkManager device states @@ -91,7 +91,7 @@ class WifiManager: # 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", encoding="utf-8") + dongle_id = Params().get("DongleId") if dongle_id: self._tethering_ssid += "-" + dongle_id[:4] self.running: bool = True @@ -101,8 +101,8 @@ class WifiManager: """Connect to the DBus system bus.""" try: self.bus = await MessageBus(bus_type=BusType.SYSTEM).connect() - if not await self._find_wifi_device(): - raise ValueError("No Wi-Fi device found") + while not await self._find_wifi_device(): + await asyncio.sleep(1) await self._setup_signals(self.device_path) self.active_ap_path = await self.get_active_access_point() @@ -204,7 +204,7 @@ class WifiManager: 'connection': { 'type': Variant('s', '802-11-wireless'), 'uuid': Variant('s', str(uuid.uuid4())), - 'id': Variant('s', ssid), + 'id': Variant('s', f'openpilot connection {ssid}'), 'autoconnect-retries': Variant('i', 0), }, '802-11-wireless': { @@ -212,7 +212,10 @@ class WifiManager: 'hidden': Variant('b', is_hidden), 'mode': Variant('s', 'infrastructure'), }, - 'ipv4': {'method': Variant('s', 'auto')}, + 'ipv4': { + 'method': Variant('s', 'auto'), + 'dns-priority': Variant('i', 600), + }, 'ipv6': {'method': Variant('s', 'ignore')}, } @@ -438,33 +441,33 @@ class WifiManager: settings_iface.on_connection_removed(self._on_connection_removed) def _on_properties_changed(self, interface: str, changed: dict, invalidated: list): - # print("property changed", interface, changed, invalidated) - if 'LastScan' in changed: + 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 - asyncio.create_task(self._refresh_networks()) def _on_state_changed(self, new_state: int, old_state: int, reason: int): - print("State changed", new_state, old_state, reason) if new_state == NMDeviceState.ACTIVATED: if self.callbacks.activated: self.callbacks.activated() - asyncio.create_task(self._refresh_networks()) 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: @@ -483,9 +486,6 @@ class WifiManager: if self.callbacks.forgotten: self.callbacks.forgotten(ssid) - - # Update network list to reflect the removed saved connection - asyncio.create_task(self._refresh_networks()) break async def _add_saved_connection(self, path: str) -> None: @@ -494,14 +494,13 @@ class WifiManager: settings = await self._get_connection_settings(path) if ssid := self._extract_ssid(settings): self.saved_connections[ssid] = path - await self._refresh_networks() 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 ''.join(chr(b) for b in ssid_variant) if ssid_variant else None + 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.""" @@ -531,7 +530,7 @@ class WifiManager: 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 = ''.join(chr(byte) for byte in ssid_variant) + ssid = bytes(ssid_variant).decode('utf-8') if not ssid: continue @@ -540,18 +539,28 @@ class WifiManager: flags = properties['Flags'].value wpa_flags = properties['WpaFlags'].value rsn_flags = properties['RsnFlags'].value - existing_network = network_dict.get(ssid) - if not existing_network or ((not existing_network.bssid and bssid) or (existing_network.strength < strength)): + + # 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=strength, + strength=0, security_type=self._get_security_type(flags, wpa_flags, rsn_flags), - path=ap_path, - bssid=bssid, - is_connected=self.active_ap_path == ap_path and self._current_connection_ssid != ssid, + 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: diff --git a/system/ui/reset.py b/system/ui/reset.py index c0dbd1d956..b1bf5a6b6a 100755 --- a/system/ui/reset.py +++ b/system/ui/reset.py @@ -1,18 +1,21 @@ #!/usr/bin/env python3 import os -import pyray as rl import sys import threading +import time from enum import IntEnum +import pyray as rl + from openpilot.system.hardware import PC from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_label, gui_text_box -from openpilot.system.ui.lib.widget import Widget +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 class ResetMode(IntEnum): @@ -31,8 +34,16 @@ class ResetState(IntEnum): class Reset(Widget): def __init__(self, mode): super().__init__() - self.mode = mode - self.reset_state = ResetState.NONE + self._mode = mode + self._previous_reset_state = None + self._reset_state = ResetState.NONE + self._cancel_button = Button("Cancel", self._cancel_callback) + self._confirm_button = Button("Confirm", self._confirm, button_style=ButtonStyle.PRIMARY) + self._reboot_button = Button("Reboot", lambda: os.system("sudo reboot")) + self._render_status = True + + def _cancel_callback(self): + self._render_status = False def _do_erase(self): if PC: @@ -50,53 +61,58 @@ class Reset(Widget): if rm == 0 or fmt == 0: os.system("sudo reboot") else: - self.reset_state = ResetState.FAILED + self._reset_state = ResetState.FAILED def start_reset(self): - self.reset_state = ResetState.RESETTING + self._reset_state = ResetState.RESETTING threading.Timer(0.1, self._do_erase).start() + def _update_state(self): + if self._reset_state != self._previous_reset_state: + self._previous_reset_state = self._reset_state + self._timeout_st = time.monotonic() + elif self._reset_state != ResetState.RESETTING and (time.monotonic() - self._timeout_st) > TIMEOUT: + exit(0) + def _render(self, rect: rl.Rectangle): label_rect = rl.Rectangle(rect.x + 140, rect.y, rect.width - 280, 100) gui_label(label_rect, "System Reset", 100, font_weight=FontWeight.BOLD) text_rect = rl.Rectangle(rect.x + 140, rect.y + 140, rect.width - 280, rect.height - 90 - 100) - gui_text_box(text_rect, self.get_body_text(), 90) + gui_text_box(text_rect, self._get_body_text(), 90) button_height = 160 button_spacing = 50 button_top = rect.y + rect.height - button_height button_width = (rect.width - button_spacing) / 2.0 - if self.reset_state != ResetState.RESETTING: - if self.mode == ResetMode.RECOVER or self.reset_state == ResetState.FAILED: - if gui_button(rl.Rectangle(rect.x, button_top, button_width, button_height), "Reboot"): - os.system("sudo reboot") - elif self.mode == ResetMode.USER_RESET: - if gui_button(rl.Rectangle(rect.x, button_top, button_width, button_height), "Cancel"): - return False + if self._reset_state != ResetState.RESETTING: + if self._mode == ResetMode.RECOVER: + self._reboot_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) + elif self._mode == ResetMode.USER_RESET: + self._cancel_button.render(rl.Rectangle(rect.x, button_top, button_width, button_height)) - if self.reset_state != ResetState.FAILED: - if gui_button(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height), - "Confirm", button_style=ButtonStyle.PRIMARY): - self.confirm() + if self._reset_state != ResetState.FAILED: + self._confirm_button.render(rl.Rectangle(rect.x + button_width + 50, button_top, button_width, button_height)) + else: + self._reboot_button.render(rl.Rectangle(rect.x, button_top, rect.width, button_height)) - return True + return self._render_status - def confirm(self): - if self.reset_state == ResetState.CONFIRM: + def _confirm(self): + if self._reset_state == ResetState.CONFIRM: self.start_reset() else: - self.reset_state = ResetState.CONFIRM + self._reset_state = ResetState.CONFIRM - def get_body_text(self): - if self.reset_state == ResetState.CONFIRM: + def _get_body_text(self): + if self._reset_state == ResetState.CONFIRM: return "Are you sure you want to reset your device?" - if self.reset_state == ResetState.RESETTING: + if self._reset_state == ResetState.RESETTING: return "Resetting device...\nThis may take up to a minute." - if self.reset_state == ResetState.FAILED: + if self._reset_state == ResetState.FAILED: return "Reset failed. Reboot to try again." - if self.mode == ResetMode.RECOVER: + if self._mode == ResetMode.RECOVER: return "Unable to mount data partition. Partition may be corrupted. Press confirm to erase and reset your device." return "System reset triggered. Press confirm to erase all content and settings. Press cancel to resume boot." @@ -109,7 +125,7 @@ def main(): elif sys.argv[1] == "--format": mode = ResetMode.FORMAT - gui_app.init_window("System Reset") + gui_app.init_window("System Reset", 20) reset = Reset(mode) if mode == ResetMode.FORMAT: diff --git a/system/ui/setup.py b/system/ui/setup.py index 4d9d269480..800ca7662c 100755 --- a/system/ui/setup.py +++ b/system/ui/setup.py @@ -4,17 +4,22 @@ import re import threading import time import urllib.request +from urllib.parse import urlparse from enum import IntEnum +import shutil + import pyray as rl from cereal import log +from openpilot.common.run import run_cmd from openpilot.system.hardware import HARDWARE +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_label, gui_text_box -from openpilot.system.ui.lib.widget import Widget -from openpilot.system.ui.widgets.network import WifiManagerUI, WifiManagerWrapper +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 NetworkType = log.DeviceState.NetworkType @@ -29,15 +34,29 @@ BUTTON_SPACING = 50 OPENPILOT_URL = "https://openpilot.comma.ai" USER_AGENT = f"AGNOSSetup-{HARDWARE.get_os_version()}" +CONTINUE_PATH = "/data/continue.sh" +TMP_CONTINUE_PATH = "/data/continue.sh.new" +INSTALL_PATH = "/data/openpilot" +VALID_CACHE_PATH = "/data/.openpilot_cache" +INSTALLER_SOURCE_PATH = "/usr/comma/installer" +INSTALLER_DESTINATION_PATH = "/tmp/installer" +INSTALLER_URL_PATH = "/tmp/installer_url" + +CONTINUE = """#!/usr/bin/env bash + +cd /data/openpilot +exec ./launch_openpilot.sh +""" class SetupState(IntEnum): LOW_VOLTAGE = 0 GETTING_STARTED = 1 NETWORK_SETUP = 2 SOFTWARE_SELECTION = 3 - CUSTOM_URL = 4 + CUSTOM_SOFTWARE = 4 DOWNLOADING = 5 DOWNLOAD_FAILED = 6 + CUSTOM_SOFTWARE_WARNING = 7 class Setup(Widget): @@ -57,10 +76,57 @@ class Setup(Widget): self.wifi_ui = WifiManagerUI(self.wifi_manager) self.keyboard = Keyboard() self.selected_radio = None - self.warning = gui_app.texture("icons/warning.png", 150, 150) self.checkmark = gui_app.texture("icons/circled_check.png", 100, 100) + self._low_voltage_title_label = Label("WARNING: Low Voltage", TITLE_FONT_SIZE, FontWeight.MEDIUM, TextAlignment.LEFT, text_color=rl.Color(255, 89, 79, 255)) + self._low_voltage_body_label = Label("Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE, + text_alignment=TextAlignment.LEFT) + self._low_voltage_continue_button = Button("Continue", self._low_voltage_continue_button_callback) + self._low_voltage_poweroff_button = Button("Power Off", HARDWARE.shutdown) + + self._getting_started_button = Button("", self._getting_started_button_callback, button_style=ButtonStyle.PRIMARY, border_radius=0) + self._getting_started_title_label = Label("Getting Started", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._getting_started_body_label = Label("Before we get on the road, let's finish installation and cover some details.", + BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + + self._software_selection_openpilot_button = ButtonRadio("openpilot", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) + self._software_selection_custom_software_button = ButtonRadio("Custom Software", self.checkmark, font_size=BODY_FONT_SIZE, text_padding=80) + self._software_selection_continue_button = Button("Continue", self._software_selection_continue_button_callback, + button_style=ButtonStyle.PRIMARY) + self._software_selection_continue_button.set_enabled(False) + self._software_selection_back_button = Button("Back", self._software_selection_back_button_callback) + self._software_selection_title_label = Label("Choose Software to Use", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + + self._download_failed_reboot_button = Button("Reboot device", HARDWARE.reboot) + self._download_failed_startover_button = Button("Start over", self._download_failed_startover_button_callback, button_style=ButtonStyle.PRIMARY) + self._download_failed_title_label = Label("Download Failed", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + self._download_failed_url_label = Label("", 64, FontWeight.NORMAL, TextAlignment.LEFT) + self._download_failed_body_label = Label("", BODY_FONT_SIZE, text_alignment=TextAlignment.LEFT) + + self._network_setup_back_button = Button("Back", self._network_setup_back_button_callback) + self._network_setup_continue_button = Button("Waiting for internet", self._network_setup_continue_button_callback, + button_style=ButtonStyle.PRIMARY) + self._network_setup_continue_button.set_enabled(False) + self._network_setup_title_label = Label("Connect to Wi-Fi", TITLE_FONT_SIZE, FontWeight.BOLD, TextAlignment.LEFT) + + self._custom_software_warning_continue_button = Button("Scroll to continue", self._custom_software_warning_continue_button_callback, + button_style=ButtonStyle.PRIMARY) + self._custom_software_warning_continue_button.set_enabled(False) + self._custom_software_warning_back_button = Button("Back", self._custom_software_warning_back_button_callback) + self._custom_software_warning_title_label = Label("WARNING: Custom Software", 100, FontWeight.BOLD, TextAlignment.LEFT, text_color=rl.Color(255,89,79,255), + text_padding=60) + self._custom_software_warning_body_label = Label("Use caution when installing third-party software.\n\n" + + "âš ï¸ It has not been tested by comma.\n\n" + + "âš ï¸ It may not comply with relevant safety standards.\n\n" + + "âš ï¸ It may cause damage to your device and/or vehicle.\n\n" + + "If you'd like to proceed, use https://flash.comma.ai " + + "to restore your device to a factory state later.", + 85, text_alignment=TextAlignment.LEFT, text_padding=60) + self._custom_software_warning_body_scroll_panel = GuiScrollPanel() + + self._downloading_body_label = Label("Downloading...", TITLE_FONT_SIZE, FontWeight.MEDIUM) + try: with open("/sys/class/hwmon/hwmon1/in1_input") as f: voltage = float(f.read().strip()) / 1000.0 @@ -78,48 +144,71 @@ class Setup(Widget): self.render_network_setup(rect) elif self.state == SetupState.SOFTWARE_SELECTION: self.render_software_selection(rect) - elif self.state == SetupState.CUSTOM_URL: - self.render_custom_url() + elif self.state == SetupState.CUSTOM_SOFTWARE_WARNING: + self.render_custom_software_warning(rect) + elif self.state == SetupState.CUSTOM_SOFTWARE: + self.render_custom_software() elif self.state == SetupState.DOWNLOADING: self.render_downloading(rect) elif self.state == SetupState.DOWNLOAD_FAILED: self.render_download_failed(rect) + def _low_voltage_continue_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _custom_software_warning_back_button_callback(self): + self.state = SetupState.SOFTWARE_SELECTION + + def _custom_software_warning_continue_button_callback(self): + self.state = SetupState.NETWORK_SETUP + self.stop_network_check_thread.clear() + self.start_network_check() + + def _getting_started_button_callback(self): + self.state = SetupState.SOFTWARE_SELECTION + + def _software_selection_back_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _software_selection_continue_button_callback(self): + if self._software_selection_openpilot_button.selected: + self.use_openpilot() + else: + self.state = SetupState.CUSTOM_SOFTWARE_WARNING + + def _download_failed_startover_button_callback(self): + self.state = SetupState.GETTING_STARTED + + def _network_setup_back_button_callback(self): + self.state = SetupState.SOFTWARE_SELECTION + + def _network_setup_continue_button_callback(self): + self.stop_network_check_thread.set() + if self._software_selection_openpilot_button.selected: + self.download(OPENPILOT_URL) + else: + self.state = SetupState.CUSTOM_SOFTWARE + def render_low_voltage(self, rect: rl.Rectangle): rl.draw_texture(self.warning, int(rect.x + 150), int(rect.y + 110), rl.WHITE) - title_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE) - gui_label(title_rect, "WARNING: Low Voltage", TITLE_FONT_SIZE, rl.Color(255, 89, 79, 255), FontWeight.MEDIUM) - - body_rect = rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100 + TITLE_FONT_SIZE + 25, rect.width - 500 - 150, BODY_FONT_SIZE * 3) - gui_text_box(body_rect, "Power your device in a car with a harness or proceed at your own risk.", BODY_FONT_SIZE) + self._low_voltage_title_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 100, rect.width - 500 - 150, TITLE_FONT_SIZE)) + self._low_voltage_body_label.render(rl.Rectangle(rect.x + 150, rect.y + 110 + 150 + 150, rect.width - 500, BODY_FONT_SIZE * 3)) button_width = (rect.width - MARGIN * 3) / 2 button_y = rect.height - MARGIN - BUTTON_HEIGHT - - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Power off"): - HARDWARE.shutdown() - - if gui_button(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT), "Continue"): - self.state = SetupState.GETTING_STARTED + self._low_voltage_poweroff_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._low_voltage_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) def render_getting_started(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE) - gui_label(title_rect, "Getting Started", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) - - desc_rect = rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE + 90, rect.width - 500, BODY_FONT_SIZE * 3) - gui_text_box(desc_rect, "Before we get on the road, let's finish installation and cover some details.", BODY_FONT_SIZE) + self._getting_started_title_label.render(rl.Rectangle(rect.x + 165, rect.y + 280, rect.width - 265, TITLE_FONT_SIZE)) + self._getting_started_body_label.render(rl.Rectangle(rect.x + 165, rect.y + 280 + TITLE_FONT_SIZE, rect.width - 500, BODY_FONT_SIZE * 3)) btn_rect = rl.Rectangle(rect.width - NEXT_BUTTON_WIDTH, 0, NEXT_BUTTON_WIDTH, rect.height) - - ret = gui_button(btn_rect, "", button_style=ButtonStyle.PRIMARY, border_radius=0) + self._getting_started_button.render(btn_rect) triangle = gui_app.texture("images/button_continue_triangle.png", 54, int(btn_rect.height)) rl.draw_texture_v(triangle, rl.Vector2(btn_rect.x + btn_rect.width / 2 - triangle.width / 2, btn_rect.height / 2 - triangle.height / 2), rl.WHITE) - if ret: - self.state = SetupState.NETWORK_SETUP - self.start_network_check() - def check_network_connectivity(self): while not self.stop_network_check_thread.is_set(): if self.state == SetupState.NETWORK_SETUP: @@ -145,8 +234,7 @@ class Setup(Widget): self.network_check_thread.join() def render_network_setup(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) - gui_label(title_rect, "Connect to Wi-Fi", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._network_setup_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) wifi_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN + 25, rect.width - MARGIN * 2, rect.height - TITLE_FONT_SIZE - 25 - BUTTON_HEIGHT - MARGIN * 3) @@ -157,102 +245,79 @@ class Setup(Widget): button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): - self.state = SetupState.GETTING_STARTED + self._network_setup_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) # Check network connectivity status continue_enabled = self.network_connected.is_set() + self._network_setup_continue_button.set_enabled(continue_enabled) continue_text = ("Continue" if self.wifi_connected.is_set() else "Continue without Wi-Fi") if continue_enabled else "Waiting for internet" - - if gui_button( - rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), - continue_text, - button_style=ButtonStyle.PRIMARY if continue_enabled else ButtonStyle.NORMAL, - is_enabled=continue_enabled, - ): - self.state = SetupState.SOFTWARE_SELECTION - self.stop_network_check_thread.set() + self._network_setup_continue_button.set_text(continue_text) + self._network_setup_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_software_selection(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE) - gui_label(title_rect, "Choose Software to Install", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._software_selection_title_label.render(rl.Rectangle(rect.x + MARGIN, rect.y + MARGIN, rect.width - MARGIN * 2, TITLE_FONT_SIZE)) radio_height = 230 radio_spacing = 30 + self._software_selection_continue_button.set_enabled(False) + openpilot_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2, rect.width - MARGIN * 2, radio_height) - openpilot_selected = self.selected_radio == "openpilot" + self._software_selection_openpilot_button.render(openpilot_rect) - rl.draw_rectangle_rounded(openpilot_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if openpilot_selected else rl.Color(79, 79, 79, 255)) - gui_label(rl.Rectangle(openpilot_rect.x + 100, openpilot_rect.y, openpilot_rect.width - 200, radio_height), "openpilot", BODY_FONT_SIZE) - - if openpilot_selected: - checkmark_pos = rl.Vector2(openpilot_rect.x + openpilot_rect.width - 100 - self.checkmark.width, - openpilot_rect.y + radio_height / 2 - self.checkmark.height / 2) - rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) + if self._software_selection_openpilot_button.selected: + self._software_selection_continue_button.set_enabled(True) + self._software_selection_custom_software_button.selected = False custom_rect = rl.Rectangle(rect.x + MARGIN, rect.y + TITLE_FONT_SIZE + MARGIN * 2 + radio_height + radio_spacing, rect.width - MARGIN * 2, radio_height) - custom_selected = self.selected_radio == "custom" + self._software_selection_custom_software_button.render(custom_rect) - rl.draw_rectangle_rounded(custom_rect, 0.1, 10, rl.Color(70, 91, 234, 255) if custom_selected else rl.Color(79, 79, 79, 255)) - gui_label(rl.Rectangle(custom_rect.x + 100, custom_rect.y, custom_rect.width - 200, radio_height), "Custom Software", BODY_FONT_SIZE) - - if custom_selected: - checkmark_pos = rl.Vector2(custom_rect.x + custom_rect.width - 100 - self.checkmark.width, custom_rect.y + radio_height / 2 - self.checkmark.height / 2) - rl.draw_texture_v(self.checkmark, checkmark_pos, rl.WHITE) - - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): - if rl.check_collision_point_rec(mouse_pos, openpilot_rect): - self.selected_radio = "openpilot" - elif rl.check_collision_point_rec(mouse_pos, custom_rect): - self.selected_radio = "custom" + if self._software_selection_custom_software_button.selected: + self._software_selection_continue_button.set_enabled(True) + self._software_selection_openpilot_button.selected = False button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Back"): - self.state = SetupState.NETWORK_SETUP - - continue_enabled = self.selected_radio is not None - if gui_button( - rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), - "Continue", - button_style=ButtonStyle.PRIMARY, - is_enabled=continue_enabled, - ): - if continue_enabled: - if self.selected_radio == "openpilot": - self.download(OPENPILOT_URL) - else: - self.state = SetupState.CUSTOM_URL + self._software_selection_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._software_selection_continue_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) def render_downloading(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE) - gui_label(title_rect, "Downloading...", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) + self._downloading_body_label.render(rl.Rectangle(rect.x, rect.y + rect.height / 2 - TITLE_FONT_SIZE / 2, rect.width, TITLE_FONT_SIZE)) def render_download_failed(self, rect: rl.Rectangle): - title_rect = rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE) - gui_label(title_rect, "Download Failed", TITLE_FONT_SIZE, font_weight=FontWeight.MEDIUM) + self._download_failed_title_label.render(rl.Rectangle(rect.x + 117, rect.y + 185, rect.width - 117, TITLE_FONT_SIZE)) + self._download_failed_url_label.set_text(self.failed_url) + self._download_failed_url_label.render(rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64)) - url_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67, rect.width - 117 - 100, 64) - gui_label(url_rect, self.failed_url, 64, font_weight=FontWeight.NORMAL) - - error_rect = rl.Rectangle(rect.x + 117, rect.y + 185 + TITLE_FONT_SIZE + 67 + 64 + 48, - rect.width - 117 - 100, rect.height - 185 + TITLE_FONT_SIZE + 67 + 64 + 48 - BUTTON_HEIGHT - MARGIN * 2) - gui_text_box(error_rect, self.failed_reason, BODY_FONT_SIZE) + self._download_failed_body_label.set_text(self.failed_reason) + self._download_failed_body_label.render(rl.Rectangle(rect.x + 117, rect.y, rect.width - 117 - 100, rect.height)) button_width = (rect.width - BUTTON_SPACING - MARGIN * 2) / 2 button_y = rect.height - BUTTON_HEIGHT - MARGIN + self._download_failed_reboot_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._download_failed_startover_button.render(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT)) - if gui_button(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT), "Reboot device"): - HARDWARE.reboot() + def render_custom_software_warning(self, rect: rl.Rectangle): + warn_rect = rl.Rectangle(rect.x, rect.y, rect.width, 1500) + offset = self._custom_software_warning_body_scroll_panel.handle_scroll(rect, warn_rect) - if gui_button(rl.Rectangle(rect.x + MARGIN + button_width + BUTTON_SPACING, button_y, button_width, BUTTON_HEIGHT), "Start over", - button_style=ButtonStyle.PRIMARY): - self.state = SetupState.GETTING_STARTED + button_width = (rect.width - MARGIN * 3) / 2 + button_y = rect.height - MARGIN - BUTTON_HEIGHT - def render_custom_url(self): + rl.begin_scissor_mode(int(rect.x), int(rect.y), int(rect.width), int(button_y - BODY_FONT_SIZE)) + y_offset = rect.y + offset.y + self._custom_software_warning_title_label.render(rl.Rectangle(rect.x + 50, y_offset + 150, rect.width - 265, TITLE_FONT_SIZE)) + self._custom_software_warning_body_label.render(rl.Rectangle(rect.x + 50, y_offset + 200 , rect.width - 50, BODY_FONT_SIZE * 3)) + rl.end_scissor_mode() + + self._custom_software_warning_back_button.render(rl.Rectangle(rect.x + MARGIN, button_y, button_width, BUTTON_HEIGHT)) + self._custom_software_warning_continue_button.render(rl.Rectangle(rect.x + MARGIN * 2 + button_width, button_y, button_width, BUTTON_HEIGHT)) + if offset.y < (rect.height - warn_rect.height): + self._custom_software_warning_continue_button.set_enabled(True) + self._custom_software_warning_continue_button.set_text("Continue") + + def render_custom_software(self): def handle_keyboard_result(result): # Enter pressed if result == 1: @@ -265,15 +330,35 @@ class Setup(Widget): elif result == 0: self.state = SetupState.SOFTWARE_SELECTION + self.keyboard.reset() self.keyboard.set_title("Enter URL", "for Custom Software") gui_app.set_modal_overlay(self.keyboard, callback=handle_keyboard_result) + def use_openpilot(self): + if os.path.isdir(INSTALL_PATH) and os.path.isfile(VALID_CACHE_PATH): + os.remove(VALID_CACHE_PATH) + with open(TMP_CONTINUE_PATH, "w") as f: + f.write(CONTINUE) + run_cmd(["chmod", "+x", TMP_CONTINUE_PATH]) + shutil.move(TMP_CONTINUE_PATH, CONTINUE_PATH) + shutil.copyfile(INSTALLER_SOURCE_PATH, INSTALLER_DESTINATION_PATH) + + # give time for installer UI to take over + time.sleep(1) + gui_app.request_close() + else: + self.state = SetupState.NETWORK_SETUP + self.stop_network_check_thread.clear() + self.start_network_check() + def download(self, url: str): # autocomplete incomplete URLs if re.match("^([^/.]+)/([^/]+)$", url): url = f"https://installer.comma.ai/{url}" - self.download_url = url + parsed = urlparse(url, scheme='https') + self.download_url = (urlparse(f"https://{url}") if not parsed.netloc else parsed).geturl() + self.state = SetupState.DOWNLOADING self.download_thread = threading.Thread(target=self._download_thread, daemon=True) @@ -283,7 +368,7 @@ class Setup(Widget): try: import tempfile - _, tmpfile = tempfile.mkstemp(prefix="installer_") + fd, tmpfile = tempfile.mkstemp(prefix="installer_") headers = {"User-Agent": USER_AGENT, "X-openpilot-serial": HARDWARE.get_serial()} req = urllib.request.Request(self.download_url, headers=headers) @@ -313,12 +398,16 @@ class Setup(Widget): self.download_failed(self.download_url, "No custom software found at this URL.") return - os.rename(tmpfile, "/tmp/installer") - os.chmod("/tmp/installer", 0o755) + # AGNOS might try to execute the installer before this process exits. + # Therefore, important to close the fd before renaming the installer. + os.close(fd) + os.rename(tmpfile, INSTALLER_DESTINATION_PATH) - with open("/tmp/installer_url", "w") as f: + with open(INSTALLER_URL_PATH, "w") as f: f.write(self.download_url) + # give time for installer UI to take over + time.sleep(5) gui_app.request_close() except Exception: @@ -333,7 +422,7 @@ class Setup(Widget): def main(): try: - gui_app.init_window("Setup") + gui_app.init_window("Setup", 20) setup = Setup() for _ in gui_app.render(): setup.render(rl.Rectangle(0, 0, gui_app.width, gui_app.height)) diff --git a/system/ui/spinner.py b/system/ui/spinner.py index 279ddf034d..dd7fadc538 100755 --- a/system/ui/spinner.py +++ b/system/ui/spinner.py @@ -5,8 +5,8 @@ import sys from openpilot.system.ui.lib.application import gui_app from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.text import wrap_text +from openpilot.system.ui.widgets import Widget from openpilot.system.ui.sunnypilot.lib.application import gui_app_sp diff --git a/system/ui/text.py b/system/ui/text.py index 01a580a12b..61ac043b72 100755 --- a/system/ui/text.py +++ b/system/ui/text.py @@ -3,11 +3,11 @@ import re import sys import pyray as rl from openpilot.system.hardware import HARDWARE, PC -from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle MARGIN = 50 SPACING = 40 diff --git a/system/ui/updater.py b/system/ui/updater.py index 3fd60cfb1d..31799d3628 100755 --- a/system/ui/updater.py +++ b/system/ui/updater.py @@ -7,10 +7,10 @@ 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.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_text_box, gui_label from openpilot.system.ui.lib.wifi_manager import WifiManagerWrapper -from openpilot.system.ui.lib.widget import Widget +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 from openpilot.system.ui.widgets.network import WifiManagerUI # Constants diff --git a/system/ui/lib/widget.py b/system/ui/widgets/__init__.py similarity index 52% rename from system/ui/lib/widget.py rename to system/ui/widgets/__init__.py index 3539f5594b..d45f48ac38 100644 --- a/system/ui/lib/widget.py +++ b/system/ui/widgets/__init__.py @@ -2,6 +2,7 @@ import abc import pyray as rl from enum import IntEnum from collections.abc import Callable +from openpilot.system.ui.lib.application import gui_app, MousePos, MAX_TOUCH_SLOTS class DialogResult(IntEnum): @@ -14,29 +15,18 @@ class Widget(abc.ABC): def __init__(self): self._rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) self._parent_rect: rl.Rectangle = rl.Rectangle(0, 0, 0, 0) - self._is_pressed = False + self._is_pressed = [False] * MAX_TOUCH_SLOTS + # if current mouse/touch down started within the widget's rectangle + self._tracking_is_pressed = [False] * MAX_TOUCH_SLOTS + self._enabled: bool | Callable[[], bool] = True self._is_visible: bool | Callable[[], bool] = True self._touch_valid_callback: Callable[[], bool] | None = None - - def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: - """Set a callback to determine if the widget can be clicked.""" - self._touch_valid_callback = touch_callback - - def _touch_valid(self) -> bool: - """Check if the widget can be touched.""" - return self._touch_valid_callback() if self._touch_valid_callback else True - - @property - def is_visible(self) -> bool: - return self._is_visible() if callable(self._is_visible) else self._is_visible + self._multi_touch = False @property def rect(self) -> rl.Rectangle: return self._rect - def set_visible(self, visible: bool | Callable[[], bool]) -> None: - self._is_visible = visible - def set_rect(self, rect: rl.Rectangle) -> None: changed = (self._rect.x != rect.x or self._rect.y != rect.y or self._rect.width != rect.width or self._rect.height != rect.height) @@ -48,6 +38,32 @@ class Widget(abc.ABC): """Can be used like size hint in QT""" self._parent_rect = parent_rect + @property + def is_pressed(self) -> bool: + return any(self._is_pressed) + + @property + def enabled(self) -> bool: + return self._enabled() if callable(self._enabled) else self._enabled + + def set_enabled(self, enabled: bool | Callable[[], bool]) -> None: + self._enabled = enabled + + @property + def is_visible(self) -> bool: + return self._is_visible() if callable(self._is_visible) else self._is_visible + + def set_visible(self, visible: bool | Callable[[], bool]) -> None: + self._is_visible = visible + + def set_touch_valid_callback(self, touch_callback: Callable[[], bool]) -> None: + """Set a callback to determine if the widget can be clicked.""" + self._touch_valid_callback = touch_callback + + def _touch_valid(self) -> bool: + """Check if the widget can be touched.""" + return self._touch_valid_callback() if self._touch_valid_callback else True + def set_position(self, x: float, y: float) -> None: changed = (self._rect.x != x or self._rect.y != y) self._rect.x, self._rect.y = x, y @@ -66,18 +82,37 @@ class Widget(abc.ABC): ret = self._render(self._rect) # Keep track of whether mouse down started within the widget's rectangle - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._touch_valid(): - if rl.check_collision_point_rec(mouse_pos, self._rect): - self._is_pressed = True + if self.enabled: + for mouse_event in gui_app.mouse_events: + if not self._multi_touch and mouse_event.slot != 0: + continue - elif not self._touch_valid(): - self._is_pressed = False + # Ignores touches/presses that start outside our rect + # Allows touch to leave the rect and come back in focus if mouse did not release + if mouse_event.left_pressed and self._touch_valid(): + if rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._is_pressed[mouse_event.slot] = True + self._tracking_is_pressed[mouse_event.slot] = True - elif rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT): - if self._is_pressed and rl.check_collision_point_rec(mouse_pos, self._rect): - self._handle_mouse_release(mouse_pos) - self._is_pressed = False + # Callback such as scroll panel signifies user is scrolling + elif not self._touch_valid(): + self._is_pressed[mouse_event.slot] = False + self._tracking_is_pressed[mouse_event.slot] = False + + elif mouse_event.left_released: + if self._is_pressed[mouse_event.slot] and rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._handle_mouse_release(mouse_event.pos) + self._is_pressed[mouse_event.slot] = False + self._tracking_is_pressed[mouse_event.slot] = False + + # Mouse/touch is still within our rect + elif rl.check_collision_point_rec(mouse_event.pos, self._rect): + if self._tracking_is_pressed[mouse_event.slot]: + self._is_pressed[mouse_event.slot] = True + + # Mouse/touch left our rect but may come back into focus later + elif not rl.check_collision_point_rec(mouse_event.pos, self._rect): + self._is_pressed[mouse_event.slot] = False return ret @@ -91,6 +126,6 @@ class Widget(abc.ABC): def _update_layout_rects(self) -> None: """Optionally update any layout rects on Widget rect change.""" - def _handle_mouse_release(self, mouse_pos: rl.Vector2) -> bool: + def _handle_mouse_release(self, mouse_pos: MousePos) -> bool: """Optionally handle mouse release events.""" return False diff --git a/system/ui/lib/button.py b/system/ui/widgets/button.py similarity index 55% rename from system/ui/lib/button.py rename to system/ui/widgets/button.py index 121e7f86fa..a5dab19789 100644 --- a/system/ui/lib/button.py +++ b/system/ui/widgets/button.py @@ -1,7 +1,12 @@ -import pyray as rl +from collections.abc import Callable from enum import IntEnum -from openpilot.system.ui.lib.application import gui_app, FontWeight + +import pyray as rl + +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.widgets import Widget +from openpilot.system.ui.widgets.label import TextAlignment, Label class ButtonStyle(IntEnum): @@ -11,17 +16,15 @@ class ButtonStyle(IntEnum): TRANSPARENT = 3 # For buttons with transparent background and border ACTION = 4 LIST_ACTION = 5 # For list items with action buttons - - -class TextAlignment(IntEnum): - LEFT = 0 - CENTER = 1 - RIGHT = 2 + NO_EFFECT = 6 + KEYBOARD = 7 + FORGET_WIFI = 8 ICON_PADDING = 15 DEFAULT_BUTTON_FONT_SIZE = 60 BUTTON_DISABLED_TEXT_COLOR = rl.Color(228, 228, 228, 51) +BUTTON_DISABLED_BACKGROUND_COLOR = rl.Color(51, 51, 51, 255) ACTION_BUTTON_FONT_SIZE = 48 BUTTON_TEXT_COLOR = { @@ -29,8 +32,11 @@ BUTTON_TEXT_COLOR = { ButtonStyle.PRIMARY: rl.Color(228, 228, 228, 255), ButtonStyle.DANGER: rl.Color(228, 228, 228, 255), ButtonStyle.TRANSPARENT: rl.BLACK, - ButtonStyle.ACTION: rl.Color(0, 0, 0, 255), + ButtonStyle.ACTION: rl.BLACK, ButtonStyle.LIST_ACTION: rl.Color(228, 228, 228, 255), + ButtonStyle.NO_EFFECT: rl.Color(228, 228, 228, 255), + ButtonStyle.KEYBOARD: rl.Color(221, 221, 221, 255), + ButtonStyle.FORGET_WIFI: rl.Color(51, 51, 51, 255), } BUTTON_BACKGROUND_COLORS = { @@ -40,6 +46,9 @@ BUTTON_BACKGROUND_COLORS = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(189, 189, 189, 255), ButtonStyle.LIST_ACTION: rl.Color(57, 57, 57, 255), + ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), + ButtonStyle.KEYBOARD: rl.Color(68, 68, 68, 255), + ButtonStyle.FORGET_WIFI: rl.Color(189, 189, 189, 255), } BUTTON_PRESSED_BACKGROUND_COLORS = { @@ -49,11 +58,16 @@ BUTTON_PRESSED_BACKGROUND_COLORS = { ButtonStyle.TRANSPARENT: rl.BLACK, ButtonStyle.ACTION: rl.Color(130, 130, 130, 255), ButtonStyle.LIST_ACTION: rl.Color(74, 74, 74, 74), + ButtonStyle.NO_EFFECT: rl.Color(51, 51, 51, 255), + ButtonStyle.KEYBOARD: rl.Color(51, 51, 51, 255), + ButtonStyle.FORGET_WIFI: rl.Color(130, 130, 130, 255), } _pressed_buttons: set[str] = set() # Track mouse press state globally +# TODO: This should be a Widget class + def gui_button( rect: rl.Rectangle, text: str, @@ -146,3 +160,92 @@ def gui_button( rl.draw_text_ex(font, text, text_pos, font_size, 0, color) return result + + +class Button(Widget): + def __init__(self, + text: str, + click_callback: Callable[[], None] = None, + font_size: int = DEFAULT_BUTTON_FONT_SIZE, + font_weight: FontWeight = FontWeight.MEDIUM, + button_style: ButtonStyle = ButtonStyle.NORMAL, + border_radius: int = 10, + text_alignment: TextAlignment = TextAlignment.CENTER, + text_padding: int = 20, + icon = None, + multi_touch: bool = False, + ): + + super().__init__() + self._button_style = button_style + self._border_radius = border_radius + self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] + + self._label = Label(text, font_size, font_weight, text_alignment, text_padding, + BUTTON_TEXT_COLOR[self._button_style], icon=icon) + + self._click_callback = click_callback + self._multi_touch = multi_touch + + def set_text(self, text): + self._label.set_text(text) + + def _handle_mouse_release(self, mouse_pos: MousePos): + if self._click_callback and self.enabled: + self._click_callback() + + def _update_state(self): + if self.enabled: + self._label.set_text_color(BUTTON_TEXT_COLOR[self._button_style]) + if self.is_pressed: + self._background_color = BUTTON_PRESSED_BACKGROUND_COLORS[self._button_style] + else: + self._background_color = BUTTON_BACKGROUND_COLORS[self._button_style] + elif self._button_style != ButtonStyle.NO_EFFECT: + self._background_color = BUTTON_DISABLED_BACKGROUND_COLOR + self._label.set_text_color(BUTTON_DISABLED_TEXT_COLOR) + + def _render(self, _): + roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) + self._label.render(self._rect) + + +class ButtonRadio(Button): + def __init__(self, + text: str, + icon, + click_callback: Callable[[], None] = None, + font_size: int = DEFAULT_BUTTON_FONT_SIZE, + text_alignment: TextAlignment = TextAlignment.LEFT, + border_radius: int = 10, + text_padding: int = 20, + ): + + super().__init__(text, click_callback=click_callback, font_size=font_size, + border_radius=border_radius, text_padding=text_padding, + text_alignment=text_alignment) + self._text_padding = text_padding + self._icon = icon + self.selected = False + + def _handle_mouse_release(self, mouse_pos: MousePos): + self.selected = not self.selected + if self._click_callback: + self._click_callback() + + def _update_state(self): + if self.selected: + self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.PRIMARY] + else: + self._background_color = BUTTON_BACKGROUND_COLORS[ButtonStyle.NORMAL] + + def _render(self, _): + roundness = self._border_radius / (min(self._rect.width, self._rect.height) / 2) + rl.draw_rectangle_rounded(self._rect, roundness, 10, self._background_color) + self._label.render(self._rect) + + if self._icon and self.selected: + icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 + icon_x = self._rect.x + self._rect.width - self._icon.width - self._text_padding - ICON_PADDING + rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE if self.enabled else rl.Color(255, 255, 255, 100)) diff --git a/system/ui/widgets/confirm_dialog.py b/system/ui/widgets/confirm_dialog.py index 00361905d6..1021b5452b 100644 --- a/system/ui/widgets/confirm_dialog.py +++ b/system/ui/widgets/confirm_dialog.py @@ -1,8 +1,9 @@ import pyray as rl from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.label import gui_text_box -from openpilot.system.ui.lib.widget import DialogResult +from openpilot.system.ui.widgets import DialogResult +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, Button +from openpilot.system.ui.widgets.label import gui_text_box, Label +from openpilot.system.ui.widgets import Widget DIALOG_WIDTH = 1520 DIALOG_HEIGHT = 600 @@ -11,6 +12,59 @@ MARGIN = 50 TEXT_AREA_HEIGHT_REDUCTION = 200 BACKGROUND_COLOR = rl.Color(27, 27, 27, 255) +class ConfirmDialog(Widget): + def __init__(self, text: str, confirm_text: str, cancel_text: str = "Cancel"): + super().__init__() + self._label = Label(text, 70, FontWeight.BOLD) + self._cancel_button = Button(cancel_text, self._cancel_button_callback) + self._confirm_button = Button(confirm_text, self._confirm_button_callback, button_style=ButtonStyle.PRIMARY) + self._dialog_result = DialogResult.NO_ACTION + self._cancel_text = cancel_text + + def set_text(self, text): + self._label.set_text(text) + + def reset(self): + self._dialog_result = DialogResult.NO_ACTION + + def _cancel_button_callback(self): + self._dialog_result = DialogResult.CANCEL + + def _confirm_button_callback(self): + self._dialog_result = DialogResult.CONFIRM + + def _render(self, rect: rl.Rectangle): + dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 + dialog_y = (gui_app.height - DIALOG_HEIGHT) / 2 + dialog_rect = rl.Rectangle(dialog_x, dialog_y, DIALOG_WIDTH, DIALOG_HEIGHT) + + bottom = dialog_rect.y + dialog_rect.height + button_width = (dialog_rect.width - 3 * MARGIN) // 2 + cancel_button_x = dialog_rect.x + MARGIN + confirm_button_x = dialog_rect.x + dialog_rect.width - button_width - MARGIN + button_y = bottom - BUTTON_HEIGHT - MARGIN + cancel_button = rl.Rectangle(cancel_button_x, button_y, button_width, BUTTON_HEIGHT) + confirm_button = rl.Rectangle(confirm_button_x, button_y, button_width, BUTTON_HEIGHT) + + rl.draw_rectangle_rec(dialog_rect, BACKGROUND_COLOR) + + text_rect = rl.Rectangle(dialog_rect.x + MARGIN, dialog_rect.y, dialog_rect.width - 2 * MARGIN, dialog_rect.height - TEXT_AREA_HEIGHT_REDUCTION) + self._label.render(text_rect) + + if rl.is_key_pressed(rl.KeyboardKey.KEY_ENTER): + self._dialog_result = DialogResult.CONFIRM + elif rl.is_key_pressed(rl.KeyboardKey.KEY_ESCAPE): + self._dialog_result = DialogResult.CANCEL + + if self._cancel_text: + self._confirm_button.render(confirm_button) + self._cancel_button.render(cancel_button) + else: + centered_button_x = dialog_rect.x + (dialog_rect.width - button_width) / 2 + centered_confirm_button = rl.Rectangle(centered_button_x, button_y, button_width, BUTTON_HEIGHT) + self._confirm_button.render(centered_confirm_button) + + return self._dialog_result def confirm_dialog(message: str, confirm_text: str, cancel_text: str = "Cancel") -> DialogResult: dialog_x = (gui_app.width - DIALOG_WIDTH) / 2 diff --git a/system/ui/widgets/html_render.py b/system/ui/widgets/html_render.py index 07741af456..247b7a5492 100644 --- a/system/ui/widgets/html_render.py +++ b/system/ui/widgets/html_render.py @@ -4,10 +4,10 @@ from dataclasses import dataclass from enum import Enum from typing import Any from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.wrap_text import wrap_text from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.widget import Widget, DialogResult -from openpilot.system.ui.lib.button import gui_button, ButtonStyle +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget, DialogResult +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle class ElementType(Enum): diff --git a/system/ui/lib/inputbox.py b/system/ui/widgets/inputbox.py similarity index 85% rename from system/ui/lib/inputbox.py rename to system/ui/widgets/inputbox.py index ccf718ba56..239d63037e 100644 --- a/system/ui/lib/inputbox.py +++ b/system/ui/widgets/inputbox.py @@ -1,8 +1,8 @@ import pyray as rl import time -from openpilot.system.ui.lib.application import gui_app +from openpilot.system.ui.lib.application import gui_app, MousePos from openpilot.system.ui.lib.text_measure import measure_text_cached -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget PASSWORD_MASK_CHAR = "•" PASSWORD_MASK_DELAY = 1.5 # Seconds to show character before masking @@ -107,9 +107,6 @@ class InputBox(Widget): self._visible_width = rect.width self._font_size = font_size - # Handle mouse input - self._handle_mouse_input(rect, font_size) - # Draw input box rl.draw_rectangle_rec(rect, color) @@ -169,30 +166,27 @@ class InputBox(Widget): return masked_text - def _handle_mouse_input(self, rect, font_size): - """Handle mouse clicks to position cursor.""" - mouse_pos = rl.get_mouse_position() - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec(mouse_pos, rect): - # Calculate cursor position from click - if len(self._input_text) > 0: - font = gui_app.font() - display_text = self._get_display_text() + def _handle_mouse_release(self, mouse_pos: MousePos): + # Calculate cursor position from click + if len(self._input_text) > 0: + font = gui_app.font() + display_text = self._get_display_text() - # Find the closest character position to the click - relative_x = mouse_pos.x - (rect.x + 10) + self._text_offset - best_pos = 0 - min_distance = float('inf') + # Find the closest character position to the click + relative_x = mouse_pos.x - (self._rect.x + 10) + self._text_offset + best_pos = 0 + min_distance = float('inf') - for i in range(len(self._input_text) + 1): - char_width = measure_text_cached(font, display_text[:i], font_size).x - distance = abs(relative_x - char_width) - if distance < min_distance: - min_distance = distance - best_pos = i + for i in range(len(self._input_text) + 1): + char_width = measure_text_cached(font, display_text[:i], self._font_size).x + distance = abs(relative_x - char_width) + if distance < min_distance: + min_distance = distance + best_pos = i - self.set_cursor_position(best_pos) - else: - self.set_cursor_position(0) + self.set_cursor_position(best_pos) + else: + self.set_cursor_position(0) def _handle_keyboard_input(self): # Handle navigation keys diff --git a/system/ui/widgets/keyboard.py b/system/ui/widgets/keyboard.py index 338706e73d..25843d1ce8 100644 --- a/system/ui/widgets/keyboard.py +++ b/system/ui/widgets/keyboard.py @@ -1,11 +1,14 @@ +from functools import partial import time from typing import Literal + import pyray as rl + from openpilot.system.ui.lib.application import gui_app, FontWeight -from openpilot.system.ui.lib.button import ButtonStyle, gui_button -from openpilot.system.ui.lib.inputbox import InputBox -from openpilot.system.ui.lib.label import gui_label -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import ButtonStyle, Button +from openpilot.system.ui.widgets.inputbox import InputBox +from openpilot.system.ui.widgets.label import Label, TextAlignment KEY_FONT_SIZE = 96 DOUBLE_CLICK_THRESHOLD = 0.5 # seconds @@ -59,8 +62,8 @@ class Keyboard(Widget): self._layout_name: Literal["lowercase", "uppercase", "numbers", "specials"] = "lowercase" self._caps_lock = False self._last_shift_press_time = 0 - self._title = "" - self._sub_title = "" + self._title = Label("", 90, FontWeight.BOLD, TextAlignment.LEFT) + self._sub_title = Label("", 55, FontWeight.NORMAL, TextAlignment.LEFT) self._max_text_size = max_text_size self._min_text_size = min_text_size @@ -73,6 +76,11 @@ class Keyboard(Widget): self._backspace_press_time: float = 0.0 self._backspace_last_repeat: float = 0.0 + self._render_return_status = -1 + self._cancel_button = Button("Cancel", self._cancel_button_callback) + + self._eye_button = Button("", self._eye_button_callback, button_style=ButtonStyle.TRANSPARENT) + self._eye_open_texture = gui_app.texture("icons/eye_open.png", 81, 54) self._eye_closed_texture = gui_app.texture("icons/eye_closed.png", 81, 54) self._key_icons = { @@ -83,6 +91,19 @@ class Keyboard(Widget): ENTER_KEY: gui_app.texture("icons/arrow-right.png", 80, 80), } + self._all_keys = {} + for l in KEYBOARD_LAYOUTS: + for _, keys in enumerate(KEYBOARD_LAYOUTS[l]): + for _, key in enumerate(keys): + if key in self._key_icons: + texture = self._key_icons[key] + self._all_keys[key] = Button("", partial(self._key_callback, key), icon=texture, + button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.KEYBOARD, multi_touch=True) + else: + self._all_keys[key] = Button(key, partial(self._key_callback, key), button_style=ButtonStyle.KEYBOARD, font_size=85, multi_touch=True) + self._all_keys[CAPS_LOCK_KEY] = Button("", partial(self._key_callback, CAPS_LOCK_KEY), icon=self._key_icons[CAPS_LOCK_KEY], + button_style=ButtonStyle.KEYBOARD, multi_touch=True) + @property def text(self): return self._input_box.text @@ -94,16 +115,27 @@ class Keyboard(Widget): self._backspace_pressed = False def set_title(self, title: str, sub_title: str = ""): - self._title = title - self._sub_title = sub_title + self._title.set_text(title) + self._sub_title.set_text(sub_title) + + def _eye_button_callback(self): + self._password_mode = not self._password_mode + + def _cancel_button_callback(self): + self.clear() + self._render_return_status = 0 + + def _key_callback(self, k): + if k == ENTER_KEY: + self._render_return_status = 1 + else: + self.handle_key_press(k) def _render(self, rect: rl.Rectangle): rect = rl.Rectangle(rect.x + CONTENT_MARGIN, rect.y + CONTENT_MARGIN, rect.width - 2 * CONTENT_MARGIN, rect.height - 2 * CONTENT_MARGIN) - gui_label(rl.Rectangle(rect.x, rect.y, rect.width, 95), self._title, 90, font_weight=FontWeight.BOLD) - gui_label(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60), self._sub_title, 55, font_weight=FontWeight.NORMAL) - if gui_button(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125), "Cancel"): - self.clear() - return 0 + self._title.render(rl.Rectangle(rect.x, rect.y, rect.width, 95)) + self._sub_title.render(rl.Rectangle(rect.x, rect.y + 95, rect.width, 60)) + self._cancel_button.render(rl.Rectangle(rect.x + rect.width - 386, rect.y, 386, 125)) # Draw input box and password toggle input_margin = 25 @@ -111,7 +143,7 @@ class Keyboard(Widget): self._render_input_area(input_box_rect) # Process backspace key repeat if it's held down - if not rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT): + if not self._all_keys[BACKSPACE_KEY].is_pressed: self._backspace_pressed = False if self._backspace_pressed: @@ -146,33 +178,22 @@ class Keyboard(Widget): start_x += new_width is_enabled = key != ENTER_KEY or len(self._input_box.text) >= self._min_text_size - result = -1 - # Check for backspace key press-and-hold - mouse_pos = rl.get_mouse_position() - mouse_over_key = rl.check_collision_point_rec(mouse_pos, key_rect) - - if key == BACKSPACE_KEY and mouse_over_key: - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT): - self._backspace_pressed = True - self._backspace_press_time = time.monotonic() - self._backspace_last_repeat = time.monotonic() + if key == BACKSPACE_KEY and self._all_keys[BACKSPACE_KEY].is_pressed and not self._backspace_pressed: + self._backspace_pressed = True + self._backspace_press_time = time.monotonic() + self._backspace_last_repeat = time.monotonic() if key in self._key_icons: if key == SHIFT_ACTIVE_KEY and self._caps_lock: key = CAPS_LOCK_KEY - texture = self._key_icons[key] - result = gui_button(key_rect, "", icon=texture, button_style=ButtonStyle.PRIMARY if key == ENTER_KEY else ButtonStyle.NORMAL, is_enabled=is_enabled) + self._all_keys[key].set_enabled(is_enabled) + self._all_keys[key].render(key_rect) else: - result = gui_button(key_rect, key, KEY_FONT_SIZE, is_enabled=is_enabled) + self._all_keys[key].set_enabled(is_enabled) + self._all_keys[key].render(key_rect) - if result: - if key == ENTER_KEY: - return 1 - else: - self.handle_key_press(key) - - return -1 + return self._render_return_status def _render_input_area(self, input_rect: rl.Rectangle): if self._show_password_toggle: @@ -183,16 +204,12 @@ class Keyboard(Widget): eye_texture = self._eye_closed_texture if self._password_mode else self._eye_open_texture eye_rect = rl.Rectangle(input_rect.x + input_rect.width - 90, input_rect.y, 80, input_rect.height) + self._eye_button.render(eye_rect) + eye_x = eye_rect.x + (eye_rect.width - eye_texture.width) / 2 eye_y = eye_rect.y + (eye_rect.height - eye_texture.height) / 2 rl.draw_texture_v(eye_texture, rl.Vector2(eye_x, eye_y), rl.WHITE) - - # Handle click on eye icon - if rl.is_mouse_button_pressed(rl.MouseButton.MOUSE_BUTTON_LEFT) and rl.check_collision_point_rec( - rl.get_mouse_position(), eye_rect - ): - self._password_mode = not self._password_mode else: self._input_box.render(input_rect) @@ -226,6 +243,10 @@ class Keyboard(Widget): if not self._caps_lock and self._layout_name == "uppercase": self._layout_name = "lowercase" + def reset(self): + self._render_return_status = -1 + self.clear() + if __name__ == "__main__": gui_app.init_window("Keyboard") diff --git a/system/ui/widgets/label.py b/system/ui/widgets/label.py new file mode 100644 index 0000000000..2da9a9f8df --- /dev/null +++ b/system/ui/widgets/label.py @@ -0,0 +1,177 @@ +from enum import IntEnum +from itertools import zip_longest + +import pyray as rl + +from openpilot.system.ui.lib.application import gui_app, FontWeight, DEFAULT_TEXT_SIZE, DEFAULT_TEXT_COLOR +from openpilot.system.ui.lib.text_measure import measure_text_cached +from openpilot.system.ui.lib.utils import GuiStyleContext +from openpilot.system.ui.lib.emoji import find_emoji, emoji_tex +from openpilot.system.ui.lib.wrap_text import wrap_text +from openpilot.system.ui.widgets import Widget + +ICON_PADDING = 15 + +class TextAlignment(IntEnum): + LEFT = 0 + CENTER = 1 + RIGHT = 2 + +# TODO: This should be a Widget class +def gui_label( + rect: rl.Rectangle, + text: str, + font_size: int = DEFAULT_TEXT_SIZE, + color: rl.Color = DEFAULT_TEXT_COLOR, + font_weight: FontWeight = FontWeight.NORMAL, + alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE, + elide_right: bool = True +): + font = gui_app.font(font_weight) + text_size = measure_text_cached(font, text, font_size) + display_text = text + + # Elide text to fit within the rectangle + if elide_right and text_size.x > rect.width: + ellipsis = "..." + left, right = 0, len(text) + while left < right: + mid = (left + right) // 2 + candidate = text[:mid] + ellipsis + candidate_size = measure_text_cached(font, candidate, font_size) + if candidate_size.x <= rect.width: + left = mid + 1 + else: + right = mid + display_text = text[: left - 1] + ellipsis if left > 0 else ellipsis + text_size = measure_text_cached(font, display_text, font_size) + + # Calculate horizontal position based on alignment + text_x = rect.x + { + rl.GuiTextAlignment.TEXT_ALIGN_LEFT: 0, + rl.GuiTextAlignment.TEXT_ALIGN_CENTER: (rect.width - text_size.x) / 2, + rl.GuiTextAlignment.TEXT_ALIGN_RIGHT: rect.width - text_size.x, + }.get(alignment, 0) + + # Calculate vertical position based on alignment + text_y = rect.y + { + rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP: 0, + rl.GuiTextAlignmentVertical.TEXT_ALIGN_MIDDLE: (rect.height - text_size.y) / 2, + rl.GuiTextAlignmentVertical.TEXT_ALIGN_BOTTOM: rect.height - text_size.y, + }.get(alignment_vertical, 0) + + # Draw the text in the specified rectangle + rl.draw_text_ex(font, display_text, rl.Vector2(text_x, text_y), font_size, 0, color) + + +def gui_text_box( + rect: rl.Rectangle, + text: str, + font_size: int = DEFAULT_TEXT_SIZE, + color: rl.Color = DEFAULT_TEXT_COLOR, + alignment: int = rl.GuiTextAlignment.TEXT_ALIGN_LEFT, + alignment_vertical: int = rl.GuiTextAlignmentVertical.TEXT_ALIGN_TOP, + font_weight: FontWeight = FontWeight.NORMAL, +): + styles = [ + (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_COLOR_NORMAL, rl.color_to_int(color)), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_SIZE, font_size), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_LINE_SPACING, font_size), + (rl.GuiControl.DEFAULT, rl.GuiControlProperty.TEXT_ALIGNMENT, alignment), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_ALIGNMENT_VERTICAL, alignment_vertical), + (rl.GuiControl.DEFAULT, rl.GuiDefaultProperty.TEXT_WRAP_MODE, rl.GuiTextWrapMode.TEXT_WRAP_WORD) + ] + if font_weight != FontWeight.NORMAL: + rl.gui_set_font(gui_app.font(font_weight)) + + with GuiStyleContext(styles): + rl.gui_label(rect, text) + + if font_weight != FontWeight.NORMAL: + rl.gui_set_font(gui_app.font(FontWeight.NORMAL)) + + +# Non-interactive text area. Can render emojis and an optional specified icon. +class Label(Widget): + def __init__(self, + text: str, + font_size: int = DEFAULT_TEXT_SIZE, + font_weight: FontWeight = FontWeight.NORMAL, + text_alignment: TextAlignment = TextAlignment.CENTER, + text_padding: int = 20, + text_color: rl.Color = DEFAULT_TEXT_COLOR, + icon = None, + ): + + super().__init__() + self._font_weight = font_weight + self._font = gui_app.font(self._font_weight) + self._font_size = font_size + self._text_alignment = text_alignment + self._text_padding = text_padding + self._text_color = text_color + self._icon = icon + self.set_text(text) + + def set_text(self, text): + self._text_raw = text + self._update_text(self._text_raw) + + def set_text_color(self, color): + self._text_color = color + + def _update_layout_rects(self): + self._update_text(self._text_raw) + + def _update_text(self, text): + self._emojis = [] + self._text_size = [] + self._text = wrap_text(self._font, text, self._font_size, self._rect.width - (self._text_padding*2)) + for t in self._text: + self._emojis.append(find_emoji(t)) + self._text_size.append(measure_text_cached(self._font, t, self._font_size)) + + def _render(self, _): + text = self._text[0] if self._text else None + text_size = self._text_size[0] if self._text_size else rl.Vector2(0.0, 0.0) + text_pos = rl.Vector2(0, (self._rect.y + (self._rect.height - (text_size.y)) // 2)) + + if self._icon: + icon_y = self._rect.y + (self._rect.height - self._icon.height) / 2 + if text: + if self._text_alignment == TextAlignment.LEFT: + icon_x = self._rect.x + self._text_padding + text_pos.x = self._icon.width + ICON_PADDING + elif self._text_alignment == TextAlignment.CENTER: + total_width = self._icon.width + ICON_PADDING + text_size.x + icon_x = self._rect.x + (self._rect.width - total_width) / 2 + text_pos.x = self._icon.width + ICON_PADDING + else: + icon_x = (self._rect.x + self._rect.width - text_size.x - self._text_padding) - ICON_PADDING - self._icon.width + else: + icon_x = self._rect.x + (self._rect.width - self._icon.width) / 2 + rl.draw_texture_v(self._icon, rl.Vector2(icon_x, icon_y), rl.WHITE) + + for text, text_size, emojis in zip_longest(self._text, self._text_size, self._emojis, fillvalue=[]): + line_pos = rl.Vector2(text_pos.x, text_pos.y) + if self._text_alignment == TextAlignment.LEFT: + line_pos.x += self._rect.x + self._text_padding + elif self._text_alignment == TextAlignment.CENTER: + line_pos.x += self._rect.x + (self._rect.width - text_size.x) // 2 + elif self._text_alignment == TextAlignment.RIGHT: + line_pos.x += self._rect.x + self._rect.width - text_size.x - self._text_padding + + prev_index = 0 + for start, end, emoji in emojis: + text_before = text[prev_index:start] + width_before = measure_text_cached(self._font, text_before, self._font_size) + rl.draw_text_ex(self._font, text_before, line_pos, self._font_size, 0, self._text_color) + line_pos.x += width_before.x + + tex = emoji_tex(emoji) + rl.draw_texture_ex(tex, line_pos, 0.0, self._font_size / tex.height, self._text_color) + line_pos.x += self._font_size + prev_index = end + rl.draw_text_ex(self._font, text[prev_index:], line_pos, self._font_size, 0, self._text_color) + text_pos.y += text_size.y or self._font_size diff --git a/system/ui/lib/list_view.py b/system/ui/widgets/list_view.py similarity index 97% rename from system/ui/lib/list_view.py rename to system/ui/widgets/list_view.py index f5581be23e..e871eef0a1 100644 --- a/system/ui/lib/list_view.py +++ b/system/ui/widgets/list_view.py @@ -2,12 +2,12 @@ import os import pyray as rl from collections.abc import Callable from abc import ABC -from openpilot.system.ui.lib.application import gui_app, FontWeight +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.wrap_text import wrap_text -from openpilot.system.ui.lib.button import gui_button, ButtonStyle -from openpilot.system.ui.lib.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle +from openpilot.system.ui.widgets.toggle import Toggle, WIDTH as TOGGLE_WIDTH, HEIGHT as TOGGLE_HEIGHT ITEM_BASE_WIDTH = 600 ITEM_BASE_HEIGHT = 170 @@ -167,7 +167,7 @@ class MultipleButtonAction(ItemAction): # Check button state mouse_pos = rl.get_mouse_position() is_hovered = rl.check_collision_point_rec(mouse_pos, button_rect) - is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed + is_pressed = is_hovered and rl.is_mouse_button_down(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed is_selected = i == self.selected_button # Button colors @@ -188,7 +188,7 @@ class MultipleButtonAction(ItemAction): rl.draw_text_ex(self._font, text, rl.Vector2(text_x, text_y), 40, 0, rl.Color(228, 228, 228, 255)) # Handle click - if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self._is_pressed: + if is_hovered and rl.is_mouse_button_released(rl.MouseButton.MOUSE_BUTTON_LEFT) and self.is_pressed: clicked = i if clicked >= 0: @@ -229,7 +229,7 @@ class ListItem(Widget): super().set_parent_rect(parent_rect) self._rect.width = parent_rect.width - def _handle_mouse_release(self, mouse_pos: rl.Vector2): + def _handle_mouse_release(self, mouse_pos: MousePos): if not self.is_visible: return diff --git a/system/ui/widgets/network.py b/system/ui/widgets/network.py index 0613bb1f12..0bb759a919 100644 --- a/system/ui/widgets/network.py +++ b/system/ui/widgets/network.py @@ -1,16 +1,17 @@ -from dataclasses import dataclass +from enum import IntEnum +from functools import partial from threading import Lock -from typing import Literal +from typing import cast import pyray as rl from openpilot.system.ui.lib.application import gui_app -from openpilot.system.ui.lib.button import ButtonStyle, gui_button -from openpilot.system.ui.lib.label import gui_label 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.widgets import Widget +from openpilot.system.ui.widgets.button import ButtonStyle, Button +from openpilot.system.ui.widgets.confirm_dialog import ConfirmDialog from openpilot.system.ui.widgets.keyboard import Keyboard -from openpilot.system.ui.widgets.confirm_dialog import confirm_dialog -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets.label import TextAlignment, gui_label NM_DEVICE_STATE_NEED_AUTH = 60 MIN_PASSWORD_LENGTH = 8 @@ -26,49 +27,30 @@ STRENGTH_ICONS = [ ] -@dataclass -class StateIdle: - action: Literal["idle"] = "idle" - - -@dataclass -class StateConnecting: - network: NetworkInfo - action: Literal["connecting"] = "connecting" - - -@dataclass -class StateNeedsAuth: - network: NetworkInfo - action: Literal["needs_auth"] = "needs_auth" - - -@dataclass -class StateShowForgetConfirm: - network: NetworkInfo - action: Literal["show_forget_confirm"] = "show_forget_confirm" - - -@dataclass -class StateForgetting: - network: NetworkInfo - action: Literal["forgetting"] = "forgetting" - - -UIState = StateIdle | StateConnecting | StateNeedsAuth | StateShowForgetConfirm | StateForgetting +class UIState(IntEnum): + IDLE = 0 + CONNECTING = 1 + NEEDS_AUTH = 2 + SHOW_FORGET_CONFIRM = 3 + FORGETTING = 4 class WifiManagerUI(Widget): def __init__(self, wifi_manager: WifiManagerWrapper): super().__init__() - self.state: UIState = StateIdle() + self.state: UIState = UIState.IDLE + self._state_network: NetworkInfo | 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._networks: list[NetworkInfo] = [] + 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( @@ -88,15 +70,16 @@ class WifiManagerUI(Widget): gui_label(rect, "Scanning Wi-Fi networks...", 72, alignment=rl.GuiTextAlignment.TEXT_ALIGN_CENTER) return - match self.state: - case StateNeedsAuth(network): - self.keyboard.set_title("Enter password", f"for {network.ssid}") - gui_app.set_modal_overlay(self.keyboard, lambda result: self._on_password_entered(network, result)) - case StateShowForgetConfirm(network): - gui_app.set_modal_overlay(lambda: confirm_dialog(f'Forget Wi-Fi Network "{network.ssid}"?', "Forget"), - callback=lambda result: self.on_forgot_confirm_finished(network, result)) - case _: - self._draw_network_list(rect) + 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)) + 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: NetworkInfo, result: int): if result == 1: @@ -106,13 +89,13 @@ class WifiManagerUI(Widget): if len(password) >= MIN_PASSWORD_LENGTH: self.connect_to_network(network, password) elif result == 0: - self.state = StateIdle() + self.state = UIState.IDLE def on_forgot_confirm_finished(self, network, result: int): if result == 1: self.forget_network(network) elif result == 0: - self.state = StateIdle() + self.state = UIState.IDLE def _draw_network_list(self, rect: rl.Rectangle): content_rect = rl.Rectangle(rect.x, rect.y, rect.width, len(self._networks) * ITEM_HEIGHT) @@ -139,16 +122,21 @@ class WifiManagerUI(Widget): signal_icon_rect = rl.Rectangle(rect.x + rect.width - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) security_icon_rect = rl.Rectangle(signal_icon_rect.x - spacing - ICON_SIZE, rect.y + (ITEM_HEIGHT - ICON_SIZE) / 2, ICON_SIZE, ICON_SIZE) - gui_label(ssid_rect, network.ssid, 55) - status_text = "" - match self.state: - case StateConnecting(network=connecting): - if connecting.ssid == network.ssid: - status_text = "CONNECTING..." - case StateForgetting(network=forgetting): - if forgetting.ssid == network.ssid: - status_text = "FORGETTING..." + if self.state == UIState.CONNECTING and self._state_network: + if self._state_network.ssid == network.ssid: + self._networks_buttons[network.ssid].set_enabled(False) + status_text = "CONNECTING..." + elif self.state == UIState.FORGETTING and self._state_network: + if self._state_network.ssid == network.ssid: + self._networks_buttons[network.ssid].set_enabled(False) + status_text = "FORGETTING..." + elif network.security_type == SecurityType.UNSUPPORTED: + self._networks_buttons[network.ssid].set_enabled(False) + else: + self._networks_buttons[network.ssid].set_enabled(True) + + self._networks_buttons[network.ssid].render(ssid_rect) if status_text: status_text_rect = rl.Rectangle(security_icon_rect.x - 410, rect.y, 410, ITEM_HEIGHT) @@ -162,18 +150,25 @@ class WifiManagerUI(Widget): self.btn_width, 80, ) - if isinstance(self.state, StateIdle) and gui_button(forget_btn_rect, "Forget", button_style=ButtonStyle.ACTION) and clicked: - self.state = StateShowForgetConfirm(network) + self._forget_networks_buttons[network.ssid].render(forget_btn_rect) self._draw_status_icon(security_icon_rect, network) self._draw_signal_strength_icon(signal_icon_rect, network) - if isinstance(self.state, StateIdle) and rl.check_collision_point_rec(rl.get_mouse_position(), ssid_rect) and clicked: + def _networks_buttons_callback(self, network): + if self.scroll_panel.is_touch_valid(): if not network.is_saved and network.security_type != SecurityType.OPEN: - self.state = StateNeedsAuth(network) + self.state = UIState.NEEDS_AUTH + self._state_network = network + self._password_retry = False elif not network.is_connected: self.connect_to_network(network) + def _forget_networks_buttons_callback(self, network): + if self.scroll_panel.is_touch_valid(): + self.state = UIState.SHOW_FORGET_CONFIRM + self._state_network = network + def _draw_status_icon(self, rect, network: NetworkInfo): """Draw the status icon based on network's connection state""" icon_file = None @@ -197,41 +192,50 @@ class WifiManagerUI(Widget): 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=''): - self.state = StateConnecting(network) + self.state = UIState.CONNECTING + self._state_network = network if network.is_saved and not password: self.wifi_manager.activate_connection(network.ssid) else: self.wifi_manager.connect_to_network(network.ssid, password) def forget_network(self, network: NetworkInfo): - self.state = StateForgetting(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]): 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) 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 = StateNeedsAuth(network) + self.state = UIState.NEEDS_AUTH + self._state_network = network + self._password_retry = True def _on_activated(self): with self._lock: - if isinstance(self.state, StateConnecting): - self.state = StateIdle() + if self.state == UIState.CONNECTING: + self.state = UIState.IDLE def _on_forgotten(self, ssid): with self._lock: - if isinstance(self.state, StateForgetting): - self.state = StateIdle() + if self.state == UIState.FORGETTING: + self.state = UIState.IDLE def _on_connection_failed(self, ssid: str, error: str): with self._lock: - if isinstance(self.state, StateConnecting): - self.state = StateIdle() + if self.state == UIState.CONNECTING: + self.state = UIState.IDLE def main(): diff --git a/system/ui/widgets/option_dialog.py b/system/ui/widgets/option_dialog.py index a2958ee1f4..3140f419f5 100644 --- a/system/ui/widgets/option_dialog.py +++ b/system/ui/widgets/option_dialog.py @@ -1,9 +1,9 @@ import pyray as rl from openpilot.system.ui.lib.application import FontWeight -from openpilot.system.ui.lib.button import gui_button, ButtonStyle, TextAlignment -from openpilot.system.ui.lib.label import gui_label from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.widgets import Widget +from openpilot.system.ui.widgets.button import gui_button, ButtonStyle, TextAlignment +from openpilot.system.ui.widgets.label import gui_label # Constants MARGIN = 50 diff --git a/system/ui/lib/scroller.py b/system/ui/widgets/scroller.py similarity index 97% rename from system/ui/lib/scroller.py rename to system/ui/widgets/scroller.py index 8a10221772..f7304bf6a6 100644 --- a/system/ui/lib/scroller.py +++ b/system/ui/widgets/scroller.py @@ -1,6 +1,6 @@ import pyray as rl -from openpilot.system.ui.lib.widget import Widget from openpilot.system.ui.lib.scroll_panel import GuiScrollPanel +from openpilot.system.ui.widgets import Widget ITEM_SPACING = 40 LINE_COLOR = rl.GRAY diff --git a/system/ui/lib/toggle.py b/system/ui/widgets/toggle.py similarity index 92% rename from system/ui/lib/toggle.py rename to system/ui/widgets/toggle.py index 8ed9a655ec..eba5ef9e58 100644 --- a/system/ui/lib/toggle.py +++ b/system/ui/widgets/toggle.py @@ -1,5 +1,6 @@ import pyray as rl -from openpilot.system.ui.lib.widget import Widget +from openpilot.system.ui.lib.application import MousePos +from openpilot.system.ui.widgets import Widget ON_COLOR = rl.Color(51, 171, 76, 255) OFF_COLOR = rl.Color(0x39, 0x39, 0x39, 255) @@ -23,7 +24,7 @@ class Toggle(Widget): def set_rect(self, rect: rl.Rectangle): self._rect = rl.Rectangle(rect.x, rect.y, WIDTH, HEIGHT) - def _handle_mouse_release(self, mouse_pos: rl.Vector2): + def _handle_mouse_release(self, mouse_pos: MousePos): if not self._enabled: return @@ -37,9 +38,6 @@ class Toggle(Widget): self._state = state self._target = 1.0 if state else 0.0 - def set_enabled(self, enabled: bool): - self._enabled = enabled - def is_enabled(self): return self._enabled diff --git a/system/updated/tests/test_base.py b/system/updated/tests/test_base.py index 52287c58f9..699a0f0bd3 100644 --- a/system/updated/tests/test_base.py +++ b/system/updated/tests/test_base.py @@ -132,8 +132,8 @@ class TestBaseUpdate: class ParamsBaseUpdateTest(TestBaseUpdate): def _test_finalized_update(self, branch, version, agnos_version, release_notes): - assert self.params.get("UpdaterNewDescription", encoding="utf-8").startswith(f"{version} / {branch}") - assert self.params.get("UpdaterNewReleaseNotes", encoding="utf-8") == f"{release_notes}\n" + assert self.params.get("UpdaterNewDescription").startswith(f"{version} / {branch}") + assert self.params.get("UpdaterNewReleaseNotes") == f"{release_notes}\n" super()._test_finalized_update(branch, version, agnos_version, release_notes) def send_check_for_updates_signal(self, updated: ManagerProcess): @@ -143,16 +143,16 @@ class ParamsBaseUpdateTest(TestBaseUpdate): updated.signal(signal.SIGHUP.value) def _test_params(self, branch, fetch_available, update_available): - assert self.params.get("UpdaterTargetBranch", encoding="utf-8") == branch + assert self.params.get("UpdaterTargetBranch") == branch assert self.params.get_bool("UpdaterFetchAvailable") == fetch_available assert self.params.get_bool("UpdateAvailable") == update_available def wait_for_idle(self): - self.wait_for_condition(lambda: self.params.get("UpdaterState", encoding="utf-8") == "idle") + self.wait_for_condition(lambda: self.params.get("UpdaterState") == "idle") def wait_for_failed(self): - self.wait_for_condition(lambda: self.params.get("UpdateFailedCount", encoding="utf-8") is not None and \ - int(self.params.get("UpdateFailedCount", encoding="utf-8")) > 0) + self.wait_for_condition(lambda: self.params.get("UpdateFailedCount") is not None and \ + self.params.get("UpdateFailedCount") > 0) def wait_for_fetch_available(self): self.wait_for_condition(lambda: self.params.get_bool("UpdaterFetchAvailable")) diff --git a/system/updated/updated.py b/system/updated/updated.py index 0759c0a7aa..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 @@ -31,8 +30,13 @@ FINALIZED = os.path.join(STAGING_ROOT, "finalized") OVERLAY_INIT = Path(os.path.join(BASEDIR, ".overlay_init")) -DAYS_NO_CONNECTIVITY_MAX = 14 # do not allow to engage after this many days -DAYS_NO_CONNECTIVITY_PROMPT = 10 # send an offroad prompt after this many days +# do not allow to engage after this many hours onroad and this many routes +HOURS_NO_CONNECTIVITY_MAX = 27 +ROUTES_NO_CONNECTIVITY_MAX = 84 +# send an offroad prompt after this many hours onroad and this many routes +HOURS_NO_CONNECTIVITY_PROMPT = 23 +ROUTES_NO_CONNECTIVITY_PROMPT = 80 + class UserRequest: NONE = 0 @@ -61,15 +65,7 @@ class WaitTimeHelper: def write_time_to_param(params, param) -> None: t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - params.put(param, t.isoformat().encode('utf8')) - -def read_time_from_param(params, param) -> datetime.datetime | None: - t = params.get(param, encoding='utf8') - try: - return datetime.datetime.fromisoformat(t) - except (TypeError, ValueError): - pass - return None + params.put(param, t) def run(cmd: list[str], cwd: str = None) -> str: return subprocess.check_output(cmd, cwd=cwd, stderr=subprocess.STDOUT, encoding='utf8') @@ -193,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") @@ -242,9 +229,12 @@ class Updater: @property def target_branch(self) -> str: - b: str | None = self.params.get("UpdaterTargetBranch", encoding='utf-8') + 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) return b @property @@ -272,20 +262,22 @@ class Updater: return run(["git", "rev-parse", "HEAD"], path).rstrip() def set_params(self, update_success: bool, failed_count: int, exception: str | None) -> None: - self.params.put("UpdateFailedCount", str(failed_count)) + self.params.put("UpdateFailedCount", failed_count) self.params.put("UpdaterTargetBranch", self.target_branch) self.params.put_bool("UpdaterFetchAvailable", self.update_available) if len(self.branches): self.params.put("UpdaterAvailableBranches", ','.join(self.branches.keys())) - last_update = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + last_uptime_onroad = self.params.get("UptimeOnroad", return_default=True) + last_route_count = self.params.get("RouteCount", return_default=True) if update_success: - write_time_to_param(self.params, "LastUpdateTime") + self.params.put("LastUpdateTime", datetime.datetime.now(datetime.UTC).replace(tzinfo=None)) + self.params.put("LastUpdateUptimeOnroad", last_uptime_onroad) + self.params.put("LastUpdateRouteCount", last_route_count) else: - t = read_time_from_param(self.params, "LastUpdateTime") - if t is not None: - last_update = t + last_uptime_onroad = self.params.get("LastUpdateUptimeOnroad", return_default=True) + last_route_count = self.params.get("LastUpdateRouteCount", return_default=True) if exception is None: self.params.remove("LastUpdateException") @@ -323,8 +315,8 @@ class Updater: for alert in ("Offroad_UpdateFailed", "Offroad_ConnectivityNeeded", "Offroad_ConnectivityNeededPrompt"): set_offroad_alert(alert, False) - now = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - dt = now - last_update + dt_uptime_onroad = (self.params.get("UptimeOnroad", return_default=True) - last_uptime_onroad) / (60*60) + dt_route_count = self.params.get("RouteCount", return_default=True) - last_route_count build_metadata = get_build_metadata() if failed_count > 15 and exception is not None and self.has_internet: if build_metadata.tested_channel: @@ -333,11 +325,11 @@ class Updater: extra_text = exception set_offroad_alert("Offroad_UpdateFailed", True, extra_text=extra_text) elif failed_count > 0: - if dt.days > DAYS_NO_CONNECTIVITY_MAX: + if dt_uptime_onroad > HOURS_NO_CONNECTIVITY_MAX and dt_route_count > ROUTES_NO_CONNECTIVITY_MAX: set_offroad_alert("Offroad_ConnectivityNeeded", True) - elif dt.days > DAYS_NO_CONNECTIVITY_PROMPT: - remaining = max(DAYS_NO_CONNECTIVITY_MAX - dt.days, 1) - set_offroad_alert("Offroad_ConnectivityNeededPrompt", True, extra_text=f"{remaining} day{'' if remaining == 1 else 's'}.") + elif dt_uptime_onroad > HOURS_NO_CONNECTIVITY_PROMPT and dt_route_count > ROUTES_NO_CONNECTIVITY_PROMPT: + remaining = max(HOURS_NO_CONNECTIVITY_MAX - dt_uptime_onroad, 1) + set_offroad_alert("Offroad_ConnectivityNeededPrompt", True, extra_text=f"{remaining} hour{'' if remaining == 1 else 's'}.") def check_for_update(self) -> None: cloudlog.info("checking for updates") @@ -429,8 +421,8 @@ def main() -> None: cloudlog.event("update installed") if not params.get("InstallDate"): - t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None).isoformat() - params.put("InstallDate", t.encode('utf8')) + t = datetime.datetime.now(datetime.UTC).replace(tzinfo=None) + params.put("InstallDate", t) updater = Updater() update_failed_count = 0 # TODO: Load from param? @@ -468,7 +460,7 @@ def main() -> None: updater.check_for_update() # download update - last_fetch = read_time_from_param(params, "UpdaterLastFetchTime") + last_fetch = params.get("UpdaterLastFetchTime") timed_out = last_fetch is None or (datetime.datetime.now(datetime.UTC).replace(tzinfo=None) - last_fetch > datetime.timedelta(days=3)) user_requested_fetch = wait_helper.user_request == UserRequest.FETCH if params.get_bool("NetworkMetered") and not timed_out and not user_requested_fetch: diff --git a/system/version.py b/system/version.py index 11c6ced2b7..5e65a7c64b 100755 --- a/system/version.py +++ b/system/version.py @@ -12,14 +12,14 @@ from openpilot.common.git import get_commit, get_origin, get_branch, get_short_b RELEASE_SP_BRANCHES = ['release-c3'] TESTED_SP_BRANCHES = ['staging-c3', 'staging-c3-new'] -MASTER_SP_BRANCHES = ['master', 'master-new'] -RELEASE_BRANCHES = ['release3-staging', 'release3', 'nightly'] + RELEASE_SP_BRANCHES +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 BUILD_METADATA_FILENAME = "build.json" -training_version: bytes = b"0.2.0" -terms_version: bytes = b"2" +training_version: str = "0.2.0" +terms_version: str = "2" def get_version(path: str = BASEDIR) -> str: @@ -83,6 +83,11 @@ class OpenpilotMetadata: # touch this to get rid of the orange startup alert. there's better ways to do that return self.git_normalized_origin == "github.com/commaai/openpilot" + @property + def sunnypilot_remote(self) -> bool: + return self.git_normalized_origin in ("github.com/sunnypilot/sunnypilot", + "github.com/sunnypilot/openpilot") + @property def git_normalized_origin(self) -> str: return self.git_origin \ @@ -117,9 +122,13 @@ class BuildMetadata: def master_channel(self) -> bool: return self.channel in MASTER_SP_BRANCHES + @property + def development_channel(self) -> bool: + return self.channel.startswith("dev-") or self.channel.endswith("-prebuilt") + @property def channel_type(self) -> str: - if self.channel.startswith("dev-"): + if self.development_channel: return "development" elif self.channel.startswith("staging-"): return "staging" 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³'Ù +#else +#include +#endif + +#include + +namespace { + +const char frame_vertex_shader[] = +#ifdef __APPLE__ + "#version 330 core\n" +#else + "#version 300 es\n" +#endif + "layout(location = 0) in vec4 aPosition;\n" + "layout(location = 1) in vec2 aTexCoord;\n" + "uniform mat4 uTransform;\n" + "out vec2 vTexCoord;\n" + "void main() {\n" + " gl_Position = uTransform * aPosition;\n" + " vTexCoord = aTexCoord;\n" + "}\n"; + +const char frame_fragment_shader[] = +#ifdef __APPLE__ + "#version 330 core\n" +#else + "#version 300 es\n" + "precision mediump float;\n" +#endif + "uniform sampler2D uTextureY;\n" + "uniform sampler2D uTextureUV;\n" + "in vec2 vTexCoord;\n" + "out vec4 colorOut;\n" + "void main() {\n" + " float y = texture(uTextureY, vTexCoord).r;\n" + " vec2 uv = texture(uTextureUV, vTexCoord).rg - 0.5;\n" + " float r = y + 1.402 * uv.y;\n" + " float g = y - 0.344 * uv.x - 0.714 * uv.y;\n" + " float b = y + 1.772 * uv.x;\n" + " colorOut = vec4(r, g, b, 1.0);\n" + "}\n"; + +} // namespace + +CameraWidget::CameraWidget(std::string stream_name, VisionStreamType type, QWidget* parent) : + stream_name(stream_name), active_stream_type(type), requested_stream_type(type), QOpenGLWidget(parent) { + setAttribute(Qt::WA_OpaquePaintEvent); + qRegisterMetaType>("availableStreams"); + QObject::connect(this, &CameraWidget::vipcThreadConnected, this, &CameraWidget::vipcConnected, Qt::BlockingQueuedConnection); + QObject::connect(this, &CameraWidget::vipcThreadFrameReceived, this, &CameraWidget::vipcFrameReceived, Qt::QueuedConnection); + QObject::connect(this, &CameraWidget::vipcAvailableStreamsUpdated, this, &CameraWidget::availableStreamsUpdated, Qt::QueuedConnection); + QObject::connect(QApplication::instance(), &QCoreApplication::aboutToQuit, this, &CameraWidget::stopVipcThread); +} + +CameraWidget::~CameraWidget() { + makeCurrent(); + stopVipcThread(); + if (isValid()) { + glDeleteVertexArrays(1, &frame_vao); + glDeleteBuffers(1, &frame_vbo); + glDeleteBuffers(1, &frame_ibo); + glDeleteTextures(2, textures); + shader_program_.reset(); + } + doneCurrent(); +} + +void CameraWidget::initializeGL() { + initializeOpenGLFunctions(); + + shader_program_ = std::make_unique(context()); + shader_program_->addShaderFromSourceCode(QOpenGLShader::Vertex, frame_vertex_shader); + shader_program_->addShaderFromSourceCode(QOpenGLShader::Fragment, frame_fragment_shader); + shader_program_->link(); + + GLint frame_pos_loc = shader_program_->attributeLocation("aPosition"); + GLint frame_texcoord_loc = shader_program_->attributeLocation("aTexCoord"); + + auto [x1, x2, y1, y2] = requested_stream_type == VISION_STREAM_DRIVER ? std::tuple(0.f, 1.f, 1.f, 0.f) : std::tuple(1.f, 0.f, 1.f, 0.f); + const uint8_t frame_indicies[] = {0, 1, 2, 0, 2, 3}; + const float frame_coords[4][4] = { + {-1.0, -1.0, x2, y1}, // bl + {-1.0, 1.0, x2, y2}, // tl + { 1.0, 1.0, x1, y2}, // tr + { 1.0, -1.0, x1, y1}, // br + }; + + glGenVertexArrays(1, &frame_vao); + glBindVertexArray(frame_vao); + glGenBuffers(1, &frame_vbo); + glBindBuffer(GL_ARRAY_BUFFER, frame_vbo); + glBufferData(GL_ARRAY_BUFFER, sizeof(frame_coords), frame_coords, GL_STATIC_DRAW); + glEnableVertexAttribArray(frame_pos_loc); + glVertexAttribPointer(frame_pos_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(frame_coords[0]), (const void *)0); + glEnableVertexAttribArray(frame_texcoord_loc); + glVertexAttribPointer(frame_texcoord_loc, 2, GL_FLOAT, GL_FALSE, + sizeof(frame_coords[0]), (const void *)(sizeof(float) * 2)); + glGenBuffers(1, &frame_ibo); + glBindBuffer(GL_ELEMENT_ARRAY_BUFFER, frame_ibo); + glBufferData(GL_ELEMENT_ARRAY_BUFFER, sizeof(frame_indicies), frame_indicies, GL_STATIC_DRAW); + glBindBuffer(GL_ARRAY_BUFFER, 0); + glBindVertexArray(0); + + glGenTextures(2, textures); + + shader_program_->bind(); + shader_program_->setUniformValue("uTextureY", 0); + shader_program_->setUniformValue("uTextureUV", 1); + shader_program_->release(); +} + +void CameraWidget::showEvent(QShowEvent *event) { + if (!vipc_thread) { + clearFrames(); + vipc_thread = new QThread(); + connect(vipc_thread, &QThread::started, [=]() { vipcThread(); }); + connect(vipc_thread, &QThread::finished, vipc_thread, &QObject::deleteLater); + vipc_thread->start(); + } +} + +void CameraWidget::stopVipcThread() { + makeCurrent(); + if (vipc_thread) { + vipc_thread->requestInterruption(); + vipc_thread->quit(); + vipc_thread->wait(); + vipc_thread = nullptr; + } +} + +void CameraWidget::availableStreamsUpdated(std::set streams) { + available_streams = streams; +} + +void CameraWidget::paintGL() { + glClearColor(bg.redF(), bg.greenF(), bg.blueF(), bg.alphaF()); + glClear(GL_STENCIL_BUFFER_BIT | GL_COLOR_BUFFER_BIT); + + std::lock_guard lk(frame_lock); + if (!current_frame_) return; + + // Scale for aspect ratio + float widget_ratio = (float)width() / height(); + float frame_ratio = (float)stream_width / stream_height; + float scale_x = std::min(frame_ratio / widget_ratio, 1.0f); + float scale_y = std::min(widget_ratio / frame_ratio, 1.0f); + + glViewport(0, 0, width() * devicePixelRatio(), height() * devicePixelRatio()); + + shader_program_->bind(); + QMatrix4x4 transform; + transform.scale(scale_x, scale_y, 1.0f); + shader_program_->setUniformValue("uTransform", transform); + + glPixelStorei(GL_UNPACK_ALIGNMENT, 1); + + glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, textures[0]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width, stream_height, GL_RED, GL_UNSIGNED_BYTE, current_frame_->y); + + glPixelStorei(GL_UNPACK_ROW_LENGTH, stream_stride/2); + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, textures[1]); + glTexSubImage2D(GL_TEXTURE_2D, 0, 0, 0, stream_width/2, stream_height/2, GL_RG, GL_UNSIGNED_BYTE, current_frame_->uv); + + glBindVertexArray(frame_vao); + glDrawElements(GL_TRIANGLES, 6, GL_UNSIGNED_BYTE, nullptr); + glBindVertexArray(0); + + // Reset both texture units + glActiveTexture(GL_TEXTURE1); + glBindTexture(GL_TEXTURE_2D, 0); + glActiveTexture(GL_TEXTURE0); + glBindTexture(GL_TEXTURE_2D, 0); + glPixelStorei(GL_UNPACK_ALIGNMENT, 4); + glPixelStorei(GL_UNPACK_ROW_LENGTH, 0); + + shader_program_->release(); +} + +void CameraWidget::vipcConnected(VisionIpcClient *vipc_client) { + makeCurrent(); + stream_width = vipc_client->buffers[0].width; + stream_height = vipc_client->buffers[0].height; + stream_stride = vipc_client->buffers[0].stride; + + glBindTexture(GL_TEXTURE_2D, textures[0]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_R8, stream_width, stream_height, 0, GL_RED, GL_UNSIGNED_BYTE, nullptr); + assert(glGetError() == GL_NO_ERROR); + + glBindTexture(GL_TEXTURE_2D, textures[1]); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_LINEAR); + glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_LINEAR); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, GL_CLAMP_TO_EDGE); + glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP_TO_EDGE); + glTexImage2D(GL_TEXTURE_2D, 0, GL_RG8, stream_width/2, stream_height/2, 0, GL_RG, GL_UNSIGNED_BYTE, nullptr); + assert(glGetError() == GL_NO_ERROR); +} + +void CameraWidget::vipcFrameReceived() { + update(); +} + +void CameraWidget::vipcThread() { + VisionStreamType cur_stream = requested_stream_type; + std::unique_ptr vipc_client; + VisionIpcBufExtra frame_meta = {}; + + while (!QThread::currentThread()->isInterruptionRequested()) { + if (!vipc_client || cur_stream != requested_stream_type) { + clearFrames(); + qDebug().nospace() << "connecting to stream " << requested_stream_type << ", was connected to " << cur_stream; + cur_stream = requested_stream_type; + vipc_client.reset(new VisionIpcClient(stream_name, cur_stream, false)); + } + active_stream_type = cur_stream; + + if (!vipc_client->connected) { + clearFrames(); + auto streams = VisionIpcClient::getAvailableStreams(stream_name, false); + if (streams.empty()) { + QThread::msleep(100); + continue; + } + emit vipcAvailableStreamsUpdated(streams); + + if (!vipc_client->connect(false)) { + QThread::msleep(100); + continue; + } + emit vipcThreadConnected(vipc_client.get()); + } + + if (VisionBuf *buf = vipc_client->recv(&frame_meta, 100)) { + { + std::lock_guard lk(frame_lock); + current_frame_ = buf; + frame_meta_ = frame_meta; + } + emit vipcThreadFrameReceived(); + } + } +} + +void CameraWidget::clearFrames() { + std::lock_guard lk(frame_lock); + current_frame_ = nullptr; + available_streams.clear(); +} diff --git a/tools/cabana/cameraview.h b/tools/cabana/cameraview.h new file mode 100644 index 0000000000..930b13d82c --- /dev/null +++ b/tools/cabana/cameraview.h @@ -0,0 +1,64 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "msgq/visionipc/visionipc_client.h" + +class CameraWidget : public QOpenGLWidget, protected QOpenGLFunctions { + Q_OBJECT + +public: + using QOpenGLWidget::QOpenGLWidget; + explicit CameraWidget(std::string stream_name, VisionStreamType stream_type, QWidget* parent = nullptr); + ~CameraWidget(); + void setStreamType(VisionStreamType type) { requested_stream_type = type; } + VisionStreamType getStreamType() { return active_stream_type; } + void stopVipcThread(); + +signals: + void clicked(); + void vipcThreadConnected(VisionIpcClient *); + void vipcThreadFrameReceived(); + void vipcAvailableStreamsUpdated(std::set); + +protected: + void paintGL() override; + void initializeGL() override; + void showEvent(QShowEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override { emit clicked(); } + void vipcThread(); + void clearFrames(); + + GLuint frame_vao, frame_vbo, frame_ibo; + GLuint textures[2]; + std::unique_ptr shader_program_; + QColor bg = Qt::black; + + std::string stream_name; + int stream_width = 0; + int stream_height = 0; + int stream_stride = 0; + std::atomic active_stream_type; + std::atomic requested_stream_type; + std::set available_streams; + QThread *vipc_thread = nullptr; + std::recursive_mutex frame_lock; + VisionBuf* current_frame_ = nullptr; + VisionIpcBufExtra frame_meta_ = {}; + +protected slots: + void vipcConnected(VisionIpcClient *vipc_client); + void vipcFrameReceived(); + void availableStreamsUpdated(std::set streams); +}; + +Q_DECLARE_METATYPE(std::set); diff --git a/tools/cabana/chart/chart.cc b/tools/cabana/chart/chart.cc index 3588c5edc6..505b2b8053 100644 --- a/tools/cabana/chart/chart.cc +++ b/tools/cabana/chart/chart.cc @@ -45,7 +45,7 @@ ChartView::ChartView(const std::pair &x_range, ChartsWidget *par createToolButtons(); setRubberBand(QChartView::HorizontalRubberBand); setMouseTracking(true); - setTheme(settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); + setTheme(utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight); signal_value_font.setPointSize(9); QObject::connect(axis_y, &QValueAxis::rangeChanged, this, &ChartView::resetChartCache); @@ -747,7 +747,7 @@ void ChartView::drawTimeline(QPainter *painter) { QRectF time_str_rect(QPointF(x - time_str_size.width() / 2.0, plot_area.bottom() + AXIS_X_TOP_MARGIN), time_str_size); QPainterPath path; path.addRoundedRect(time_str_rect, 3, 3); - painter->fillPath(path, settings.theme == DARK_THEME ? Qt::darkGray : Qt::gray); + painter->fillPath(path, utils::isDarkTheme() ? Qt::darkGray : Qt::gray); painter->setPen(palette().color(QPalette::BrightText)); painter->setFont(axis_x->labelsFont()); painter->drawText(time_str_rect, Qt::AlignCenter, time_str); diff --git a/tools/cabana/chart/chartswidget.cc b/tools/cabana/chart/chartswidget.cc index 08840f4663..3e9e452b90 100644 --- a/tools/cabana/chart/chartswidget.cc +++ b/tools/cabana/chart/chartswidget.cc @@ -254,7 +254,7 @@ void ChartsWidget::settingChanged() { if (std::exchange(current_theme, settings.theme) != current_theme) { undo_zoom_action->setIcon(utils::icon("arrow-counterclockwise")); redo_zoom_action->setIcon(utils::icon("arrow-clockwise")); - auto theme = settings.theme == DARK_THEME ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; + auto theme = utils::isDarkTheme() ? QChart::QChart::ChartThemeDark : QChart::ChartThemeLight; for (auto c : charts) { c->setTheme(theme); } diff --git a/tools/cabana/chart/sparkline.cc b/tools/cabana/chart/sparkline.cc index 09f86a095a..91435cd5ac 100644 --- a/tools/cabana/chart/sparkline.cc +++ b/tools/cabana/chart/sparkline.cc @@ -4,51 +4,97 @@ #include #include -void Sparkline::update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size) { - points.clear(); - double value = 0; - auto [first, last] = can->eventsInRange(msg_id, std::make_pair(last_msg_ts -range, last_msg_ts)); - for (auto it = first; it != last; ++it) { - if (sig->getValue((*it)->dat, (*it)->size, &value)) { - points.emplace_back(((*it)->mono_time - (*first)->mono_time) / 1e9, value); - } - } - - if (points.empty() || size.isEmpty()) { +void Sparkline::update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size) { + if (first == last || size.isEmpty()) { pixmap = QPixmap(); return; } - const auto [min, max] = std::minmax_element(points.begin(), points.end(), - [](auto &l, auto &r) { return l.y() < r.y(); }); - min_val = min->y() == max->y() ? min->y() - 1 : min->y(); - max_val = min->y() == max->y() ? max->y() + 1 : max->y(); - freq_ = points.size() / std::max(points.back().x() - points.front().x(), 1.0); + points_.clear(); + min_val = std::numeric_limits::max(); + max_val = std::numeric_limits::lowest(); + points_.reserve(std::distance(first, last)); + + uint64_t start_time = (*first)->mono_time; + double value = 0.0; + for (auto it = first; it != last; ++it) { + if (sig->getValue((*it)->dat, (*it)->size, &value)) { + min_val = std::min(min_val, value); + max_val = std::max(max_val, value); + points_.emplace_back(((*it)->mono_time - start_time) / 1e9, value); + } + } + + if (points_.empty()) { + pixmap = QPixmap(); + return; + } + + freq_ = points_.size() / std::max(points_.back().x() - points_.front().x(), 1.0); render(sig->color, range, size); } void Sparkline::render(const QColor &color, int range, QSize size) { - const double xscale = (size.width() - 1) / (double)range; - const double yscale = (size.height() - 3) / (max_val - min_val); - for (auto &v : points) { - v = QPoint(v.x() * xscale, 1 + std::abs(v.y() - max_val) * yscale); + // Adjust for flat lines + bool is_flat_line = min_val == max_val; + if (is_flat_line) { + min_val -= 1.0; + max_val += 1.0; } + // Calculate scaling + const double xscale = (size.width() - 1) / (double)range; + const double yscale = (size.height() - 3) / (max_val - min_val); + bool draw_individual_points = (points_.back().x() * xscale / points_.size()) > 8.0; + + // Transform or downsample points + render_points_.reserve(points_.size()); + render_points_.clear(); + if (draw_individual_points) { + for (const auto &p : points_) { + render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - p.y()) * yscale); + } + } else if (is_flat_line) { + double y = size.height() / 2.0; + render_points_.emplace_back(0.0, y); + render_points_.emplace_back(points_.back().x() * xscale, y); + } else { + double prev_y = points_.front().y(); + render_points_.emplace_back(points_.front().x() * xscale, 1.0 + (max_val - prev_y) * yscale); + bool in_flat = false; + + for (size_t i = 1; i < points_.size(); ++i) { + const auto &p = points_[i]; + double y = p.y(); + if (std::abs(y - prev_y) < 1e-6) { + in_flat = true; + } else { + if (in_flat) render_points_.emplace_back(points_[i - 1].x() * xscale, 1.0 + (max_val - prev_y) * yscale); + render_points_.emplace_back(p.x() * xscale, 1.0 + (max_val - y) * yscale); + in_flat = false; + } + prev_y = y; + } + if (in_flat) render_points_.emplace_back(points_.back().x() * xscale, 1.0 + (max_val - prev_y) * yscale); + } + + // Render to pixmap qreal dpr = qApp->devicePixelRatio(); - size *= dpr; - if (size != pixmap.size()) { - pixmap = QPixmap(size); + const QSize pixmap_size = size * dpr; + if (pixmap.size() != pixmap_size) { + pixmap = QPixmap(pixmap_size); } pixmap.setDevicePixelRatio(dpr); pixmap.fill(Qt::transparent); QPainter painter(&pixmap); - painter.setRenderHint(QPainter::Antialiasing, points.size() < 500); + painter.setRenderHint(QPainter::Antialiasing, render_points_.size() <= 500); painter.setPen(color); - painter.drawPolyline(points.data(), points.size()); + painter.drawPolyline(render_points_.data(), render_points_.size()); + painter.setPen(QPen(color, 3)); - if ((points.back().x() - points.front().x()) / points.size() > 8) { - painter.drawPoints(points.data(), points.size()); + if (draw_individual_points) { + painter.drawPoints(render_points_.data(), render_points_.size()); } else { - painter.drawPoint(points.back()); + painter.drawPoint(render_points_.back()); } } diff --git a/tools/cabana/chart/sparkline.h b/tools/cabana/chart/sparkline.h index 806f0a61eb..7f30047d0d 100644 --- a/tools/cabana/chart/sparkline.h +++ b/tools/cabana/chart/sparkline.h @@ -9,7 +9,7 @@ class Sparkline { public: - void update(const MessageId &msg_id, const cabana::Signal *sig, double last_msg_ts, int range, QSize size); + void update(const cabana::Signal *sig, CanEventIter first, CanEventIter last, int range, QSize size); inline double freq() const { return freq_; } bool isEmpty() const { return pixmap.isNull(); } @@ -20,6 +20,7 @@ public: private: void render(const QColor &color, int range, QSize size); - std::vector points; + std::vector points_; + std::vector render_points_; double freq_ = 0; }; diff --git a/tools/cabana/chart/tiplabel.cc b/tools/cabana/chart/tiplabel.cc index be71602838..233fa80373 100644 --- a/tools/cabana/chart/tiplabel.cc +++ b/tools/cabana/chart/tiplabel.cc @@ -7,6 +7,7 @@ #include #include "tools/cabana/settings.h" +#include "tools/cabana/utils/util.h" TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::FramelessWindowHint) { setAttribute(Qt::WA_ShowWithoutActivating); @@ -19,7 +20,7 @@ TipLabel::TipLabel(QWidget *parent) : QLabel(parent, Qt::ToolTip | Qt::Frameless font.setPointSizeF(8.34563465); setFont(font); auto palette = QToolTip::palette(); - if (settings.theme != DARK_THEME) { + if (!utils::isDarkTheme()) { palette.setColor(QPalette::ToolTipBase, QApplication::palette().color(QPalette::Base)); palette.setColor(QPalette::ToolTipText, QRgb(0x404044)); // same color as chart label brush } diff --git a/tools/cabana/dbc/dbc.cc b/tools/cabana/dbc/dbc.cc index e9c869fce7..9b0de92218 100644 --- a/tools/cabana/dbc/dbc.cc +++ b/tools/cabana/dbc/dbc.cc @@ -109,7 +109,7 @@ void cabana::Msg::update() { mask[i] |= ((1ULL << sz) - 1) << shift; - bits -= size; + bits -= sz; i = sig->is_little_endian ? i - 1 : i + 1; } } diff --git a/tools/cabana/historylog.cc b/tools/cabana/historylog.cc index 7e73da55f0..3dbdf5a7cd 100644 --- a/tools/cabana/historylog.cc +++ b/tools/cabana/historylog.cc @@ -153,7 +153,7 @@ void HeaderView::paintSection(QPainter *painter, const QRect &rect, int logicalI painter->fillRect(rect, bg_role.value()); } QString text = model()->headerData(logicalIndex, Qt::Horizontal, Qt::DisplayRole).toString(); - painter->setPen(palette().color(settings.theme == DARK_THEME ? QPalette::BrightText : QPalette::Text)); + painter->setPen(palette().color(utils::isDarkTheme() ? QPalette::BrightText : QPalette::Text)); painter->drawText(rect.adjusted(5, 3, -5, -3), defaultAlignment(), text.replace(QChar('_'), ' ')); } diff --git a/tools/cabana/panda.cc b/tools/cabana/panda.cc new file mode 100644 index 0000000000..0612d67746 --- /dev/null +++ b/tools/cabana/panda.cc @@ -0,0 +1,346 @@ +#include "tools/cabana/panda.h" + +#include +#include +#include +#include +#include + +#include "cereal/messaging/messaging.h" +#include "common/swaglog.h" +#include "common/util.h" + +static libusb_context *init_usb_ctx() { + libusb_context *context = nullptr; + int err = libusb_init(&context); + if (err != 0) { + LOGE("libusb initialization error"); + return nullptr; + } + +#if LIBUSB_API_VERSION >= 0x01000106 + libusb_set_option(context, LIBUSB_OPTION_LOG_LEVEL, LIBUSB_LOG_LEVEL_INFO); +#else + libusb_set_debug(context, 3); +#endif + return context; +} + +Panda::Panda(std::string serial, uint32_t bus_offset) : bus_offset(bus_offset) { + if (!init_usb_connection(serial)) { + throw std::runtime_error("Error connecting to panda"); + } + + LOGW("connected to %s over USB", serial.c_str()); + hw_type = get_hw_type(); + can_reset_communications(); +} + +Panda::~Panda() { + cleanup_usb(); +} + +bool Panda::connected() { + return connected_flag; +} + +bool Panda::comms_healthy() { + return comms_healthy_flag; +} + +std::string Panda::hw_serial() { + return hw_serial_str; +} + +std::vector Panda::list(bool usb_only) { + static std::unique_ptr context(init_usb_ctx(), libusb_exit); + + ssize_t num_devices; + libusb_device **dev_list = NULL; + std::vector serials; + if (!context) { return serials; } + + num_devices = libusb_get_device_list(context.get(), &dev_list); + if (num_devices < 0) { + LOGE("libusb can't get device list"); + goto finish; + } + + for (size_t i = 0; i < num_devices; ++i) { + libusb_device *device = dev_list[i]; + libusb_device_descriptor desc; + libusb_get_device_descriptor(device, &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + libusb_device_handle *handle = NULL; + int ret = libusb_open(device, &handle); + if (ret < 0) { goto finish; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + libusb_close(handle); + if (ret < 0) { goto finish; } + + serials.push_back(std::string((char *)desc_serial, ret)); + } + } + +finish: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + return serials; +} + +void Panda::set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param) { + control_write(0xdc, (uint16_t)safety_model, safety_param); +} + + +cereal::PandaState::PandaType Panda::get_hw_type() { + unsigned char hw_query[1] = {0}; + + control_read(0xc1, 0, 0, hw_query, 1); + return (cereal::PandaState::PandaType)(hw_query[0]); +} + + + + +void Panda::send_heartbeat(bool engaged) { + control_write(0xf3, engaged, 0); +} + +void Panda::set_can_speed_kbps(uint16_t bus, uint16_t speed) { + control_write(0xde, bus, (speed * 10)); +} + + +void Panda::set_data_speed_kbps(uint16_t bus, uint16_t speed) { + control_write(0xf9, bus, (speed * 10)); +} + + + +bool Panda::can_receive(std::vector& out_vec) { + // Check if enough space left in buffer to store RECV_SIZE data + assert(receive_buffer_size + RECV_SIZE <= sizeof(receive_buffer)); + + int recv = bulk_read(0x81, &receive_buffer[receive_buffer_size], RECV_SIZE); + if (!comms_healthy()) { + return false; + } + + bool ret = true; + if (recv > 0) { + receive_buffer_size += recv; + ret = unpack_can_buffer(receive_buffer, receive_buffer_size, out_vec); + } + return ret; +} + +void Panda::can_reset_communications() { + control_write(0xc0, 0, 0); +} + +bool Panda::unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector &out_vec) { + int pos = 0; + + while (pos <= size - sizeof(can_header)) { + can_header header; + memcpy(&header, &data[pos], sizeof(can_header)); + + const uint8_t data_len = dlc_to_len[header.data_len_code]; + if (pos + sizeof(can_header) + data_len > size) { + // we don't have all the data for this message yet + break; + } + + if (calculate_checksum(&data[pos], sizeof(can_header) + data_len) != 0) { + LOGE("Panda CAN checksum failed"); + size = 0; + can_reset_communications(); + return false; + } + + can_frame &canData = out_vec.emplace_back(); + canData.address = header.addr; + canData.src = header.bus + bus_offset; + if (header.rejected) { + canData.src += CAN_REJECTED_BUS_OFFSET; + } + if (header.returned) { + canData.src += CAN_RETURNED_BUS_OFFSET; + } + + canData.dat.assign((char *)&data[pos + sizeof(can_header)], data_len); + + pos += sizeof(can_header) + data_len; + } + + // move the overflowing data to the beginning of the buffer for the next round + memmove(data, &data[pos], size - pos); + size -= pos; + + return true; +} + +uint8_t Panda::calculate_checksum(uint8_t *data, uint32_t len) { + uint8_t checksum = 0U; + for (uint32_t i = 0U; i < len; i++) { + checksum ^= data[i]; + } + return checksum; +} + +// USB implementation methods +bool Panda::init_usb_connection(const std::string& serial) { + ssize_t num_devices; + libusb_device **dev_list = NULL; + int err = 0; + + ctx = init_usb_ctx(); + if (!ctx) { goto fail; } + + // connect by serial + num_devices = libusb_get_device_list(ctx, &dev_list); + if (num_devices < 0) { goto fail; } + + for (size_t i = 0; i < num_devices; ++i) { + libusb_device_descriptor desc; + libusb_get_device_descriptor(dev_list[i], &desc); + if (desc.idVendor == 0x3801 && desc.idProduct == 0xddcc) { + int ret = libusb_open(dev_list[i], &dev_handle); + if (dev_handle == NULL || ret < 0) { goto fail; } + + unsigned char desc_serial[26] = { 0 }; + ret = libusb_get_string_descriptor_ascii(dev_handle, desc.iSerialNumber, desc_serial, std::size(desc_serial)); + if (ret < 0) { goto fail; } + + hw_serial_str = std::string((char *)desc_serial, ret); + if (serial.empty() || serial == hw_serial_str) { + break; + } + libusb_close(dev_handle); + dev_handle = NULL; + } + } + if (dev_handle == NULL) goto fail; + libusb_free_device_list(dev_list, 1); + + if (libusb_kernel_driver_active(dev_handle, 0) == 1) { + libusb_detach_kernel_driver(dev_handle, 0); + } + + err = libusb_set_configuration(dev_handle, 1); + if (err != 0) { goto fail; } + + err = libusb_claim_interface(dev_handle, 0); + if (err != 0) { goto fail; } + + return true; + +fail: + if (dev_list != NULL) { + libusb_free_device_list(dev_list, 1); + } + cleanup_usb(); + return false; +} + +void Panda::cleanup_usb() { + if (dev_handle) { + libusb_release_interface(dev_handle, 0); + libusb_close(dev_handle); + dev_handle = nullptr; + } + + if (ctx) { + libusb_exit(ctx); + ctx = nullptr; + } + connected_flag = false; +} + +void Panda::handle_usb_issue(int err, const char func[]) { + LOGE_100("usb error %d \"%s\" in %s", err, libusb_strerror((enum libusb_error)err), func); + if (err == LIBUSB_ERROR_NO_DEVICE) { + LOGE("lost connection"); + connected_flag = false; + } +} + +int Panda::control_write(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_OUT | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected_flag) { + return LIBUSB_ERROR_NO_DEVICE; + } + + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, NULL, 0, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected_flag); + + return err; +} + +int Panda::control_read(uint8_t bRequest, uint16_t wValue, uint16_t wIndex, unsigned char *data, uint16_t wLength, unsigned int timeout) { + int err; + const uint8_t bmRequestType = LIBUSB_ENDPOINT_IN | LIBUSB_REQUEST_TYPE_VENDOR | LIBUSB_RECIPIENT_DEVICE; + + if (!connected_flag) { + return LIBUSB_ERROR_NO_DEVICE; + } + + do { + err = libusb_control_transfer(dev_handle, bmRequestType, bRequest, wValue, wIndex, data, wLength, timeout); + if (err < 0) handle_usb_issue(err, __func__); + } while (err < 0 && connected_flag); + + return err; +} + +int Panda::bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected_flag) { + return 0; + } + + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + if (err == LIBUSB_ERROR_TIMEOUT) { + LOGW("Transmit buffer full"); + break; + } else if (err != 0 || length != transferred) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected_flag); + + return transferred; +} + +int Panda::bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout) { + int err; + int transferred = 0; + + if (!connected_flag) { + return 0; + } + + do { + err = libusb_bulk_transfer(dev_handle, endpoint, data, length, &transferred, timeout); + if (err == LIBUSB_ERROR_TIMEOUT) { + break; // timeout is okay to exit, recv still happened + } else if (err == LIBUSB_ERROR_OVERFLOW) { + comms_healthy_flag = false; + LOGE_100("overflow got 0x%x", transferred); + } else if (err != 0) { + handle_usb_issue(err, __func__); + } + } while (err != 0 && connected_flag); + + return transferred; +} diff --git a/tools/cabana/panda.h b/tools/cabana/panda.h new file mode 100644 index 0000000000..d318c33f4d --- /dev/null +++ b/tools/cabana/panda.h @@ -0,0 +1,95 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +#include "cereal/gen/cpp/car.capnp.h" +#include "cereal/gen/cpp/log.capnp.h" +#include "panda/board/health.h" +#include "panda/board/can.h" + +#define USB_TX_SOFT_LIMIT (0x100U) +#define USBPACKET_MAX_SIZE (0x40) +#define RECV_SIZE (0x4000U) +#define TIMEOUT 0 + +#define CAN_REJECTED_BUS_OFFSET 0xC0U +#define CAN_RETURNED_BUS_OFFSET 0x80U + +#define PANDA_BUS_OFFSET 4 + +struct __attribute__((packed)) can_header { + uint8_t reserved : 1; + uint8_t bus : 3; + uint8_t data_len_code : 4; + uint8_t rejected : 1; + uint8_t returned : 1; + uint8_t extended : 1; + uint32_t addr : 29; + uint8_t checksum : 8; +}; + +struct can_frame { + long address; + std::string dat; + long src; +}; + + +class Panda { +public: + Panda(std::string serial="", uint32_t bus_offset=0); + ~Panda(); + + cereal::PandaState::PandaType hw_type = cereal::PandaState::PandaType::UNKNOWN; + const uint32_t bus_offset; + + bool connected(); + bool comms_healthy(); + std::string hw_serial(); + + // Static functions + static std::vector list(bool usb_only=false); + + // Panda functionality + cereal::PandaState::PandaType get_hw_type(); + void set_safety_model(cereal::CarParams::SafetyModel safety_model, uint16_t safety_param=0U); + void send_heartbeat(bool engaged); + void set_can_speed_kbps(uint16_t bus, uint16_t speed); + void set_data_speed_kbps(uint16_t bus, uint16_t speed); + bool can_receive(std::vector& out_vec); + void can_reset_communications(); + +private: + // USB connection members + libusb_context *ctx = nullptr; + libusb_device_handle *dev_handle = nullptr; + std::string hw_serial_str; + std::atomic connected_flag = true; + std::atomic comms_healthy_flag = true; + + // CAN buffer members + uint8_t receive_buffer[RECV_SIZE + sizeof(can_header) + 64]; + uint32_t receive_buffer_size = 0; + + // Internal methods + bool init_usb_connection(const std::string& serial); + void cleanup_usb(); + void handle_usb_issue(int err, const char func[]); + int control_write(uint8_t request, uint16_t param1, uint16_t param2, unsigned int timeout=TIMEOUT); + int control_read(uint8_t request, uint16_t param1, uint16_t param2, unsigned char *data, uint16_t length, unsigned int timeout=TIMEOUT); + int bulk_write(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + int bulk_read(unsigned char endpoint, unsigned char* data, int length, unsigned int timeout=TIMEOUT); + bool unpack_can_buffer(uint8_t *data, uint32_t &size, std::vector &out_vec); + uint8_t calculate_checksum(uint8_t *data, uint32_t len); +}; diff --git a/tools/cabana/signalview.cc b/tools/cabana/signalview.cc index 9fe70c3ee8..35537d75e3 100644 --- a/tools/cabana/signalview.cc +++ b/tools/cabana/signalview.cc @@ -634,11 +634,12 @@ void SignalView::updateState(const std::set *msgs) { QSize size(available_width - value_width, delegate->button_size.height() - style()->pixelMetric(QStyle::PM_FocusFrameVMargin) * 2); + auto [first, last] = can->eventsInRange(model->msg_id, std::make_pair(last_msg.ts -settings.sparkline_range, last_msg.ts)); QFutureSynchronizer synchronizer; for (int i = first_visible.row(); i <= last_visible.row(); ++i) { auto item = model->getItem(model->index(i, 1)); synchronizer.addFuture(QtConcurrent::run( - &item->sparkline, &Sparkline::update, model->msg_id, item->sig, last_msg.ts, settings.sparkline_range, size)); + &item->sparkline, &Sparkline::update, item->sig, first, last, settings.sparkline_range, size)); } synchronizer.waitForFinished(); } diff --git a/tools/cabana/streams/pandastream.cc b/tools/cabana/streams/pandastream.cc index e66d72e59d..a2430c665f 100644 --- a/tools/cabana/streams/pandastream.cc +++ b/tools/cabana/streams/pandastream.cc @@ -24,7 +24,7 @@ bool PandaStream::connect() { return false; } - panda->set_safety_model(cereal::CarParams::SafetyModel::SILENT); + panda->set_safety_model(cereal::CarParams::SafetyModel::NO_OUTPUT); for (int bus = 0; bus < config.bus_config.size(); bus++) { panda->set_can_speed_kbps(bus, config.bus_config[bus].can_speed_kbps); @@ -72,7 +72,7 @@ void PandaStream::streamThread() { handleEvent(capnp::messageToFlatArray(msg)); - panda->send_heartbeat(false, false); + panda->send_heartbeat(false); } } diff --git a/tools/cabana/streams/pandastream.h b/tools/cabana/streams/pandastream.h index 826b1aa986..e17ad887fc 100644 --- a/tools/cabana/streams/pandastream.h +++ b/tools/cabana/streams/pandastream.h @@ -7,7 +7,7 @@ #include #include "tools/cabana/streams/livestream.h" -#include "selfdrive/pandad/panda.h" +#include "tools/cabana/panda.h" const uint32_t speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U}; const uint32_t data_speeds[] = {10U, 20U, 50U, 100U, 125U, 250U, 500U, 1000U, 2000U, 5000U}; diff --git a/tools/cabana/utils/util.cc b/tools/cabana/utils/util.cc index 4556f90850..f27df4bf1e 100644 --- a/tools/cabana/utils/util.cc +++ b/tools/cabana/utils/util.cc @@ -191,8 +191,15 @@ DoubleValidator::DoubleValidator(QObject *parent) : QDoubleValidator(parent) { } namespace utils { + +bool isDarkTheme() { + QColor windowColor = QApplication::palette().color(QPalette::Window); + return windowColor.lightness() < 128; +} + QPixmap icon(const QString &id) { - bool dark_theme = settings.theme == DARK_THEME; + bool dark_theme = isDarkTheme(); + QPixmap pm; QString key = "bootstrap_" % id % (dark_theme ? "1" : "0"); if (!QPixmapCache::find(key, &pm)) { diff --git a/tools/cabana/utils/util.h b/tools/cabana/utils/util.h index 1aad3103db..5c1cbe2d69 100644 --- a/tools/cabana/utils/util.h +++ b/tools/cabana/utils/util.h @@ -98,7 +98,9 @@ public: }; namespace utils { + QPixmap icon(const QString &id); +bool isDarkTheme(); void setTheme(int theme); QString formatSeconds(double sec, bool include_milliseconds = false, bool absolute_time = false); inline void drawStaticText(QPainter *p, const QRect &r, const QStaticText &text) { diff --git a/tools/cabana/videowidget.cc b/tools/cabana/videowidget.cc index 3d715b0319..c857c9ffbe 100644 --- a/tools/cabana/videowidget.cc +++ b/tools/cabana/videowidget.cc @@ -19,7 +19,7 @@ const int THUMBNAIL_MARGIN = 3; static const QColor timeline_colors[] = { [(int)TimelineType::None] = QColor(111, 143, 175), [(int)TimelineType::Engaged] = QColor(0, 163, 108), - [(int)TimelineType::UserFlag] = Qt::magenta, + [(int)TimelineType::UserBookmark] = Qt::magenta, [(int)TimelineType::AlertInfo] = Qt::green, [(int)TimelineType::AlertWarning] = QColor(255, 195, 0), [(int)TimelineType::AlertCritical] = QColor(199, 0, 57), @@ -64,7 +64,7 @@ VideoWidget::VideoWidget(QWidget *parent) : QFrame(parent) { Pause/Resume:  space  )").arg(timeline_colors[(int)TimelineType::None].name(), timeline_colors[(int)TimelineType::Engaged].name(), - timeline_colors[(int)TimelineType::UserFlag].name(), + timeline_colors[(int)TimelineType::UserBookmark].name(), timeline_colors[(int)TimelineType::AlertInfo].name(), timeline_colors[(int)TimelineType::AlertWarning].name(), timeline_colors[(int)TimelineType::AlertCritical].name())); @@ -261,14 +261,23 @@ void Slider::paintEvent(QPaintEvent *ev) { QStyleOptionSlider opt; initStyleOption(&opt); - QRect r = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); - p.fillRect(r, timeline_colors[(int)TimelineType::None]); + QRect handle_rect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderHandle, this); + QRect groove_rect = style()->subControlRect(QStyle::CC_Slider, &opt, QStyle::SC_SliderGroove, this); + + // Adjust groove height to match handle height + int handle_height = handle_rect.height(); + groove_rect.setHeight(handle_height * 0.5); + groove_rect.moveCenter(QPoint(groove_rect.center().x(), rect().center().y())); + + p.fillRect(groove_rect, timeline_colors[(int)TimelineType::None]); double min = minimum() / factor; double max = maximum() / factor; auto fillRange = [&](double begin, double end, const QColor &color) { if (begin > max || end < min) return; + + QRect r = groove_rect; r.setLeft(((std::max(min, begin) - min) / (max - min)) * width()); r.setRight(((std::min(max, end) - min) / (max - min)) * width()); p.fillRect(r, color); diff --git a/tools/cabana/videowidget.h b/tools/cabana/videowidget.h index 6f756448c0..6da0023123 100644 --- a/tools/cabana/videowidget.h +++ b/tools/cabana/videowidget.h @@ -11,7 +11,7 @@ #include #include -#include "selfdrive/ui/qt/widgets/cameraview.h" +#include "tools/cabana/cameraview.h" #include "tools/cabana/utils/util.h" #include "tools/replay/logreader.h" #include "tools/cabana/streams/replaystream.h" diff --git a/tools/camerastream/compressed_vipc.py b/tools/camerastream/compressed_vipc.py index 8c773f9dfe..4dc74272ea 100755 --- a/tools/camerastream/compressed_vipc.py +++ b/tools/camerastream/compressed_vipc.py @@ -62,7 +62,7 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): print("waiting for iframe") continue time_q.append(time.monotonic()) - network_latency = (int(time.time()*1e9) - evta.unixTimestampNanos)/1e6 + network_latency = (int(time.time()*1e9) - evta.unixTimestampNanos)/1e6 # noqa: TID251 frame_latency = ((evta.idx.timestampEof/1e9) - (evta.idx.timestampSof/1e9))*1000 process_latency = ((evt.logMonoTime/1e9) - (evta.idx.timestampEof/1e9))*1000 @@ -107,7 +107,7 @@ def decoder(addr, vipc_server, vst, nvidia, W, H, debug=False): class CompressedVipc: - def __init__(self, addr, vision_streams, nvidia=False, debug=False): + def __init__(self, addr, vision_streams, server_name, nvidia=False, debug=False): print("getting frame sizes") os.environ["ZMQ"] = "1" messaging.reset_context() @@ -117,7 +117,7 @@ class CompressedVipc: os.environ.pop("ZMQ") messaging.reset_context() - self.vipc_server = VisionIpcServer("camerad") + self.vipc_server = VisionIpcServer(server_name) for vst in vision_streams: ed = sm[ENCODE_SOCKETS[vst]] self.vipc_server.create_buffers(vst, 4, ed.width, ed.height) @@ -144,6 +144,7 @@ if __name__ == "__main__": parser.add_argument("addr", help="Address of comma three") parser.add_argument("--nvidia", action="store_true", help="Use nvidia instead of ffmpeg") parser.add_argument("--cams", default="0,1,2", help="Cameras to decode") + parser.add_argument("--server", default="camerad", help="choose vipc server name") parser.add_argument("--silent", action="store_true", help="Suppress debug output") args = parser.parse_args() @@ -154,7 +155,7 @@ if __name__ == "__main__": ] vsts = [vision_streams[int(x)] for x in args.cams.split(",")] - cvipc = CompressedVipc(args.addr, vsts, args.nvidia, debug=(not args.silent)) + cvipc = CompressedVipc(args.addr, vsts, args.server, args.nvidia, debug=(not args.silent)) # register exit handler signal.signal(signal.SIGINT, lambda sig, frame: cvipc.kill()) diff --git a/tools/car_porting/examples/find_segments_with_message.ipynb b/tools/car_porting/examples/find_segments_with_message.ipynb index 4e688cc65b..af17bde52b 100644 --- a/tools/car_porting/examples/find_segments_with_message.ipynb +++ b/tools/car_porting/examples/find_segments_with_message.ipynb @@ -76,7 +76,7 @@ " if platform not in database:\n", " print(f\"No segments available for {platform}\")\n", " continue\n", - " \n", + "\n", " all_segments = database[platform]\n", " NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n", " TEST_SEGMENTS.extend(random.sample(all_segments, NUM_SEGMENTS))\n", @@ -147,7 +147,7 @@ } ], "source": [ - "from openpilot.tools.lib.logreader import LogReader\n", + "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", "from tqdm.notebook import tqdm, tnrange\n", "\n", "# Example search for CAN ignition messages\n", @@ -169,7 +169,7 @@ "progress_bar = tnrange(len(TEST_SEGMENTS), desc=\"segments searched\")\n", "\n", "for segment in TEST_SEGMENTS:\n", - " lr = LogReader(segment)\n", + " lr = LogReader(segment, sources=[comma_car_segments_source])\n", " CP = lr.first(\"carParams\")\n", " if CP is None:\n", " progress_bar.update()\n", diff --git a/tools/car_porting/examples/ford_vin_fingerprint.ipynb b/tools/car_porting/examples/ford_vin_fingerprint.ipynb index 7b0dd656da..6b806d22d2 100644 --- a/tools/car_porting/examples/ford_vin_fingerprint.ipynb +++ b/tools/car_porting/examples/ford_vin_fingerprint.ipynb @@ -20,7 +20,7 @@ "source": [ "\"\"\"In this example, we use the public comma car segments database to check if vin fingerprinting is feasible for ford.\"\"\"\n", "\n", - "from openpilot.tools.lib.logreader import LogReader\n", + "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", "from openpilot.tools.lib.comma_car_segments import get_comma_car_segments_database\n", "from opendbc.car.ford.values import CAR\n", "\n", @@ -100,7 +100,7 @@ " if platform not in database:\n", " print(f\"Skipping platform: {platform}, no data available\")\n", " continue\n", - " \n", + "\n", " all_segments = database[platform]\n", "\n", " NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n", @@ -110,7 +110,7 @@ " segments = random.sample(all_segments, NUM_SEGMENTS)\n", "\n", " for segment in segments:\n", - " lr = LogReader(segment)\n", + " lr = LogReader(segment, sources=[comma_car_segments_source])\n", " CP = lr.first(\"carParams\")\n", " if \"FORD\" not in CP.carFingerprint:\n", " print(segment, CP.carFingerprint)\n", diff --git a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb index 5fdbdda684..f0bca8decc 100644 --- a/tools/car_porting/examples/hkg_canfd_gear_message.ipynb +++ b/tools/car_porting/examples/hkg_canfd_gear_message.ipynb @@ -72,7 +72,7 @@ " #if platform not in database:\n", " # print(f\"Skipping platform: {platform}, no data available\")\n", " # continue\n", - " \n", + "\n", " all_segments = database[platform]\n", "\n", " NUM_SEGMENTS = min(len(all_segments), MAX_SEGS_PER_PLATFORM)\n", @@ -198,12 +198,12 @@ "from opendbc.car.hyundai.hyundaicanfd import CanBus\n", "\n", "from openpilot.selfdrive.pandad import can_capnp_to_list\n", - "from openpilot.tools.lib.logreader import LogReader\n", + "from openpilot.tools.lib.logreader import LogReader, comma_car_segments_source\n", "\n", "message_names = [\"GEAR_SHIFTER\", \"ACCELERATOR\", \"GEAR\", \"GEAR_ALT\", \"GEAR_ALT_2\"]\n", "\n", "for segment in TEST_SEGMENTS:\n", - " lr = LogReader(segment)\n", + " lr = LogReader(segment, sources=[comma_car_segments_source])\n", " CP = lr.first(\"carParams\")\n", " if CP is None:\n", " continue\n", diff --git a/tools/clip/run.py b/tools/clip/run.py index a84290f9c4..8fa0e8eda3 100755 --- a/tools/clip/run.py +++ b/tools/clip/run.py @@ -1,6 +1,5 @@ #!/usr/bin/env python3 -import atexit import logging import os import platform @@ -11,13 +10,14 @@ from argparse import ArgumentParser, ArgumentTypeError from collections.abc import Sequence from pathlib import Path from random import randint -from subprocess import Popen, PIPE +from subprocess import Popen from typing import Literal from cereal.messaging import SubMaster from openpilot.common.basedir import BASEDIR from openpilot.common.params import Params, UnknownKeyName from openpilot.common.prefix import OpenpilotPrefix +from openpilot.common.run import managed_proc from openpilot.tools.lib.route import Route from openpilot.tools.lib.logreader import LogReader @@ -29,7 +29,7 @@ FRAMERATE = 20 PIXEL_DEPTH = '24' RESOLUTION = '2160x1080' SECONDS_TO_WARM = 2 -PROC_WAIT_SECONDS = 30 +PROC_WAIT_SECONDS = 30*10 OPENPILOT_FONT = str(Path(BASEDIR, 'selfdrive/assets/fonts/Inter-Regular.ttf').resolve()) REPLAY = str(Path(BASEDIR, 'tools/replay/replay').resolve()) @@ -38,22 +38,23 @@ UI = str(Path(BASEDIR, 'selfdrive/ui/ui').resolve()) logger = logging.getLogger('clip.py') -def check_for_failure(proc: Popen): - exit_code = proc.poll() - if exit_code is not None and exit_code != 0: - cmd = str(proc.args) - if isinstance(proc.args, str): - cmd = proc.args - elif isinstance(proc.args, Sequence): - cmd = str(proc.args[0]) - msg = f'{cmd} failed, exit code {exit_code}' - logger.error(msg) - stdout, stderr = proc.communicate() - if stdout: - logger.error(stdout.decode()) - if stderr: - logger.error(stderr.decode()) - raise ChildProcessError(msg) +def check_for_failure(procs: list[Popen]): + for proc in procs: + exit_code = proc.poll() + if exit_code is not None and exit_code != 0: + cmd = str(proc.args) + if isinstance(proc.args, str): + cmd = proc.args + elif isinstance(proc.args, Sequence): + cmd = str(proc.args[0]) + msg = f'{cmd} failed, exit code {exit_code}' + logger.error(msg) + stdout, stderr = proc.communicate() + if stdout: + logger.error(stdout.decode()) + if stderr: + logger.error(stderr.decode()) + raise ChildProcessError(msg) def escape_ffmpeg_text(value: str): @@ -130,17 +131,13 @@ def populate_car_params(lr: LogReader): for cp in entries: key, value = cp.key, cp.value try: - params.put(key, value) + params.put(key, params.cpp2python(key, value)) except UnknownKeyName: # forks of openpilot may have other Params keys configured. ignore these logger.warning(f"unknown Params key '{key}', skipping") logger.debug('persisted CarParams') -def start_proc(args: list[str], env: dict[str, str]): - return Popen(args, env=env, stdout=PIPE, stderr=PIPE) - - def validate_env(parser: ArgumentParser): if platform.system() not in ['Linux']: parser.exit(1, f'clip.py: error: {platform.system()} is not a supported operating system\n') @@ -176,8 +173,7 @@ def wait_for_frames(procs: list[Popen]): while no_frames_drawn: sm.update() no_frames_drawn = sm['uiDebug'].drawTimeMillis == 0. - for proc in procs: - check_for_failure(proc) + check_for_failure(procs) def clip( @@ -188,6 +184,7 @@ def clip( out: str, start: int, end: int, + speed: int, target_mb: int, title: str | None, ): @@ -212,6 +209,12 @@ def clip( if title: overlays.append(f"drawtext=text='{escape_ffmpeg_text(title)}':fontfile={OPENPILOT_FONT}:fontcolor=white:fontsize=32:{box_style}:x=(w-text_w)/2:y=53") + if speed > 1: + overlays += [ + f"setpts=PTS/{speed}", + "fps=60", + ] + ffmpeg_cmd = [ 'ffmpeg', '-y', '-video_size', RESOLUTION, @@ -246,35 +249,22 @@ def clip( with OpenpilotPrefix(prefix, shared_download_cache=True): populate_car_params(lr) - env = os.environ.copy() env['DISPLAY'] = display - xvfb_proc = start_proc(xvfb_cmd, env) - atexit.register(lambda: xvfb_proc.terminate()) - ui_proc = start_proc(ui_cmd, env) - atexit.register(lambda: ui_proc.terminate()) - replay_proc = start_proc(replay_cmd, env) - atexit.register(lambda: replay_proc.terminate()) - procs = [replay_proc, ui_proc, xvfb_proc] - - logger.info('waiting for replay to begin (loading segments, may take a while)...') - wait_for_frames(procs) - - logger.debug(f'letting UI warm up ({SECONDS_TO_WARM}s)...') - time.sleep(SECONDS_TO_WARM) - for proc in procs: - check_for_failure(proc) - - ffmpeg_proc = start_proc(ffmpeg_cmd, env) - procs.append(ffmpeg_proc) - atexit.register(lambda: ffmpeg_proc.terminate()) - - logger.info(f'recording in progress ({duration}s)...') - ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS) - for proc in procs: - check_for_failure(proc) - logger.info(f'recording complete: {Path(out).resolve()}') + with managed_proc(xvfb_cmd, env) as xvfb_proc, managed_proc(ui_cmd, env) as ui_proc, managed_proc(replay_cmd, env) as replay_proc: + procs = [xvfb_proc, ui_proc, replay_proc] + logger.info('waiting for replay to begin (loading segments, may take a while)...') + wait_for_frames(procs) + logger.debug(f'letting UI warm up ({SECONDS_TO_WARM}s)...') + time.sleep(SECONDS_TO_WARM) + check_for_failure(procs) + with managed_proc(ffmpeg_cmd, env) as ffmpeg_proc: + procs.append(ffmpeg_proc) + logger.info(f'recording in progress ({duration}s)...') + ffmpeg_proc.wait(duration + PROC_WAIT_SECONDS) + check_for_failure(procs) + logger.info(f'recording complete: {Path(out).resolve()}') def main(): @@ -289,6 +279,7 @@ def main(): p.add_argument('-o', '--output', help='output clip to (.mp4)', type=validate_output_file, default=DEFAULT_OUTPUT) p.add_argument('-p', '--prefix', help='openpilot prefix', default=f'clip_{randint(100, 99999)}') p.add_argument('-q', '--quality', help='quality of camera (low = qcam, high = hevc)', choices=['low', 'high'], default='high') + p.add_argument('-x', '--speed', help='record the clip at this speed multiple', type=int, default=1) p.add_argument('-s', '--start', help='start clipping at seconds', type=int) p.add_argument('-t', '--title', help='overlay this title on the video (e.g. "Chill driving across the Golden Gate Bridge")', type=validate_title) args = parse_args(p) @@ -302,6 +293,7 @@ def main(): out=args.output, start=args.start, end=args.end, + speed=args.speed, target_mb=args.file_size, title=args.title, ) @@ -310,9 +302,7 @@ def main(): logger.exception('interrupted by user', exc_info=e) except Exception as e: logger.exception('encountered error', exc_info=e) - finally: - atexit._run_exitfuncs() - sys.exit(exit_code) + sys.exit(exit_code) if __name__ == '__main__': diff --git a/tools/lib/api.py b/tools/lib/api.py index c2e1b1a8cd..c6e2d98914 100644 --- a/tools/lib/api.py +++ b/tools/lib/api.py @@ -2,6 +2,8 @@ import os import requests API_HOST = os.getenv('API_HOST', 'https://api.commadotai.com') +# TODO: this should be merged into common.api + class CommaApi: def __init__(self, token=None): self.session = requests.Session() diff --git a/tools/lib/comma_car_segments.py b/tools/lib/comma_car_segments.py index fe9c350a9b..cd19356d66 100644 --- a/tools/lib/comma_car_segments.py +++ b/tools/lib/comma_car_segments.py @@ -14,7 +14,8 @@ def get_comma_car_segments_database(): ret = {} for platform in database: - ret[MIGRATION.get(platform, platform)] = database[platform] + # TODO: remove this when commaCarSegments is updated to remove selector + ret[MIGRATION.get(platform, platform)] = [s.rstrip('/s') for s in database[platform]] return ret @@ -86,5 +87,5 @@ def get_repo_url(path): return get_repo_raw_url(path) -def get_url(route, segment, file="rlog.bz2"): +def get_url(route, segment, file="rlog.zst"): return get_repo_url(f"segments/{route.replace('|', '/')}/{segment}/{file}") diff --git a/tools/lib/file_sources.py b/tools/lib/file_sources.py new file mode 100755 index 0000000000..cb7bf15114 --- /dev/null +++ b/tools/lib/file_sources.py @@ -0,0 +1,57 @@ +from collections.abc import Callable + +from openpilot.tools.lib.comma_car_segments import get_url as get_comma_segments_url +from openpilot.tools.lib.openpilotci import get_url +from openpilot.tools.lib.filereader import DATA_ENDPOINT, file_exists, internal_source_available +from openpilot.tools.lib.route import Route, SegmentRange, FileName + +# When passed a tuple of file names, each source will return the first that exists (rlog.zst, rlog.bz2) +FileNames = tuple[str, ...] +Source = Callable[[SegmentRange, list[int], FileNames], dict[int, str]] + +InternalUnavailableException = Exception("Internal source not available") + + +def comma_api_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: + route = Route(sr.route_name) + + # comma api will have already checked if the file exists + if fns == FileName.RLOG: + return {seg: route.log_paths()[seg] for seg in seg_idxs if route.log_paths()[seg] is not None} + else: + return {seg: route.qlog_paths()[seg] for seg in seg_idxs if route.qlog_paths()[seg] is not None} + + +def internal_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames, endpoint_url: str = DATA_ENDPOINT) -> dict[int, str]: + if not internal_source_available(endpoint_url): + raise InternalUnavailableException + + def get_internal_url(sr: SegmentRange, seg, file): + return f"{endpoint_url.rstrip('/')}/{sr.dongle_id}/{sr.log_id}/{seg}/{file}" + + return eval_source({seg: [get_internal_url(sr, seg, fn) for fn in fns] for seg in seg_idxs}) + + +def openpilotci_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: + return eval_source({seg: [get_url(sr.route_name, seg, fn) for fn in fns] for seg in seg_idxs}) + + +def comma_car_segments_source(sr: SegmentRange, seg_idxs: list[int], fns: FileNames) -> dict[int, str]: + return eval_source({seg: get_comma_segments_url(sr.route_name, seg) for seg in seg_idxs}) + + +def eval_source(files: dict[int, list[str] | str]) -> dict[int, str]: + # Returns valid file URLs given a list of possible file URLs for each segment (e.g. rlog.bz2, rlog.zst) + valid_files: dict[int, str] = {} + + for seg_idx, urls in files.items(): + if isinstance(urls, str): + urls = [urls] + + # Add first valid file URL + for url in urls: + if file_exists(url): + valid_files[seg_idx] = url + break + + return valid_files diff --git a/tools/lib/filereader.py b/tools/lib/filereader.py index 0bb4abd2fa..02f5fd1b95 100644 --- a/tools/lib/filereader.py +++ b/tools/lib/filereader.py @@ -1,6 +1,8 @@ import os import posixpath import socket +from functools import cache +from openpilot.common.retry import retry from urllib.parse import urlparse from openpilot.tools.lib.url_file import URLFile @@ -8,14 +10,16 @@ from openpilot.tools.lib.url_file import URLFile DATA_ENDPOINT = os.getenv("DATA_ENDPOINT", "http://data-raw.comma.internal/") -def internal_source_available(url=DATA_ENDPOINT): +@cache +@retry(delay=0.0) +def internal_source_available(url: str) -> bool: if os.path.isdir(url): return True try: hostname = urlparse(url).hostname port = urlparse(url).port or 80 - with socket.socket(socket.AF_INET,socket.SOCK_STREAM) as s: + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: s.settimeout(0.5) s.connect((hostname, port)) return True @@ -30,6 +34,7 @@ def resolve_name(fn): return fn +@cache def file_exists(fn): fn = resolve_name(fn) if fn.startswith(("http://", "https://")): diff --git a/tools/lib/framereader.py b/tools/lib/framereader.py index 8f7f723c28..30ce1f80fe 100644 --- a/tools/lib/framereader.py +++ b/tools/lib/framereader.py @@ -56,6 +56,7 @@ def decompress_video_data(rawdat, w, h, pix_fmt="rgb24", vid_fmt='hevc') -> np.n "-"] dat = subprocess.check_output(args, input=rawdat) + ret: np.ndarray if pix_fmt == "rgb24": ret = np.frombuffer(dat, dtype=np.uint8).reshape(-1, h, w, 3) elif pix_fmt in ["nv12", "yuv420p"]: diff --git a/tools/lib/helpers.py b/tools/lib/helpers.py index 7c34e17cb1..8c976e7ecc 100644 --- a/tools/lib/helpers.py +++ b/tools/lib/helpers.py @@ -9,7 +9,7 @@ class RE: INDEX = r'-?[0-9]+' SLICE = fr'(?P{INDEX})?:?(?P{INDEX})?:?(?P{INDEX})?' - SEGMENT_RANGE = fr'{ROUTE_NAME}(?:(--|/)(?P({SLICE})))?(?:/(?P([qras])))?' + SEGMENT_RANGE = fr'{ROUTE_NAME}(?:(--|/)(?P({SLICE})))?(?:/(?P([qra])))?' BOOTLOG_NAME = ROUTE_NAME diff --git a/tools/lib/logreader.py b/tools/lib/logreader.py index 34d3e5ea9f..8d84cdbd5d 100755 --- a/tools/lib/logreader.py +++ b/tools/lib/logreader.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 import bz2 -from functools import cache, partial +from functools import partial import multiprocessing import capnp import enum @@ -12,15 +12,15 @@ import urllib.parse import warnings import zstandard as zstd -from collections.abc import Callable, Iterable, Iterator +from collections.abc import Iterable, Iterator +from typing import cast from urllib.parse import parse_qs, urlparse from cereal import log as capnp_log from openpilot.common.swaglog import cloudlog -from openpilot.tools.lib.comma_car_segments import get_url as get_comma_segments_url -from openpilot.tools.lib.openpilotci import get_url -from openpilot.tools.lib.filereader import FileReader, file_exists, internal_source_available -from openpilot.tools.lib.route import Route, SegmentRange +from openpilot.tools.lib.filereader import FileReader +from openpilot.tools.lib.file_sources import comma_api_source, internal_source, openpilotci_source, comma_car_segments_source, Source +from openpilot.tools.lib.route import SegmentRange, FileName from openpilot.tools.lib.log_time_series import msgs_to_time_series LogMessage = type[capnp._DynamicStructReader] @@ -39,6 +39,7 @@ def save_log(dest, log_msgs, compress=True): with open(dest, "wb") as f: f.write(dat) + def decompress_stream(data: bytes): dctx = zstd.ZstdDecompressor() decompressed_data = b"" @@ -48,8 +49,46 @@ def decompress_stream(data: bytes): return decompressed_data + +class CachedEventReader: + __slots__ = ('_evt', '_enum') + + def __init__(self, evt: capnp._DynamicStructReader, _enum: str | None = None): + """All capnp attribute accesses are expensive, and which() is often called multiple times""" + self._evt = evt + self._enum: str | None = _enum + + # fast pickle support + def __reduce__(self): + return CachedEventReader._reducer, (self._evt.as_builder().to_bytes(), self._enum) + + @staticmethod + def _reducer(data: bytes, _enum: str | None = None): + with capnp_log.Event.from_bytes(data) as evt: + return CachedEventReader(evt, _enum) + + def __repr__(self): + return self._evt.__repr__() + + def __str__(self): + return self._evt.__str__() + + def __dir__(self): + return dir(self._evt) + + def which(self) -> str: + if self._enum is None: + self._enum = self._evt.which() + return self._enum + + def __getattr__(self, name: str): + if name.startswith("__") and name.endswith("__"): + return getattr(self, name) + return getattr(self._evt, name) + + class _LogFileReader: - def __init__(self, fn, canonicalize=True, only_union_types=False, sort_by_time=False, dat=None): + def __init__(self, fn, only_union_types=False, sort_by_time=False, dat=None): self.data_version = None self._only_union_types = only_union_types @@ -74,7 +113,7 @@ class _LogFileReader: self._ents = [] try: for e in ents: - self._ents.append(e) + self._ents.append(CachedEventReader(e)) except capnp.KjException: warnings.warn("Corrupted events detected", RuntimeWarning, stacklevel=1) @@ -96,155 +135,91 @@ class _LogFileReader: class ReadMode(enum.StrEnum): RLOG = "r" # only read rlogs QLOG = "q" # only read qlogs - SANITIZED = "s" # read from the commaCarSegments database AUTO = "a" # default to rlogs, fallback to qlogs AUTO_INTERACTIVE = "i" # default to rlogs, fallback to qlogs with a prompt from the user -LogPath = str | None -ValidFileCallable = Callable[[LogPath], bool] -Source = Callable[[SegmentRange, ReadMode], list[LogPath]] - -InternalUnavailableException = Exception("Internal source not available") - - class LogsUnavailable(Exception): pass -@cache -def default_valid_file(fn: LogPath) -> bool: - return fn is not None and file_exists(fn) - - -def auto_strategy(rlog_paths: list[LogPath], qlog_paths: list[LogPath], interactive: bool, valid_file: ValidFileCallable) -> list[LogPath]: - # auto select logs based on availability - missing_rlogs = [rlog is None or not valid_file(rlog) for rlog in rlog_paths].count(True) - if missing_rlogs != 0: - if interactive: - if input(f"{missing_rlogs}/{len(rlog_paths)} rlogs were not found, would you like to fallback to qlogs for those segments? (y/n) ").lower() != "y": - return rlog_paths - else: - cloudlog.warning(f"{missing_rlogs}/{len(rlog_paths)} rlogs were not found, falling back to qlogs for those segments...") - - return [rlog if valid_file(rlog) else (qlog if valid_file(qlog) else None) - for (rlog, qlog) in zip(rlog_paths, qlog_paths, strict=True)] - return rlog_paths - - -def apply_strategy(mode: ReadMode, rlog_paths: list[LogPath], qlog_paths: list[LogPath], valid_file: ValidFileCallable = default_valid_file) -> list[LogPath]: - if mode == ReadMode.RLOG: - return rlog_paths - elif mode == ReadMode.QLOG: - return qlog_paths - elif mode == ReadMode.AUTO: - return auto_strategy(rlog_paths, qlog_paths, False, valid_file) - elif mode == ReadMode.AUTO_INTERACTIVE: - return auto_strategy(rlog_paths, qlog_paths, True, valid_file) - raise ValueError(f"invalid mode: {mode}") - - -def comma_api_source(sr: SegmentRange, mode: ReadMode) -> list[LogPath]: - route = Route(sr.route_name) - - rlog_paths = [route.log_paths()[seg] for seg in sr.seg_idxs] - qlog_paths = [route.qlog_paths()[seg] for seg in sr.seg_idxs] - - # comma api will have already checked if the file exists - def valid_file(fn): - return fn is not None - - return apply_strategy(mode, rlog_paths, qlog_paths, valid_file=valid_file) - - -def internal_source(sr: SegmentRange, mode: ReadMode, file_ext: str = "bz2") -> list[LogPath]: - if not internal_source_available(): - raise InternalUnavailableException - - def get_internal_url(sr: SegmentRange, seg, file): - return f"cd:/{sr.dongle_id}/{sr.log_id}/{seg}/{file}.{file_ext}" - - # TODO: list instead of using static URLs to support routes with multiple file extensions - rlog_paths = [get_internal_url(sr, seg, "rlog") for seg in sr.seg_idxs] - qlog_paths = [get_internal_url(sr, seg, "qlog") for seg in sr.seg_idxs] - - return apply_strategy(mode, rlog_paths, qlog_paths) - - -def internal_source_zst(sr: SegmentRange, mode: ReadMode, file_ext: str = "zst") -> list[LogPath]: - return internal_source(sr, mode, file_ext) - - -def openpilotci_source(sr: SegmentRange, mode: ReadMode, file_ext: str = "bz2") -> list[LogPath]: - rlog_paths = [get_url(sr.route_name, seg, f"rlog.{file_ext}") for seg in sr.seg_idxs] - qlog_paths = [get_url(sr.route_name, seg, f"qlog.{file_ext}") for seg in sr.seg_idxs] - - return apply_strategy(mode, rlog_paths, qlog_paths) - - -def openpilotci_source_zst(sr: SegmentRange, mode: ReadMode) -> list[LogPath]: - return openpilotci_source(sr, mode, "zst") - - -def comma_car_segments_source(sr: SegmentRange, mode=ReadMode.RLOG) -> list[LogPath]: - return [get_comma_segments_url(sr.route_name, seg) for seg in sr.seg_idxs] - - -def testing_closet_source(sr: SegmentRange, mode=ReadMode.RLOG) -> list[LogPath]: - if not internal_source_available('http://testing.comma.life'): - raise InternalUnavailableException - return [f"http://testing.comma.life/download/{sr.route_name.replace('|', '/')}/{seg}/rlog" for seg in sr.seg_idxs] - - -def direct_source(file_or_url: str) -> list[LogPath]: +def direct_source(file_or_url: str) -> list[str]: return [file_or_url] -def get_invalid_files(files): - for f in files: - if f is None or not file_exists(f): - yield f - - -def check_source(source: Source, *args) -> list[LogPath]: - files = source(*args) - assert len(files) > 0, "No files on source" - assert next(get_invalid_files(files), False) is False, "Some files are invalid" - return files - - -def auto_source(sr: SegmentRange, mode=ReadMode.RLOG, sources: list[Source] = None) -> list[LogPath]: - if mode == ReadMode.SANITIZED: - return comma_car_segments_source(sr, mode) - - if sources is None: - sources = [internal_source, internal_source_zst, openpilotci_source, openpilotci_source_zst, - comma_api_source, comma_car_segments_source, testing_closet_source] +# TODO this should apply to camera files as well +def auto_source(identifier: str, sources: list[Source], default_mode: ReadMode) -> list[str]: exceptions = {} - # for automatic fallback modes, auto_source needs to first check if rlogs exist for any source - if mode in [ReadMode.AUTO, ReadMode.AUTO_INTERACTIVE]: + sr = SegmentRange(identifier) + needed_seg_idxs = sr.seg_idxs + + mode = default_mode if sr.selector is None else ReadMode(sr.selector) + if mode == ReadMode.QLOG: + try_fns = [FileName.QLOG] + else: + try_fns = [FileName.RLOG] + + # If selector allows it, fallback to qlogs + if mode in (ReadMode.AUTO, ReadMode.AUTO_INTERACTIVE): + try_fns.append(FileName.QLOG) + + # Build a dict of valid files as we evaluate each source. May contain mix of rlogs, qlogs, and None. + # This function only returns when we've sourced all files, or throws an exception + valid_files: dict[int, str] = {} + for fn in try_fns: for source in sources: try: - return check_source(source, sr, ReadMode.RLOG) - except Exception: - pass + files = source(sr, needed_seg_idxs, fn) - # Automatically determine viable source - for source in sources: - try: - return check_source(source, sr, mode) - except Exception as e: - exceptions[source.__name__] = e + # Build a dict of valid files + valid_files |= files - raise LogsUnavailable("auto_source could not find any valid source, exceptions for sources:\n - " + - "\n - ".join([f"{k}: {repr(v)}" for k, v in exceptions.items()])) + # Don't check for segment files that have already been found + needed_seg_idxs = [idx for idx in needed_seg_idxs if idx not in valid_files] + + # We've found all files, return them + if len(needed_seg_idxs) == 0: + return cast(list[str], list(valid_files.values())) + + except Exception as e: + exceptions[source.__name__] = e + + if fn == try_fns[0]: + missing_logs = len(needed_seg_idxs) + if mode == ReadMode.AUTO: + cloudlog.warning(f"{missing_logs}/{len(sr.seg_idxs)} rlogs were not found, falling back to qlogs for those segments...") + elif mode == ReadMode.AUTO_INTERACTIVE: + if input(f"{missing_logs}/{len(sr.seg_idxs)} rlogs were not found, would you like to fallback to qlogs for those segments? (y/N) ").lower() != "y": + break + + missing_logs = len(needed_seg_idxs) + raise LogsUnavailable(f"{missing_logs}/{len(sr.seg_idxs)} logs were not found, please ensure all logs " + + "are uploaded. You can fall back to qlogs with '/a' selector at the end of the route name.\n\n" + + "Exceptions for sources:\n - " + "\n - ".join([f"{k}: {repr(v)}" for k, v in exceptions.items()])) def parse_indirect(identifier: str) -> str: if "useradmin.comma.ai" in identifier: query = parse_qs(urlparse(identifier).query) - return query["onebox"][0] + identifier = query["onebox"][0] + elif "connect.comma.ai" in identifier: + path = urlparse(identifier).path.strip("/").split("/") + path = ['/'.join(path[:2]), *path[2:]] # recombine log id + + identifier = path[0] + if len(path) > 2: + # convert url with seconds to segments + start, end = int(path[1]) // 60, int(path[2]) // 60 + 1 + identifier = f"{identifier}/{start}:{end}" + + # add selector if it exists + if len(path) > 3: + identifier += f"/{path[3]}" + else: + # add selector if it exists + identifier = "/".join(path) + return identifier @@ -255,7 +230,7 @@ def parse_direct(identifier: str): class LogReader: - def _parse_identifier(self, identifier: str) -> list[LogPath]: + def _parse_identifier(self, identifier: str) -> list[str]: # useradmin, etc. identifier = parse_indirect(identifier) @@ -264,20 +239,16 @@ class LogReader: if direct_parsed is not None: return direct_source(identifier) - sr = SegmentRange(identifier) - mode = self.default_mode if sr.selector is None else ReadMode(sr.selector) - - identifiers = self.source(sr, mode) - - invalid_count = len(list(get_invalid_files(identifiers))) - assert invalid_count == 0, (f"{invalid_count}/{len(identifiers)} invalid log(s) found, please ensure all logs " + - "are uploaded or auto fallback to qlogs with '/a' selector at the end of the route name.") + identifiers = auto_source(identifier, self.sources, self.default_mode) return identifiers def __init__(self, identifier: str | list[str], default_mode: ReadMode = ReadMode.RLOG, - source: Source = auto_source, sort_by_time=False, only_union_types=False): + sources: list[Source] = None, sort_by_time=False, only_union_types=False): + if sources is None: + sources = [internal_source, comma_api_source, openpilotci_source, comma_car_segments_source] + self.default_mode = default_mode - self.source = source + self.sources = sources self.identifier = identifier if isinstance(identifier, str): self.identifier = [identifier] @@ -327,6 +298,7 @@ class LogReader: def time_series(self): return msgs_to_time_series(self) + if __name__ == "__main__": import codecs diff --git a/tools/lib/route.py b/tools/lib/route.py index 0a4700f083..1fc26fb996 100644 --- a/tools/lib/route.py +++ b/tools/lib/route.py @@ -10,12 +10,15 @@ from openpilot.tools.lib.auth_config import get_token from openpilot.tools.lib.api import APIError, CommaApi from openpilot.tools.lib.helpers import RE -QLOG_FILENAMES = ['qlog', 'qlog.bz2', 'qlog.zst'] -QCAMERA_FILENAMES = ['qcamera.ts'] -LOG_FILENAMES = ['rlog', 'rlog.bz2', 'raw_log.bz2', 'rlog.zst'] -CAMERA_FILENAMES = ['fcamera.hevc', 'video.hevc'] -DCAMERA_FILENAMES = ['dcamera.hevc'] -ECAMERA_FILENAMES = ['ecamera.hevc'] + +class FileName: + RLOG = ("rlog.zst", "rlog.bz2") + QLOG = ("qlog.zst", "qlog.bz2") + QCAMERA = ('qcamera.ts',) + FCAMERA = ('fcamera.hevc',) + ECAMERA = ('ecamera.hevc',) + DCAMERA = ('dcamera.hevc',) + BOOTLOG = ('bootlog.zst', 'bootlog.bz2') class Route: @@ -81,23 +84,23 @@ class Route: if segments.get(segment_name): segments[segment_name] = Segment( segment_name, - url if fn in LOG_FILENAMES else segments[segment_name].log_path, - url if fn in QLOG_FILENAMES else segments[segment_name].qlog_path, - url if fn in CAMERA_FILENAMES else segments[segment_name].camera_path, - url if fn in DCAMERA_FILENAMES else segments[segment_name].dcamera_path, - url if fn in ECAMERA_FILENAMES else segments[segment_name].ecamera_path, - url if fn in QCAMERA_FILENAMES else segments[segment_name].qcamera_path, + url if fn in FileName.RLOG else segments[segment_name].log_path, + url if fn in FileName.QLOG else segments[segment_name].qlog_path, + url if fn in FileName.FCAMERA else segments[segment_name].camera_path, + url if fn in FileName.DCAMERA else segments[segment_name].dcamera_path, + url if fn in FileName.ECAMERA else segments[segment_name].ecamera_path, + url if fn in FileName.QCAMERA else segments[segment_name].qcamera_path, self.metadata['url'], ) else: segments[segment_name] = Segment( segment_name, - url if fn in LOG_FILENAMES else None, - url if fn in QLOG_FILENAMES else None, - url if fn in CAMERA_FILENAMES else None, - url if fn in DCAMERA_FILENAMES else None, - url if fn in ECAMERA_FILENAMES else None, - url if fn in QCAMERA_FILENAMES else None, + url if fn in FileName.RLOG else None, + url if fn in FileName.QLOG else None, + url if fn in FileName.FCAMERA else None, + url if fn in FileName.DCAMERA else None, + url if fn in FileName.ECAMERA else None, + url if fn in FileName.QCAMERA else None, self.metadata['url'], ) @@ -135,32 +138,32 @@ class Route: for segment, files in segment_files.items(): try: - log_path = next(path for path, filename in files if filename in LOG_FILENAMES) + log_path = next(path for path, filename in files if filename in FileName.RLOG) except StopIteration: log_path = None try: - qlog_path = next(path for path, filename in files if filename in QLOG_FILENAMES) + qlog_path = next(path for path, filename in files if filename in FileName.QLOG) except StopIteration: qlog_path = None try: - camera_path = next(path for path, filename in files if filename in CAMERA_FILENAMES) + camera_path = next(path for path, filename in files if filename in FileName.FCAMERA) except StopIteration: camera_path = None try: - dcamera_path = next(path for path, filename in files if filename in DCAMERA_FILENAMES) + dcamera_path = next(path for path, filename in files if filename in FileName.DCAMERA) except StopIteration: dcamera_path = None try: - ecamera_path = next(path for path, filename in files if filename in ECAMERA_FILENAMES) + ecamera_path = next(path for path, filename in files if filename in FileName.ECAMERA) except StopIteration: ecamera_path = None try: - qcamera_path = next(path for path, filename in files if filename in QCAMERA_FILENAMES) + qcamera_path = next(path for path, filename in files if filename in FileName.QCAMERA) except StopIteration: qcamera_path = None @@ -215,9 +218,16 @@ class RouteName: @property def dongle_id(self) -> str: return self._dongle_id + @property + def log_id(self) -> str: return self._time_str + @property def time_str(self) -> str: return self._time_str + @property + def azure_prefix(self): + return f'{self.dongle_id}/{self.log_id}' + def __str__(self) -> str: return self._canonical_name @@ -241,12 +251,23 @@ class SegmentName: @property def canonical_name(self) -> str: return self._canonical_name + # TODO should only use one name + @property + def data_name(self) -> str: return f"{self._route_name.canonical_name}/{self._num}" + + @property + def azure_prefix(self): + return f'{self.dongle_id}/{self.log_id}/{self._num}' + @property def dongle_id(self) -> str: return self._route_name.dongle_id @property def time_str(self) -> str: return self._route_name.time_str + @property + def log_id(self) -> str: return self._route_name.time_str + @property def segment_num(self) -> int: return self._num @@ -258,6 +279,30 @@ class SegmentName: def __str__(self) -> str: return self._canonical_name + @staticmethod + def from_file_name(file_name): + # ??????/xxxxxxxxxxxxxxxx|1111-11-11-11--11-11-11/1/rlog.bz2 + dongle_id, route_name, segment_num = file_name.replace('|', '/').split('/')[-4:-1] + return SegmentName(dongle_id + "|" + route_name + "--" + segment_num) + + @staticmethod + def from_device_key(dongle_id, key): + # 2018-05-07--18-56-13--5/rlog.bz2 + segment_name = key.split('/')[0] + return SegmentName(dongle_id + "|" + segment_name) + + @staticmethod + def from_file_key(key): + # 38c52c217150700f/2018-05-07--18-56-13/5/rlog.bz2 + az_prefix = '/'.join(key.split('/')[:3]) + return SegmentName.from_azure_prefix(az_prefix) + + @staticmethod + def from_azure_prefix(prefix): + # xxxxxxxx/1111-11-11-11--11-11-11/0 + dongle_id, route_name, segment_num = prefix.split("/") + return SegmentName(dongle_id + "|" + route_name + "--" + segment_num) + @cache def get_max_seg_number_cached(sr: 'SegmentRange') -> int: diff --git a/tools/lib/tests/test_logreader.py b/tools/lib/tests/test_logreader.py index 230b6a65ea..8d0870171f 100644 --- a/tools/lib/tests/test_logreader.py +++ b/tools/lib/tests/test_logreader.py @@ -10,7 +10,8 @@ import requests from parameterized import parameterized from cereal import log as capnp_log -from openpilot.tools.lib.logreader import LogIterable, LogReader, comma_api_source, parse_indirect, ReadMode, InternalUnavailableException +from openpilot.tools.lib.logreader import LogsUnavailable, LogIterable, LogReader, parse_indirect, ReadMode +from openpilot.tools.lib.file_sources import comma_api_source, InternalUnavailableException from openpilot.tools.lib.route import SegmentRange from openpilot.tools.lib.url_file import URLFileException @@ -36,12 +37,12 @@ def setup_source_scenario(mocker, is_internal=False): comma_api_source_mock.__name__ = comma_api_source_mock._mock_name if is_internal: - internal_source_mock.return_value = [QLOG_FILE] + internal_source_mock.return_value = {3: QLOG_FILE} else: internal_source_mock.side_effect = InternalUnavailableException - openpilotci_source_mock.return_value = [None] - comma_api_source_mock.return_value = [QLOG_FILE] + openpilotci_source_mock.return_value = {} + comma_api_source_mock.return_value = {3: QLOG_FILE} yield @@ -90,7 +91,7 @@ class TestLogReader: @pytest.mark.parametrize("cache_enabled", [True, False]) def test_direct_parsing(self, mocker, cache_enabled): - file_exists_mock = mocker.patch("openpilot.tools.lib.logreader.file_exists") + file_exists_mock = mocker.patch("openpilot.tools.lib.filereader.file_exists") os.environ["FILEREADER_CACHE"] = "1" if cache_enabled else "0" qlog = tempfile.NamedTemporaryFile(mode='wb', delete=False) @@ -193,28 +194,27 @@ class TestLogReader: with subtests.test("interactive_yes"): mocker.patch("sys.stdin", new=io.StringIO("y\n")) - lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, source=comma_api_source) + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, sources=[comma_api_source]) log_len = len(list(lr)) assert qlog_len == log_len with subtests.test("interactive_no"): mocker.patch("sys.stdin", new=io.StringIO("n\n")) - with pytest.raises(AssertionError): - lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, source=comma_api_source) + with pytest.raises(LogsUnavailable): + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO_INTERACTIVE, sources=[comma_api_source]) with subtests.test("non_interactive"): - lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO, source=comma_api_source) + lr = LogReader(f"{TEST_ROUTE}/0", default_mode=ReadMode.AUTO, sources=[comma_api_source]) log_len = len(list(lr)) assert qlog_len == log_len @pytest.mark.parametrize("is_internal", [True, False]) - @pytest.mark.slow def test_auto_source_scenarios(self, mocker, is_internal): lr = LogReader(QLOG_FILE) qlog_len = len(list(lr)) with setup_source_scenario(mocker, is_internal=is_internal): - lr = LogReader(f"{TEST_ROUTE}/0/q") + lr = LogReader(f"{TEST_ROUTE}/3/q") log_len = len(list(lr)) assert qlog_len == log_len diff --git a/tools/lib/url_file.py b/tools/lib/url_file.py index b2a11dc77b..e80ba1399d 100644 --- a/tools/lib/url_file.py +++ b/tools/lib/url_file.py @@ -9,12 +9,14 @@ from urllib3.util import Timeout from openpilot.common.file_helpers import atomic_write_in_dir from openpilot.system.hardware.hw import Paths + # Cache chunk size K = 1000 CHUNK_SIZE = 1000 * K logging.getLogger("urllib3").setLevel(logging.WARNING) + def hash_256(link: str) -> str: return sha256((link.split("?")[0]).encode('utf-8')).hexdigest() @@ -24,7 +26,7 @@ class URLFileException(Exception): class URLFile: - _pool_manager: PoolManager|None = None + _pool_manager: PoolManager | None = None @staticmethod def reset() -> None: @@ -33,16 +35,16 @@ class URLFile: @staticmethod def pool_manager() -> PoolManager: if URLFile._pool_manager is None: - socket_options = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1),] + socket_options = [(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)] retries = Retry(total=5, backoff_factor=0.5, status_forcelist=[409, 429, 503, 504]) URLFile._pool_manager = PoolManager(num_pools=10, maxsize=100, socket_options=socket_options, retries=retries) return URLFile._pool_manager - def __init__(self, url: str, timeout: int=10, debug: bool=False, cache: bool|None=None): + def __init__(self, url: str, timeout: int = 10, debug: bool = False, cache: bool | None = None): self._url = url self._timeout = Timeout(connect=timeout, read=timeout) self._pos = 0 - self._length: int|None = None + self._length: int | None = None self._debug = debug # True by default, false if FILEREADER_CACHE is defined, but can be overwritten by the cache input self._force_download = not int(os.environ.get("FILEREADER_CACHE", "0")) @@ -58,7 +60,7 @@ class URLFile: def __exit__(self, exc_type, exc_value, traceback) -> None: pass - def _request(self, method: str, url: str, headers: dict[str, str]|None=None) -> BaseHTTPResponse: + def _request(self, method: str, url: str, headers: dict[str, str] | None = None) -> BaseHTTPResponse: return URLFile.pool_manager().request(method, url, timeout=self._timeout, headers=headers) def get_length_online(self) -> int: @@ -85,7 +87,7 @@ class URLFile: file_length.write(str(self._length)) return self._length - def read(self, ll: int|None=None) -> bytes: + def read(self, ll: int | None = None) -> bytes: if self._force_download: return self.read_aux(ll=ll) @@ -117,7 +119,7 @@ class URLFile: self._pos = file_end return response - def read_aux(self, ll: int|None=None) -> bytes: + def read_aux(self, ll: int | None = None) -> bytes: download_range = False headers = {} if self._pos != 0 or ll is not None: @@ -131,13 +133,13 @@ class URLFile: download_range = True if self._debug: - t1 = time.time() + t1 = time.monotonic() response = self._request('GET', self._url, headers=headers) ret = response.data if self._debug: - t2 = time.time() + t2 = time.monotonic() if t2 - t1 > 0.1: print(f"get {self._url} {headers!r} {t2 - t1:.3f} slow") @@ -152,7 +154,7 @@ class URLFile: self._pos += len(ret) return ret - def seek(self, pos:int) -> None: + def seek(self, pos: int) -> None: self._pos = pos @property diff --git a/tools/longitudinal_maneuvers/maneuversd.py b/tools/longitudinal_maneuvers/maneuversd.py index 6c6e252a57..c17ae23757 100755 --- a/tools/longitudinal_maneuvers/maneuversd.py +++ b/tools/longitudinal_maneuvers/maneuversd.py @@ -3,7 +3,7 @@ import numpy as np from dataclasses import dataclass from cereal import messaging, car -from opendbc.car.common.conversions import Conversions as CV +from openpilot.common.constants import CV from openpilot.common.realtime import DT_MDL from openpilot.common.params import Params from openpilot.common.swaglog import cloudlog diff --git a/tools/mac_setup.sh b/tools/mac_setup.sh index d23052d0f0..19ef77e01c 100755 --- a/tools/mac_setup.sh +++ b/tools/mac_setup.sh @@ -50,6 +50,7 @@ brew "zeromq" cask "gcc-arm-embedded" brew "portaudio" brew "gcc@13" +cask "font-noto-color-emoji" EOS echo "[ ] finished brew install t=$SECONDS" diff --git a/tools/op.sh b/tools/op.sh index 9142d50a18..e83b46b2d2 100755 --- a/tools/op.sh +++ b/tools/op.sh @@ -231,7 +231,7 @@ function op_setup() { echo "Getting git submodules..." st="$(date +%s)" - if ! git submodule update --filter=blob:none --jobs 4 --init --recursive; then + if ! git submodule update --jobs 4 --init --recursive; then echo -e " ↳ [${RED}✗${NC}] Getting git submodules failed!" loge "ERROR_GIT_SUBMODULES" return 1 @@ -287,6 +287,11 @@ function op_adb() { op_run_command tools/scripts/adb_ssh.sh } +function op_ssh() { + op_before_cmd + op_run_command tools/scripts/ssh.py "$@" +} + function op_check() { VERBOSE=1 op_before_cmd @@ -412,6 +417,7 @@ function op_default() { echo -e " ${BOLD}cabana${NC} Run Cabana" echo -e " ${BOLD}clip${NC} Run clip (linux only)" echo -e " ${BOLD}adb${NC} Run adb shell" + echo -e " ${BOLD}ssh${NC} comma prime SSH helper" echo "" echo -e "${BOLD}${UNDERLINE}Commands [Testing]:${NC}" echo -e " ${BOLD}sim${NC} Run openpilot in a simulator" @@ -471,6 +477,7 @@ function _op() { restart ) shift 1; op_restart "$@" ;; post-commit ) shift 1; op_install_post_commit "$@" ;; adb ) shift 1; op_adb "$@" ;; + ssh ) shift 1; op_ssh "$@" ;; * ) op_default "$@" ;; esac } diff --git a/tools/plotjuggler/README.md b/tools/plotjuggler/README.md index 794c292e68..62905a252d 100644 --- a/tools/plotjuggler/README.md +++ b/tools/plotjuggler/README.md @@ -13,14 +13,13 @@ Once you've [set up the openpilot environment](../README.md), this command will ``` $ ./juggle.py -h usage: juggle.py [-h] [--demo] [--can] [--stream] [--layout [LAYOUT]] [--install] [--dbc DBC] - [route_or_segment_name] [segment_count] + [route_or_segment_name] A helper to run PlotJuggler on openpilot routes positional arguments: route_or_segment_name The route or segment name to plot (cabana share URL accepted) (default: None) - segment_count The number of segments to plot (default: None) optional arguments: -h, --help show this help message and exit @@ -34,16 +33,20 @@ optional arguments: ``` -Examples using route name: +Example using route name: `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19"` -Examples using segment range: +Examples using segment: `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1"` `./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/1/q" # use qlogs` +Example using segment range: + +`./juggle.py "a2a0ccea32023010/2023-07-27--13-01-19/0:1"` + ## Streaming Explore live data from your car! Follow these steps to stream from your comma device to your laptop: diff --git a/tools/replay/README.md b/tools/replay/README.md index 8b4afb0acc..72216e2cc8 100644 --- a/tools/replay/README.md +++ b/tools/replay/README.md @@ -75,7 +75,7 @@ Options: --qcam load qcamera --no-hw-decoder disable HW video decoding --no-vipc do not output video - --all do output all messages including uiDebug, userFlag. + --all do output all messages including uiDebug, userBookmark. this may causes issues when used along with UI Arguments: @@ -91,14 +91,6 @@ tools/replay/replay cd selfdrive/ui && ./ui ``` -## Try Radar Point Visualization with Rerun -To visualize radar points, run rp_visualization.py while tools/replay/replay is active. - -```bash -tools/replay/replay -python3 replay/rp_visualization.py -``` - ## Work with plotjuggler If you want to use replay with plotjuggler, you can stream messages by running: diff --git a/tools/replay/consoleui.cc b/tools/replay/consoleui.cc index 871e41c273..185c7d6c36 100644 --- a/tools/replay/consoleui.cc +++ b/tools/replay/consoleui.cc @@ -257,7 +257,7 @@ void ConsoleUI::updateTimeline() { if (entry.type == TimelineType::Engaged) { mvwchgat(win, 1, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); mvwchgat(win, 2, start_pos, end_pos - start_pos + 1, A_COLOR, Color::Engaged, NULL); - } else if (entry.type == TimelineType::UserFlag) { + } else if (entry.type == TimelineType::UserBookmark) { mvwchgat(win, 3, start_pos, end_pos - start_pos + 1, ACS_S3, Color::Cyan, NULL); } else { auto color_id = Color::Green; @@ -329,7 +329,7 @@ void ConsoleUI::handleKey(char c) { } else if (c == 'd') { replay->seekToFlag(FindFlag::nextDisEngagement); } else if (c == 't') { - replay->seekToFlag(FindFlag::nextUserFlag); + replay->seekToFlag(FindFlag::nextUserBookmark); } else if (c == 'i') { replay->seekToFlag(FindFlag::nextInfo); } else if (c == 'w') { diff --git a/tools/replay/framereader.cc b/tools/replay/framereader.cc index 690645aca9..ed88626be3 100644 --- a/tools/replay/framereader.cc +++ b/tools/replay/framereader.cc @@ -80,7 +80,13 @@ bool FrameReader::loadFromFile(CameraType type, const std::string &file, bool no } input_ctx->probesize = 10 * 1024 * 1024; // 10MB - decoder_ = decoder_manager.acquire(type, input_ctx->streams[0]->codecpar, !no_hw_decoder); + video_stream_idx_ = av_find_best_stream(input_ctx, AVMEDIA_TYPE_VIDEO, -1, -1, NULL, 0); + if (video_stream_idx_ < 0) { + rError("No video stream found in file"); + return false; + } + + decoder_ = decoder_manager.acquire(type, input_ctx->streams[video_stream_idx_]->codecpar, !no_hw_decoder); if (!decoder_) { return false; } @@ -90,7 +96,9 @@ bool FrameReader::loadFromFile(CameraType type, const std::string &file, bool no AVPacket pkt; packets_info.reserve(60 * 20); // 20fps, one minute while (!(abort && *abort) && av_read_frame(input_ctx, &pkt) == 0) { - packets_info.emplace_back(PacketInfo{.flags = pkt.flags, .pos = pkt.pos}); + if (pkt.stream_index == video_stream_idx_) { + packets_info.emplace_back(PacketInfo{.flags = pkt.flags, .pos = pkt.pos}); + } av_packet_unref(&pkt); } avio_seek(input_ctx->pb, 0, SEEK_SET); @@ -168,17 +176,17 @@ bool VideoDecoder::initHardwareDecoder(AVHWDeviceType hw_device_type) { } bool VideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { - int from_idx = idx; + int current_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; + current_idx = i; break; } } - auto pos = reader->packets_info[from_idx].pos; + auto pos = reader->packets_info[current_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)); @@ -188,18 +196,27 @@ bool VideoDecoder::decode(FrameReader *reader, int idx, VisionBuf *buf) { } reader->prev_idx = idx; - bool result = false; AVPacket pkt; - for (int i = from_idx; i <= idx; ++i) { - if (av_read_frame(reader->input_ctx, &pkt) == 0) { - AVFrame *f = decodeFrame(&pkt); - if (f && i == idx) { - result = copyBuffer(f, buf); - } + while (av_read_frame(reader->input_ctx, &pkt) >= 0) { + // Skip non-video packets + if (pkt.stream_index != reader->video_stream_idx_) { av_packet_unref(&pkt); + continue; + } + + AVFrame *frame = decodeFrame(&pkt); + av_packet_unref(&pkt); + if (!frame) { + rError("Failed to decode frame at index %d", current_idx); + return false; + } + + if (current_idx++ == idx) { + return copyBuffer(frame, buf); } } - return result; + rError("Failed to find frame at index %d", idx); + return false; } AVFrame *VideoDecoder::decodeFrame(AVPacket *pkt) { diff --git a/tools/replay/framereader.h b/tools/replay/framereader.h index 03ea5be8b2..a15847e311 100644 --- a/tools/replay/framereader.h +++ b/tools/replay/framereader.h @@ -28,6 +28,7 @@ public: VideoDecoder *decoder_ = nullptr; AVFormatContext *input_ctx = nullptr; + int video_stream_idx_ = -1; int prev_idx = -1; struct PacketInfo { int flags; diff --git a/tools/replay/lib/rp_helpers.py b/tools/replay/lib/rp_helpers.py deleted file mode 100644 index aa20ab8e32..0000000000 --- a/tools/replay/lib/rp_helpers.py +++ /dev/null @@ -1,109 +0,0 @@ -import numpy as np -from openpilot.selfdrive.controls.radard import RADAR_TO_CAMERA - -# Color palette used for rerun AnnotationContext -rerunColorPalette = [(96, "red", (255, 0, 0)), - (100, "pink", (255, 36, 0)), - (124, "yellow", (255, 255, 0)), - (230, "vibrantpink", (255, 36, 170)), - (240, "orange", (255, 146, 0)), - (255, "white", (255, 255, 255)), - (110, "carColor", (255,0,127)), - (0, "background", (0, 0, 0))] - - -class UIParams: - lidar_x, lidar_y, lidar_zoom = 384, 960, 6 - lidar_car_x, lidar_car_y = lidar_x / 2., lidar_y / 1.1 - car_hwidth = 1.7272 / 2 * lidar_zoom - car_front = 2.6924 * lidar_zoom - car_back = 1.8796 * lidar_zoom - car_color = rerunColorPalette[6][0] -UP = UIParams - - -def to_topdown_pt(y, x): - px, py = x * UP.lidar_zoom + UP.lidar_car_x, -y * UP.lidar_zoom + UP.lidar_car_y - if px > 0 and py > 0 and px < UP.lidar_x and py < UP.lidar_y: - return int(px), int(py) - return -1, -1 - - -def draw_path(path, lid_overlay, lid_color=None): - x, y = np.asarray(path.x), np.asarray(path.y) - # draw lidar path point on lidar - if lid_color is not None and lid_overlay is not None: - for i in range(len(x)): - px, py = to_topdown_pt(x[i], y[i]) - if px != -1: - lid_overlay[px, py] = lid_color - - -def plot_model(m, lid_overlay): - if lid_overlay is None: - return - for lead in m.leadsV3: - if lead.prob < 0.5: - continue - x, y = lead.x[0], lead.y[0] - x_std = lead.xStd[0] - x -= RADAR_TO_CAMERA - _, py_top = to_topdown_pt(x + x_std, y) - px, py_bottom = to_topdown_pt(x - x_std, y) - lid_overlay[int(round(px - 4)):int(round(px + 4)), py_top:py_bottom] = rerunColorPalette[2][0] - - for path in m.laneLines: - draw_path(path, lid_overlay, rerunColorPalette[2][0]) - for edge in m.roadEdges: - draw_path(edge, lid_overlay, rerunColorPalette[0][0]) - draw_path(m.position, lid_overlay, rerunColorPalette[0][0]) - - -def plot_lead(rs, lid_overlay): - for lead in [rs.leadOne, rs.leadTwo]: - if not lead.status: - continue - x = lead.dRel - px_left, py = to_topdown_pt(x, -10) - px_right, _ = to_topdown_pt(x, 10) - lid_overlay[px_left:px_right, py] = rerunColorPalette[0][0] - - -def update_radar_points(lt, lid_overlay): - ar_pts = [] - if lt is not None: - ar_pts = {} - for track in lt: - ar_pts[track.trackId] = [track.dRel, track.yRel, track.vRel, track.aRel, track.oncoming, track.stationary] - for ids, pt in ar_pts.items(): - # negative here since radar is left positive - px, py = to_topdown_pt(pt[0], -pt[1]) - if px != -1: - if pt[-1]: - color = rerunColorPalette[4][0] - elif pt[-2]: - color = rerunColorPalette[3][0] - else: - color = rerunColorPalette[5][0] - if int(ids) == 1: - lid_overlay[px - 2:px + 2, py - 10:py + 10] = rerunColorPalette[1][0] - else: - lid_overlay[px - 2:px + 2, py - 2:py + 2] = color - - -def get_blank_lid_overlay(UP): - lid_overlay = np.zeros((UP.lidar_x, UP.lidar_y), 'uint8') - # Draw the car. - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)):int( - round(UP.lidar_car_x + UP.car_hwidth)), int(round(UP.lidar_car_y - - UP.car_front))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)):int( - round(UP.lidar_car_x + UP.car_hwidth)), int(round(UP.lidar_car_y + - UP.car_back))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x - UP.car_hwidth)), int( - round(UP.lidar_car_y - UP.car_front)):int(round( - UP.lidar_car_y + UP.car_back))] = UP.car_color - lid_overlay[int(round(UP.lidar_car_x + UP.car_hwidth)), int( - round(UP.lidar_car_y - UP.car_front)):int(round( - UP.lidar_car_y + UP.car_back))] = UP.car_color - return lid_overlay diff --git a/tools/replay/lib/ui_helpers.py b/tools/replay/lib/ui_helpers.py index 39431eca10..b90cbd93b0 100644 --- a/tools/replay/lib/ui_helpers.py +++ b/tools/replay/lib/ui_helpers.py @@ -198,15 +198,12 @@ def maybe_update_radar_points(lt, lid_overlay): ar_pts = {} for track in lt: ar_pts[track.trackId] = [track.dRel, track.yRel, track.vRel, track.aRel] - for ids, pt in ar_pts.items(): + for pt in ar_pts.values(): # negative here since radar is left positive px, py = to_topdown_pt(pt[0], -pt[1]) if px != -1: - color = 255 - if int(ids) == 1: - lid_overlay[px - 2:px + 2, py - 10:py + 10] = 100 - else: - lid_overlay[px - 2:px + 2, py - 2:py + 2] = color + lid_overlay[px - 4:px + 4, py - 4:py + 4] = 0 + lid_overlay[px - 2:px + 2, py - 2:py + 2] = 255 def get_blank_lid_overlay(UP): lid_overlay = np.zeros((UP.lidar_x, UP.lidar_y), 'uint8') diff --git a/tools/replay/main.cc b/tools/replay/main.cc index 38a1da292a..f950985075 100644 --- a/tools/replay/main.cc +++ b/tools/replay/main.cc @@ -30,7 +30,7 @@ Options: --qcam Load qcamera --no-hw-decoder Disable HW video decoding --no-vipc Do not output video - --all Output all messages including uiDebug, userFlag + --all Output all messages including uiDebug, userBookmark -h, --help Show this help message )"; diff --git a/tools/replay/replay.cc b/tools/replay/replay.cc index a8e5cd9d43..7ecad82873 100644 --- a/tools/replay/replay.cc +++ b/tools/replay/replay.cc @@ -20,7 +20,7 @@ Replay::Replay(const std::string &route, std::vector allow, std::ve std::signal(SIGUSR1, interrupt_sleep_handler); if (!(flags_ & REPLAY_FLAG_ALL_SERVICES)) { - block.insert(block.end(), {"uiDebug", "userFlag"}); + block.insert(block.end(), {"uiDebug", "userBookmark"}); } setupServices(allow, block); setupSegmentManager(!allow.empty() || !block.empty()); diff --git a/tools/replay/rp_visualization.py b/tools/replay/rp_visualization.py deleted file mode 100755 index 25169b72ae..0000000000 --- a/tools/replay/rp_visualization.py +++ /dev/null @@ -1,60 +0,0 @@ -#!/usr/bin/env python3 -import argparse -import os -import sys -import numpy as np -import rerun as rr -import cereal.messaging as messaging -from openpilot.common.basedir import BASEDIR -from openpilot.tools.replay.lib.rp_helpers import (UP, rerunColorPalette, - get_blank_lid_overlay, - update_radar_points, plot_lead, - plot_model) -from msgq.visionipc import VisionIpcClient, VisionStreamType - -os.environ['BASEDIR'] = BASEDIR - -UP.lidar_zoom = 6 - -def visualize(addr): - sm = messaging.SubMaster(['radarState', 'liveTracks', 'modelV2'], addr=addr) - vipc_client = VisionIpcClient("camerad", VisionStreamType.VISION_STREAM_ROAD, True) - while True: - if not vipc_client.is_connected(): - vipc_client.connect(True) - new_data = vipc_client.recv() - if new_data is None or not new_data.data.any(): - continue - - sm.update(0) - lid_overlay = get_blank_lid_overlay(UP) - if sm.recv_frame['modelV2']: - plot_model(sm['modelV2'], lid_overlay) - if sm.recv_frame['radarState']: - plot_lead(sm['radarState'], lid_overlay) - liveTracksTime = sm.logMonoTime['liveTracks'] - if sm.updated['liveTracks']: - update_radar_points(sm['liveTracks'], lid_overlay) - rr.set_time_nanos("TIMELINE", liveTracksTime) - rr.log("tracks", rr.SegmentationImage(np.flip(np.rot90(lid_overlay, k=-1), axis=1))) - - -def get_arg_parser(): - parser = argparse.ArgumentParser( - description="Show replay data in a UI.", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("ip_address", nargs="?", default="127.0.0.1", - help="The ip address on which to receive zmq messages.") - parser.add_argument("--frame-address", default=None, - help="The frame address (fully qualified ZMQ endpoint for frames) on which to receive zmq messages.") - return parser - - -if __name__ == "__main__": - args = get_arg_parser().parse_args(sys.argv[1:]) - if args.ip_address != "127.0.0.1": - os.environ["ZMQ"] = "1" - messaging.reset_context() - rr.init("RadarPoints", spawn= True) - rr.log("tracks", rr.AnnotationContext(rerunColorPalette), static=True) - visualize(args.ip_address) diff --git a/tools/replay/timeline.cc b/tools/replay/timeline.cc index a4c2ffb700..39ea8b1bed 100644 --- a/tools/replay/timeline.cc +++ b/tools/replay/timeline.cc @@ -26,7 +26,7 @@ std::optional Timeline::find(double cur_ts, FindFlag flag) const { return entry.end_time; } } else if (entry.start_time > cur_ts) { - if ((flag == FindFlag::nextUserFlag && entry.type == TimelineType::UserFlag) || + if ((flag == FindFlag::nextUserBookmark && entry.type == TimelineType::UserBookmark) || (flag == FindFlag::nextInfo && entry.type == TimelineType::AlertInfo) || (flag == FindFlag::nextWarning && entry.type == TimelineType::AlertWarning) || (flag == FindFlag::nextCritical && entry.type == TimelineType::AlertCritical)) { @@ -66,8 +66,8 @@ void Timeline::buildTimeline(const Route &route, uint64_t route_start_ts, bool l auto cs = reader.getRoot().getSelfdriveState(); updateEngagementStatus(cs, current_engaged_idx, seconds); updateAlertStatus(cs, current_alert_idx, seconds); - } else if (e.which == cereal::Event::Which::USER_FLAG) { - staging_entries_.emplace_back(Entry{seconds, seconds, TimelineType::UserFlag}); + } else if (e.which == cereal::Event::Which::USER_BOOKMARK) { + staging_entries_.emplace_back(Entry{seconds, seconds, TimelineType::UserBookmark}); } } diff --git a/tools/replay/timeline.h b/tools/replay/timeline.h index 74add9e5cc..9688b6b95e 100644 --- a/tools/replay/timeline.h +++ b/tools/replay/timeline.h @@ -7,8 +7,8 @@ #include "tools/replay/route.h" -enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserFlag }; -enum class FindFlag { nextEngagement, nextDisEngagement, nextUserFlag, nextInfo, nextWarning, nextCritical }; +enum class TimelineType { None, Engaged, AlertInfo, AlertWarning, AlertCritical, UserBookmark }; +enum class FindFlag { nextEngagement, nextDisEngagement, nextUserBookmark, nextInfo, nextWarning, nextCritical }; class Timeline { public: diff --git a/tools/replay/ui.py b/tools/replay/ui.py index 6e40579902..d71d63d1ce 100755 --- a/tools/replay/ui.py +++ b/tools/replay/ui.py @@ -149,7 +149,7 @@ def ui_thread(addr): plot_arr[-1, name_to_arr_idx['angle_steers']] = sm['carState'].steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_des']] = sm['carControl'].actuators.steeringAngleDeg plot_arr[-1, name_to_arr_idx['angle_steers_k']] = angle_steers_k - plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gas + plot_arr[-1, name_to_arr_idx['gas']] = sm['carState'].gasDEPRECATED # TODO gas is deprecated plot_arr[-1, name_to_arr_idx['computer_gas']] = np.clip(sm['carControl'].actuators.accel/4.0, 0.0, 1.0) plot_arr[-1, name_to_arr_idx['user_brake']] = sm['carState'].brake diff --git a/tools/replay/unlog_ci_segment.py b/tools/replay/unlog_ci_segment.py index 6ddc1b298a..e5a7a3ffde 100755 --- a/tools/replay/unlog_ci_segment.py +++ b/tools/replay/unlog_ci_segment.py @@ -42,7 +42,7 @@ def replay(route, segment, loop): msg = msgs[i].as_builder() next_msg = msgs[i + 1] - start_time = time.time() + start_time = time.monotonic() w = msg.which() if w == 'roadCameraState': @@ -63,7 +63,7 @@ def replay(route, segment, loop): socks[w] = None lag += (next_msg.logMonoTime - msg.logMonoTime) / 1e9 - lag -= time.time() - start_time + lag -= time.monotonic() - start_time dt = max(lag, 0.0) lag -= dt diff --git a/tools/rerun/README.md b/tools/rerun/README.md deleted file mode 100644 index 11a8d33ee1..0000000000 --- a/tools/rerun/README.md +++ /dev/null @@ -1,37 +0,0 @@ -# Rerun -Rerun is a tool to quickly visualize time series data. It supports all openpilot logs , both the `logMessages` and video logs. - -[Instructions](https://rerun.io/docs/reference/viewer/overview) for navigation within the Rerun Viewer. - -## Usage -``` -usage: run.py [-h] [--demo] [--qcam] [--fcam] [--ecam] [--dcam] [route_or_segment_name] - -A helper to run rerun on openpilot routes - -positional arguments: - route_or_segment_name - The route or segment name to plot (default: None) - -options: - -h, --help show this help message and exit - --demo Use the demo route instead of providing one (default: False) - --qcam Show low-res road camera (default: False) - --fcam Show driving camera (default: False) - --ecam Show wide camera (default: False) - --dcam Show driver monitoring camera (default: False) -``` - -Examples using route name to observe accelerometer and qcamera: - -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19"` - -Examples using segment range (more on [SegmentRange](https://github.com/commaai/openpilot/tree/master/tools/lib)): - -`./run.py --qcam "a2a0ccea32023010/2023-07-27--13-01-19/2:4"` - -## Cautions: -- Showing hevc videos (`--fcam`, `--ecam`, and `--dcam`) are expensive, and it's recommended to use `--qcam` for optimized performance. If possible, limiting your route to a few segments using `SegmentRange` will speed up logging and reduce memory usage - -## Demo -`./run.py --qcam --demo` diff --git a/tools/rerun/camera_reader.py b/tools/rerun/camera_reader.py deleted file mode 100644 index 8366a3c1cf..0000000000 --- a/tools/rerun/camera_reader.py +++ /dev/null @@ -1,93 +0,0 @@ -import tqdm -import subprocess -import multiprocessing -from enum import StrEnum -from functools import partial -from collections import namedtuple - -from openpilot.tools.lib.framereader import ffprobe - -CameraConfig = namedtuple("CameraConfig", ["qcam", "fcam", "ecam", "dcam"]) - -class CameraType(StrEnum): - qcam = "qcamera" - fcam = "fcamera" - ecam = "ecamera" - dcam = "dcamera" - - -def probe_packet_info(camera_path): - args = ["ffprobe", "-v", "quiet", "-show_packets", "-probesize", "10M", camera_path] - dat = subprocess.check_output(args) - dat = dat.decode().split() - return dat - - -class _FrameReader: - def __init__(self, camera_path, segment, h, w, start_time): - self.camera_path = camera_path - self.segment = segment - self.h = h - self.w = w - self.start_time = start_time - - self.ts = self._get_ts() - - def _read_stream_nv12(self): - frame_sz = self.w * self.h * 3 // 2 - proc = subprocess.Popen( - ["ffmpeg", "-v", "quiet", "-i", self.camera_path, "-f", "rawvideo", "-pix_fmt", "nv12", "-"], - stdin=subprocess.PIPE, - stdout=subprocess.PIPE, - stderr=subprocess.DEVNULL - ) - try: - while True: - dat = proc.stdout.read(frame_sz) - if len(dat) == 0: - break - yield dat - finally: - proc.kill() - - def _get_ts(self): - dat = probe_packet_info(self.camera_path) - try: - ret = [float(d.split('=')[1]) for d in dat if d.startswith("pts_time=")] - except ValueError: - # pts_times aren't available. Infer timestamps from duration_times - ret = [d for d in dat if d.startswith("duration_time")] - ret = [float(d.split('=')[1])*(i+1)+(self.segment*60)+self.start_time for i, d in enumerate(ret)] - return ret - - def __iter__(self): - for i, frame in enumerate(self._read_stream_nv12()): - yield self.ts[i], frame - - -class CameraReader: - def __init__(self, camera_paths, start_time, seg_idxs): - self.seg_idxs = seg_idxs - self.camera_paths = camera_paths - self.start_time = start_time - - probe = ffprobe(camera_paths[0])["streams"][0] - self.h = probe["height"] - self.w = probe["width"] - - self.__frs = {} - - def _get_fr(self, i): - if i not in self.__frs: - self.__frs[i] = _FrameReader(self.camera_paths[i], segment=i, h=self.h, w=self.w, start_time=self.start_time) - return self.__frs[i] - - def _run_on_segment(self, func, i): - return func(self._get_fr(i)) - - def run_across_segments(self, num_processes, func, desc=None): - with multiprocessing.Pool(num_processes) as pool: - num_segs = len(self.seg_idxs) - for _ in tqdm.tqdm(pool.imap_unordered(partial(self._run_on_segment, func), self.seg_idxs), total=num_segs, desc=desc): - continue - diff --git a/tools/rerun/run.py b/tools/rerun/run.py deleted file mode 100755 index 6ce8a37937..0000000000 --- a/tools/rerun/run.py +++ /dev/null @@ -1,180 +0,0 @@ -#!/usr/bin/env python3 - -import sys -import argparse -import multiprocessing -import rerun as rr -import rerun.blueprint as rrb -from functools import partial -from collections import defaultdict - -from cereal.services import SERVICE_LIST -from openpilot.tools.rerun.camera_reader import probe_packet_info, CameraReader, CameraConfig, CameraType -from openpilot.tools.lib.logreader import LogReader -from openpilot.tools.lib.route import Route, SegmentRange - - -NUM_CPUS = multiprocessing.cpu_count() -DEMO_ROUTE = "a2a0ccea32023010|2023-07-27--13-01-19" -RR_TIMELINE_NAME = "Timeline" -RR_WIN = "openpilot logs" - - -""" -Relevant upstream Rerun issues: -- loading videos directly: https://github.com/rerun-io/rerun/issues/6532 -""" - -class Rerunner: - def __init__(self, route, segment_range, camera_config): - self.lr = LogReader(route_or_segment_name) - - # hevc files don't have start_time. We get it from qcamera.ts - start_time = 0 - dat = probe_packet_info(route.qcamera_paths()[0]) - for d in dat: - if d.startswith("pts_time="): - start_time = float(d.split('=')[1]) - break - - qcam, fcam, ecam, dcam = camera_config - self.camera_readers = {} - if qcam: - self.camera_readers[CameraType.qcam] = CameraReader(route.qcamera_paths(), start_time, segment_range.seg_idxs) - if fcam: - self.camera_readers[CameraType.fcam] = CameraReader(route.camera_paths(), start_time, segment_range.seg_idxs) - if ecam: - self.camera_readers[CameraType.ecam] = CameraReader(route.ecamera_paths(), start_time, segment_range.seg_idxs) - if dcam: - self.camera_readers[CameraType.dcam] = CameraReader(route.dcamera_paths(), start_time, segment_range.seg_idxs) - - def _create_blueprint(self): - blueprint = None - service_views = [] - - for topic in sorted(SERVICE_LIST.keys()): - View = rrb.TimeSeriesView if topic != "thumbnail" else rrb.Spatial2DView - service_views.append(View(name=topic, origin=f"/{topic}/", visible=False)) - rr.log(topic, rr.SeriesLine(name=topic), timeless=True) - - center_view = [rrb.Vertical(*service_views, name="streams")] - if len(self.camera_readers): - center_view.append(rrb.Vertical(*[rrb.Spatial2DView(name=cam_type, origin=cam_type) for cam_type in self.camera_readers.keys()], name="cameras")) - - blueprint = rrb.Blueprint( - rrb.Horizontal( - *center_view - ), - rrb.SelectionPanel(expanded=False), - rrb.TimePanel(expanded=False), - ) - return blueprint - - @staticmethod - def _parse_msg(msg, parent_key=''): - stack = [(msg, parent_key)] - while stack: - current_msg, current_parent_key = stack.pop() - if isinstance(current_msg, list): - for index, item in enumerate(current_msg): - new_key = f"{current_parent_key}/{index}" - if isinstance(item, (int, float)): - yield new_key, item - elif isinstance(item, dict): - stack.append((item, new_key)) - elif isinstance(current_msg, dict): - for key, value in current_msg.items(): - new_key = f"{current_parent_key}/{key}" - if isinstance(value, (int, float)): - yield new_key, value - elif isinstance(value, dict): - stack.append((value, new_key)) - elif isinstance(value, list): - for index, item in enumerate(value): - if isinstance(item, (int, float)): - yield f"{new_key}/{index}", item - else: - pass # Not a plottable value - - @staticmethod - @rr.shutdown_at_exit - def _process_log_msgs(blueprint, lr): - rr.init(RR_WIN) - rr.connect() - rr.send_blueprint(blueprint) - - log_msgs = defaultdict(lambda: defaultdict(list)) - for msg in lr: - msg_type = msg.which() - - if msg_type == "thumbnail": - continue - - for entity_path, dat in Rerunner._parse_msg(msg.to_dict()[msg_type], msg_type): - log_msgs[entity_path]["times"].append(msg.logMonoTime) - log_msgs[entity_path]["data"].append(dat) - - for entity_path, log_msg in log_msgs.items(): - rr.send_columns( - entity_path, - times=[rr.TimeNanosColumn(RR_TIMELINE_NAME, log_msg["times"])], - components=[rr.components.ScalarBatch(log_msg["data"])] - ) - - return [] - - @staticmethod - @rr.shutdown_at_exit - def _process_cam_readers(blueprint, cam_type, h, w, fr): - rr.init(RR_WIN) - rr.connect() - rr.send_blueprint(blueprint) - - for ts, frame in fr: - rr.set_time_nanos(RR_TIMELINE_NAME, int(ts * 1e9)) - rr.log(cam_type, rr.Image(bytes=frame, width=w, height=h, pixel_format=rr.PixelFormat.NV12)) - - def load_data(self): - rr.init(RR_WIN, spawn=True) - - startup_blueprint = self._create_blueprint() - self.lr.run_across_segments(NUM_CPUS, partial(self._process_log_msgs, startup_blueprint), desc="Log messages") - for cam_type, cr in self.camera_readers.items(): - cr.run_across_segments(NUM_CPUS, partial(self._process_cam_readers, startup_blueprint, cam_type, cr.h, cr.w), desc=cam_type) - - rr.send_blueprint(self._create_blueprint()) - - -if __name__ == '__main__': - parser = argparse.ArgumentParser(description="A helper to run rerun on openpilot routes", - formatter_class=argparse.ArgumentDefaultsHelpFormatter) - parser.add_argument("--demo", action="store_true", help="Use the demo route instead of providing one") - parser.add_argument("--qcam", action="store_true", help="Show low-res road camera") - parser.add_argument("--fcam", action="store_true", help="Show driving camera") - parser.add_argument("--ecam", action="store_true", help="Show wide camera") - parser.add_argument("--dcam", action="store_true", help="Show driver monitoring camera") - parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name to plot") - args = parser.parse_args() - - if not args.demo and not args.route_or_segment_name: - parser.print_help() - sys.exit() - - camera_config = CameraConfig(args.qcam, args.fcam, args.ecam, args.dcam) - route_or_segment_name = DEMO_ROUTE if args.demo else args.route_or_segment_name.strip() - - sr = SegmentRange(route_or_segment_name) - r = Route(sr.route_name) - - hevc_requested = any(camera_config[1:]) - if len(sr.seg_idxs) > 1 and hevc_requested: - print("You're requesting more than 1 segment with hevc videos, " + \ - "please be aware that might take a lot of memory " + \ - "since rerun isn't yet well supported for high resolution video logging") - response = input("Do you wish to continue? (Y/n): ") - if response.strip().lower() != "y": - sys.exit() - - rerunner = Rerunner(r, sr, camera_config) - rerunner.load_data() - diff --git a/tools/scripts/extract_audio.py b/tools/scripts/extract_audio.py new file mode 100755 index 0000000000..2a6ad9f9e3 --- /dev/null +++ b/tools/scripts/extract_audio.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +import os +import sys +import wave +import argparse +import numpy as np + +from openpilot.tools.lib.logreader import LogReader, ReadMode + + +def extract_audio(route_or_segment_name, output_file=None, play=False): + lr = LogReader(route_or_segment_name, default_mode=ReadMode.AUTO_INTERACTIVE) + audio_messages = list(lr.filter("rawAudioData")) + if not audio_messages: + print("No rawAudioData messages found in logs") + return + sample_rate = audio_messages[0].sampleRate + + audio_chunks = [] + total_frames = 0 + for msg in audio_messages: + audio_array = np.frombuffer(msg.data, dtype=np.int16) + audio_chunks.append(audio_array) + total_frames += len(audio_array) + full_audio = np.concatenate(audio_chunks) + + print(f"Found {total_frames} frames from {len(audio_messages)} audio messages at {sample_rate} Hz") + + if output_file: + if write_wav_file(output_file, full_audio, sample_rate): + print(f"Audio written to {output_file}") + else: + print("Audio extraction canceled.") + if play: + play_audio(full_audio, sample_rate) + + +def write_wav_file(filename, audio_data, sample_rate): + if os.path.exists(filename): + if input(f"File '{filename}' exists. Overwrite? (y/N): ").lower() not in ['y', 'yes']: + return False + + with wave.open(filename, 'wb') as wav_file: + wav_file.setnchannels(1) # Mono + wav_file.setsampwidth(2) # 16-bit + wav_file.setframerate(sample_rate) + wav_file.writeframes(audio_data.tobytes()) + return True + + +def play_audio(audio_data, sample_rate): + try: + import sounddevice as sd + + print("Playing audio... Press Ctrl+C to stop") + sd.play(audio_data, sample_rate) + sd.wait() + except KeyboardInterrupt: + print("\nPlayback stopped") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="Extract audio data from openpilot logs") + parser.add_argument("-o", "--output", help="Output WAV file path") + parser.add_argument("--play", action="store_true", help="Play audio with sounddevice") + parser.add_argument("route_or_segment_name", nargs='?', help="The route or segment name") + + if len(sys.argv) == 1: + parser.print_help() + sys.exit() + args = parser.parse_args() + + output_file = args.output + if not args.output and not args.play: + output_file = "extracted_audio.wav" + + extract_audio(args.route_or_segment_name.strip(), output_file, args.play) diff --git a/tools/scripts/ssh.py b/tools/scripts/ssh.py new file mode 100755 index 0000000000..0429799a2e --- /dev/null +++ b/tools/scripts/ssh.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +import os +import sys +import argparse +import re + +from openpilot.common.basedir import BASEDIR +from openpilot.tools.lib.auth_config import get_token +from openpilot.tools.lib.api import CommaApi + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description="A helper for connecting to devices over the comma prime SSH proxy.\ + Adding your SSH key to your SSH config is recommended for more convenient use; see https://docs.comma.ai/how-to/connect-to-comma/.") + parser.add_argument("device", help="device name or dongle id") + parser.add_argument("--host", help="ssh jump server host", default="ssh.comma.ai") + parser.add_argument("--port", help="ssh jump server port", default=22, type=int) + parser.add_argument("--key", help="ssh key", default=os.path.join(BASEDIR, "system/hardware/tici/id_rsa")) + parser.add_argument("--debug", help="enable debug output", action="store_true") + args = parser.parse_args() + + r = CommaApi(get_token()).get("v1/me/devices") + devices = {x['dongle_id']: x['alias'] for x in r} + + if not re.match("[0-9a-zA-Z]{16}", args.device): + user_input = args.device.replace(" ", "").lower() + matches = { k: v for k, v in devices.items() if isinstance(v, str) and user_input in v.replace(" ", "").lower() } + if len(matches) == 1: + dongle_id = list(matches.keys())[0] + else: + print(f"failed to look up dongle id for \"{args.device}\"", file=sys.stderr) + if len(matches) > 1: + print("found multiple matches:", file=sys.stderr) + for k, v in matches.items(): + print(f" \"{v}\" ({k})", file=sys.stderr) + exit(1) + else: + dongle_id = args.device + + name = dongle_id + if dongle_id in devices: + name = f"{devices[dongle_id]} ({dongle_id})" + print(f"connecting to {name} through {args.host}:{args.port} ...") + + command = [ + "ssh", + "-i", args.key, + "-o", f"ProxyCommand=ssh -i {args.key} -W %h:%p -p %p %h@{args.host}", + "-p", str(args.port), + ] + if args.debug: + command += ["-v"] + command += [ + f"comma@{dongle_id}", + ] + if args.debug: + print(" ".join([f"'{c}'" if " " in c else c for c in command])) + os.execvp(command[0], command) diff --git a/tools/sim/lib/simulated_car.py b/tools/sim/lib/simulated_car.py index ad0f4de5d0..2681b26904 100644 --- a/tools/sim/lib/simulated_car.py +++ b/tools/sim/lib/simulated_car.py @@ -46,7 +46,7 @@ class SimulatedCar: msg.append(self.packer.make_can_msg("SCM_BUTTONS", 0, {"CRUISE_BUTTONS": simulator_state.cruise_button})) - msg.append(self.packer.make_can_msg("GEARBOX", 0, {"GEAR": 4, "GEAR_SHIFTER": 8})) + msg.append(self.packer.make_can_msg("GEARBOX_AUTO", 0, {"GEAR_SHIFTER": 4})) msg.append(self.packer.make_can_msg("GAS_PEDAL_2", 0, {})) msg.append(self.packer.make_can_msg("SEATBELT_STATUS", 0, {"SEATBELT_DRIVER_LATCHED": 1})) msg.append(self.packer.make_can_msg("STEER_STATUS", 0, {"STEER_TORQUE_SENSOR": simulator_state.user_torque})) @@ -56,7 +56,6 @@ class SimulatedCar: msg.append(self.packer.make_can_msg("STEER_MOTOR_TORQUE", 0, {})) msg.append(self.packer.make_can_msg("EPB_STATUS", 0, {})) msg.append(self.packer.make_can_msg("DOORS_STATUS", 0, {})) - msg.append(self.packer.make_can_msg("CRUISE_PARAMS", 0, {})) msg.append(self.packer.make_can_msg("CRUISE", 0, {})) msg.append(self.packer.make_can_msg("CRUISE_FAULT_STATUS", 0, {})) msg.append(self.packer.make_can_msg("SCM_FEEDBACK", 0, diff --git a/tools/sim/lib/simulated_sensors.py b/tools/sim/lib/simulated_sensors.py index e78f984736..a8374a00cd 100644 --- a/tools/sim/lib/simulated_sensors.py +++ b/tools/sim/lib/simulated_sensors.py @@ -53,7 +53,7 @@ class SimulatedSensors: for _ in range(10): dat = messaging.new_message('gpsLocationExternal', valid=True) dat.gpsLocationExternal = { - "unixTimestampMillis": int(time.time() * 1000), + "unixTimestampMillis": int(time.time() * 1000), # noqa: TID251 "flags": 1, # valid fix "horizontalAccuracy": 1.0, "verticalAccuracy": 1.0, @@ -109,7 +109,7 @@ class SimulatedSensors: self.camerad.cam_send_yuv_wide_road(yuv) def update(self, simulator_state: 'SimulatorState', world: 'World'): - now = time.time() + now = time.monotonic() self.send_imu_message(simulator_state) self.send_gps_message(simulator_state) diff --git a/tools/tuning/measure_steering_accuracy.py b/tools/tuning/measure_steering_accuracy.py index a2f437819f..7e4e975742 100755 --- a/tools/tuning/measure_steering_accuracy.py +++ b/tools/tuning/measure_steering_accuracy.py @@ -49,7 +49,7 @@ class SteeringAccuracyTool: active = sm['controlsState'].active steer = sm['carOutput'].actuatorsOutput.torque standstill = sm['carState'].standstill - steer_limited_by_controls = abs(sm['carControl'].actuators.torque - sm['carControl'].actuatorsOutput.torque) > 1e-2 + steer_limited_by_safety = abs(sm['carControl'].actuators.torque - sm['carControl'].actuatorsOutput.torque) > 1e-2 overriding = sm['carState'].steeringPressed changing_lanes = sm['modelV2'].meta.laneChangeState != 0 model_points = sm['modelV2'].position.y @@ -77,7 +77,7 @@ class SteeringAccuracyTool: self.speed_group_stats[group][angle_abs]["steer"] += abs(steer) if len(model_points): self.speed_group_stats[group][angle_abs]["dpp"] += abs(model_points[0]) - if steer_limited_by_controls: + if steer_limited_by_safety: self.speed_group_stats[group][angle_abs]["limited"] += 1 if control_state.saturated: self.speed_group_stats[group][angle_abs]["saturated"] += 1 diff --git a/tools/webcam/camera.py b/tools/webcam/camera.py index 45f9379038..7926766c6f 100644 --- a/tools/webcam/camera.py +++ b/tools/webcam/camera.py @@ -11,7 +11,14 @@ class Camera: self.stream_type = stream_type self.cur_frame_id = 0 + print(f"Opening {cam_type_state} at {camera_id}") + self.cap = cv.VideoCapture(camera_id) + + self.cap.set(cv.CAP_PROP_FRAME_WIDTH, 1280.0) + self.cap.set(cv.CAP_PROP_FRAME_HEIGHT, 720.0) + self.cap.set(cv.CAP_PROP_FPS, 25.0) + self.W = self.cap.get(cv.CAP_PROP_FRAME_WIDTH) self.H = self.cap.get(cv.CAP_PROP_FRAME_HEIGHT) @@ -25,6 +32,8 @@ class Camera: ret, frame = self.cap.read() if not ret: break + # Rotate the frame 180 degrees (flip both axes) + frame = cv.flip(frame, -1) yuv = Camera.bgr2nv12(frame) yield yuv.data.tobytes() self.cap.release() diff --git a/tools/webcam/camerad.py b/tools/webcam/camerad.py index cb096dc00b..f1e70d948a 100755 --- a/tools/webcam/camerad.py +++ b/tools/webcam/camerad.py @@ -1,6 +1,7 @@ #!/usr/bin/env python3 import threading import os +import platform from collections import namedtuple from msgq.visionipc import VisionIpcServer, VisionStreamType @@ -30,8 +31,7 @@ class Camerad: self.cameras = [] for c in CAMERAS: - cam_device = f"/dev/video{c.cam_id}" - print(f"opening {c.msg_name} at {cam_device}") + cam_device = f"/dev/video{c.cam_id}" if platform.system() != "Darwin" else c.cam_id cam = Camera(c.msg_name, c.stream_type, cam_device) self.cameras.append(cam) self.vipc_server.create_buffers(c.stream_type, 20, cam.W, cam.H) diff --git a/uv.lock b/uv.lock index 530db4ec9d..7010cdfb83 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 2 +revision = 3 requires-python = ">=3.11, <3.13" resolution-markers = [ "python_full_version >= '3.12' and sys_platform == 'darwin'", @@ -21,7 +21,7 @@ wheels = [ [[package]] name = "aiohttp" -version = "3.12.13" +version = "3.12.15" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "aiohappyeyeballs" }, @@ -32,42 +32,42 @@ dependencies = [ { name = "propcache" }, { name = "yarl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/6e/ab88e7cb2a4058bed2f7870276454f85a7c56cd6da79349eb314fc7bbcaa/aiohttp-3.12.13.tar.gz", hash = "sha256:47e2da578528264a12e4e3dd8dd72a7289e5f812758fe086473fab037a10fcce", size = 7819160, upload-time = "2025-06-14T15:15:41.354Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9b/e7/d92a237d8802ca88483906c388f7c201bbe96cd80a165ffd0ac2f6a8d59f/aiohttp-3.12.15.tar.gz", hash = "sha256:4fc61385e9c98d72fcdf47e6dd81833f47b2f77c114c29cd64a361be57a763a2", size = 7823716, upload-time = "2025-07-29T05:52:32.215Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/6a/65/5566b49553bf20ffed6041c665a5504fb047cefdef1b701407b8ce1a47c4/aiohttp-3.12.13-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:7c229b1437aa2576b99384e4be668af1db84b31a45305d02f61f5497cfa6f60c", size = 709401, upload-time = "2025-06-14T15:13:30.774Z" }, - { url = "https://files.pythonhosted.org/packages/14/b5/48e4cc61b54850bdfafa8fe0b641ab35ad53d8e5a65ab22b310e0902fa42/aiohttp-3.12.13-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:04076d8c63471e51e3689c93940775dc3d12d855c0c80d18ac5a1c68f0904358", size = 481669, upload-time = "2025-06-14T15:13:32.316Z" }, - { url = "https://files.pythonhosted.org/packages/04/4f/e3f95c8b2a20a0437d51d41d5ccc4a02970d8ad59352efb43ea2841bd08e/aiohttp-3.12.13-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:55683615813ce3601640cfaa1041174dc956d28ba0511c8cbd75273eb0587014", size = 469933, upload-time = "2025-06-14T15:13:34.104Z" }, - { url = "https://files.pythonhosted.org/packages/41/c9/c5269f3b6453b1cfbd2cfbb6a777d718c5f086a3727f576c51a468b03ae2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:921bc91e602d7506d37643e77819cb0b840d4ebb5f8d6408423af3d3bf79a7b7", size = 1740128, upload-time = "2025-06-14T15:13:35.604Z" }, - { url = "https://files.pythonhosted.org/packages/6f/49/a3f76caa62773d33d0cfaa842bdf5789a78749dbfe697df38ab1badff369/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e72d17fe0974ddeae8ed86db297e23dba39c7ac36d84acdbb53df2e18505a013", size = 1688796, upload-time = "2025-06-14T15:13:37.125Z" }, - { url = "https://files.pythonhosted.org/packages/ad/e4/556fccc4576dc22bf18554b64cc873b1a3e5429a5bdb7bbef7f5d0bc7664/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0653d15587909a52e024a261943cf1c5bdc69acb71f411b0dd5966d065a51a47", size = 1787589, upload-time = "2025-06-14T15:13:38.745Z" }, - { url = "https://files.pythonhosted.org/packages/b9/3d/d81b13ed48e1a46734f848e26d55a7391708421a80336e341d2aef3b6db2/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a77b48997c66722c65e157c06c74332cdf9c7ad00494b85ec43f324e5c5a9b9a", size = 1826635, upload-time = "2025-06-14T15:13:40.733Z" }, - { url = "https://files.pythonhosted.org/packages/75/a5/472e25f347da88459188cdaadd1f108f6292f8a25e62d226e63f860486d1/aiohttp-3.12.13-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d6946bae55fd36cfb8e4092c921075cde029c71c7cb571d72f1079d1e4e013bc", size = 1729095, upload-time = "2025-06-14T15:13:42.312Z" }, - { url = "https://files.pythonhosted.org/packages/b9/fe/322a78b9ac1725bfc59dfc301a5342e73d817592828e4445bd8f4ff83489/aiohttp-3.12.13-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f95db8c8b219bcf294a53742c7bda49b80ceb9d577c8e7aa075612b7f39ffb7", size = 1666170, upload-time = "2025-06-14T15:13:44.884Z" }, - { url = "https://files.pythonhosted.org/packages/7a/77/ec80912270e231d5e3839dbd6c065472b9920a159ec8a1895cf868c2708e/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:03d5eb3cfb4949ab4c74822fb3326cd9655c2b9fe22e4257e2100d44215b2e2b", size = 1714444, upload-time = "2025-06-14T15:13:46.401Z" }, - { url = "https://files.pythonhosted.org/packages/21/b2/fb5aedbcb2b58d4180e58500e7c23ff8593258c27c089abfbcc7db65bd40/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:6383dd0ffa15515283c26cbf41ac8e6705aab54b4cbb77bdb8935a713a89bee9", size = 1709604, upload-time = "2025-06-14T15:13:48.377Z" }, - { url = "https://files.pythonhosted.org/packages/e3/15/a94c05f7c4dc8904f80b6001ad6e07e035c58a8ebfcc15e6b5d58500c858/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:6548a411bc8219b45ba2577716493aa63b12803d1e5dc70508c539d0db8dbf5a", size = 1689786, upload-time = "2025-06-14T15:13:50.401Z" }, - { url = "https://files.pythonhosted.org/packages/1d/fd/0d2e618388f7a7a4441eed578b626bda9ec6b5361cd2954cfc5ab39aa170/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:81b0fcbfe59a4ca41dc8f635c2a4a71e63f75168cc91026c61be665945739e2d", size = 1783389, upload-time = "2025-06-14T15:13:51.945Z" }, - { url = "https://files.pythonhosted.org/packages/a6/6b/6986d0c75996ef7e64ff7619b9b7449b1d1cbbe05c6755e65d92f1784fe9/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:6a83797a0174e7995e5edce9dcecc517c642eb43bc3cba296d4512edf346eee2", size = 1803853, upload-time = "2025-06-14T15:13:53.533Z" }, - { url = "https://files.pythonhosted.org/packages/21/65/cd37b38f6655d95dd07d496b6d2f3924f579c43fd64b0e32b547b9c24df5/aiohttp-3.12.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a5734d8469a5633a4e9ffdf9983ff7cdb512524645c7a3d4bc8a3de45b935ac3", size = 1716909, upload-time = "2025-06-14T15:13:55.148Z" }, - { url = "https://files.pythonhosted.org/packages/fd/20/2de7012427dc116714c38ca564467f6143aec3d5eca3768848d62aa43e62/aiohttp-3.12.13-cp311-cp311-win32.whl", hash = "sha256:fef8d50dfa482925bb6b4c208b40d8e9fa54cecba923dc65b825a72eed9a5dbd", size = 427036, upload-time = "2025-06-14T15:13:57.076Z" }, - { url = "https://files.pythonhosted.org/packages/f8/b6/98518bcc615ef998a64bef371178b9afc98ee25895b4f476c428fade2220/aiohttp-3.12.13-cp311-cp311-win_amd64.whl", hash = "sha256:9a27da9c3b5ed9d04c36ad2df65b38a96a37e9cfba6f1381b842d05d98e6afe9", size = 451427, upload-time = "2025-06-14T15:13:58.505Z" }, - { url = "https://files.pythonhosted.org/packages/b4/6a/ce40e329788013cd190b1d62bbabb2b6a9673ecb6d836298635b939562ef/aiohttp-3.12.13-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0aa580cf80558557285b49452151b9c69f2fa3ad94c5c9e76e684719a8791b73", size = 700491, upload-time = "2025-06-14T15:14:00.048Z" }, - { url = "https://files.pythonhosted.org/packages/28/d9/7150d5cf9163e05081f1c5c64a0cdf3c32d2f56e2ac95db2a28fe90eca69/aiohttp-3.12.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b103a7e414b57e6939cc4dece8e282cfb22043efd0c7298044f6594cf83ab347", size = 475104, upload-time = "2025-06-14T15:14:01.691Z" }, - { url = "https://files.pythonhosted.org/packages/f8/91/d42ba4aed039ce6e449b3e2db694328756c152a79804e64e3da5bc19dffc/aiohttp-3.12.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78f64e748e9e741d2eccff9597d09fb3cd962210e5b5716047cbb646dc8fe06f", size = 467948, upload-time = "2025-06-14T15:14:03.561Z" }, - { url = "https://files.pythonhosted.org/packages/99/3b/06f0a632775946981d7c4e5a865cddb6e8dfdbaed2f56f9ade7bb4a1039b/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c955989bf4c696d2ededc6b0ccb85a73623ae6e112439398935362bacfaaf6", size = 1714742, upload-time = "2025-06-14T15:14:05.558Z" }, - { url = "https://files.pythonhosted.org/packages/92/a6/2552eebad9ec5e3581a89256276009e6a974dc0793632796af144df8b740/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d640191016763fab76072c87d8854a19e8e65d7a6fcfcbf017926bdbbb30a7e5", size = 1697393, upload-time = "2025-06-14T15:14:07.194Z" }, - { url = "https://files.pythonhosted.org/packages/d8/9f/bd08fdde114b3fec7a021381b537b21920cdd2aa29ad48c5dffd8ee314f1/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4dc507481266b410dede95dd9f26c8d6f5a14315372cc48a6e43eac652237d9b", size = 1752486, upload-time = "2025-06-14T15:14:08.808Z" }, - { url = "https://files.pythonhosted.org/packages/f7/e1/affdea8723aec5bd0959171b5490dccd9a91fcc505c8c26c9f1dca73474d/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8a94daa873465d518db073bd95d75f14302e0208a08e8c942b2f3f1c07288a75", size = 1798643, upload-time = "2025-06-14T15:14:10.767Z" }, - { url = "https://files.pythonhosted.org/packages/f3/9d/666d856cc3af3a62ae86393baa3074cc1d591a47d89dc3bf16f6eb2c8d32/aiohttp-3.12.13-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:177f52420cde4ce0bb9425a375d95577fe082cb5721ecb61da3049b55189e4e6", size = 1718082, upload-time = "2025-06-14T15:14:12.38Z" }, - { url = "https://files.pythonhosted.org/packages/f3/ce/3c185293843d17be063dada45efd2712bb6bf6370b37104b4eda908ffdbd/aiohttp-3.12.13-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f7df1f620ec40f1a7fbcb99ea17d7326ea6996715e78f71a1c9a021e31b96b8", size = 1633884, upload-time = "2025-06-14T15:14:14.415Z" }, - { url = "https://files.pythonhosted.org/packages/3a/5b/f3413f4b238113be35dfd6794e65029250d4b93caa0974ca572217745bdb/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3062d4ad53b36e17796dce1c0d6da0ad27a015c321e663657ba1cc7659cfc710", size = 1694943, upload-time = "2025-06-14T15:14:16.48Z" }, - { url = "https://files.pythonhosted.org/packages/82/c8/0e56e8bf12081faca85d14a6929ad5c1263c146149cd66caa7bc12255b6d/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:8605e22d2a86b8e51ffb5253d9045ea73683d92d47c0b1438e11a359bdb94462", size = 1716398, upload-time = "2025-06-14T15:14:18.589Z" }, - { url = "https://files.pythonhosted.org/packages/ea/f3/33192b4761f7f9b2f7f4281365d925d663629cfaea093a64b658b94fc8e1/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:54fbbe6beafc2820de71ece2198458a711e224e116efefa01b7969f3e2b3ddae", size = 1657051, upload-time = "2025-06-14T15:14:20.223Z" }, - { url = "https://files.pythonhosted.org/packages/5e/0b/26ddd91ca8f84c48452431cb4c5dd9523b13bc0c9766bda468e072ac9e29/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:050bd277dfc3768b606fd4eae79dd58ceda67d8b0b3c565656a89ae34525d15e", size = 1736611, upload-time = "2025-06-14T15:14:21.988Z" }, - { url = "https://files.pythonhosted.org/packages/c3/8d/e04569aae853302648e2c138a680a6a2f02e374c5b6711732b29f1e129cc/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:2637a60910b58f50f22379b6797466c3aa6ae28a6ab6404e09175ce4955b4e6a", size = 1764586, upload-time = "2025-06-14T15:14:23.979Z" }, - { url = "https://files.pythonhosted.org/packages/ac/98/c193c1d1198571d988454e4ed75adc21c55af247a9fda08236602921c8c8/aiohttp-3.12.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e986067357550d1aaa21cfe9897fa19e680110551518a5a7cf44e6c5638cb8b5", size = 1724197, upload-time = "2025-06-14T15:14:25.692Z" }, - { url = "https://files.pythonhosted.org/packages/e7/9e/07bb8aa11eec762c6b1ff61575eeeb2657df11ab3d3abfa528d95f3e9337/aiohttp-3.12.13-cp312-cp312-win32.whl", hash = "sha256:ac941a80aeea2aaae2875c9500861a3ba356f9ff17b9cb2dbfb5cbf91baaf5bf", size = 421771, upload-time = "2025-06-14T15:14:27.364Z" }, - { url = "https://files.pythonhosted.org/packages/52/66/3ce877e56ec0813069cdc9607cd979575859c597b6fb9b4182c6d5f31886/aiohttp-3.12.13-cp312-cp312-win_amd64.whl", hash = "sha256:671f41e6146a749b6c81cb7fd07f5a8356d46febdaaaf07b0e774ff04830461e", size = 447869, upload-time = "2025-06-14T15:14:29.05Z" }, + { url = "https://files.pythonhosted.org/packages/20/19/9e86722ec8e835959bd97ce8c1efa78cf361fa4531fca372551abcc9cdd6/aiohttp-3.12.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:d3ce17ce0220383a0f9ea07175eeaa6aa13ae5a41f30bc61d84df17f0e9b1117", size = 711246, upload-time = "2025-07-29T05:50:15.937Z" }, + { url = "https://files.pythonhosted.org/packages/71/f9/0a31fcb1a7d4629ac9d8f01f1cb9242e2f9943f47f5d03215af91c3c1a26/aiohttp-3.12.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:010cc9bbd06db80fe234d9003f67e97a10fe003bfbedb40da7d71c1008eda0fe", size = 483515, upload-time = "2025-07-29T05:50:17.442Z" }, + { url = "https://files.pythonhosted.org/packages/62/6c/94846f576f1d11df0c2e41d3001000527c0fdf63fce7e69b3927a731325d/aiohttp-3.12.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:3f9d7c55b41ed687b9d7165b17672340187f87a773c98236c987f08c858145a9", size = 471776, upload-time = "2025-07-29T05:50:19.568Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6c/f766d0aaafcee0447fad0328da780d344489c042e25cd58fde566bf40aed/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bc4fbc61bb3548d3b482f9ac7ddd0f18c67e4225aaa4e8552b9f1ac7e6bda9e5", size = 1741977, upload-time = "2025-07-29T05:50:21.665Z" }, + { url = "https://files.pythonhosted.org/packages/17/e5/fb779a05ba6ff44d7bc1e9d24c644e876bfff5abe5454f7b854cace1b9cc/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7fbc8a7c410bb3ad5d595bb7118147dfbb6449d862cc1125cf8867cb337e8728", size = 1690645, upload-time = "2025-07-29T05:50:23.333Z" }, + { url = "https://files.pythonhosted.org/packages/37/4e/a22e799c2035f5d6a4ad2cf8e7c1d1bd0923192871dd6e367dafb158b14c/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:74dad41b3458dbb0511e760fb355bb0b6689e0630de8a22b1b62a98777136e16", size = 1789437, upload-time = "2025-07-29T05:50:25.007Z" }, + { url = "https://files.pythonhosted.org/packages/28/e5/55a33b991f6433569babb56018b2fb8fb9146424f8b3a0c8ecca80556762/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b6f0af863cf17e6222b1735a756d664159e58855da99cfe965134a3ff63b0b0", size = 1828482, upload-time = "2025-07-29T05:50:26.693Z" }, + { url = "https://files.pythonhosted.org/packages/c6/82/1ddf0ea4f2f3afe79dffed5e8a246737cff6cbe781887a6a170299e33204/aiohttp-3.12.15-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b5b7fe4972d48a4da367043b8e023fb70a04d1490aa7d68800e465d1b97e493b", size = 1730944, upload-time = "2025-07-29T05:50:28.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/96/784c785674117b4cb3877522a177ba1b5e4db9ce0fd519430b5de76eec90/aiohttp-3.12.15-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6443cca89553b7a5485331bc9bedb2342b08d073fa10b8c7d1c60579c4a7b9bd", size = 1668020, upload-time = "2025-07-29T05:50:30.032Z" }, + { url = "https://files.pythonhosted.org/packages/12/8a/8b75f203ea7e5c21c0920d84dd24a5c0e971fe1e9b9ebbf29ae7e8e39790/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6c5f40ec615e5264f44b4282ee27628cea221fcad52f27405b80abb346d9f3f8", size = 1716292, upload-time = "2025-07-29T05:50:31.983Z" }, + { url = "https://files.pythonhosted.org/packages/47/0b/a1451543475bb6b86a5cfc27861e52b14085ae232896a2654ff1231c0992/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:2abbb216a1d3a2fe86dbd2edce20cdc5e9ad0be6378455b05ec7f77361b3ab50", size = 1711451, upload-time = "2025-07-29T05:50:33.989Z" }, + { url = "https://files.pythonhosted.org/packages/55/fd/793a23a197cc2f0d29188805cfc93aa613407f07e5f9da5cd1366afd9d7c/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:db71ce547012a5420a39c1b744d485cfb823564d01d5d20805977f5ea1345676", size = 1691634, upload-time = "2025-07-29T05:50:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/ca/bf/23a335a6670b5f5dfc6d268328e55a22651b440fca341a64fccf1eada0c6/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:ced339d7c9b5030abad5854aa5413a77565e5b6e6248ff927d3e174baf3badf7", size = 1785238, upload-time = "2025-07-29T05:50:37.597Z" }, + { url = "https://files.pythonhosted.org/packages/57/4f/ed60a591839a9d85d40694aba5cef86dde9ee51ce6cca0bb30d6eb1581e7/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:7c7dd29c7b5bda137464dc9bfc738d7ceea46ff70309859ffde8c022e9b08ba7", size = 1805701, upload-time = "2025-07-29T05:50:39.591Z" }, + { url = "https://files.pythonhosted.org/packages/85/e0/444747a9455c5de188c0f4a0173ee701e2e325d4b2550e9af84abb20cdba/aiohttp-3.12.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:421da6fd326460517873274875c6c5a18ff225b40da2616083c5a34a7570b685", size = 1718758, upload-time = "2025-07-29T05:50:41.292Z" }, + { url = "https://files.pythonhosted.org/packages/36/ab/1006278d1ffd13a698e5dd4bfa01e5878f6bddefc296c8b62649753ff249/aiohttp-3.12.15-cp311-cp311-win32.whl", hash = "sha256:4420cf9d179ec8dfe4be10e7d0fe47d6d606485512ea2265b0d8c5113372771b", size = 428868, upload-time = "2025-07-29T05:50:43.063Z" }, + { url = "https://files.pythonhosted.org/packages/10/97/ad2b18700708452400278039272032170246a1bf8ec5d832772372c71f1a/aiohttp-3.12.15-cp311-cp311-win_amd64.whl", hash = "sha256:edd533a07da85baa4b423ee8839e3e91681c7bfa19b04260a469ee94b778bf6d", size = 453273, upload-time = "2025-07-29T05:50:44.613Z" }, + { url = "https://files.pythonhosted.org/packages/63/97/77cb2450d9b35f517d6cf506256bf4f5bda3f93a66b4ad64ba7fc917899c/aiohttp-3.12.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:802d3868f5776e28f7bf69d349c26fc0efadb81676d0afa88ed00d98a26340b7", size = 702333, upload-time = "2025-07-29T05:50:46.507Z" }, + { url = "https://files.pythonhosted.org/packages/83/6d/0544e6b08b748682c30b9f65640d006e51f90763b41d7c546693bc22900d/aiohttp-3.12.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2800614cd560287be05e33a679638e586a2d7401f4ddf99e304d98878c29444", size = 476948, upload-time = "2025-07-29T05:50:48.067Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/c8c40e611e5094330284b1aea8a4b02ca0858f8458614fa35754cab42b9c/aiohttp-3.12.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8466151554b593909d30a0a125d638b4e5f3836e5aecde85b66b80ded1cb5b0d", size = 469787, upload-time = "2025-07-29T05:50:49.669Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/b76438e70319796bfff717f325d97ce2e9310f752a267bfdf5192ac6082b/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2e5a495cb1be69dae4b08f35a6c4579c539e9b5706f606632102c0f855bcba7c", size = 1716590, upload-time = "2025-07-29T05:50:51.368Z" }, + { url = "https://files.pythonhosted.org/packages/79/b1/60370d70cdf8b269ee1444b390cbd72ce514f0d1cd1a715821c784d272c9/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6404dfc8cdde35c69aaa489bb3542fb86ef215fc70277c892be8af540e5e21c0", size = 1699241, upload-time = "2025-07-29T05:50:53.628Z" }, + { url = "https://files.pythonhosted.org/packages/a3/2b/4968a7b8792437ebc12186db31523f541943e99bda8f30335c482bea6879/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3ead1c00f8521a5c9070fcb88f02967b1d8a0544e6d85c253f6968b785e1a2ab", size = 1754335, upload-time = "2025-07-29T05:50:55.394Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c1/49524ed553f9a0bec1a11fac09e790f49ff669bcd14164f9fab608831c4d/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6990ef617f14450bc6b34941dba4f12d5613cbf4e33805932f853fbd1cf18bfb", size = 1800491, upload-time = "2025-07-29T05:50:57.202Z" }, + { url = "https://files.pythonhosted.org/packages/de/5e/3bf5acea47a96a28c121b167f5ef659cf71208b19e52a88cdfa5c37f1fcc/aiohttp-3.12.15-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd736ed420f4db2b8148b52b46b88ed038d0354255f9a73196b7bbce3ea97545", size = 1719929, upload-time = "2025-07-29T05:50:59.192Z" }, + { url = "https://files.pythonhosted.org/packages/39/94/8ae30b806835bcd1cba799ba35347dee6961a11bd507db634516210e91d8/aiohttp-3.12.15-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c5092ce14361a73086b90c6efb3948ffa5be2f5b6fbcf52e8d8c8b8848bb97c", size = 1635733, upload-time = "2025-07-29T05:51:01.394Z" }, + { url = "https://files.pythonhosted.org/packages/7a/46/06cdef71dd03acd9da7f51ab3a9107318aee12ad38d273f654e4f981583a/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aaa2234bb60c4dbf82893e934d8ee8dea30446f0647e024074237a56a08c01bd", size = 1696790, upload-time = "2025-07-29T05:51:03.657Z" }, + { url = "https://files.pythonhosted.org/packages/02/90/6b4cfaaf92ed98d0ec4d173e78b99b4b1a7551250be8937d9d67ecb356b4/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6d86a2fbdd14192e2f234a92d3b494dd4457e683ba07e5905a0b3ee25389ac9f", size = 1718245, upload-time = "2025-07-29T05:51:05.911Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e6/2593751670fa06f080a846f37f112cbe6f873ba510d070136a6ed46117c6/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a041e7e2612041a6ddf1c6a33b883be6a421247c7afd47e885969ee4cc58bd8d", size = 1658899, upload-time = "2025-07-29T05:51:07.753Z" }, + { url = "https://files.pythonhosted.org/packages/8f/28/c15bacbdb8b8eb5bf39b10680d129ea7410b859e379b03190f02fa104ffd/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5015082477abeafad7203757ae44299a610e89ee82a1503e3d4184e6bafdd519", size = 1738459, upload-time = "2025-07-29T05:51:09.56Z" }, + { url = "https://files.pythonhosted.org/packages/00/de/c269cbc4faa01fb10f143b1670633a8ddd5b2e1ffd0548f7aa49cb5c70e2/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:56822ff5ddfd1b745534e658faba944012346184fbfe732e0d6134b744516eea", size = 1766434, upload-time = "2025-07-29T05:51:11.423Z" }, + { url = "https://files.pythonhosted.org/packages/52/b0/4ff3abd81aa7d929b27d2e1403722a65fc87b763e3a97b3a2a494bfc63bc/aiohttp-3.12.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b2acbbfff69019d9014508c4ba0401822e8bae5a5fdc3b6814285b71231b60f3", size = 1726045, upload-time = "2025-07-29T05:51:13.689Z" }, + { url = "https://files.pythonhosted.org/packages/71/16/949225a6a2dd6efcbd855fbd90cf476052e648fb011aa538e3b15b89a57a/aiohttp-3.12.15-cp312-cp312-win32.whl", hash = "sha256:d849b0901b50f2185874b9a232f38e26b9b3d4810095a7572eacea939132d4e1", size = 423591, upload-time = "2025-07-29T05:51:15.452Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d8/fa65d2a349fe938b76d309db1a56a75c4fb8cc7b17a398b698488a939903/aiohttp-3.12.15-cp312-cp312-win_amd64.whl", hash = "sha256:b390ef5f62bb508a9d67cb3bba9b8356e23b3996da7062f1a57ce1a79d2b3d34", size = 450266, upload-time = "2025-07-29T05:51:17.239Z" }, ] [[package]] @@ -110,14 +110,15 @@ wheels = [ [[package]] name = "aiosignal" -version = "1.3.2" +version = "1.4.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "frozenlist" }, + { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ba/b5/6d55e80f6d8a08ce22b982eafa278d823b541c925f11ee774b0b9c43473d/aiosignal-1.3.2.tar.gz", hash = "sha256:a8c255c66fafb1e499c9351d0bf32ff2d8a0321595ebac3b93713656d2436f54", size = 19424, upload-time = "2024-12-13T17:10:40.86Z" } +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/6a/bc7e17a3e87a2985d3e8f4da4cd0f481060eb78fb08596c42be62c90a4d9/aiosignal-1.3.2-py2.py3-none-any.whl", hash = "sha256:45cde58e409a301715980c2b01d0c28bdde3770d8290b5eb2173759d9acb31a5", size = 7597, upload-time = "2024-12-13T17:10:38.469Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] [[package]] @@ -151,21 +152,21 @@ wheels = [ [[package]] name = "azure-core" -version = "1.34.0" +version = "1.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "requests" }, { name = "six" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/c9/29/ff7a519a315e41c85bab92a7478c6acd1cf0b14353139a08caee4c691f77/azure_core-1.34.0.tar.gz", hash = "sha256:bdb544989f246a0ad1c85d72eeb45f2f835afdcbc5b45e43f0dbde7461c81ece", size = 297999, upload-time = "2025-05-01T23:17:27.59Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/89/f53968635b1b2e53e4aad2dd641488929fef4ca9dfb0b97927fa7697ddf3/azure_core-1.35.0.tar.gz", hash = "sha256:c0be528489485e9ede59b6971eb63c1eaacf83ef53001bfe3904e475e972be5c", size = 339689, upload-time = "2025-07-03T00:55:23.496Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/9e/5c87b49f65bb16571599bc789857d0ded2f53014d3392bc88a5d1f3ad779/azure_core-1.34.0-py3-none-any.whl", hash = "sha256:0615d3b756beccdb6624d1c0ae97284f38b78fb59a2a9839bf927c66fbbdddd6", size = 207409, upload-time = "2025-05-01T23:17:29.818Z" }, + { url = "https://files.pythonhosted.org/packages/d4/78/bf94897361fdd650850f0f2e405b2293e2f12808239046232bdedf554301/azure_core-1.35.0-py3-none-any.whl", hash = "sha256:8db78c72868a58f3de8991eb4d22c4d368fae226dac1002998d6c50437e7dad1", size = 210708, upload-time = "2025-07-03T00:55:25.238Z" }, ] [[package]] name = "azure-identity" -version = "1.23.0" +version = "1.24.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -174,14 +175,14 @@ dependencies = [ { name = "msal-extensions" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/41/52/458c1be17a5d3796570ae2ed3c6b7b55b134b22d5ef8132b4f97046a9051/azure_identity-1.23.0.tar.gz", hash = "sha256:d9cdcad39adb49d4bb2953a217f62aec1f65bbb3c63c9076da2be2a47e53dde4", size = 265280, upload-time = "2025-05-14T00:18:30.408Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/44/f3ee20bacb220b6b4a2b0a6cf7e742eecb383a5ccf604dd79ec27c286b7e/azure_identity-1.24.0.tar.gz", hash = "sha256:6c3a40b2a70af831e920b89e6421e8dcd4af78a0cb38b9642d86c67643d4930c", size = 271630, upload-time = "2025-08-07T22:27:36.258Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/07/16/a51d47780f41e4b87bb2d454df6aea90a44a346e918ac189d3700f3d728d/azure_identity-1.23.0-py3-none-any.whl", hash = "sha256:dbbeb64b8e5eaa81c44c565f264b519ff2de7ff0e02271c49f3cb492762a50b0", size = 186097, upload-time = "2025-05-14T00:18:32.734Z" }, + { url = "https://files.pythonhosted.org/packages/a9/74/17428cb429e8d52f6d0d69ed685f4760a545cb0156594963a9337b53b6c9/azure_identity-1.24.0-py3-none-any.whl", hash = "sha256:9e04997cde0ab02ed66422c74748548e620b7b29361c72ce622acab0267ff7c4", size = 187890, upload-time = "2025-08-07T22:27:38.033Z" }, ] [[package]] name = "azure-storage-blob" -version = "12.25.1" +version = "12.26.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "azure-core" }, @@ -189,41 +190,41 @@ dependencies = [ { name = "isodate" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/8b/f3/f764536c25cc3829d36857167f03933ce9aee2262293179075439f3cd3ad/azure_storage_blob-12.25.1.tar.gz", hash = "sha256:4f294ddc9bc47909ac66b8934bd26b50d2000278b10ad82cc109764fdc6e0e3b", size = 570541, upload-time = "2025-03-27T17:13:05.424Z" } +sdist = { url = "https://files.pythonhosted.org/packages/96/95/3e3414491ce45025a1cde107b6ae72bf72049e6021597c201cd6a3029b9a/azure_storage_blob-12.26.0.tar.gz", hash = "sha256:5dd7d7824224f7de00bfeb032753601c982655173061e242f13be6e26d78d71f", size = 583332, upload-time = "2025-07-16T21:34:07.644Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/57/33/085d9352d416e617993821b9d9488222fbb559bc15c3641d6cbd6d16d236/azure_storage_blob-12.25.1-py3-none-any.whl", hash = "sha256:1f337aab12e918ec3f1b638baada97550673911c4ceed892acc8e4e891b74167", size = 406990, upload-time = "2025-03-27T17:13:06.879Z" }, + { url = "https://files.pythonhosted.org/packages/5b/64/63dbfdd83b31200ac58820a7951ddfdeed1fbee9285b0f3eae12d1357155/azure_storage_blob-12.26.0-py3-none-any.whl", hash = "sha256:8c5631b8b22b4f53ec5fff2f3bededf34cfef111e2af613ad42c9e6de00a77fe", size = 412907, upload-time = "2025-07-16T21:34:09.367Z" }, ] [[package]] name = "casadi" -version = "3.7.0" +version = "3.7.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/b3/49/f8b8eb7c8e98e28e0cca1b41988932025d59a602fb2075f737cbaecf764d/casadi-3.7.0.tar.gz", hash = "sha256:21254f17eb5551c4a938641cc1b815ff3da27271ab2c36e44a3e90ec50ba471f", size = 6027731, upload-time = "2025-03-29T22:31:31.309Z" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/21/1b4ea75dbddfcdbc7cd70d83147dde72899667097c0c05b288fd7015ef10/casadi-3.7.1.tar.gz", hash = "sha256:12577155cd3cd79ba162381bfed6add1541bc770ba3f1f1334e4eb159d9b39ba", size = 6065586, upload-time = "2025-07-23T21:47:56.483Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/83/cf/7d1dc6b16f690f1fac22a0a43684de3a03b3f631a1d8c25393f320b5f971/casadi-3.7.0-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:50bba75bb9575baa09a2e25ce3c1968803320c49b9dcc218139321dab02f0400", size = 48302536, upload-time = "2025-03-30T07:31:37.067Z" }, - { url = "https://files.pythonhosted.org/packages/74/d2/f69156aaf4545543995c791a0f97551b66f9c9bc8d92361973ee6378ad25/casadi-3.7.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:8f138bf370603f6f820385d78882f840168e8e0866bea5de08b6b54a6e22c093", size = 44713683, upload-time = "2025-03-30T07:33:37.446Z" }, - { url = "https://files.pythonhosted.org/packages/2e/3b/13a7d92c27543e0a2eb559f2a5acc7278f1a2ef0217772b09df50ac1c2dc/casadi-3.7.0-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:4d844a1281c7ff527115d27ee8f2c58315d0008cac835656323a6d502de27010", size = 45518255, upload-time = "2025-03-30T07:36:03.844Z" }, - { url = "https://files.pythonhosted.org/packages/bb/88/00adcb26a0c313ebeaa2db8b2691e37418d6b7c3e06b4a90a3ac9380b91b/casadi-3.7.0-cp311-none-manylinux2014_i686.whl", hash = "sha256:b8640486db98b75a75eb05b6191d5a7f0d44774c36c07c7da327d4d740914284", size = 74329105, upload-time = "2025-03-30T07:39:19.069Z" }, - { url = "https://files.pythonhosted.org/packages/60/64/b31fd4ce5b93f97fd16a9ba7ce8d4a8d36334a518f1ad00525340db31ff4/casadi-3.7.0-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:65d9a61660c75ff8f0c30abf52e2f9cefcdee1cbb9301e26678f64116f509ee7", size = 77330636, upload-time = "2025-03-30T07:46:55.954Z" }, - { url = "https://files.pythonhosted.org/packages/c4/25/79bb6b866a7005186ccdfcb89e01032885a9efa90acfe2d079253685622b/casadi-3.7.0-cp311-none-win_amd64.whl", hash = "sha256:391b5ee886cde28bf813e820958bfcdef98314bd367a93c95e7bef1bf713b886", size = 50949252, upload-time = "2025-03-30T07:49:05.348Z" }, - { url = "https://files.pythonhosted.org/packages/b8/27/6ca4c831d9131eb15ca34346398c1379a577363c264ad47983c1be65b3e1/casadi-3.7.0-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:ceb4d67c1c5cdf80f233866fcdcaf7d77523115983081fe909b70fe89fd7b708", size = 48303214, upload-time = "2025-03-30T07:51:05.246Z" }, - { url = "https://files.pythonhosted.org/packages/b2/cb/cd8be8aeac6453e2aad96793101fa287f54f86b643d542375969738930c3/casadi-3.7.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:83dbc27125044e7600b04af648f24e6924d46e81afe6ff088218e4b7e9a37f08", size = 44713758, upload-time = "2025-03-30T07:52:44.766Z" }, - { url = "https://files.pythonhosted.org/packages/a4/3d/c8a392eac772b1537f63ee16b57be87bcdef9d9bc9530b54c95b1a122960/casadi-3.7.0-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:53731171b41ca0640b76657d2320b929634a137ff30f12f985196df35411885b", size = 45517623, upload-time = "2025-03-30T07:54:25.318Z" }, - { url = "https://files.pythonhosted.org/packages/68/d2/2acd3b8cf8fa25bca342cd539f69dc94b0fa1bf3acaa30f13848fa0f31ee/casadi-3.7.0-cp312-none-manylinux2014_i686.whl", hash = "sha256:6b30fb73d8c140fbbe51e60d00412aaefe5a7b775257a422ea244f423bd2351c", size = 74319729, upload-time = "2025-03-30T07:57:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/0f/35/ec351423c854a74884218501a431e018c6eee79461dde8e91f9ce6b4e2b5/casadi-3.7.0-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:b795371bf09ec0bfd22eaa0a1e81ce2cd3ecd811c9d370b98f6853b87e8397c9", size = 77323153, upload-time = "2025-03-30T08:00:32.507Z" }, - { url = "https://files.pythonhosted.org/packages/d5/4b/ab605b5948795fe15b5930ecc5ac7ba72ebad116a9003c0117421cbe34a8/casadi-3.7.0-cp312-none-win_amd64.whl", hash = "sha256:e77173aad67b817ebf4320c31cef37c7d666f9895bc5970f3370b2ae6fdad587", size = 50946897, upload-time = "2025-03-30T08:02:37.836Z" }, + { url = "https://files.pythonhosted.org/packages/e6/4d/cfe092b2deb65dbf913d38148e30dfe41bbe9f289f892ea7a5d3526532d2/casadi-3.7.1-cp311-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:c13fafdff0cdf73392fe2002245befe7496b5ac99b70adb64babff0099068c8a", size = 47100775, upload-time = "2025-07-23T19:56:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/13/41/c4ad627e447a083fbfd2826f1f4684223f0072fdc49f58157ca6a6ce0a77/casadi-3.7.1-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:74d9c6bad3c7cb347494d52cc3e6faf3fbf3300046fab0e1ddc9439d3fd68486", size = 42282155, upload-time = "2025-07-23T19:59:11.191Z" }, + { url = "https://files.pythonhosted.org/packages/ca/e2/53733933d454657e319cc8074e004ec46660c4797cbffcaf7695a874b0da/casadi-3.7.1-cp311-none-manylinux2014_aarch64.whl", hash = "sha256:6fbc0803e6c7ffb9ba9a41ba38e7c1dadee73835edbacacce3530343f078f3f9", size = 47272638, upload-time = "2025-07-23T20:03:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/e1/72/8002d4fe64325842eb6a7f7ac1e4574c02584e85e5b9d93a08a789095505/casadi-3.7.1-cp311-none-manylinux2014_i686.whl", hash = "sha256:38a16b0c7caff5297df91c7e3a65091824979367e6fc8e6eb0e5ab9f1f832af8", size = 72418203, upload-time = "2025-07-23T20:07:18.681Z" }, + { url = "https://files.pythonhosted.org/packages/96/e5/065287b78c4f6a00a010d7c3f5316df1b95f7c5bda644f2151b54a79a964/casadi-3.7.1-cp311-none-manylinux2014_x86_64.whl", hash = "sha256:2dd75cc1b5ca8009586186e4a63e0cdd83a672bf308edc75ef84f9c896cd971d", size = 75558461, upload-time = "2025-07-23T21:33:20.771Z" }, + { url = "https://files.pythonhosted.org/packages/b0/4c/084d2f3bca33d0b3d509be17c3b678f6bcc03f3409d42e88750cb1e44966/casadi-3.7.1-cp311-none-win_amd64.whl", hash = "sha256:3ab68c6dd621b5f150635bf06aa8e1697cae2fbe0137e9d47270c54d305c334c", size = 50987255, upload-time = "2025-07-23T21:34:20.349Z" }, + { url = "https://files.pythonhosted.org/packages/e1/a3/65942d3e0f7589a72e69dd9dee575963ecf51504e985f132bce8b0d24988/casadi-3.7.1-cp312-none-macosx_10_13_x86_64.macosx_10_13_intel.whl", hash = "sha256:4a29ae3212b0ae6931d439ca3bfa2ac279826e99c2c9a443704e84021933ab76", size = 47102059, upload-time = "2025-07-23T20:36:47.174Z" }, + { url = "https://files.pythonhosted.org/packages/2e/f4/dc371199583f6b154ba0966c4132418737a2d2e019c9f6ad227ebd279684/casadi-3.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:70edf1df3fc83401eb5a2f7053ae8394f4e612e61c2ad6de088e3d76d0ec845e", size = 42282702, upload-time = "2025-07-23T20:39:41.499Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ff/5f8a9378cd7b322d1dcc8b8aa9fbc454ad57152b754c94b50ffdc67254a0/casadi-3.7.1-cp312-none-manylinux2014_aarch64.whl", hash = "sha256:a1717bbd07a6eb98acddf5ea5da7e78a9b85b75dbabb55190cc6b0f2812ef39d", size = 47271636, upload-time = "2025-07-23T20:42:59.961Z" }, + { url = "https://files.pythonhosted.org/packages/e1/11/e1b6a76bf288fc86dc8919cbcd2d3a654e7ead4240d9928deb8ed4ee91da/casadi-3.7.1-cp312-none-manylinux2014_i686.whl", hash = "sha256:57ae29d16af0716ebc15d161bcf0bcd97d35631b10c0e80838290d249cac80d7", size = 72410197, upload-time = "2025-07-23T21:35:35.509Z" }, + { url = "https://files.pythonhosted.org/packages/c1/76/bb43ff625066b178132172d41db0c5c962b4af9db15c88b8a23e65376d18/casadi-3.7.1-cp312-none-manylinux2014_x86_64.whl", hash = "sha256:756f3d07d3414dc54f05ebec883ac3db6e307bfe661719d188340504b6bed420", size = 75552890, upload-time = "2025-07-23T21:37:14.638Z" }, + { url = "https://files.pythonhosted.org/packages/11/0c/8026b0b238e00a2e63c7142505cbcd0931b4db4140d55c83962f9d8e0b02/casadi-3.7.1-cp312-none-win_amd64.whl", hash = "sha256:b228fbcd1f41eb8d5405dbc1e0ecb780daf89808392706bb77fd2b240f8a437e", size = 50984453, upload-time = "2025-07-23T21:38:18.21Z" }, ] [[package]] name = "certifi" -version = "2025.6.15" +version = "2025.8.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +sdist = { url = "https://files.pythonhosted.org/packages/dc/67/960ebe6bf230a96cda2e0abcf73af550ec4f090005363542f0765df162e0/certifi-2025.8.3.tar.gz", hash = "sha256:e564105f78ded564e3ae7c923924435e1daa7463faeab5bb932bc53ffae63407", size = 162386, upload-time = "2025-08-03T03:07:47.08Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, + { url = "https://files.pythonhosted.org/packages/e5/48/1549795ba7742c948d2ad169c1c8cdbae65bc450d6cd753d124b17c8cd32/certifi-2025.8.3-py3-none-any.whl", hash = "sha256:f6c12493cfb1b06ba2ff328595af9350c65d6644968e5d3a2ffd78699af217a5", size = 161216, upload-time = "2025-08-03T03:07:45.777Z" }, ] [[package]] @@ -262,37 +263,33 @@ wheels = [ [[package]] name = "charset-normalizer" -version = "3.4.2" +version = "3.4.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +sdist = { url = "https://files.pythonhosted.org/packages/83/2d/5fd176ceb9b2fc619e63405525573493ca23441330fcdaee6bef9460e924/charset_normalizer-3.4.3.tar.gz", hash = "sha256:6fce4b8500244f6fcb71465d4a4930d132ba9ab8e71a7859e6a5d59851068d14", size = 122371, upload-time = "2025-08-09T07:57:28.46Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, - { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, - { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, - { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, - { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, - { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, - { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, - { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, - { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, - { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, - { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, - { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, - { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, - { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, - { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, - { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, - { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, - { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, - { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, - { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, - { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, - { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, - { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, - { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, - { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, - { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b5/991245018615474a60965a7c9cd2b4efbaabd16d582a5547c47ee1c7730b/charset_normalizer-3.4.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:b256ee2e749283ef3ddcff51a675ff43798d92d746d1a6e4631bf8c707d22d0b", size = 204483, upload-time = "2025-08-09T07:55:53.12Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2a/ae245c41c06299ec18262825c1569c5d3298fc920e4ddf56ab011b417efd/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13faeacfe61784e2559e690fc53fa4c5ae97c6fcedb8eb6fb8d0a15b475d2c64", size = 145520, upload-time = "2025-08-09T07:55:54.712Z" }, + { url = "https://files.pythonhosted.org/packages/3a/a4/b3b6c76e7a635748c4421d2b92c7b8f90a432f98bda5082049af37ffc8e3/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:00237675befef519d9af72169d8604a067d92755e84fe76492fef5441db05b91", size = 158876, upload-time = "2025-08-09T07:55:56.024Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e6/63bb0e10f90a8243c5def74b5b105b3bbbfb3e7bb753915fe333fb0c11ea/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:585f3b2a80fbd26b048a0be90c5aae8f06605d3c92615911c3a2b03a8a3b796f", size = 156083, upload-time = "2025-08-09T07:55:57.582Z" }, + { url = "https://files.pythonhosted.org/packages/87/df/b7737ff046c974b183ea9aa111b74185ac8c3a326c6262d413bd5a1b8c69/charset_normalizer-3.4.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0e78314bdc32fa80696f72fa16dc61168fda4d6a0c014e0380f9d02f0e5d8a07", size = 150295, upload-time = "2025-08-09T07:55:59.147Z" }, + { url = "https://files.pythonhosted.org/packages/61/f1/190d9977e0084d3f1dc169acd060d479bbbc71b90bf3e7bf7b9927dec3eb/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:96b2b3d1a83ad55310de8c7b4a2d04d9277d5591f40761274856635acc5fcb30", size = 148379, upload-time = "2025-08-09T07:56:00.364Z" }, + { url = "https://files.pythonhosted.org/packages/4c/92/27dbe365d34c68cfe0ca76f1edd70e8705d82b378cb54ebbaeabc2e3029d/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:939578d9d8fd4299220161fdd76e86c6a251987476f5243e8864a7844476ba14", size = 160018, upload-time = "2025-08-09T07:56:01.678Z" }, + { url = "https://files.pythonhosted.org/packages/99/04/baae2a1ea1893a01635d475b9261c889a18fd48393634b6270827869fa34/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:fd10de089bcdcd1be95a2f73dbe6254798ec1bda9f450d5828c96f93e2536b9c", size = 157430, upload-time = "2025-08-09T07:56:02.87Z" }, + { url = "https://files.pythonhosted.org/packages/2f/36/77da9c6a328c54d17b960c89eccacfab8271fdaaa228305330915b88afa9/charset_normalizer-3.4.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1e8ac75d72fa3775e0b7cb7e4629cec13b7514d928d15ef8ea06bca03ef01cae", size = 151600, upload-time = "2025-08-09T07:56:04.089Z" }, + { url = "https://files.pythonhosted.org/packages/64/d4/9eb4ff2c167edbbf08cdd28e19078bf195762e9bd63371689cab5ecd3d0d/charset_normalizer-3.4.3-cp311-cp311-win32.whl", hash = "sha256:6cf8fd4c04756b6b60146d98cd8a77d0cdae0e1ca20329da2ac85eed779b6849", size = 99616, upload-time = "2025-08-09T07:56:05.658Z" }, + { url = "https://files.pythonhosted.org/packages/f4/9c/996a4a028222e7761a96634d1820de8a744ff4327a00ada9c8942033089b/charset_normalizer-3.4.3-cp311-cp311-win_amd64.whl", hash = "sha256:31a9a6f775f9bcd865d88ee350f0ffb0e25936a7f930ca98995c05abf1faf21c", size = 107108, upload-time = "2025-08-09T07:56:07.176Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/14c94999e418d9b87682734589404a25854d5f5d0408df68bc15b6ff54bb/charset_normalizer-3.4.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e28e334d3ff134e88989d90ba04b47d84382a828c061d0d1027b1b12a62b39b1", size = 205655, upload-time = "2025-08-09T07:56:08.475Z" }, + { url = "https://files.pythonhosted.org/packages/7d/a8/c6ec5d389672521f644505a257f50544c074cf5fc292d5390331cd6fc9c3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0cacf8f7297b0c4fcb74227692ca46b4a5852f8f4f24b3c766dd94a1075c4884", size = 146223, upload-time = "2025-08-09T07:56:09.708Z" }, + { url = "https://files.pythonhosted.org/packages/fc/eb/a2ffb08547f4e1e5415fb69eb7db25932c52a52bed371429648db4d84fb1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c6fd51128a41297f5409deab284fecbe5305ebd7e5a1f959bee1c054622b7018", size = 159366, upload-time = "2025-08-09T07:56:11.326Z" }, + { url = "https://files.pythonhosted.org/packages/82/10/0fd19f20c624b278dddaf83b8464dcddc2456cb4b02bb902a6da126b87a1/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cfb2aad70f2c6debfbcb717f23b7eb55febc0bb23dcffc0f076009da10c6392", size = 157104, upload-time = "2025-08-09T07:56:13.014Z" }, + { url = "https://files.pythonhosted.org/packages/16/ab/0233c3231af734f5dfcf0844aa9582d5a1466c985bbed6cedab85af9bfe3/charset_normalizer-3.4.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1606f4a55c0fd363d754049cdf400175ee96c992b1f8018b993941f221221c5f", size = 151830, upload-time = "2025-08-09T07:56:14.428Z" }, + { url = "https://files.pythonhosted.org/packages/ae/02/e29e22b4e02839a0e4a06557b1999d0a47db3567e82989b5bb21f3fbbd9f/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:027b776c26d38b7f15b26a5da1044f376455fb3766df8fc38563b4efbc515154", size = 148854, upload-time = "2025-08-09T07:56:16.051Z" }, + { url = "https://files.pythonhosted.org/packages/05/6b/e2539a0a4be302b481e8cafb5af8792da8093b486885a1ae4d15d452bcec/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:42e5088973e56e31e4fa58eb6bd709e42fc03799c11c42929592889a2e54c491", size = 160670, upload-time = "2025-08-09T07:56:17.314Z" }, + { url = "https://files.pythonhosted.org/packages/31/e7/883ee5676a2ef217a40ce0bffcc3d0dfbf9e64cbcfbdf822c52981c3304b/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cc34f233c9e71701040d772aa7490318673aa7164a0efe3172b2981218c26d93", size = 158501, upload-time = "2025-08-09T07:56:18.641Z" }, + { url = "https://files.pythonhosted.org/packages/c1/35/6525b21aa0db614cf8b5792d232021dca3df7f90a1944db934efa5d20bb1/charset_normalizer-3.4.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:320e8e66157cc4e247d9ddca8e21f427efc7a04bbd0ac8a9faf56583fa543f9f", size = 153173, upload-time = "2025-08-09T07:56:20.289Z" }, + { url = "https://files.pythonhosted.org/packages/50/ee/f4704bad8201de513fdc8aac1cabc87e38c5818c93857140e06e772b5892/charset_normalizer-3.4.3-cp312-cp312-win32.whl", hash = "sha256:fb6fecfd65564f208cbf0fba07f107fb661bcd1a7c389edbced3f7a493f70e37", size = 99822, upload-time = "2025-08-09T07:56:21.551Z" }, + { url = "https://files.pythonhosted.org/packages/39/f5/3b3836ca6064d0992c58c7561c6b6eee1b3892e9665d650c803bd5614522/charset_normalizer-3.4.3-cp312-cp312-win_amd64.whl", hash = "sha256:86df271bf921c2ee3818f0522e9a5b8092ca2ad8b065ece5d7d9d0e9f4849bcc", size = 107543, upload-time = "2025-08-09T07:56:23.115Z" }, + { url = "https://files.pythonhosted.org/packages/8a/1f/f041989e93b001bc4e44bb1669ccdcf54d3f00e628229a85b08d330615c5/charset_normalizer-3.4.3-py3-none-any.whl", hash = "sha256:ce571ab16d890d23b5c278547ba694193a45011ff86a9162a71307ed9f86759a", size = 53175, upload-time = "2025-08-09T07:57:26.864Z" }, ] [[package]] @@ -336,73 +333,40 @@ wheels = [ [[package]] name = "contourpy" -version = "1.3.2" +version = "1.3.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "numpy" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, - { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, - { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, - { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, - { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, - { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, - { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, - { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, - { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, - { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, - { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, - { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, - { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, - { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, - { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, - { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, - { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, - { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, - { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, - { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, - { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, -] - -[[package]] -name = "coverage" -version = "7.9.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e7/e0/98670a80884f64578f0c22cd70c5e81a6e07b08167721c7487b4d70a7ca0/coverage-7.9.1.tar.gz", hash = "sha256:6cf43c78c4282708a28e466316935ec7489a9c487518a77fa68f716c67909cec", size = 813650, upload-time = "2025-06-13T13:02:28.627Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/60/34/fa69372a07d0903a78ac103422ad34db72281c9fc625eba94ac1185da66f/coverage-7.9.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:95c765060e65c692da2d2f51a9499c5e9f5cf5453aeaf1420e3fc847cc060582", size = 212146, upload-time = "2025-06-13T13:00:48.496Z" }, - { url = "https://files.pythonhosted.org/packages/27/f0/da1894915d2767f093f081c42afeba18e760f12fdd7a2f4acbe00564d767/coverage-7.9.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ba383dc6afd5ec5b7a0d0c23d38895db0e15bcba7fb0fa8901f245267ac30d86", size = 212536, upload-time = "2025-06-13T13:00:51.535Z" }, - { url = "https://files.pythonhosted.org/packages/10/d5/3fc33b06e41e390f88eef111226a24e4504d216ab8e5d1a7089aa5a3c87a/coverage-7.9.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:37ae0383f13cbdcf1e5e7014489b0d71cc0106458878ccde52e8a12ced4298ed", size = 245092, upload-time = "2025-06-13T13:00:52.883Z" }, - { url = "https://files.pythonhosted.org/packages/0a/39/7aa901c14977aba637b78e95800edf77f29f5a380d29768c5b66f258305b/coverage-7.9.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:69aa417a030bf11ec46149636314c24c8d60fadb12fc0ee8f10fda0d918c879d", size = 242806, upload-time = "2025-06-13T13:00:54.571Z" }, - { url = "https://files.pythonhosted.org/packages/43/fc/30e5cfeaf560b1fc1989227adedc11019ce4bb7cce59d65db34fe0c2d963/coverage-7.9.1-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a4be2a28656afe279b34d4f91c3e26eccf2f85500d4a4ff0b1f8b54bf807338", size = 244610, upload-time = "2025-06-13T13:00:56.932Z" }, - { url = "https://files.pythonhosted.org/packages/bf/15/cca62b13f39650bc87b2b92bb03bce7f0e79dd0bf2c7529e9fc7393e4d60/coverage-7.9.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:382e7ddd5289f140259b610e5f5c58f713d025cb2f66d0eb17e68d0a94278875", size = 244257, upload-time = "2025-06-13T13:00:58.545Z" }, - { url = "https://files.pythonhosted.org/packages/cd/1a/c0f2abe92c29e1464dbd0ff9d56cb6c88ae2b9e21becdb38bea31fcb2f6c/coverage-7.9.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e5532482344186c543c37bfad0ee6069e8ae4fc38d073b8bc836fc8f03c9e250", size = 242309, upload-time = "2025-06-13T13:00:59.836Z" }, - { url = "https://files.pythonhosted.org/packages/57/8d/c6fd70848bd9bf88fa90df2af5636589a8126d2170f3aade21ed53f2b67a/coverage-7.9.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:a39d18b3f50cc121d0ce3838d32d58bd1d15dab89c910358ebefc3665712256c", size = 242898, upload-time = "2025-06-13T13:01:02.506Z" }, - { url = "https://files.pythonhosted.org/packages/c2/9e/6ca46c7bff4675f09a66fe2797cd1ad6a24f14c9c7c3b3ebe0470a6e30b8/coverage-7.9.1-cp311-cp311-win32.whl", hash = "sha256:dd24bd8d77c98557880def750782df77ab2b6885a18483dc8588792247174b32", size = 214561, upload-time = "2025-06-13T13:01:04.012Z" }, - { url = "https://files.pythonhosted.org/packages/a1/30/166978c6302010742dabcdc425fa0f938fa5a800908e39aff37a7a876a13/coverage-7.9.1-cp311-cp311-win_amd64.whl", hash = "sha256:6b55ad10a35a21b8015eabddc9ba31eb590f54adc9cd39bcf09ff5349fd52125", size = 215493, upload-time = "2025-06-13T13:01:05.702Z" }, - { url = "https://files.pythonhosted.org/packages/60/07/a6d2342cd80a5be9f0eeab115bc5ebb3917b4a64c2953534273cf9bc7ae6/coverage-7.9.1-cp311-cp311-win_arm64.whl", hash = "sha256:6ad935f0016be24c0e97fc8c40c465f9c4b85cbbe6eac48934c0dc4d2568321e", size = 213869, upload-time = "2025-06-13T13:01:09.345Z" }, - { url = "https://files.pythonhosted.org/packages/68/d9/7f66eb0a8f2fce222de7bdc2046ec41cb31fe33fb55a330037833fb88afc/coverage-7.9.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a8de12b4b87c20de895f10567639c0797b621b22897b0af3ce4b4e204a743626", size = 212336, upload-time = "2025-06-13T13:01:10.909Z" }, - { url = "https://files.pythonhosted.org/packages/20/20/e07cb920ef3addf20f052ee3d54906e57407b6aeee3227a9c91eea38a665/coverage-7.9.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5add197315a054e92cee1b5f686a2bcba60c4c3e66ee3de77ace6c867bdee7cb", size = 212571, upload-time = "2025-06-13T13:01:12.518Z" }, - { url = "https://files.pythonhosted.org/packages/78/f8/96f155de7e9e248ca9c8ff1a40a521d944ba48bec65352da9be2463745bf/coverage-7.9.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:600a1d4106fe66f41e5d0136dfbc68fe7200a5cbe85610ddf094f8f22e1b0300", size = 246377, upload-time = "2025-06-13T13:01:14.87Z" }, - { url = "https://files.pythonhosted.org/packages/3e/cf/1d783bd05b7bca5c10ded5f946068909372e94615a4416afadfe3f63492d/coverage-7.9.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2a876e4c3e5a2a1715a6608906aa5a2e0475b9c0f68343c2ada98110512ab1d8", size = 243394, upload-time = "2025-06-13T13:01:16.23Z" }, - { url = "https://files.pythonhosted.org/packages/02/dd/e7b20afd35b0a1abea09fb3998e1abc9f9bd953bee548f235aebd2b11401/coverage-7.9.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:81f34346dd63010453922c8e628a52ea2d2ccd73cb2487f7700ac531b247c8a5", size = 245586, upload-time = "2025-06-13T13:01:17.532Z" }, - { url = "https://files.pythonhosted.org/packages/4e/38/b30b0006fea9d617d1cb8e43b1bc9a96af11eff42b87eb8c716cf4d37469/coverage-7.9.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:888f8eee13f2377ce86d44f338968eedec3291876b0b8a7289247ba52cb984cd", size = 245396, upload-time = "2025-06-13T13:01:19.164Z" }, - { url = "https://files.pythonhosted.org/packages/31/e4/4d8ec1dc826e16791f3daf1b50943e8e7e1eb70e8efa7abb03936ff48418/coverage-7.9.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9969ef1e69b8c8e1e70d591f91bbc37fc9a3621e447525d1602801a24ceda898", size = 243577, upload-time = "2025-06-13T13:01:22.433Z" }, - { url = "https://files.pythonhosted.org/packages/25/f4/b0e96c5c38e6e40ef465c4bc7f138863e2909c00e54a331da335faf0d81a/coverage-7.9.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:60c458224331ee3f1a5b472773e4a085cc27a86a0b48205409d364272d67140d", size = 244809, upload-time = "2025-06-13T13:01:24.143Z" }, - { url = "https://files.pythonhosted.org/packages/8a/65/27e0a1fa5e2e5079bdca4521be2f5dabf516f94e29a0defed35ac2382eb2/coverage-7.9.1-cp312-cp312-win32.whl", hash = "sha256:5f646a99a8c2b3ff4c6a6e081f78fad0dde275cd59f8f49dc4eab2e394332e74", size = 214724, upload-time = "2025-06-13T13:01:25.435Z" }, - { url = "https://files.pythonhosted.org/packages/9b/a8/d5b128633fd1a5e0401a4160d02fa15986209a9e47717174f99dc2f7166d/coverage-7.9.1-cp312-cp312-win_amd64.whl", hash = "sha256:30f445f85c353090b83e552dcbbdad3ec84c7967e108c3ae54556ca69955563e", size = 215535, upload-time = "2025-06-13T13:01:27.861Z" }, - { url = "https://files.pythonhosted.org/packages/a3/37/84bba9d2afabc3611f3e4325ee2c6a47cd449b580d4a606b240ce5a6f9bf/coverage-7.9.1-cp312-cp312-win_arm64.whl", hash = "sha256:af41da5dca398d3474129c58cb2b106a5d93bbb196be0d307ac82311ca234342", size = 213904, upload-time = "2025-06-13T13:01:29.202Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e5/c723545c3fd3204ebde3b4cc4b927dce709d3b6dc577754bb57f63ca4a4a/coverage-7.9.1-pp39.pp310.pp311-none-any.whl", hash = "sha256:db0f04118d1db74db6c9e1cb1898532c7dcc220f1d2718f058601f7c3f499514", size = 204009, upload-time = "2025-06-13T13:02:25.787Z" }, - { url = "https://files.pythonhosted.org/packages/08/b8/7ddd1e8ba9701dea08ce22029917140e6f66a859427406579fd8d0ca7274/coverage-7.9.1-py3-none-any.whl", hash = "sha256:66b974b145aa189516b6bf2d8423e888b742517d37872f6ee4c5be0073bd9a3c", size = 204000, upload-time = "2025-06-13T13:02:27.173Z" }, -] - -[package.optional-dependencies] -toml = [ - { name = "tomli", marker = "python_full_version <= '3.11'" }, + { url = "https://files.pythonhosted.org/packages/91/2e/c4390a31919d8a78b90e8ecf87cd4b4c4f05a5b48d05ec17db8e5404c6f4/contourpy-1.3.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:709a48ef9a690e1343202916450bc48b9e51c049b089c7f79a267b46cffcdaa1", size = 288773, upload-time = "2025-07-26T12:01:02.277Z" }, + { url = "https://files.pythonhosted.org/packages/0d/44/c4b0b6095fef4dc9c420e041799591e3b63e9619e3044f7f4f6c21c0ab24/contourpy-1.3.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23416f38bfd74d5d28ab8429cc4d63fa67d5068bd711a85edb1c3fb0c3e2f381", size = 270149, upload-time = "2025-07-26T12:01:04.072Z" }, + { url = "https://files.pythonhosted.org/packages/30/2e/dd4ced42fefac8470661d7cb7e264808425e6c5d56d175291e93890cce09/contourpy-1.3.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:929ddf8c4c7f348e4c0a5a3a714b5c8542ffaa8c22954862a46ca1813b667ee7", size = 329222, upload-time = "2025-07-26T12:01:05.688Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/cc6ec2548e3d276c71389ea4802a774b7aa3558223b7bade3f25787fafc2/contourpy-1.3.3-cp311-cp311-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9e999574eddae35f1312c2b4b717b7885d4edd6cb46700e04f7f02db454e67c1", size = 377234, upload-time = "2025-07-26T12:01:07.054Z" }, + { url = "https://files.pythonhosted.org/packages/03/b3/64ef723029f917410f75c09da54254c5f9ea90ef89b143ccadb09df14c15/contourpy-1.3.3-cp311-cp311-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf67e0e3f482cb69779dd3061b534eb35ac9b17f163d851e2a547d56dba0a3a", size = 380555, upload-time = "2025-07-26T12:01:08.801Z" }, + { url = "https://files.pythonhosted.org/packages/5f/4b/6157f24ca425b89fe2eb7e7be642375711ab671135be21e6faa100f7448c/contourpy-1.3.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:51e79c1f7470158e838808d4a996fa9bac72c498e93d8ebe5119bc1e6becb0db", size = 355238, upload-time = "2025-07-26T12:01:10.319Z" }, + { url = "https://files.pythonhosted.org/packages/98/56/f914f0dd678480708a04cfd2206e7c382533249bc5001eb9f58aa693e200/contourpy-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:598c3aaece21c503615fd59c92a3598b428b2f01bfb4b8ca9c4edeecc2438620", size = 1326218, upload-time = "2025-07-26T12:01:12.659Z" }, + { url = "https://files.pythonhosted.org/packages/fb/d7/4a972334a0c971acd5172389671113ae82aa7527073980c38d5868ff1161/contourpy-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:322ab1c99b008dad206d406bb61d014cf0174df491ae9d9d0fac6a6fda4f977f", size = 1392867, upload-time = "2025-07-26T12:01:15.533Z" }, + { url = "https://files.pythonhosted.org/packages/75/3e/f2cc6cd56dc8cff46b1a56232eabc6feea52720083ea71ab15523daab796/contourpy-1.3.3-cp311-cp311-win32.whl", hash = "sha256:fd907ae12cd483cd83e414b12941c632a969171bf90fc937d0c9f268a31cafff", size = 183677, upload-time = "2025-07-26T12:01:17.088Z" }, + { url = "https://files.pythonhosted.org/packages/98/4b/9bd370b004b5c9d8045c6c33cf65bae018b27aca550a3f657cdc99acdbd8/contourpy-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:3519428f6be58431c56581f1694ba8e50626f2dd550af225f82fb5f5814d2a42", size = 225234, upload-time = "2025-07-26T12:01:18.256Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b6/71771e02c2e004450c12b1120a5f488cad2e4d5b590b1af8bad060360fe4/contourpy-1.3.3-cp311-cp311-win_arm64.whl", hash = "sha256:15ff10bfada4bf92ec8b31c62bf7c1834c244019b4a33095a68000d7075df470", size = 193123, upload-time = "2025-07-26T12:01:19.848Z" }, + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/a5/29/8dcfe16f0107943fa92388c23f6e05cff0ba58058c4c95b00280d4c75a14/contourpy-1.3.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd5dfcaeb10f7b7f9dc8941717c6c2ade08f587be2226222c12b25f0483ed497", size = 278809, upload-time = "2025-07-26T12:02:52.74Z" }, + { url = "https://files.pythonhosted.org/packages/85/a9/8b37ef4f7dafeb335daee3c8254645ef5725be4d9c6aa70b50ec46ef2f7e/contourpy-1.3.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:0c1fc238306b35f246d61a1d416a627348b5cf0648648a031e14bb8705fcdfe8", size = 261593, upload-time = "2025-07-26T12:02:54.037Z" }, + { url = "https://files.pythonhosted.org/packages/0a/59/ebfb8c677c75605cc27f7122c90313fd2f375ff3c8d19a1694bda74aaa63/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70f9aad7de812d6541d29d2bbf8feb22ff7e1c299523db288004e3157ff4674e", size = 302202, upload-time = "2025-07-26T12:02:55.947Z" }, + { url = "https://files.pythonhosted.org/packages/3c/37/21972a15834d90bfbfb009b9d004779bd5a07a0ec0234e5ba8f64d5736f4/contourpy-1.3.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ed3657edf08512fc3fe81b510e35c2012fbd3081d2e26160f27ca28affec989", size = 329207, upload-time = "2025-07-26T12:02:57.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/58/bd257695f39d05594ca4ad60df5bcb7e32247f9951fd09a9b8edb82d1daa/contourpy-1.3.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:3d1a3799d62d45c18bafd41c5fa05120b96a28079f2393af559b843d1a966a77", size = 225315, upload-time = "2025-07-26T12:02:58.801Z" }, ] [[package]] @@ -451,31 +415,31 @@ wheels = [ [[package]] name = "cython" -version = "3.1.2" +version = "3.1.3" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/40/7b17cd866158238db704965da1b5849af261dbad393ea3ac966f934b2d39/cython-3.1.2.tar.gz", hash = "sha256:6bbf7a953fa6762dfecdec015e3b054ba51c0121a45ad851fa130f63f5331381", size = 3184825, upload-time = "2025-06-09T07:08:48.465Z" } +sdist = { url = "https://files.pythonhosted.org/packages/18/ab/915337fb39ab4f4539a313df38fc69938df3bf14141b90d61dfd5c2919de/cython-3.1.3.tar.gz", hash = "sha256:10ee785e42328924b78f75a74f66a813cb956b4a9bc91c44816d089d5934c089", size = 3186689, upload-time = "2025-08-13T06:19:13.619Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1f/de/502ddebaf5fe78f13cd6361acdd74710d3a5b15c22a9edc0ea4c873a59a5/cython-3.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5548573e0912d7dc80579827493315384c462e2f15797b91a8ed177686d31eb9", size = 3007792, upload-time = "2025-06-09T07:09:28.777Z" }, - { url = "https://files.pythonhosted.org/packages/bb/c8/91b00bc68effba9ba1ff5b33988052ac4d98fc1ac3021ade7261661299c6/cython-3.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bf3ea5bc50d80762c490f42846820a868a6406fdb5878ae9e4cc2f11b50228a", size = 2870798, upload-time = "2025-06-09T07:09:30.745Z" }, - { url = "https://files.pythonhosted.org/packages/f4/4b/29d290f14607785112c00a5e1685d766f433531bbd6a11ad229ab61b7a70/cython-3.1.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20ce53951d06ab2bca39f153d9c5add1d631c2a44d58bf67288c9d631be9724e", size = 3131280, upload-time = "2025-06-09T07:09:32.785Z" }, - { url = "https://files.pythonhosted.org/packages/38/3c/7c61e9ce25377ec7c4aa0b7ceeed34559ebca7b5cfd384672ba64eeaa4ba/cython-3.1.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e05a36224e3002d48c7c1c695b3771343bd16bc57eab60d6c5d5e08f3cbbafd8", size = 3223898, upload-time = "2025-06-09T07:09:35.345Z" }, - { url = "https://files.pythonhosted.org/packages/10/96/2d3fbe7e50e98b53ac86fefb48b64262b2e1304b3495e8e25b3cd1c3473e/cython-3.1.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:dbc0fc0777c7ab82297c01c61a1161093a22a41714f62e8c35188a309bd5db8e", size = 3291527, upload-time = "2025-06-09T07:09:37.502Z" }, - { url = "https://files.pythonhosted.org/packages/bd/e4/4cd3624e250d86f05bdb121a567865b9cca75cdc6dce4eedd68e626ea4f8/cython-3.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:18161ef3dd0e90a944daa2be468dd27696712a5f792d6289e97d2a31298ad688", size = 3184034, upload-time = "2025-06-09T07:09:40.225Z" }, - { url = "https://files.pythonhosted.org/packages/24/de/f8c1243c3e50ec95cb81f3a7936c8cf162f28050db8683e291c3861b46a0/cython-3.1.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:ca45020950cd52d82189d6dfb6225737586be6fe7b0b9d3fadd7daca62eff531", size = 3386084, upload-time = "2025-06-09T07:09:42.206Z" }, - { url = "https://files.pythonhosted.org/packages/c8/95/2365937da44741ef0781bb9ecc1f8f52b38b65acb7293b5fc7c3eaee5346/cython-3.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:aaae97d6d07610224be2b73a93e9e3dd85c09aedfd8e47054e3ef5a863387dae", size = 3309974, upload-time = "2025-06-09T07:09:44.801Z" }, - { url = "https://files.pythonhosted.org/packages/9b/b8/280eed114110a1a3aa9e2e76bcd06cdd5ef0df7ab77c0be9d5378ca28c57/cython-3.1.2-cp311-cp311-win32.whl", hash = "sha256:3d439d9b19e7e70f6ff745602906d282a853dd5219d8e7abbf355de680c9d120", size = 2482942, upload-time = "2025-06-09T07:09:46.583Z" }, - { url = "https://files.pythonhosted.org/packages/a2/50/0aa65be5a4ab65bde3224b8fd23ed795f699d1e724ac109bb0a32036b82d/cython-3.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:8efa44ee2f1876e40eb5e45f6513a19758077c56bf140623ccab43d31f873b61", size = 2686535, upload-time = "2025-06-09T07:09:48.345Z" }, - { url = "https://files.pythonhosted.org/packages/22/86/9393ab7204d5bb65f415dd271b658c18f57b9345d06002cae069376a5a7a/cython-3.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9c2c4b6f9a941c857b40168b3f3c81d514e509d985c2dcd12e1a4fea9734192e", size = 3015898, upload-time = "2025-06-09T07:09:50.79Z" }, - { url = "https://files.pythonhosted.org/packages/f9/b8/3d10ac37ab7b7ee60bc6bfb48f6682ebee7fddaccf56e1e135f0d46ca79f/cython-3.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:bdbc115bbe1b8c1dcbcd1b03748ea87fa967eb8dfc3a1a9bb243d4a382efcff4", size = 2846204, upload-time = "2025-06-09T07:09:52.832Z" }, - { url = "https://files.pythonhosted.org/packages/f8/34/637771d8e10ebabc34a34cdd0d63fe797b66c334e150189955bf6442d710/cython-3.1.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05111f89db1ca98edc0675cfaa62be47b3ff519a29876eb095532a9f9e052b8", size = 3080671, upload-time = "2025-06-09T07:09:54.924Z" }, - { url = "https://files.pythonhosted.org/packages/6b/c8/383ad1851fb272920a152c5a30bb6f08c3471b5438079d9488fc3074a170/cython-3.1.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6e7188df8709be32cfdfadc7c3782e361c929df9132f95e1bbc90a340dca3c7", size = 3199022, upload-time = "2025-06-09T07:09:56.978Z" }, - { url = "https://files.pythonhosted.org/packages/e6/11/20adc8f2db37a29f245e8fd4b8b8a8245fce4bbbd128185cc9a7b1065e4c/cython-3.1.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1c0ecc71e60a051732c2607b8eb8f2a03a5dac09b28e52b8af323c329db9987b", size = 3241337, upload-time = "2025-06-09T07:09:59.156Z" }, - { url = "https://files.pythonhosted.org/packages/6f/0b/491f1fd3e177cccb6bb6d52f9609f78d395edde83ac47ebb06d21717ca29/cython-3.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f27143cf88835c8bcc9bf3304953f23f377d1d991e8942982fe7be344c7cfce3", size = 3131808, upload-time = "2025-06-09T07:10:01.31Z" }, - { url = "https://files.pythonhosted.org/packages/db/d2/5e7053a3214c9baa7ad72940555eb87cf4750e597f10b2bb43db62c3f39f/cython-3.1.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d8c43566701133f53bf13485839d8f3f309095fe0d3b9d0cd5873073394d2edc", size = 3340319, upload-time = "2025-06-09T07:10:03.485Z" }, - { url = "https://files.pythonhosted.org/packages/95/42/4842f8ddac9b36c94ae08b23c7fcde3f930c1dd49ac8992bb5320a4d96b5/cython-3.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a3bb893e85f027a929c1764bb14db4c31cbdf8a96f59a78f608f2ba7cfbbce95", size = 3287370, upload-time = "2025-06-09T07:10:05.637Z" }, - { url = "https://files.pythonhosted.org/packages/03/0d/417745ed75d414176e50310087b43299a3e611e75c379ff998f60f2ca1a8/cython-3.1.2-cp312-cp312-win32.whl", hash = "sha256:12c5902f105e43ca9af7874cdf87a23627f98c15d5a4f6d38bc9d334845145c0", size = 2487734, upload-time = "2025-06-09T07:10:07.591Z" }, - { url = "https://files.pythonhosted.org/packages/8e/82/df61d09ab81979ba171a8252af8fb8a3b26a0f19d1330c2679c11fe41667/cython-3.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:06789eb7bd2e55b38b9dd349e9309f794aee0fed99c26ea5c9562d463877763f", size = 2695542, upload-time = "2025-06-09T07:10:09.545Z" }, - { url = "https://files.pythonhosted.org/packages/25/d6/ef8557d5e75cc57d55df579af4976935ee111a85bbee4a5b72354e257066/cython-3.1.2-py3-none-any.whl", hash = "sha256:d23fd7ffd7457205f08571a42b108a3cf993e83a59fe4d72b42e6fc592cf2639", size = 1224753, upload-time = "2025-06-09T07:08:44.849Z" }, + { url = "https://files.pythonhosted.org/packages/b5/51/54f5d1bed7b7d003d0584996a030542497aeb05b9241782c217ea71061f5/cython-3.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4b3b2f6587b42efdece2d174a2aa4234da4524cc6673f3955c2e62b60c6d11fd", size = 3002973, upload-time = "2025-08-13T06:19:50.777Z" }, + { url = "https://files.pythonhosted.org/packages/05/07/b4043fed60070ee21b0d9ff3a877d2ecdc79231e55119ce852b79b690306/cython-3.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:963cf640d049fcca1cefd62d1653f859892d6dc8e4d958eb49a5babc491de6a1", size = 2865389, upload-time = "2025-08-13T06:19:52.675Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e3/67d349b5310a40f281e153e826ca24d9f7ba2997c06800eda781253dccfd/cython-3.1.3-cp311-cp311-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2ab05d1bf2d5522ecff35d94ca233b77db2300413597c3ca0b6448377fa4bd7c", size = 3410316, upload-time = "2025-08-13T06:19:55.217Z" }, + { url = "https://files.pythonhosted.org/packages/49/c4/84aae921135174111091547fa726ea5f8bba718cd12b2589a70c2aab8d5c/cython-3.1.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cd517e3be052fb49443585b01f02f46080b3408e32c1108a0fdc4cc25b3c9d30", size = 3189568, upload-time = "2025-08-13T06:19:57.503Z" }, + { url = "https://files.pythonhosted.org/packages/e2/85/1bf18883f1a1f656ad83a671e54553caedb1ee2f39a3e792a14aa82557a2/cython-3.1.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a48e2180d74e3c528561d85b48f9a939a429537f9ea8aac7fb16180e7bff47e2", size = 3312649, upload-time = "2025-08-13T06:19:59.894Z" }, + { url = "https://files.pythonhosted.org/packages/68/da/cc1373decc0d14a25f1b434f47de5e27696f3092319aa5e19fcf84157415/cython-3.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7c9daa90b15f59aa2a0d638ac1b36777a7e80122099952a0295c71190ce14bc", size = 3203821, upload-time = "2025-08-13T06:20:02.124Z" }, + { url = "https://files.pythonhosted.org/packages/0a/32/e10582d6f7b02ee63607f388dbbd34e329c54559c85961ddc5c655266519/cython-3.1.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:08ac646ff42781827f23b7a9b61669cdb92055f52724cd8cbe0e1defc56fce2e", size = 3426853, upload-time = "2025-08-13T06:20:04.474Z" }, + { url = "https://files.pythonhosted.org/packages/e0/da/5b0d1b5a4c7622ec812ad9374c9c6b426d69818f13f51e36f4f25ca01c86/cython-3.1.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bb0e0e7fceaffa22e4dc9600f7f75998eef5cc6ac5a8c0733b482851ba765ef2", size = 3328872, upload-time = "2025-08-13T06:20:06.834Z" }, + { url = "https://files.pythonhosted.org/packages/b0/5f/f102f6c8d27338f0baf094754c67f920828a19612053abc903e66f84506f/cython-3.1.3-cp311-cp311-win32.whl", hash = "sha256:42b1c3ebe36a52e2a8e939c0651e9ca5d30b81d03f800bbf0499deb0503ab565", size = 2480850, upload-time = "2025-08-13T06:20:08.988Z" }, + { url = "https://files.pythonhosted.org/packages/b7/60/415d0f0f7c2e227707baec11c968387d33507d2eb7f26a2950a323c705e5/cython-3.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:34a973844998281951bf54cdd0b6a9946ba03ba94580820738583a00da167d8f", size = 2712560, upload-time = "2025-08-13T06:20:11.877Z" }, + { url = "https://files.pythonhosted.org/packages/79/26/f433fdfd5182b9231819a99acc49a1f856669532306e7a38dce63ba7297e/cython-3.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:849ef3d15d4354e5f74cdb6d3c80d80b03209b3bf1f4ff93315890b19da18944", size = 3014237, upload-time = "2025-08-13T06:20:13.77Z" }, + { url = "https://files.pythonhosted.org/packages/e5/6c/1bebf44f5f177f8c750e608f82c08cd699b8f28cc233e799379bfcf2a2c2/cython-3.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:93dd0f62a3f8e93166d8584f8b243180d681ba8fed1f530b55d5f70c348c5797", size = 2844261, upload-time = "2025-08-13T06:20:15.619Z" }, + { url = "https://files.pythonhosted.org/packages/c7/74/983005ce5954f6dc8360b105a561e51a60ea619c53296afac98253d1c9d7/cython-3.1.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ff4a2cb84798faffb3988bd94636c3ad31a95ff44ef017f09121abffc56f84cf", size = 3388846, upload-time = "2025-08-13T06:20:17.679Z" }, + { url = "https://files.pythonhosted.org/packages/68/50/dbe7edefe9b652bc72d49da07785173e89197b9a2d64a2ac45da9795eccf/cython-3.1.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4b05319e36f34d5388deea5cc2bcfd65f9ebf76f4ea050829421a69625dbba4a", size = 3167022, upload-time = "2025-08-13T06:20:19.863Z" }, + { url = "https://files.pythonhosted.org/packages/4a/55/b50548b77203e22262002feae23a172f95282b4b8deb5ad104f08e3dc20d/cython-3.1.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7ac902a17934a6da46f80755f49413bc4c03a569ae3c834f5d66da7288ba7e6c", size = 3317782, upload-time = "2025-08-13T06:20:21.962Z" }, + { url = "https://files.pythonhosted.org/packages/f6/1b/20a97507d528dc330d67be4fbad6bc767c681d56192bce8f7117a187b2ad/cython-3.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7d7a555a864b1b08576f9e8a67f3789796a065837544f9f683ebf3188012fdbd", size = 3179818, upload-time = "2025-08-13T06:20:24.419Z" }, + { url = "https://files.pythonhosted.org/packages/43/a5/7b32a19c4c6bb0e2cc7ae52269b6b23a42f67f730e4abd4f8dca63660f7a/cython-3.1.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:b827ce7d97ef8624adcf2bdda594b3dcb6c9b4f124d8f72001d8aea27d69dc1c", size = 3403206, upload-time = "2025-08-13T06:20:26.846Z" }, + { url = "https://files.pythonhosted.org/packages/ba/e1/08cfd4c5e99f79e62d4a7b0009bbd906748633270935c8a55eee51fead76/cython-3.1.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7851107204085f4f02d0eb6b660ddcad2ce4846e8b7a1eaba724a0bd3cd68a6b", size = 3327837, upload-time = "2025-08-13T06:20:28.946Z" }, + { url = "https://files.pythonhosted.org/packages/23/1b/f3d384be86fa2a6e110b42f42bf97295a513197aaa5532f451c73bf5b48e/cython-3.1.3-cp312-cp312-win32.whl", hash = "sha256:ed20f1b45b2da5a4f8e71a80025bca1cdc96ba35820b0b17658a4a025be920b0", size = 2485990, upload-time = "2025-08-13T06:20:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/de/f0/17cff75e3c141bda73d770f7412632f53e55778d3bfdadfeec4dd3a7d1d6/cython-3.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc4ca0f4dec55124cd79ddcfc555be1cbe0092cc99bcf1403621d17b9c6218bb", size = 2704002, upload-time = "2025-08-13T06:20:33.773Z" }, + { 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]] @@ -511,7 +475,7 @@ version = "0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "python-xlib", marker = "sys_platform == 'linux'" }, - { name = "typing-extensions" }, + { name = "typing-extensions", marker = "sys_platform != 'darwin'" }, ] wheels = [ { url = "https://files.pythonhosted.org/packages/2f/3a/46ca34abf0725a754bc44ef474ad34aedcc3ea23b052d97b18b76715a6a9/EWMHlib-0.2-py3-none-any.whl", hash = "sha256:f5b07d8cfd4c7734462ee744c32d490f2f3233fa7ab354240069344208d2f6f5", size = 46657, upload-time = "2024-04-17T08:15:56.338Z" }, @@ -537,36 +501,36 @@ wheels = [ [[package]] name = "filelock" -version = "3.18.0" +version = "3.19.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +sdist = { url = "https://files.pythonhosted.org/packages/40/bb/0ab3e58d22305b6f5440629d20683af28959bf793d98d11950e305c1c326/filelock-3.19.1.tar.gz", hash = "sha256:66eda1888b0171c998b35be2bcc0f6d75c388a7ce20c3f3f37aa8e96c2dddf58", size = 17687, upload-time = "2025-08-14T16:56:03.016Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, + { url = "https://files.pythonhosted.org/packages/42/14/42b2651a2f46b022ccd948bca9f2d5af0fd8929c4eec235b8d6d844fbe67/filelock-3.19.1-py3-none-any.whl", hash = "sha256:d38e30481def20772f5baf097c122c3babc4fcdb7e14e57049eb9d88c6dc017d", size = 15988, upload-time = "2025-08-14T16:56:01.633Z" }, ] [[package]] name = "fonttools" -version = "4.58.4" +version = "4.59.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2e/5a/1124b2c8cb3a8015faf552e92714040bcdbc145dfa29928891b02d147a18/fonttools-4.58.4.tar.gz", hash = "sha256:928a8009b9884ed3aae17724b960987575155ca23c6f0b8146e400cc9e0d44ba", size = 3525026, upload-time = "2025-06-13T17:25:15.426Z" } +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/17/7b/cc6e9bb41bab223bd2dc70ba0b21386b85f604e27f4c3206b4205085a2ab/fonttools-4.58.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a3841991c9ee2dc0562eb7f23d333d34ce81e8e27c903846f0487da21e0028eb", size = 2768901, upload-time = "2025-06-13T17:24:05.901Z" }, - { url = "https://files.pythonhosted.org/packages/3d/15/98d75df9f2b4e7605f3260359ad6e18e027c11fa549f74fce567270ac891/fonttools-4.58.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3c98f91b6a9604e7ffb5ece6ea346fa617f967c2c0944228801246ed56084664", size = 2328696, upload-time = "2025-06-13T17:24:09.18Z" }, - { url = "https://files.pythonhosted.org/packages/a8/c8/dc92b80f5452c9c40164e01b3f78f04b835a00e673bd9355ca257008ff61/fonttools-4.58.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ab9f891eb687ddf6a4e5f82901e00f992e18012ca97ab7acd15f13632acd14c1", size = 5018830, upload-time = "2025-06-13T17:24:11.282Z" }, - { url = "https://files.pythonhosted.org/packages/19/48/8322cf177680505d6b0b6062e204f01860cb573466a88077a9b795cb70e8/fonttools-4.58.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:891c5771e8f0094b7c0dc90eda8fc75e72930b32581418f2c285a9feedfd9a68", size = 4960922, upload-time = "2025-06-13T17:24:14.9Z" }, - { url = "https://files.pythonhosted.org/packages/14/e0/2aff149ed7eb0916de36da513d473c6fff574a7146891ce42de914899395/fonttools-4.58.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:43ba4d9646045c375d22e3473b7d82b18b31ee2ac715cd94220ffab7bc2d5c1d", size = 4997135, upload-time = "2025-06-13T17:24:16.959Z" }, - { url = "https://files.pythonhosted.org/packages/e6/6f/4d9829b29a64a2e63a121cb11ecb1b6a9524086eef3e35470949837a1692/fonttools-4.58.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33d19f16e6d2ffd6669bda574a6589941f6c99a8d5cfb9f464038244c71555de", size = 5108701, upload-time = "2025-06-13T17:24:18.849Z" }, - { url = "https://files.pythonhosted.org/packages/6f/1e/2d656ddd1b0cd0d222f44b2d008052c2689e66b702b9af1cd8903ddce319/fonttools-4.58.4-cp311-cp311-win32.whl", hash = "sha256:b59e5109b907da19dc9df1287454821a34a75f2632a491dd406e46ff432c2a24", size = 2200177, upload-time = "2025-06-13T17:24:20.823Z" }, - { url = "https://files.pythonhosted.org/packages/fb/83/ba71ad053fddf4157cb0697c8da8eff6718d059f2a22986fa5f312b49c92/fonttools-4.58.4-cp311-cp311-win_amd64.whl", hash = "sha256:3d471a5b567a0d1648f2e148c9a8bcf00d9ac76eb89e976d9976582044cc2509", size = 2247892, upload-time = "2025-06-13T17:24:22.927Z" }, - { url = "https://files.pythonhosted.org/packages/04/3c/1d1792bfe91ef46f22a3d23b4deb514c325e73c17d4f196b385b5e2faf1c/fonttools-4.58.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:462211c0f37a278494e74267a994f6be9a2023d0557aaa9ecbcbfce0f403b5a6", size = 2754082, upload-time = "2025-06-13T17:24:24.862Z" }, - { url = "https://files.pythonhosted.org/packages/2a/1f/2b261689c901a1c3bc57a6690b0b9fc21a9a93a8b0c83aae911d3149f34e/fonttools-4.58.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0c7a12fb6f769165547f00fcaa8d0df9517603ae7e04b625e5acb8639809b82d", size = 2321677, upload-time = "2025-06-13T17:24:26.815Z" }, - { url = "https://files.pythonhosted.org/packages/fe/6b/4607add1755a1e6581ae1fc0c9a640648e0d9cdd6591cc2d581c2e07b8c3/fonttools-4.58.4-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:2d42c63020a922154add0a326388a60a55504629edc3274bc273cd3806b4659f", size = 4896354, upload-time = "2025-06-13T17:24:28.428Z" }, - { url = "https://files.pythonhosted.org/packages/cd/95/34b4f483643d0cb11a1f830b72c03fdd18dbd3792d77a2eb2e130a96fada/fonttools-4.58.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8f2b4e6fd45edc6805f5f2c355590b092ffc7e10a945bd6a569fc66c1d2ae7aa", size = 4941633, upload-time = "2025-06-13T17:24:30.568Z" }, - { url = "https://files.pythonhosted.org/packages/81/ac/9bafbdb7694059c960de523e643fa5a61dd2f698f3f72c0ca18ae99257c7/fonttools-4.58.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f155b927f6efb1213a79334e4cb9904d1e18973376ffc17a0d7cd43d31981f1e", size = 4886170, upload-time = "2025-06-13T17:24:32.724Z" }, - { url = "https://files.pythonhosted.org/packages/ae/44/a3a3b70d5709405f7525bb7cb497b4e46151e0c02e3c8a0e40e5e9fe030b/fonttools-4.58.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e38f687d5de97c7fb7da3e58169fb5ba349e464e141f83c3c2e2beb91d317816", size = 5037851, upload-time = "2025-06-13T17:24:35.034Z" }, - { url = "https://files.pythonhosted.org/packages/21/cb/e8923d197c78969454eb876a4a55a07b59c9c4c46598f02b02411dc3b45c/fonttools-4.58.4-cp312-cp312-win32.whl", hash = "sha256:636c073b4da9db053aa683db99580cac0f7c213a953b678f69acbca3443c12cc", size = 2187428, upload-time = "2025-06-13T17:24:36.996Z" }, - { url = "https://files.pythonhosted.org/packages/46/e6/fe50183b1a0e1018e7487ee740fa8bb127b9f5075a41e20d017201e8ab14/fonttools-4.58.4-cp312-cp312-win_amd64.whl", hash = "sha256:82e8470535743409b30913ba2822e20077acf9ea70acec40b10fcf5671dceb58", size = 2236649, upload-time = "2025-06-13T17:24:38.985Z" }, - { url = "https://files.pythonhosted.org/packages/0b/2f/c536b5b9bb3c071e91d536a4d11f969e911dbb6b227939f4c5b0bca090df/fonttools-4.58.4-py3-none-any.whl", hash = "sha256:a10ce13a13f26cbb9f37512a4346bb437ad7e002ff6fa966a7ce7ff5ac3528bd", size = 1114660, upload-time = "2025-06-13T17:25:13.321Z" }, + { 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]] @@ -655,7 +619,7 @@ wheels = [ [[package]] name = "gymnasium" -version = "1.1.1" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cloudpickle" }, @@ -663,9 +627,9 @@ dependencies = [ { name = "numpy" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/90/69/70cd29e9fc4953d013b15981ee71d4c9ef4d8b2183e6ef2fe89756746dce/gymnasium-1.1.1.tar.gz", hash = "sha256:8bd9ea9bdef32c950a444ff36afc785e1d81051ec32d30435058953c20d2456d", size = 829326, upload-time = "2025-03-06T16:30:36.428Z" } +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 = [ - { url = "https://files.pythonhosted.org/packages/f9/68/2bdc7b46b5f543dd865575f9d19716866bdb76e50dd33b71ed1a3dd8bb42/gymnasium-1.1.1-py3-none-any.whl", hash = "sha256:9c167ec0a2b388666e37f63b2849cd2552f7f5b71938574c637bb36487eb928a", size = 965410, upload-time = "2025-03-06T16:30:34.443Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e2/a111dbb8625af467ea4760a1373d6ef27aac3137931219902406ccc05423/gymnasium-1.2.0-py3-none-any.whl", hash = "sha256:fc4a1e4121a9464c29b4d7dc6ade3fbeaa36dea448682f5f71a6d2c17489ea76", size = 944301, upload-time = "2025-06-27T08:21:18.83Z" }, ] [[package]] @@ -749,40 +713,41 @@ wheels = [ [[package]] name = "kiwisolver" -version = "1.4.8" +version = "1.4.9" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +sdist = { url = "https://files.pythonhosted.org/packages/5c/3c/85844f1b0feb11ee581ac23fe5fce65cd049a200c1446708cc1b7f922875/kiwisolver-1.4.9.tar.gz", hash = "sha256:c3b22c26c6fd6811b0ae8363b95ca8ce4ea3c202d3d0975b2914310ceb1bcc4d", size = 97564, upload-time = "2025-08-10T21:27:49.279Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, - { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, - { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, - { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, - { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, - { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, - { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, - { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, - { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, - { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, - { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, - { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, - { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, - { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, - { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, - { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, - { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, - { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, - { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, - { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, - { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, - { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, - { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, - { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, - { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, - { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, - { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, - { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, - { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, - { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/6f/ab/c80b0d5a9d8a1a65f4f815f2afff9798b12c3b9f31f1d304dd233dd920e2/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:eb14a5da6dc7642b0f3a18f13654847cd8b7a2550e2645a5bda677862b03ba16", size = 124167, upload-time = "2025-08-10T21:25:53.403Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c0/27fe1a68a39cf62472a300e2879ffc13c0538546c359b86f149cc19f6ac3/kiwisolver-1.4.9-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:39a219e1c81ae3b103643d2aedb90f1ef22650deb266ff12a19e7773f3e5f089", size = 66579, upload-time = "2025-08-10T21:25:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/31/a2/a12a503ac1fd4943c50f9822678e8015a790a13b5490354c68afb8489814/kiwisolver-1.4.9-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2405a7d98604b87f3fc28b1716783534b1b4b8510d8142adca34ee0bc3c87543", size = 65309, upload-time = "2025-08-10T21:25:55.76Z" }, + { url = "https://files.pythonhosted.org/packages/66/e1/e533435c0be77c3f64040d68d7a657771194a63c279f55573188161e81ca/kiwisolver-1.4.9-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:dc1ae486f9abcef254b5618dfb4113dd49f94c68e3e027d03cf0143f3f772b61", size = 1435596, upload-time = "2025-08-10T21:25:56.861Z" }, + { url = "https://files.pythonhosted.org/packages/67/1e/51b73c7347f9aabdc7215aa79e8b15299097dc2f8e67dee2b095faca9cb0/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a1f570ce4d62d718dce3f179ee78dac3b545ac16c0c04bb363b7607a949c0d1", size = 1246548, upload-time = "2025-08-10T21:25:58.246Z" }, + { url = "https://files.pythonhosted.org/packages/21/aa/72a1c5d1e430294f2d32adb9542719cfb441b5da368d09d268c7757af46c/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb27e7b78d716c591e88e0a09a2139c6577865d7f2e152488c2cc6257f460872", size = 1263618, upload-time = "2025-08-10T21:25:59.857Z" }, + { url = "https://files.pythonhosted.org/packages/a3/af/db1509a9e79dbf4c260ce0cfa3903ea8945f6240e9e59d1e4deb731b1a40/kiwisolver-1.4.9-cp311-cp311-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:15163165efc2f627eb9687ea5f3a28137217d217ac4024893d753f46bce9de26", size = 1317437, upload-time = "2025-08-10T21:26:01.105Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f2/3ea5ee5d52abacdd12013a94130436e19969fa183faa1e7c7fbc89e9a42f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bdee92c56a71d2b24c33a7d4c2856bd6419d017e08caa7802d2963870e315028", size = 2195742, upload-time = "2025-08-10T21:26:02.675Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9b/1efdd3013c2d9a2566aa6a337e9923a00590c516add9a1e89a768a3eb2fc/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:412f287c55a6f54b0650bd9b6dce5aceddb95864a1a90c87af16979d37c89771", size = 2290810, upload-time = "2025-08-10T21:26:04.009Z" }, + { url = "https://files.pythonhosted.org/packages/fb/e5/cfdc36109ae4e67361f9bc5b41323648cb24a01b9ade18784657e022e65f/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:2c93f00dcba2eea70af2be5f11a830a742fe6b579a1d4e00f47760ef13be247a", size = 2461579, upload-time = "2025-08-10T21:26:05.317Z" }, + { url = "https://files.pythonhosted.org/packages/62/86/b589e5e86c7610842213994cdea5add00960076bef4ae290c5fa68589cac/kiwisolver-1.4.9-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f117e1a089d9411663a3207ba874f31be9ac8eaa5b533787024dc07aeb74f464", size = 2268071, upload-time = "2025-08-10T21:26:06.686Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c6/f8df8509fd1eee6c622febe54384a96cfaf4d43bf2ccec7a0cc17e4715c9/kiwisolver-1.4.9-cp311-cp311-win_amd64.whl", hash = "sha256:be6a04e6c79819c9a8c2373317d19a96048e5a3f90bec587787e86a1153883c2", size = 73840, upload-time = "2025-08-10T21:26:07.94Z" }, + { url = "https://files.pythonhosted.org/packages/e2/2d/16e0581daafd147bc11ac53f032a2b45eabac897f42a338d0a13c1e5c436/kiwisolver-1.4.9-cp311-cp311-win_arm64.whl", hash = "sha256:0ae37737256ba2de764ddc12aed4956460277f00c4996d51a197e72f62f5eec7", size = 65159, upload-time = "2025-08-10T21:26:09.048Z" }, + { url = "https://files.pythonhosted.org/packages/86/c9/13573a747838aeb1c76e3267620daa054f4152444d1f3d1a2324b78255b5/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ac5a486ac389dddcc5bef4f365b6ae3ffff2c433324fb38dd35e3fab7c957999", size = 123686, upload-time = "2025-08-10T21:26:10.034Z" }, + { url = "https://files.pythonhosted.org/packages/51/ea/2ecf727927f103ffd1739271ca19c424d0e65ea473fbaeea1c014aea93f6/kiwisolver-1.4.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f2ba92255faa7309d06fe44c3a4a97efe1c8d640c2a79a5ef728b685762a6fd2", size = 66460, upload-time = "2025-08-10T21:26:11.083Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/51f5464373ce2aeb5194508298a508b6f21d3867f499556263c64c621914/kiwisolver-1.4.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4a2899935e724dd1074cb568ce7ac0dce28b2cd6ab539c8e001a8578eb106d14", size = 64952, upload-time = "2025-08-10T21:26:12.058Z" }, + { url = "https://files.pythonhosted.org/packages/70/90/6d240beb0f24b74371762873e9b7f499f1e02166a2d9c5801f4dbf8fa12e/kiwisolver-1.4.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f6008a4919fdbc0b0097089f67a1eb55d950ed7e90ce2cc3e640abadd2757a04", size = 1474756, upload-time = "2025-08-10T21:26:13.096Z" }, + { url = "https://files.pythonhosted.org/packages/12/42/f36816eaf465220f683fb711efdd1bbf7a7005a2473d0e4ed421389bd26c/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:67bb8b474b4181770f926f7b7d2f8c0248cbcb78b660fdd41a47054b28d2a752", size = 1276404, upload-time = "2025-08-10T21:26:14.457Z" }, + { url = "https://files.pythonhosted.org/packages/2e/64/bc2de94800adc830c476dce44e9b40fd0809cddeef1fde9fcf0f73da301f/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2327a4a30d3ee07d2fbe2e7933e8a37c591663b96ce42a00bc67461a87d7df77", size = 1294410, upload-time = "2025-08-10T21:26:15.73Z" }, + { url = "https://files.pythonhosted.org/packages/5f/42/2dc82330a70aa8e55b6d395b11018045e58d0bb00834502bf11509f79091/kiwisolver-1.4.9-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a08b491ec91b1d5053ac177afe5290adacf1f0f6307d771ccac5de30592d198", size = 1343631, upload-time = "2025-08-10T21:26:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/22/fd/f4c67a6ed1aab149ec5a8a401c323cee7a1cbe364381bb6c9c0d564e0e20/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d8fc5c867c22b828001b6a38d2eaeb88160bf5783c6cb4a5e440efc981ce286d", size = 2224963, upload-time = "2025-08-10T21:26:18.737Z" }, + { url = "https://files.pythonhosted.org/packages/45/aa/76720bd4cb3713314677d9ec94dcc21ced3f1baf4830adde5bb9b2430a5f/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:3b3115b2581ea35bb6d1f24a4c90af37e5d9b49dcff267eeed14c3893c5b86ab", size = 2321295, upload-time = "2025-08-10T21:26:20.11Z" }, + { url = "https://files.pythonhosted.org/packages/80/19/d3ec0d9ab711242f56ae0dc2fc5d70e298bb4a1f9dfab44c027668c673a1/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858e4c22fb075920b96a291928cb7dea5644e94c0ee4fcd5af7e865655e4ccf2", size = 2487987, upload-time = "2025-08-10T21:26:21.49Z" }, + { url = "https://files.pythonhosted.org/packages/39/e9/61e4813b2c97e86b6fdbd4dd824bf72d28bcd8d4849b8084a357bc0dd64d/kiwisolver-1.4.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ed0fecd28cc62c54b262e3736f8bb2512d8dcfdc2bcf08be5f47f96bf405b145", size = 2291817, upload-time = "2025-08-10T21:26:22.812Z" }, + { url = "https://files.pythonhosted.org/packages/a0/41/85d82b0291db7504da3c2defe35c9a8a5c9803a730f297bd823d11d5fb77/kiwisolver-1.4.9-cp312-cp312-win_amd64.whl", hash = "sha256:f68208a520c3d86ea51acf688a3e3002615a7f0238002cccc17affecc86a8a54", size = 73895, upload-time = "2025-08-10T21:26:24.37Z" }, + { url = "https://files.pythonhosted.org/packages/e2/92/5f3068cf15ee5cb624a0c7596e67e2a0bb2adee33f71c379054a491d07da/kiwisolver-1.4.9-cp312-cp312-win_arm64.whl", hash = "sha256:2c1a4f57df73965f3f14df20b80ee29e6a7930a57d2d9e8491a25f676e197c60", size = 64992, upload-time = "2025-08-10T21:26:25.732Z" }, + { url = "https://files.pythonhosted.org/packages/a3/0f/36d89194b5a32c054ce93e586d4049b6c2c22887b0eb229c61c68afd3078/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:720e05574713db64c356e86732c0f3c5252818d05f9df320f0ad8380641acea5", size = 60104, upload-time = "2025-08-10T21:27:43.287Z" }, + { url = "https://files.pythonhosted.org/packages/52/ba/4ed75f59e4658fd21fe7dde1fee0ac397c678ec3befba3fe6482d987af87/kiwisolver-1.4.9-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:17680d737d5335b552994a2008fab4c851bcd7de33094a82067ef3a576ff02fa", size = 58592, upload-time = "2025-08-10T21:27:44.314Z" }, + { url = "https://files.pythonhosted.org/packages/33/01/a8ea7c5ea32a9b45ceeaee051a04c8ed4320f5add3c51bfa20879b765b70/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:85b5352f94e490c028926ea567fc569c52ec79ce131dadb968d3853e809518c2", size = 80281, upload-time = "2025-08-10T21:27:45.369Z" }, + { url = "https://files.pythonhosted.org/packages/da/e3/dbd2ecdce306f1d07a1aaf324817ee993aab7aee9db47ceac757deabafbe/kiwisolver-1.4.9-pp311-pypy311_pp73-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:464415881e4801295659462c49461a24fb107c140de781d55518c4b80cb6790f", size = 78009, upload-time = "2025-08-10T21:27:46.376Z" }, + { url = "https://files.pythonhosted.org/packages/da/e9/0d4add7873a73e462aeb45c036a2dead2562b825aa46ba326727b3f31016/kiwisolver-1.4.9-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:fb940820c63a9590d31d88b815e7a3aa5915cad3ce735ab45f0c730b39547de1", size = 73929, upload-time = "2025-08-10T21:27:48.236Z" }, ] [[package]] @@ -796,64 +761,50 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/94/6d/344a164d32d65d503ffe9201cd74cf13a020099dc446554d1e50b07f167b/libusb1-3.3.1-py3-none-win_amd64.whl", hash = "sha256:6e21b772d80d6487fbb55d3d2141218536db302da82f1983754e96c72781c102", size = 141080, upload-time = "2025-03-24T05:36:46.594Z" }, ] -[[package]] -name = "llvmlite" -version = "0.44.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/89/6a/95a3d3610d5c75293d5dbbb2a76480d5d4eeba641557b69fe90af6c5b84e/llvmlite-0.44.0.tar.gz", hash = "sha256:07667d66a5d150abed9157ab6c0b9393c9356f229784a4385c02f99e94fc94d4", size = 171880, upload-time = "2025-01-20T11:14:41.342Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/e2/86b245397052386595ad726f9742e5223d7aea999b18c518a50e96c3aca4/llvmlite-0.44.0-cp311-cp311-macosx_10_14_x86_64.whl", hash = "sha256:eed7d5f29136bda63b6d7804c279e2b72e08c952b7c5df61f45db408e0ee52f3", size = 28132305, upload-time = "2025-01-20T11:12:53.936Z" }, - { url = "https://files.pythonhosted.org/packages/ff/ec/506902dc6870249fbe2466d9cf66d531265d0f3a1157213c8f986250c033/llvmlite-0.44.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ace564d9fa44bb91eb6e6d8e7754977783c68e90a471ea7ce913bff30bd62427", size = 26201090, upload-time = "2025-01-20T11:12:59.847Z" }, - { url = "https://files.pythonhosted.org/packages/99/fe/d030f1849ebb1f394bb3f7adad5e729b634fb100515594aca25c354ffc62/llvmlite-0.44.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c5d22c3bfc842668168a786af4205ec8e3ad29fb1bc03fd11fd48460d0df64c1", size = 42361858, upload-time = "2025-01-20T11:13:07.623Z" }, - { url = "https://files.pythonhosted.org/packages/d7/7a/ce6174664b9077fc673d172e4c888cb0b128e707e306bc33fff8c2035f0d/llvmlite-0.44.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f01a394e9c9b7b1d4e63c327b096d10f6f0ed149ef53d38a09b3749dcf8c9610", size = 41184200, upload-time = "2025-01-20T11:13:20.058Z" }, - { url = "https://files.pythonhosted.org/packages/5f/c6/258801143975a6d09a373f2641237992496e15567b907a4d401839d671b8/llvmlite-0.44.0-cp311-cp311-win_amd64.whl", hash = "sha256:d8489634d43c20cd0ad71330dde1d5bc7b9966937a263ff1ec1cebb90dc50955", size = 30331193, upload-time = "2025-01-20T11:13:26.976Z" }, - { url = "https://files.pythonhosted.org/packages/15/86/e3c3195b92e6e492458f16d233e58a1a812aa2bfbef9bdd0fbafcec85c60/llvmlite-0.44.0-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:1d671a56acf725bf1b531d5ef76b86660a5ab8ef19bb6a46064a705c6ca80aad", size = 28132297, upload-time = "2025-01-20T11:13:32.57Z" }, - { url = "https://files.pythonhosted.org/packages/d6/53/373b6b8be67b9221d12b24125fd0ec56b1078b660eeae266ec388a6ac9a0/llvmlite-0.44.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5f79a728e0435493611c9f405168682bb75ffd1fbe6fc360733b850c80a026db", size = 26201105, upload-time = "2025-01-20T11:13:38.744Z" }, - { url = "https://files.pythonhosted.org/packages/cb/da/8341fd3056419441286c8e26bf436923021005ece0bff5f41906476ae514/llvmlite-0.44.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0143a5ef336da14deaa8ec26c5449ad5b6a2b564df82fcef4be040b9cacfea9", size = 42361901, upload-time = "2025-01-20T11:13:46.711Z" }, - { url = "https://files.pythonhosted.org/packages/53/ad/d79349dc07b8a395a99153d7ce8b01d6fcdc9f8231355a5df55ded649b61/llvmlite-0.44.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d752f89e31b66db6f8da06df8b39f9b91e78c5feea1bf9e8c1fba1d1c24c065d", size = 41184247, upload-time = "2025-01-20T11:13:56.159Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3b/a9a17366af80127bd09decbe2a54d8974b6d8b274b39bf47fbaedeec6307/llvmlite-0.44.0-cp312-cp312-win_amd64.whl", hash = "sha256:eae7e2d4ca8f88f89d315b48c6b741dcb925d6a1042da694aa16ab3dd4cbd3a1", size = 30332380, upload-time = "2025-01-20T11:14:02.442Z" }, -] - [[package]] name = "lxml" -version = "5.4.0" +version = "6.0.1" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/76/3d/14e82fc7c8fb1b7761f7e748fd47e2ec8276d137b6acfe5a4bb73853e08f/lxml-5.4.0.tar.gz", hash = "sha256:d12832e1dbea4be280b22fd0ea7c9b87f0d8fc51ba06e92dc62d52f804f78ebd", size = 3679479, upload-time = "2025-04-23T01:50:29.322Z" } +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/81/2d/67693cc8a605a12e5975380d7ff83020dcc759351b5a066e1cced04f797b/lxml-5.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:98a3912194c079ef37e716ed228ae0dcb960992100461b704aea4e93af6b0bb9", size = 8083240, upload-time = "2025-04-23T01:45:18.566Z" }, - { url = "https://files.pythonhosted.org/packages/73/53/b5a05ab300a808b72e848efd152fe9c022c0181b0a70b8bca1199f1bed26/lxml-5.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0ea0252b51d296a75f6118ed0d8696888e7403408ad42345d7dfd0d1e93309a7", size = 4387685, upload-time = "2025-04-23T01:45:21.387Z" }, - { url = "https://files.pythonhosted.org/packages/d8/cb/1a3879c5f512bdcd32995c301886fe082b2edd83c87d41b6d42d89b4ea4d/lxml-5.4.0-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b92b69441d1bd39f4940f9eadfa417a25862242ca2c396b406f9272ef09cdcaa", size = 4991164, upload-time = "2025-04-23T01:45:23.849Z" }, - { url = "https://files.pythonhosted.org/packages/f9/94/bbc66e42559f9d04857071e3b3d0c9abd88579367fd2588a4042f641f57e/lxml-5.4.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:20e16c08254b9b6466526bc1828d9370ee6c0d60a4b64836bc3ac2917d1e16df", size = 4746206, upload-time = "2025-04-23T01:45:26.361Z" }, - { url = "https://files.pythonhosted.org/packages/66/95/34b0679bee435da2d7cae895731700e519a8dfcab499c21662ebe671603e/lxml-5.4.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7605c1c32c3d6e8c990dd28a0970a3cbbf1429d5b92279e37fda05fb0c92190e", size = 5342144, upload-time = "2025-04-23T01:45:28.939Z" }, - { url = "https://files.pythonhosted.org/packages/e0/5d/abfcc6ab2fa0be72b2ba938abdae1f7cad4c632f8d552683ea295d55adfb/lxml-5.4.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ecf4c4b83f1ab3d5a7ace10bafcb6f11df6156857a3c418244cef41ca9fa3e44", size = 4825124, upload-time = "2025-04-23T01:45:31.361Z" }, - { url = "https://files.pythonhosted.org/packages/5a/78/6bd33186c8863b36e084f294fc0a5e5eefe77af95f0663ef33809cc1c8aa/lxml-5.4.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0cef4feae82709eed352cd7e97ae062ef6ae9c7b5dbe3663f104cd2c0e8d94ba", size = 4876520, upload-time = "2025-04-23T01:45:34.191Z" }, - { url = "https://files.pythonhosted.org/packages/3b/74/4d7ad4839bd0fc64e3d12da74fc9a193febb0fae0ba6ebd5149d4c23176a/lxml-5.4.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:df53330a3bff250f10472ce96a9af28628ff1f4efc51ccba351a8820bca2a8ba", size = 4765016, upload-time = "2025-04-23T01:45:36.7Z" }, - { url = "https://files.pythonhosted.org/packages/24/0d/0a98ed1f2471911dadfc541003ac6dd6879fc87b15e1143743ca20f3e973/lxml-5.4.0-cp311-cp311-manylinux_2_28_ppc64le.whl", hash = "sha256:aefe1a7cb852fa61150fcb21a8c8fcea7b58c4cb11fbe59c97a0a4b31cae3c8c", size = 5362884, upload-time = "2025-04-23T01:45:39.291Z" }, - { url = "https://files.pythonhosted.org/packages/48/de/d4f7e4c39740a6610f0f6959052b547478107967362e8424e1163ec37ae8/lxml-5.4.0-cp311-cp311-manylinux_2_28_s390x.whl", hash = "sha256:ef5a7178fcc73b7d8c07229e89f8eb45b2908a9238eb90dcfc46571ccf0383b8", size = 4902690, upload-time = "2025-04-23T01:45:42.386Z" }, - { url = "https://files.pythonhosted.org/packages/07/8c/61763abd242af84f355ca4ef1ee096d3c1b7514819564cce70fd18c22e9a/lxml-5.4.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:d2ed1b3cb9ff1c10e6e8b00941bb2e5bb568b307bfc6b17dffbbe8be5eecba86", size = 4944418, upload-time = "2025-04-23T01:45:46.051Z" }, - { url = "https://files.pythonhosted.org/packages/f9/c5/6d7e3b63e7e282619193961a570c0a4c8a57fe820f07ca3fe2f6bd86608a/lxml-5.4.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:72ac9762a9f8ce74c9eed4a4e74306f2f18613a6b71fa065495a67ac227b3056", size = 4827092, upload-time = "2025-04-23T01:45:48.943Z" }, - { url = "https://files.pythonhosted.org/packages/71/4a/e60a306df54680b103348545706a98a7514a42c8b4fbfdcaa608567bb065/lxml-5.4.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f5cb182f6396706dc6cc1896dd02b1c889d644c081b0cdec38747573db88a7d7", size = 5418231, upload-time = "2025-04-23T01:45:51.481Z" }, - { url = "https://files.pythonhosted.org/packages/27/f2/9754aacd6016c930875854f08ac4b192a47fe19565f776a64004aa167521/lxml-5.4.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:3a3178b4873df8ef9457a4875703488eb1622632a9cee6d76464b60e90adbfcd", size = 5261798, upload-time = "2025-04-23T01:45:54.146Z" }, - { url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" }, - { url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" }, - { url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" }, - { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" }, - { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" }, - { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" }, - { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" }, - { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" }, - { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" }, - { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" }, - { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" }, - { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" }, - { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" }, - { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" }, - { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" }, - { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" }, - { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" }, - { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" }, - { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" }, + { 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]] @@ -895,7 +846,7 @@ wheels = [ [[package]] name = "matplotlib" -version = "3.10.3" +version = "3.10.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "contourpy" }, @@ -908,20 +859,25 @@ dependencies = [ { name = "pyparsing" }, { name = "python-dateutil" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +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/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, - { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, - { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, - { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, - { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, - { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, - { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, - { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, - { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, - { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, - { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, - { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { 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]] @@ -1047,16 +1003,16 @@ wheels = [ [[package]] name = "msal" -version = "1.32.3" +version = "1.33.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cryptography" }, { name = "pyjwt", extra = ["crypto"] }, { name = "requests" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/3f/90/81dcc50f0be11a8c4dcbae1a9f761a26e5f905231330a7cacc9f04ec4c61/msal-1.32.3.tar.gz", hash = "sha256:5eea038689c78a5a70ca8ecbe1245458b55a857bd096efb6989c69ba15985d35", size = 151449, upload-time = "2025-04-25T13:12:34.204Z" } +sdist = { url = "https://files.pythonhosted.org/packages/d5/da/81acbe0c1fd7e9e4ec35f55dadeba9833a847b9a6ba2e2d1e4432da901dd/msal-1.33.0.tar.gz", hash = "sha256:836ad80faa3e25a7d71015c990ce61f704a87328b1e73bcbb0623a18cbf17510", size = 153801, upload-time = "2025-07-22T19:36:33.693Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/04/bf/81516b9aac7fd867709984d08eb4db1d2e3fe1df795c8e442cde9b568962/msal-1.32.3-py3-none-any.whl", hash = "sha256:b2798db57760b1961b142f027ffb7c8169536bf77316e99a0df5c4aaebb11569", size = 115358, upload-time = "2025-04-25T13:12:33.034Z" }, + { url = "https://files.pythonhosted.org/packages/86/5b/fbc73e91f7727ae1e79b21ed833308e99dc11cc1cd3d4717f579775de5e9/msal-1.33.0-py3-none-any.whl", hash = "sha256:c0cd41cecf8eaed733ee7e3be9e040291eba53b0f262d3ae9c58f38b04244273", size = 116853, upload-time = "2025-07-22T19:36:32.403Z" }, ] [[package]] @@ -1073,73 +1029,73 @@ wheels = [ [[package]] name = "multidict" -version = "6.5.0" +version = "6.6.4" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/46/b5/59f27b4ce9951a4bce56b88ba5ff5159486797ab18863f2b4c1c5e8465bd/multidict-6.5.0.tar.gz", hash = "sha256:942bd8002492ba819426a8d7aefde3189c1b87099cdf18aaaefefcf7f3f7b6d2", size = 98512, upload-time = "2025-06-17T14:15:56.556Z" } +sdist = { url = "https://files.pythonhosted.org/packages/69/7f/0652e6ed47ab288e3756ea9c0df8b14950781184d4bd7883f4d87dd41245/multidict-6.6.4.tar.gz", hash = "sha256:d2d4e4787672911b48350df02ed3fa3fffdc2f2e8ca06dd6afdf34189b76a9dd", size = 101843, upload-time = "2025-08-11T12:08:48.217Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/75/ba/484f8e96ee58ec4fef42650eb9dbbedb24f9bc155780888398a4725d2270/multidict-6.5.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:8b4bf6bb15a05796a07a248084e3e46e032860c899c7a9b981030e61368dba95", size = 73283, upload-time = "2025-06-17T14:13:50.406Z" }, - { url = "https://files.pythonhosted.org/packages/71/48/01d62ea6199d76934c87746695b3ed16aeedfdd564e8d89184577037baac/multidict-6.5.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:46bb05d50219655c42a4b8fcda9c7ee658a09adbb719c48e65a20284e36328ea", size = 42937, upload-time = "2025-06-17T14:13:51.45Z" }, - { url = "https://files.pythonhosted.org/packages/da/cf/bb462d920f26d9e2e0aff8a78aeb06af1225b826e9a5468870c57591910a/multidict-6.5.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:54f524d73f4d54e87e03c98f6af601af4777e4668a52b1bd2ae0a4d6fc7b392b", size = 42748, upload-time = "2025-06-17T14:13:52.505Z" }, - { url = "https://files.pythonhosted.org/packages/cd/b1/d5c11ea0fdad68d3ed45f0e2527de6496d2fac8afe6b8ca6d407c20ad00f/multidict-6.5.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529b03600466480ecc502000d62e54f185a884ed4570dee90d9a273ee80e37b5", size = 236448, upload-time = "2025-06-17T14:13:53.562Z" }, - { url = "https://files.pythonhosted.org/packages/fc/69/c3ceb264994f5b338c812911a8d660084f37779daef298fc30bd817f75c7/multidict-6.5.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:69ad681ad7c93a41ee7005cc83a144b5b34a3838bcf7261e2b5356057b0f78de", size = 228695, upload-time = "2025-06-17T14:13:54.775Z" }, - { url = "https://files.pythonhosted.org/packages/81/3d/c23dcc0d34a35ad29974184db2878021d28fe170ecb9192be6bfee73f1f2/multidict-6.5.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fe9fada8bc0839466b09fa3f6894f003137942984843ec0c3848846329a36ae", size = 247434, upload-time = "2025-06-17T14:13:56.039Z" }, - { url = "https://files.pythonhosted.org/packages/06/b3/06cf7a049129ff52525a859277abb5648e61d7afae7fb7ed02e3806be34e/multidict-6.5.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f94c6ea6405fcf81baef1e459b209a78cda5442e61b5b7a57ede39d99b5204a0", size = 239431, upload-time = "2025-06-17T14:13:57.33Z" }, - { url = "https://files.pythonhosted.org/packages/8a/72/b2fe2fafa23af0c6123aebe23b4cd23fdad01dfe7009bb85624e4636d0dd/multidict-6.5.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:84ca75ad8a39ed75f079a8931435a5b51ee4c45d9b32e1740f99969a5d1cc2ee", size = 231542, upload-time = "2025-06-17T14:13:58.597Z" }, - { url = "https://files.pythonhosted.org/packages/a1/c9/a52ca0a342a02411a31b6af197a6428a5137d805293f10946eeab614ec06/multidict-6.5.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:be4c08f3a2a6cc42b414496017928d95898964fed84b1b2dace0c9ee763061f9", size = 233069, upload-time = "2025-06-17T14:13:59.834Z" }, - { url = "https://files.pythonhosted.org/packages/9b/55/a3328a3929b8e131e2678d5e65f552b0a6874fab62123e31f5a5625650b0/multidict-6.5.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:046a7540cfbb4d5dc846a1fd9843f3ba980c6523f2e0c5b8622b4a5c94138ae6", size = 250596, upload-time = "2025-06-17T14:14:01.178Z" }, - { url = "https://files.pythonhosted.org/packages/6c/b8/aa3905a38a8287013aeb0a54c73f79ccd8b32d2f1d53e5934643a36502c2/multidict-6.5.0-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:64306121171d988af77d74be0d8c73ee1a69cf6f96aea7fa6030c88f32a152dd", size = 237858, upload-time = "2025-06-17T14:14:03.232Z" }, - { url = "https://files.pythonhosted.org/packages/d3/eb/f11d5af028014f402e5dd01ece74533964fa4e7bfae4af4824506fa8c398/multidict-6.5.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:b4ac1dd5eb0ecf6f7351d5a9137f30a83f7182209c5d37f61614dfdce5714853", size = 249175, upload-time = "2025-06-17T14:14:04.561Z" }, - { url = "https://files.pythonhosted.org/packages/ac/57/d451905a62e5ef489cb4f92e8190d34ac5329427512afd7f893121da4e96/multidict-6.5.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:bab4a8337235365f4111a7011a1f028826ca683834ebd12de4b85e2844359c36", size = 259532, upload-time = "2025-06-17T14:14:05.798Z" }, - { url = "https://files.pythonhosted.org/packages/d3/90/ff82b5ac5cabe3c79c50cf62a62f3837905aa717e67b6b4b7872804f23c8/multidict-6.5.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:a05b5604c5a75df14a63eeeca598d11b2c3745b9008539b70826ea044063a572", size = 250554, upload-time = "2025-06-17T14:14:07.382Z" }, - { url = "https://files.pythonhosted.org/packages/d5/5a/0cabc50d4bc16e61d8b0a8a74499a1409fa7b4ef32970b7662a423781fc7/multidict-6.5.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:67c4a640952371c9ca65b6a710598be246ef3be5ca83ed38c16a7660d3980877", size = 248159, upload-time = "2025-06-17T14:14:08.65Z" }, - { url = "https://files.pythonhosted.org/packages/c0/1d/adeabae0771544f140d9f42ab2c46eaf54e793325999c36106078b7f6600/multidict-6.5.0-cp311-cp311-win32.whl", hash = "sha256:fdeae096ca36c12d8aca2640b8407a9d94e961372c68435bef14e31cce726138", size = 40357, upload-time = "2025-06-17T14:14:09.91Z" }, - { url = "https://files.pythonhosted.org/packages/e1/fe/bbd85ae65c96de5c9910c332ee1f4b7be0bf0fb21563895167bcb6502a1f/multidict-6.5.0-cp311-cp311-win_amd64.whl", hash = "sha256:e2977ef8b7ce27723ee8c610d1bd1765da4f3fbe5a64f9bf1fd3b4770e31fbc0", size = 44432, upload-time = "2025-06-17T14:14:11.013Z" }, - { url = "https://files.pythonhosted.org/packages/96/af/f9052d9c4e65195b210da9f7afdea06d3b7592b3221cc0ef1b407f762faa/multidict-6.5.0-cp311-cp311-win_arm64.whl", hash = "sha256:82d0cf0ea49bae43d9e8c3851e21954eff716259ff42da401b668744d1760bcb", size = 41408, upload-time = "2025-06-17T14:14:12.112Z" }, - { url = "https://files.pythonhosted.org/packages/0a/fa/18f4950e00924f7e84c8195f4fc303295e14df23f713d64e778b8fa8b903/multidict-6.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1bb986c8ea9d49947bc325c51eced1ada6d8d9b4c5b15fd3fcdc3c93edef5a74", size = 73474, upload-time = "2025-06-17T14:14:13.528Z" }, - { url = "https://files.pythonhosted.org/packages/6c/66/0392a2a8948bccff57e4793c9dde3e5c088f01e8b7f8867ee58a2f187fc5/multidict-6.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:03c0923da300120830fc467e23805d63bbb4e98b94032bd863bc7797ea5fa653", size = 43741, upload-time = "2025-06-17T14:14:15.188Z" }, - { url = "https://files.pythonhosted.org/packages/98/3e/f48487c91b2a070566cfbab876d7e1ebe7deb0a8002e4e896a97998ae066/multidict-6.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4c78d5ec00fdd35c91680ab5cf58368faad4bd1a8721f87127326270248de9bc", size = 42143, upload-time = "2025-06-17T14:14:16.612Z" }, - { url = "https://files.pythonhosted.org/packages/3f/49/439c6cc1cd00365cf561bdd3579cc3fa1a0d38effb3a59b8d9562839197f/multidict-6.5.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aadc3cb78be90a887f8f6b73945b840da44b4a483d1c9750459ae69687940c97", size = 239303, upload-time = "2025-06-17T14:14:17.707Z" }, - { url = "https://files.pythonhosted.org/packages/c4/24/491786269e90081cb536e4d7429508725bc92ece176d1204a4449de7c41c/multidict-6.5.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5b02e1ca495d71e07e652e4cef91adae3bf7ae4493507a263f56e617de65dafc", size = 236913, upload-time = "2025-06-17T14:14:18.981Z" }, - { url = "https://files.pythonhosted.org/packages/e8/76/bbe2558b820ebeca8a317ab034541790e8160ca4b1e450415383ac69b339/multidict-6.5.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7fe92a62326eef351668eec4e2dfc494927764a0840a1895cff16707fceffcd3", size = 250752, upload-time = "2025-06-17T14:14:20.297Z" }, - { url = "https://files.pythonhosted.org/packages/3e/e3/3977f2c1123f553ceff9f53cd4de04be2c1912333c6fabbcd51531655476/multidict-6.5.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7673ee4f63879ecd526488deb1989041abcb101b2d30a9165e1e90c489f3f7fb", size = 243937, upload-time = "2025-06-17T14:14:21.935Z" }, - { url = "https://files.pythonhosted.org/packages/b6/b8/7a6e9c13c79709cdd2f22ee849f058e6da76892d141a67acc0e6c30d845c/multidict-6.5.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa097ae2a29f573de7e2d86620cbdda5676d27772d4ed2669cfa9961a0d73955", size = 237419, upload-time = "2025-06-17T14:14:23.215Z" }, - { url = "https://files.pythonhosted.org/packages/84/9d/8557f5e88da71bc7e7a8ace1ada4c28197f3bfdc2dd6e51d3b88f2e16e8e/multidict-6.5.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:300da0fa4f8457d9c4bd579695496116563409e676ac79b5e4dca18e49d1c308", size = 237222, upload-time = "2025-06-17T14:14:24.516Z" }, - { url = "https://files.pythonhosted.org/packages/a3/3b/8f023ad60e7969cb6bc0683738d0e1618f5ff5723d6d2d7818dc6df6ad3d/multidict-6.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9a19bd108c35877b57393243d392d024cfbfdefe759fd137abb98f6fc910b64c", size = 247861, upload-time = "2025-06-17T14:14:25.839Z" }, - { url = "https://files.pythonhosted.org/packages/af/1c/9cf5a099ce7e3189906cf5daa72c44ee962dcb4c1983659f3a6f8a7446ab/multidict-6.5.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:0f32a1777465a35c35ddbbd7fc1293077938a69402fcc59e40b2846d04a120dd", size = 243917, upload-time = "2025-06-17T14:14:27.164Z" }, - { url = "https://files.pythonhosted.org/packages/6c/bb/88ee66ebeef56868044bac58feb1cc25658bff27b20e3cfc464edc181287/multidict-6.5.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9cc1e10c14ce8112d1e6d8971fe3cdbe13e314f68bea0e727429249d4a6ce164", size = 249214, upload-time = "2025-06-17T14:14:28.795Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/a90e88cc4a1309f33088ab1cdd5c0487718f49dfb82c5ffc845bb17c1973/multidict-6.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e95c5e07a06594bdc288117ca90e89156aee8cb2d7c330b920d9c3dd19c05414", size = 258682, upload-time = "2025-06-17T14:14:30.066Z" }, - { url = "https://files.pythonhosted.org/packages/d2/d8/16dd69a6811920a31f4e06114ebe67b1cd922c8b05c9c82b050706d0b6fe/multidict-6.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:40ff26f58323795f5cd2855e2718a1720a1123fb90df4553426f0efd76135462", size = 254254, upload-time = "2025-06-17T14:14:31.323Z" }, - { url = "https://files.pythonhosted.org/packages/ac/a8/90193a5f5ca1bdbf92633d69a25a2ef9bcac7b412b8d48c84d01a2732518/multidict-6.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:76803a29fd71869a8b59c2118c9dcfb3b8f9c8723e2cce6baeb20705459505cf", size = 247741, upload-time = "2025-06-17T14:14:32.717Z" }, - { url = "https://files.pythonhosted.org/packages/cd/43/29c7a747153c05b41d1f67455426af39ed88d6de3f21c232b8f2724bde13/multidict-6.5.0-cp312-cp312-win32.whl", hash = "sha256:df7ecbc65a53a2ce1b3a0c82e6ad1a43dcfe7c6137733f9176a92516b9f5b851", size = 41049, upload-time = "2025-06-17T14:14:33.941Z" }, - { url = "https://files.pythonhosted.org/packages/1e/e8/8f3fc32b7e901f3a2719764d64aeaf6ae77b4ba961f1c3a3cf3867766636/multidict-6.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:0ec1c3fbbb0b655a6540bce408f48b9a7474fd94ed657dcd2e890671fefa7743", size = 44700, upload-time = "2025-06-17T14:14:35.016Z" }, - { url = "https://files.pythonhosted.org/packages/24/e4/e250806adc98d524d41e69c8d4a42bc3513464adb88cb96224df12928617/multidict-6.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:2d24a00d34808b22c1f15902899b9d82d0faeca9f56281641c791d8605eacd35", size = 41703, upload-time = "2025-06-17T14:14:36.168Z" }, - { url = "https://files.pythonhosted.org/packages/44/d8/45e8fc9892a7386d074941429e033adb4640e59ff0780d96a8cf46fe788e/multidict-6.5.0-py3-none-any.whl", hash = "sha256:5634b35f225977605385f56153bd95a7133faffc0ffe12ad26e10517537e8dfc", size = 12181, upload-time = "2025-06-17T14:15:55.156Z" }, + { url = "https://files.pythonhosted.org/packages/6b/7f/90a7f01e2d005d6653c689039977f6856718c75c5579445effb7e60923d1/multidict-6.6.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:c7a0e9b561e6460484318a7612e725df1145d46b0ef57c6b9866441bf6e27e0c", size = 76472, upload-time = "2025-08-11T12:06:29.006Z" }, + { url = "https://files.pythonhosted.org/packages/54/a3/bed07bc9e2bb302ce752f1dabc69e884cd6a676da44fb0e501b246031fdd/multidict-6.6.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6bf2f10f70acc7a2446965ffbc726e5fc0b272c97a90b485857e5c70022213eb", size = 44634, upload-time = "2025-08-11T12:06:30.374Z" }, + { url = "https://files.pythonhosted.org/packages/a7/4b/ceeb4f8f33cf81277da464307afeaf164fb0297947642585884f5cad4f28/multidict-6.6.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:66247d72ed62d5dd29752ffc1d3b88f135c6a8de8b5f63b7c14e973ef5bda19e", size = 44282, upload-time = "2025-08-11T12:06:31.958Z" }, + { url = "https://files.pythonhosted.org/packages/03/35/436a5da8702b06866189b69f655ffdb8f70796252a8772a77815f1812679/multidict-6.6.4-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:105245cc6b76f51e408451a844a54e6823bbd5a490ebfe5bdfc79798511ceded", size = 229696, upload-time = "2025-08-11T12:06:33.087Z" }, + { url = "https://files.pythonhosted.org/packages/b6/0e/915160be8fecf1fca35f790c08fb74ca684d752fcba62c11daaf3d92c216/multidict-6.6.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cbbc54e58b34c3bae389ef00046be0961f30fef7cb0dd9c7756aee376a4f7683", size = 246665, upload-time = "2025-08-11T12:06:34.448Z" }, + { url = "https://files.pythonhosted.org/packages/08/ee/2f464330acd83f77dcc346f0b1a0eaae10230291450887f96b204b8ac4d3/multidict-6.6.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:56c6b3652f945c9bc3ac6c8178cd93132b8d82dd581fcbc3a00676c51302bc1a", size = 225485, upload-time = "2025-08-11T12:06:35.672Z" }, + { url = "https://files.pythonhosted.org/packages/71/cc/9a117f828b4d7fbaec6adeed2204f211e9caf0a012692a1ee32169f846ae/multidict-6.6.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b95494daf857602eccf4c18ca33337dd2be705bccdb6dddbfc9d513e6addb9d9", size = 257318, upload-time = "2025-08-11T12:06:36.98Z" }, + { url = "https://files.pythonhosted.org/packages/25/77/62752d3dbd70e27fdd68e86626c1ae6bccfebe2bb1f84ae226363e112f5a/multidict-6.6.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e5b1413361cef15340ab9dc61523e653d25723e82d488ef7d60a12878227ed50", size = 254689, upload-time = "2025-08-11T12:06:38.233Z" }, + { url = "https://files.pythonhosted.org/packages/00/6e/fac58b1072a6fc59af5e7acb245e8754d3e1f97f4f808a6559951f72a0d4/multidict-6.6.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e167bf899c3d724f9662ef00b4f7fef87a19c22b2fead198a6f68b263618df52", size = 246709, upload-time = "2025-08-11T12:06:39.517Z" }, + { url = "https://files.pythonhosted.org/packages/01/ef/4698d6842ef5e797c6db7744b0081e36fb5de3d00002cc4c58071097fac3/multidict-6.6.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:aaea28ba20a9026dfa77f4b80369e51cb767c61e33a2d4043399c67bd95fb7c6", size = 243185, upload-time = "2025-08-11T12:06:40.796Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c9/d82e95ae1d6e4ef396934e9b0e942dfc428775f9554acf04393cce66b157/multidict-6.6.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:8c91cdb30809a96d9ecf442ec9bc45e8cfaa0f7f8bdf534e082c2443a196727e", size = 237838, upload-time = "2025-08-11T12:06:42.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/cf/f94af5c36baaa75d44fab9f02e2a6bcfa0cd90acb44d4976a80960759dbc/multidict-6.6.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:1a0ccbfe93ca114c5d65a2471d52d8829e56d467c97b0e341cf5ee45410033b3", size = 246368, upload-time = "2025-08-11T12:06:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/4a/fe/29f23460c3d995f6a4b678cb2e9730e7277231b981f0b234702f0177818a/multidict-6.6.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:55624b3f321d84c403cb7d8e6e982f41ae233d85f85db54ba6286f7295dc8a9c", size = 253339, upload-time = "2025-08-11T12:06:45.597Z" }, + { url = "https://files.pythonhosted.org/packages/29/b6/fd59449204426187b82bf8a75f629310f68c6adc9559dc922d5abe34797b/multidict-6.6.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:4a1fb393a2c9d202cb766c76208bd7945bc194eba8ac920ce98c6e458f0b524b", size = 246933, upload-time = "2025-08-11T12:06:46.841Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/d5d6b344f176a5ac3606f7a61fb44dc746e04550e1a13834dff722b8d7d6/multidict-6.6.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:43868297a5759a845fa3a483fb4392973a95fb1de891605a3728130c52b8f40f", size = 242225, upload-time = "2025-08-11T12:06:48.588Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d3/5b2281ed89ff4d5318d82478a2a2450fcdfc3300da48ff15c1778280ad26/multidict-6.6.4-cp311-cp311-win32.whl", hash = "sha256:ed3b94c5e362a8a84d69642dbeac615452e8af9b8eb825b7bc9f31a53a1051e2", size = 41306, upload-time = "2025-08-11T12:06:49.95Z" }, + { url = "https://files.pythonhosted.org/packages/74/7d/36b045c23a1ab98507aefd44fd8b264ee1dd5e5010543c6fccf82141ccef/multidict-6.6.4-cp311-cp311-win_amd64.whl", hash = "sha256:d8c112f7a90d8ca5d20213aa41eac690bb50a76da153e3afb3886418e61cb22e", size = 46029, upload-time = "2025-08-11T12:06:51.082Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5e/553d67d24432c5cd52b49047f2d248821843743ee6d29a704594f656d182/multidict-6.6.4-cp311-cp311-win_arm64.whl", hash = "sha256:3bb0eae408fa1996d87247ca0d6a57b7fc1dcf83e8a5c47ab82c558c250d4adf", size = 43017, upload-time = "2025-08-11T12:06:52.243Z" }, + { url = "https://files.pythonhosted.org/packages/05/f6/512ffd8fd8b37fb2680e5ac35d788f1d71bbaf37789d21a820bdc441e565/multidict-6.6.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0ffb87be160942d56d7b87b0fdf098e81ed565add09eaa1294268c7f3caac4c8", size = 76516, upload-time = "2025-08-11T12:06:53.393Z" }, + { url = "https://files.pythonhosted.org/packages/99/58/45c3e75deb8855c36bd66cc1658007589662ba584dbf423d01df478dd1c5/multidict-6.6.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d191de6cbab2aff5de6c5723101705fd044b3e4c7cfd587a1929b5028b9714b3", size = 45394, upload-time = "2025-08-11T12:06:54.555Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/e8c4472a93a26e4507c0b8e1f0762c0d8a32de1328ef72fd704ef9cc5447/multidict-6.6.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:38a0956dd92d918ad5feff3db8fcb4a5eb7dba114da917e1a88475619781b57b", size = 43591, upload-time = "2025-08-11T12:06:55.672Z" }, + { url = "https://files.pythonhosted.org/packages/05/51/edf414f4df058574a7265034d04c935aa84a89e79ce90fcf4df211f47b16/multidict-6.6.4-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:6865f6d3b7900ae020b495d599fcf3765653bc927951c1abb959017f81ae8287", size = 237215, upload-time = "2025-08-11T12:06:57.213Z" }, + { url = "https://files.pythonhosted.org/packages/c8/45/8b3d6dbad8cf3252553cc41abea09ad527b33ce47a5e199072620b296902/multidict-6.6.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2088c126b6f72db6c9212ad827d0ba088c01d951cee25e758c450da732c138", size = 258299, upload-time = "2025-08-11T12:06:58.946Z" }, + { url = "https://files.pythonhosted.org/packages/3c/e8/8ca2e9a9f5a435fc6db40438a55730a4bf4956b554e487fa1b9ae920f825/multidict-6.6.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0f37bed7319b848097085d7d48116f545985db988e2256b2e6f00563a3416ee6", size = 242357, upload-time = "2025-08-11T12:07:00.301Z" }, + { url = "https://files.pythonhosted.org/packages/0f/84/80c77c99df05a75c28490b2af8f7cba2a12621186e0a8b0865d8e745c104/multidict-6.6.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:01368e3c94032ba6ca0b78e7ccb099643466cf24f8dc8eefcfdc0571d56e58f9", size = 268369, upload-time = "2025-08-11T12:07:01.638Z" }, + { url = "https://files.pythonhosted.org/packages/0d/e9/920bfa46c27b05fb3e1ad85121fd49f441492dca2449c5bcfe42e4565d8a/multidict-6.6.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8fe323540c255db0bffee79ad7f048c909f2ab0edb87a597e1c17da6a54e493c", size = 269341, upload-time = "2025-08-11T12:07:02.943Z" }, + { url = "https://files.pythonhosted.org/packages/af/65/753a2d8b05daf496f4a9c367fe844e90a1b2cac78e2be2c844200d10cc4c/multidict-6.6.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8eb3025f17b0a4c3cd08cda49acf312a19ad6e8a4edd9dbd591e6506d999402", size = 256100, upload-time = "2025-08-11T12:07:04.564Z" }, + { url = "https://files.pythonhosted.org/packages/09/54/655be13ae324212bf0bc15d665a4e34844f34c206f78801be42f7a0a8aaa/multidict-6.6.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbc14f0365534d35a06970d6a83478b249752e922d662dc24d489af1aa0d1be7", size = 253584, upload-time = "2025-08-11T12:07:05.914Z" }, + { url = "https://files.pythonhosted.org/packages/5c/74/ab2039ecc05264b5cec73eb018ce417af3ebb384ae9c0e9ed42cb33f8151/multidict-6.6.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:75aa52fba2d96bf972e85451b99d8e19cc37ce26fd016f6d4aa60da9ab2b005f", size = 251018, upload-time = "2025-08-11T12:07:08.301Z" }, + { url = "https://files.pythonhosted.org/packages/af/0a/ccbb244ac848e56c6427f2392741c06302bbfba49c0042f1eb3c5b606497/multidict-6.6.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4fefd4a815e362d4f011919d97d7b4a1e566f1dde83dc4ad8cfb5b41de1df68d", size = 251477, upload-time = "2025-08-11T12:07:10.248Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b0/0ed49bba775b135937f52fe13922bc64a7eaf0a3ead84a36e8e4e446e096/multidict-6.6.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:db9801fe021f59a5b375ab778973127ca0ac52429a26e2fd86aa9508f4d26eb7", size = 263575, upload-time = "2025-08-11T12:07:11.928Z" }, + { url = "https://files.pythonhosted.org/packages/3e/d9/7fb85a85e14de2e44dfb6a24f03c41e2af8697a6df83daddb0e9b7569f73/multidict-6.6.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:a650629970fa21ac1fb06ba25dabfc5b8a2054fcbf6ae97c758aa956b8dba802", size = 259649, upload-time = "2025-08-11T12:07:13.244Z" }, + { url = "https://files.pythonhosted.org/packages/03/9e/b3a459bcf9b6e74fa461a5222a10ff9b544cb1cd52fd482fb1b75ecda2a2/multidict-6.6.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:452ff5da78d4720d7516a3a2abd804957532dd69296cb77319c193e3ffb87e24", size = 251505, upload-time = "2025-08-11T12:07:14.57Z" }, + { url = "https://files.pythonhosted.org/packages/86/a2/8022f78f041dfe6d71e364001a5cf987c30edfc83c8a5fb7a3f0974cff39/multidict-6.6.4-cp312-cp312-win32.whl", hash = "sha256:8c2fcb12136530ed19572bbba61b407f655e3953ba669b96a35036a11a485793", size = 41888, upload-time = "2025-08-11T12:07:15.904Z" }, + { url = "https://files.pythonhosted.org/packages/c7/eb/d88b1780d43a56db2cba24289fa744a9d216c1a8546a0dc3956563fd53ea/multidict-6.6.4-cp312-cp312-win_amd64.whl", hash = "sha256:047d9425860a8c9544fed1b9584f0c8bcd31bcde9568b047c5e567a1025ecd6e", size = 46072, upload-time = "2025-08-11T12:07:17.045Z" }, + { url = "https://files.pythonhosted.org/packages/9f/16/b929320bf5750e2d9d4931835a4c638a19d2494a5b519caaaa7492ebe105/multidict-6.6.4-cp312-cp312-win_arm64.whl", hash = "sha256:14754eb72feaa1e8ae528468f24250dd997b8e2188c3d2f593f9eba259e4b364", size = 43222, upload-time = "2025-08-11T12:07:18.328Z" }, + { url = "https://files.pythonhosted.org/packages/fd/69/b547032297c7e63ba2af494edba695d781af8a0c6e89e4d06cf848b21d80/multidict-6.6.4-py3-none-any.whl", hash = "sha256:27d8f8e125c07cb954e54d75d04905a9bba8a439c1d84aca94949d4d03d8601c", size = 12313, upload-time = "2025-08-11T12:08:46.891Z" }, ] [[package]] name = "mypy" -version = "1.16.1" +version = "1.17.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mypy-extensions" }, { name = "pathspec" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/81/69/92c7fa98112e4d9eb075a239caa4ef4649ad7d441545ccffbd5e34607cbb/mypy-1.16.1.tar.gz", hash = "sha256:6bd00a0a2094841c5e47e7374bb42b83d64c527a502e3334e1173a0c24437bab", size = 3324747, upload-time = "2025-06-16T16:51:35.145Z" } +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/ea637422dedf0bf36f3ef238eab4e455e2a0dcc3082b5cc067615347ab8e/mypy-1.17.1.tar.gz", hash = "sha256:25e01ec741ab5bb3eec8ba9cdb0f769230368a22c959c4937360efb89b7e9f01", size = 3352570, upload-time = "2025-07-31T07:54:19.204Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9a/61/ec1245aa1c325cb7a6c0f8570a2eee3bfc40fa90d19b1267f8e50b5c8645/mypy-1.16.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:472e4e4c100062488ec643f6162dd0d5208e33e2f34544e1fc931372e806c0cc", size = 10890557, upload-time = "2025-06-16T16:37:21.421Z" }, - { url = "https://files.pythonhosted.org/packages/6b/bb/6eccc0ba0aa0c7a87df24e73f0ad34170514abd8162eb0c75fd7128171fb/mypy-1.16.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:ea16e2a7d2714277e349e24d19a782a663a34ed60864006e8585db08f8ad1782", size = 10012921, upload-time = "2025-06-16T16:51:28.659Z" }, - { url = "https://files.pythonhosted.org/packages/5f/80/b337a12e2006715f99f529e732c5f6a8c143bb58c92bb142d5ab380963a5/mypy-1.16.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08e850ea22adc4d8a4014651575567b0318ede51e8e9fe7a68f25391af699507", size = 11802887, upload-time = "2025-06-16T16:50:53.627Z" }, - { url = "https://files.pythonhosted.org/packages/d9/59/f7af072d09793d581a745a25737c7c0a945760036b16aeb620f658a017af/mypy-1.16.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22d76a63a42619bfb90122889b903519149879ddbf2ba4251834727944c8baca", size = 12531658, upload-time = "2025-06-16T16:33:55.002Z" }, - { url = "https://files.pythonhosted.org/packages/82/c4/607672f2d6c0254b94a646cfc45ad589dd71b04aa1f3d642b840f7cce06c/mypy-1.16.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c7ce0662b6b9dc8f4ed86eb7a5d505ee3298c04b40ec13b30e572c0e5ae17c4", size = 12732486, upload-time = "2025-06-16T16:37:03.301Z" }, - { url = "https://files.pythonhosted.org/packages/b6/5e/136555ec1d80df877a707cebf9081bd3a9f397dedc1ab9750518d87489ec/mypy-1.16.1-cp311-cp311-win_amd64.whl", hash = "sha256:211287e98e05352a2e1d4e8759c5490925a7c784ddc84207f4714822f8cf99b6", size = 9479482, upload-time = "2025-06-16T16:47:37.48Z" }, - { url = "https://files.pythonhosted.org/packages/b4/d6/39482e5fcc724c15bf6280ff5806548c7185e0c090712a3736ed4d07e8b7/mypy-1.16.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:af4792433f09575d9eeca5c63d7d90ca4aeceda9d8355e136f80f8967639183d", size = 11066493, upload-time = "2025-06-16T16:47:01.683Z" }, - { url = "https://files.pythonhosted.org/packages/e6/e5/26c347890efc6b757f4d5bb83f4a0cf5958b8cf49c938ac99b8b72b420a6/mypy-1.16.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:66df38405fd8466ce3517eda1f6640611a0b8e70895e2a9462d1d4323c5eb4b9", size = 10081687, upload-time = "2025-06-16T16:48:19.367Z" }, - { url = "https://files.pythonhosted.org/packages/44/c7/b5cb264c97b86914487d6a24bd8688c0172e37ec0f43e93b9691cae9468b/mypy-1.16.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:44e7acddb3c48bd2713994d098729494117803616e116032af192871aed80b79", size = 11839723, upload-time = "2025-06-16T16:49:20.912Z" }, - { url = "https://files.pythonhosted.org/packages/15/f8/491997a9b8a554204f834ed4816bda813aefda31cf873bb099deee3c9a99/mypy-1.16.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0ab5eca37b50188163fa7c1b73c685ac66c4e9bdee4a85c9adac0e91d8895e15", size = 12722980, upload-time = "2025-06-16T16:37:40.929Z" }, - { url = "https://files.pythonhosted.org/packages/df/f0/2bd41e174b5fd93bc9de9a28e4fb673113633b8a7f3a607fa4a73595e468/mypy-1.16.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb6229b2c9086247e21a83c309754b9058b438704ad2f6807f0d8227f6ebdd", size = 12903328, upload-time = "2025-06-16T16:34:35.099Z" }, - { url = "https://files.pythonhosted.org/packages/61/81/5572108a7bec2c46b8aff7e9b524f371fe6ab5efb534d38d6b37b5490da8/mypy-1.16.1-cp312-cp312-win_amd64.whl", hash = "sha256:1f0435cf920e287ff68af3d10a118a73f212deb2ce087619eb4e648116d1fe9b", size = 9562321, upload-time = "2025-06-16T16:48:58.823Z" }, - { url = "https://files.pythonhosted.org/packages/cf/d3/53e684e78e07c1a2bf7105715e5edd09ce951fc3f47cf9ed095ec1b7a037/mypy-1.16.1-py3-none-any.whl", hash = "sha256:5fc2ac4027d0ef28d6ba69a0343737a23c4d1b83672bf38d1fe237bdc0643b37", size = 2265923, upload-time = "2025-06-16T16:48:02.366Z" }, + { url = "https://files.pythonhosted.org/packages/46/cf/eadc80c4e0a70db1c08921dcc220357ba8ab2faecb4392e3cebeb10edbfa/mypy-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ad37544be07c5d7fba814eb370e006df58fed8ad1ef33ed1649cb1889ba6ff58", size = 10921009, upload-time = "2025-07-31T07:53:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c1/c869d8c067829ad30d9bdae051046561552516cfb3a14f7f0347b7d973ee/mypy-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:064e2ff508e5464b4bd807a7c1625bc5047c5022b85c70f030680e18f37273a5", size = 10047482, upload-time = "2025-07-31T07:53:26.151Z" }, + { url = "https://files.pythonhosted.org/packages/98/b9/803672bab3fe03cee2e14786ca056efda4bb511ea02dadcedde6176d06d0/mypy-1.17.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:70401bbabd2fa1aa7c43bb358f54037baf0586f41e83b0ae67dd0534fc64edfd", size = 11832883, upload-time = "2025-07-31T07:53:47.948Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/fcdac695beca66800918c18697b48833a9a6701de288452b6715a98cfee1/mypy-1.17.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e92bdc656b7757c438660f775f872a669b8ff374edc4d18277d86b63edba6b8b", size = 12566215, upload-time = "2025-07-31T07:54:04.031Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/a932da3d3dace99ee8eb2043b6ab03b6768c36eb29a02f98f46c18c0da0e/mypy-1.17.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c1fdf4abb29ed1cb091cf432979e162c208a5ac676ce35010373ff29247bcad5", size = 12751956, upload-time = "2025-07-31T07:53:36.263Z" }, + { url = "https://files.pythonhosted.org/packages/8c/cf/6438a429e0f2f5cab8bc83e53dbebfa666476f40ee322e13cac5e64b79e7/mypy-1.17.1-cp311-cp311-win_amd64.whl", hash = "sha256:ff2933428516ab63f961644bc49bc4cbe42bbffb2cd3b71cc7277c07d16b1a8b", size = 9507307, upload-time = "2025-07-31T07:53:59.734Z" }, + { url = "https://files.pythonhosted.org/packages/17/a2/7034d0d61af8098ec47902108553122baa0f438df8a713be860f7407c9e6/mypy-1.17.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:69e83ea6553a3ba79c08c6e15dbd9bfa912ec1e493bf75489ef93beb65209aeb", size = 11086295, upload-time = "2025-07-31T07:53:28.124Z" }, + { url = "https://files.pythonhosted.org/packages/14/1f/19e7e44b594d4b12f6ba8064dbe136505cec813549ca3e5191e40b1d3cc2/mypy-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b16708a66d38abb1e6b5702f5c2c87e133289da36f6a1d15f6a5221085c6403", size = 10112355, upload-time = "2025-07-31T07:53:21.121Z" }, + { url = "https://files.pythonhosted.org/packages/5b/69/baa33927e29e6b4c55d798a9d44db5d394072eef2bdc18c3e2048c9ed1e9/mypy-1.17.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:89e972c0035e9e05823907ad5398c5a73b9f47a002b22359b177d40bdaee7056", size = 11875285, upload-time = "2025-07-31T07:53:55.293Z" }, + { url = "https://files.pythonhosted.org/packages/90/13/f3a89c76b0a41e19490b01e7069713a30949d9a6c147289ee1521bcea245/mypy-1.17.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:03b6d0ed2b188e35ee6d5c36b5580cffd6da23319991c49ab5556c023ccf1341", size = 12737895, upload-time = "2025-07-31T07:53:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/23/a1/c4ee79ac484241301564072e6476c5a5be2590bc2e7bfd28220033d2ef8f/mypy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c837b896b37cd103570d776bda106eabb8737aa6dd4f248451aecf53030cdbeb", size = 12931025, upload-time = "2025-07-31T07:54:17.125Z" }, + { url = "https://files.pythonhosted.org/packages/89/b8/7409477be7919a0608900e6320b155c72caab4fef46427c5cc75f85edadd/mypy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:665afab0963a4b39dff7c1fa563cc8b11ecff7910206db4b2e64dd1ba25aed19", size = 9584664, upload-time = "2025-07-31T07:54:12.842Z" }, + { url = "https://files.pythonhosted.org/packages/1d/f3/8fcd2af0f5b806f6cf463efaffd3c9548a28f84220493ecd38d127b6b66d/mypy-1.17.1-py3-none-any.whl", hash = "sha256:a9f52c0351c21fe24c21d8c0eb1f62967b262d6729393397b6f443c3b773c3b9", size = 2283411, upload-time = "2025-07-31T07:53:24.664Z" }, ] [[package]] @@ -1162,30 +1118,39 @@ wheels = [ [[package]] name = "numpy" -version = "2.1.3" +version = "2.3.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/25/ca/1166b75c21abd1da445b97bf1fa2f14f423c6cfb4fc7c4ef31dccf9f6a94/numpy-2.1.3.tar.gz", hash = "sha256:aa08e04e08aaf974d4458def539dece0d28146d866a39da5639596f4921fd761", size = 20166090, upload-time = "2024-11-02T17:48:55.832Z" } +sdist = { url = "https://files.pythonhosted.org/packages/37/7d/3fec4199c5ffb892bed55cff901e4f39a58c81df9c44c280499e92cad264/numpy-2.3.2.tar.gz", hash = "sha256:e0486a11ec30cdecb53f184d496d1c6a20786c81e55e41640270130056f8ee48", size = 20489306, upload-time = "2025-07-24T21:32:07.553Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/81/c8167192eba5247593cd9d305ac236847c2912ff39e11402e72ae28a4985/numpy-2.1.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4d1167c53b93f1f5d8a139a742b3c6f4d429b54e74e6b57d0eff40045187b15d", size = 21156252, upload-time = "2024-11-02T17:34:01.372Z" }, - { url = "https://files.pythonhosted.org/packages/da/74/5a60003fc3d8a718d830b08b654d0eea2d2db0806bab8f3c2aca7e18e010/numpy-2.1.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c80e4a09b3d95b4e1cac08643f1152fa71a0a821a2d4277334c88d54b2219a41", size = 13784119, upload-time = "2024-11-02T17:34:23.809Z" }, - { url = "https://files.pythonhosted.org/packages/47/7c/864cb966b96fce5e63fcf25e1e4d957fe5725a635e5f11fe03f39dd9d6b5/numpy-2.1.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:576a1c1d25e9e02ed7fa5477f30a127fe56debd53b8d2c89d5578f9857d03ca9", size = 5352978, upload-time = "2024-11-02T17:34:34.001Z" }, - { url = "https://files.pythonhosted.org/packages/09/ac/61d07930a4993dd9691a6432de16d93bbe6aa4b1c12a5e573d468eefc1ca/numpy-2.1.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:973faafebaae4c0aaa1a1ca1ce02434554d67e628b8d805e61f874b84e136b09", size = 6892570, upload-time = "2024-11-02T17:34:45.401Z" }, - { url = "https://files.pythonhosted.org/packages/27/2f/21b94664f23af2bb52030653697c685022119e0dc93d6097c3cb45bce5f9/numpy-2.1.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:762479be47a4863e261a840e8e01608d124ee1361e48b96916f38b119cfda04a", size = 13896715, upload-time = "2024-11-02T17:35:06.564Z" }, - { url = "https://files.pythonhosted.org/packages/7a/f0/80811e836484262b236c684a75dfc4ba0424bc670e765afaa911468d9f39/numpy-2.1.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc6f24b3d1ecc1eebfbf5d6051faa49af40b03be1aaa781ebdadcbc090b4539b", size = 16339644, upload-time = "2024-11-02T17:35:30.888Z" }, - { url = "https://files.pythonhosted.org/packages/fa/81/ce213159a1ed8eb7d88a2a6ef4fbdb9e4ffd0c76b866c350eb4e3c37e640/numpy-2.1.3-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:17ee83a1f4fef3c94d16dc1802b998668b5419362c8a4f4e8a491de1b41cc3ee", size = 16712217, upload-time = "2024-11-02T17:35:56.703Z" }, - { url = "https://files.pythonhosted.org/packages/7d/84/4de0b87d5a72f45556b2a8ee9fc8801e8518ec867fc68260c1f5dcb3903f/numpy-2.1.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:15cb89f39fa6d0bdfb600ea24b250e5f1a3df23f901f51c8debaa6a5d122b2f0", size = 14399053, upload-time = "2024-11-02T17:36:22.3Z" }, - { url = "https://files.pythonhosted.org/packages/7e/1c/e5fabb9ad849f9d798b44458fd12a318d27592d4bc1448e269dec070ff04/numpy-2.1.3-cp311-cp311-win32.whl", hash = "sha256:d9beb777a78c331580705326d2367488d5bc473b49a9bc3036c154832520aca9", size = 6534741, upload-time = "2024-11-02T17:36:33.552Z" }, - { url = "https://files.pythonhosted.org/packages/1e/48/a9a4b538e28f854bfb62e1dea3c8fea12e90216a276c7777ae5345ff29a7/numpy-2.1.3-cp311-cp311-win_amd64.whl", hash = "sha256:d89dd2b6da69c4fff5e39c28a382199ddedc3a5be5390115608345dec660b9e2", size = 12869487, upload-time = "2024-11-02T17:36:52.909Z" }, - { url = "https://files.pythonhosted.org/packages/8a/f0/385eb9970309643cbca4fc6eebc8bb16e560de129c91258dfaa18498da8b/numpy-2.1.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f55ba01150f52b1027829b50d70ef1dafd9821ea82905b63936668403c3b471e", size = 20849658, upload-time = "2024-11-02T17:37:23.919Z" }, - { url = "https://files.pythonhosted.org/packages/54/4a/765b4607f0fecbb239638d610d04ec0a0ded9b4951c56dc68cef79026abf/numpy-2.1.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13138eadd4f4da03074851a698ffa7e405f41a0845a6b1ad135b81596e4e9958", size = 13492258, upload-time = "2024-11-02T17:37:45.252Z" }, - { url = "https://files.pythonhosted.org/packages/bd/a7/2332679479c70b68dccbf4a8eb9c9b5ee383164b161bee9284ac141fbd33/numpy-2.1.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:a6b46587b14b888e95e4a24d7b13ae91fa22386c199ee7b418f449032b2fa3b8", size = 5090249, upload-time = "2024-11-02T17:37:54.252Z" }, - { url = "https://files.pythonhosted.org/packages/c1/67/4aa00316b3b981a822c7a239d3a8135be2a6945d1fd11d0efb25d361711a/numpy-2.1.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:0fa14563cc46422e99daef53d725d0c326e99e468a9320a240affffe87852564", size = 6621704, upload-time = "2024-11-02T17:38:05.127Z" }, - { url = "https://files.pythonhosted.org/packages/5e/da/1a429ae58b3b6c364eeec93bf044c532f2ff7b48a52e41050896cf15d5b1/numpy-2.1.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8637dcd2caa676e475503d1f8fdb327bc495554e10838019651b76d17b98e512", size = 13606089, upload-time = "2024-11-02T17:38:25.997Z" }, - { url = "https://files.pythonhosted.org/packages/9e/3e/3757f304c704f2f0294a6b8340fcf2be244038be07da4cccf390fa678a9f/numpy-2.1.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2312b2aa89e1f43ecea6da6ea9a810d06aae08321609d8dc0d0eda6d946a541b", size = 16043185, upload-time = "2024-11-02T17:38:51.07Z" }, - { url = "https://files.pythonhosted.org/packages/43/97/75329c28fea3113d00c8d2daf9bc5828d58d78ed661d8e05e234f86f0f6d/numpy-2.1.3-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a38c19106902bb19351b83802531fea19dee18e5b37b36454f27f11ff956f7fc", size = 16410751, upload-time = "2024-11-02T17:39:15.801Z" }, - { url = "https://files.pythonhosted.org/packages/ad/7a/442965e98b34e0ae9da319f075b387bcb9a1e0658276cc63adb8c9686f7b/numpy-2.1.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:02135ade8b8a84011cbb67dc44e07c58f28575cf9ecf8ab304e51c05528c19f0", size = 14082705, upload-time = "2024-11-02T17:39:38.274Z" }, - { url = "https://files.pythonhosted.org/packages/ac/b6/26108cf2cfa5c7e03fb969b595c93131eab4a399762b51ce9ebec2332e80/numpy-2.1.3-cp312-cp312-win32.whl", hash = "sha256:e6988e90fcf617da2b5c78902fe8e668361b43b4fe26dbf2d7b0f8034d4cafb9", size = 6239077, upload-time = "2024-11-02T17:39:49.299Z" }, - { url = "https://files.pythonhosted.org/packages/a6/84/fa11dad3404b7634aaab50733581ce11e5350383311ea7a7010f464c0170/numpy-2.1.3-cp312-cp312-win_amd64.whl", hash = "sha256:0d30c543f02e84e92c4b1f415b7c6b5326cbe45ee7882b6b77db7195fb971e3a", size = 12566858, upload-time = "2024-11-02T17:40:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/96/26/1320083986108998bd487e2931eed2aeedf914b6e8905431487543ec911d/numpy-2.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:852ae5bed3478b92f093e30f785c98e0cb62fa0a939ed057c31716e18a7a22b9", size = 21259016, upload-time = "2025-07-24T20:24:35.214Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2b/792b341463fa93fc7e55abbdbe87dac316c5b8cb5e94fb7a59fb6fa0cda5/numpy-2.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7a0e27186e781a69959d0230dd9909b5e26024f8da10683bd6344baea1885168", size = 14451158, upload-time = "2025-07-24T20:24:58.397Z" }, + { url = "https://files.pythonhosted.org/packages/b7/13/e792d7209261afb0c9f4759ffef6135b35c77c6349a151f488f531d13595/numpy-2.3.2-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:f0a1a8476ad77a228e41619af2fa9505cf69df928e9aaa165746584ea17fed2b", size = 5379817, upload-time = "2025-07-24T20:25:07.746Z" }, + { url = "https://files.pythonhosted.org/packages/49/ce/055274fcba4107c022b2113a213c7287346563f48d62e8d2a5176ad93217/numpy-2.3.2-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:cbc95b3813920145032412f7e33d12080f11dc776262df1712e1638207dde9e8", size = 6913606, upload-time = "2025-07-24T20:25:18.84Z" }, + { url = "https://files.pythonhosted.org/packages/17/f2/e4d72e6bc5ff01e2ab613dc198d560714971900c03674b41947e38606502/numpy-2.3.2-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f75018be4980a7324edc5930fe39aa391d5734531b1926968605416ff58c332d", size = 14589652, upload-time = "2025-07-24T20:25:40.356Z" }, + { url = "https://files.pythonhosted.org/packages/c8/b0/fbeee3000a51ebf7222016e2939b5c5ecf8000a19555d04a18f1e02521b8/numpy-2.3.2-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:20b8200721840f5621b7bd03f8dcd78de33ec522fc40dc2641aa09537df010c3", size = 16938816, upload-time = "2025-07-24T20:26:05.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/ec/2f6c45c3484cc159621ea8fc000ac5a86f1575f090cac78ac27193ce82cd/numpy-2.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1f91e5c028504660d606340a084db4b216567ded1056ea2b4be4f9d10b67197f", size = 16370512, upload-time = "2025-07-24T20:26:30.545Z" }, + { url = "https://files.pythonhosted.org/packages/b5/01/dd67cf511850bd7aefd6347aaae0956ed415abea741ae107834aae7d6d4e/numpy-2.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:fb1752a3bb9a3ad2d6b090b88a9a0ae1cd6f004ef95f75825e2f382c183b2097", size = 18884947, upload-time = "2025-07-24T20:26:58.24Z" }, + { url = "https://files.pythonhosted.org/packages/a7/17/2cf60fd3e6a61d006778735edf67a222787a8c1a7842aed43ef96d777446/numpy-2.3.2-cp311-cp311-win32.whl", hash = "sha256:4ae6863868aaee2f57503c7a5052b3a2807cf7a3914475e637a0ecd366ced220", size = 6599494, upload-time = "2025-07-24T20:27:09.786Z" }, + { url = "https://files.pythonhosted.org/packages/d5/03/0eade211c504bda872a594f045f98ddcc6caef2b7c63610946845e304d3f/numpy-2.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:240259d6564f1c65424bcd10f435145a7644a65a6811cfc3201c4a429ba79170", size = 13087889, upload-time = "2025-07-24T20:27:29.558Z" }, + { url = "https://files.pythonhosted.org/packages/13/32/2c7979d39dafb2a25087e12310fc7f3b9d3c7d960df4f4bc97955ae0ce1d/numpy-2.3.2-cp311-cp311-win_arm64.whl", hash = "sha256:4209f874d45f921bde2cff1ffcd8a3695f545ad2ffbef6d3d3c6768162efab89", size = 10459560, upload-time = "2025-07-24T20:27:46.803Z" }, + { url = "https://files.pythonhosted.org/packages/00/6d/745dd1c1c5c284d17725e5c802ca4d45cfc6803519d777f087b71c9f4069/numpy-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bc3186bea41fae9d8e90c2b4fb5f0a1f5a690682da79b92574d63f56b529080b", size = 20956420, upload-time = "2025-07-24T20:28:18.002Z" }, + { url = "https://files.pythonhosted.org/packages/bc/96/e7b533ea5740641dd62b07a790af5d9d8fec36000b8e2d0472bd7574105f/numpy-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2f4f0215edb189048a3c03bd5b19345bdfa7b45a7a6f72ae5945d2a28272727f", size = 14184660, upload-time = "2025-07-24T20:28:39.522Z" }, + { url = "https://files.pythonhosted.org/packages/2b/53/102c6122db45a62aa20d1b18c9986f67e6b97e0d6fbc1ae13e3e4c84430c/numpy-2.3.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b1224a734cd509f70816455c3cffe13a4f599b1bf7130f913ba0e2c0b2006c0", size = 5113382, upload-time = "2025-07-24T20:28:48.544Z" }, + { url = "https://files.pythonhosted.org/packages/2b/21/376257efcbf63e624250717e82b4fae93d60178f09eb03ed766dbb48ec9c/numpy-2.3.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:3dcf02866b977a38ba3ec10215220609ab9667378a9e2150615673f3ffd6c73b", size = 6647258, upload-time = "2025-07-24T20:28:59.104Z" }, + { url = "https://files.pythonhosted.org/packages/91/ba/f4ebf257f08affa464fe6036e13f2bf9d4642a40228781dc1235da81be9f/numpy-2.3.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:572d5512df5470f50ada8d1972c5f1082d9a0b7aa5944db8084077570cf98370", size = 14281409, upload-time = "2025-07-24T20:40:30.298Z" }, + { url = "https://files.pythonhosted.org/packages/59/ef/f96536f1df42c668cbacb727a8c6da7afc9c05ece6d558927fb1722693e1/numpy-2.3.2-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8145dd6d10df13c559d1e4314df29695613575183fa2e2d11fac4c208c8a1f73", size = 16641317, upload-time = "2025-07-24T20:40:56.625Z" }, + { url = "https://files.pythonhosted.org/packages/f6/a7/af813a7b4f9a42f498dde8a4c6fcbff8100eed00182cc91dbaf095645f38/numpy-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:103ea7063fa624af04a791c39f97070bf93b96d7af7eb23530cd087dc8dbe9dc", size = 16056262, upload-time = "2025-07-24T20:41:20.797Z" }, + { url = "https://files.pythonhosted.org/packages/8b/5d/41c4ef8404caaa7f05ed1cfb06afe16a25895260eacbd29b4d84dff2920b/numpy-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc927d7f289d14f5e037be917539620603294454130b6de200091e23d27dc9be", size = 18579342, upload-time = "2025-07-24T20:41:50.753Z" }, + { url = "https://files.pythonhosted.org/packages/a1/4f/9950e44c5a11636f4a3af6e825ec23003475cc9a466edb7a759ed3ea63bd/numpy-2.3.2-cp312-cp312-win32.whl", hash = "sha256:d95f59afe7f808c103be692175008bab926b59309ade3e6d25009e9a171f7036", size = 6320610, upload-time = "2025-07-24T20:42:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/7c/2f/244643a5ce54a94f0a9a2ab578189c061e4a87c002e037b0829dd77293b6/numpy-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:9e196ade2400c0c737d93465327d1ae7c06c7cb8a1756121ebf54b06ca183c7f", size = 12786292, upload-time = "2025-07-24T20:42:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/54/cd/7b5f49d5d78db7badab22d8323c1b6ae458fbf86c4fdfa194ab3cd4eb39b/numpy-2.3.2-cp312-cp312-win_arm64.whl", hash = "sha256:ee807923782faaf60d0d7331f5e86da7d5e3079e28b291973c545476c2b00d07", size = 10194071, upload-time = "2025-07-24T20:42:36.657Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ea/50ebc91d28b275b23b7128ef25c3d08152bc4068f42742867e07a870a42a/numpy-2.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:14a91ebac98813a49bc6aa1a0dfc09513dcec1d97eaf31ca21a87221a1cdcb15", size = 21130338, upload-time = "2025-07-24T20:57:54.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/57/cdd5eac00dd5f137277355c318a955c0d8fb8aa486020c22afd305f8b88f/numpy-2.3.2-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:71669b5daae692189540cffc4c439468d35a3f84f0c88b078ecd94337f6cb0ec", size = 14375776, upload-time = "2025-07-24T20:58:16.303Z" }, + { url = "https://files.pythonhosted.org/packages/83/85/27280c7f34fcd305c2209c0cdca4d70775e4859a9eaa92f850087f8dea50/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_arm64.whl", hash = "sha256:69779198d9caee6e547adb933941ed7520f896fd9656834c300bdf4dd8642712", size = 5304882, upload-time = "2025-07-24T20:58:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/48/b4/6500b24d278e15dd796f43824e69939d00981d37d9779e32499e823aa0aa/numpy-2.3.2-pp311-pypy311_pp73-macosx_14_0_x86_64.whl", hash = "sha256:2c3271cc4097beb5a60f010bcc1cc204b300bb3eafb4399376418a83a1c6373c", size = 6818405, upload-time = "2025-07-24T20:58:37.341Z" }, + { url = "https://files.pythonhosted.org/packages/9b/c9/142c1e03f199d202da8e980c2496213509291b6024fd2735ad28ae7065c7/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8446acd11fe3dc1830568c941d44449fd5cb83068e5c70bd5a470d323d448296", size = 14419651, upload-time = "2025-07-24T20:58:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/8023e87cbea31a750a6c00ff9427d65ebc5fef104a136bfa69f76266d614/numpy-2.3.2-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa098a5ab53fa407fded5870865c6275a5cd4101cfdef8d6fafc48286a96e981", size = 16760166, upload-time = "2025-07-24T21:28:56.38Z" }, + { url = "https://files.pythonhosted.org/packages/78/e3/6690b3f85a05506733c7e90b577e4762517404ea78bab2ca3a5cb1aeb78d/numpy-2.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:6936aff90dda378c09bea075af0d9c675fe3a977a9d2402f95a87f440f59f619", size = 12977811, upload-time = "2025-07-24T21:29:18.234Z" }, ] [[package]] @@ -1245,7 +1210,6 @@ dependencies = [ { name = "inputs" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "llvmlite" }, { name = "numpy" }, { name = "onnx" }, { name = "psutil" }, @@ -1300,13 +1264,11 @@ docs = [ ] testing = [ { name = "codespell" }, - { name = "coverage" }, { name = "hypothesis" }, { name = "mypy" }, { name = "pre-commit-hooks" }, { name = "pytest" }, { name = "pytest-asyncio" }, - { name = "pytest-cov" }, { name = "pytest-cpp" }, { name = "pytest-mock" }, { name = "pytest-randomly" }, @@ -1318,7 +1280,6 @@ testing = [ ] tools = [ { name = "metadrive-simulator", marker = "platform_machine != 'aarch64'" }, - { name = "rerun-sdk" }, ] [package.metadata] @@ -1331,7 +1292,6 @@ requires-dist = [ { name = "casadi", specifier = ">=3.6.6" }, { name = "cffi" }, { name = "codespell", marker = "extra == 'testing'" }, - { name = "coverage", marker = "extra == 'testing'" }, { name = "crcmod" }, { name = "cython" }, { name = "dbus-next", marker = "extra == 'dev'" }, @@ -1342,13 +1302,12 @@ requires-dist = [ { name = "jinja2", marker = "extra == 'docs'" }, { name = "json-rpc" }, { name = "libusb1" }, - { name = "llvmlite" }, { 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" }, { name = "mkdocs", marker = "extra == 'docs'" }, { name = "mypy", marker = "extra == 'testing'" }, { name = "natsort", marker = "extra == 'docs'" }, - { name = "numpy", specifier = ">=2.0,<2.2" }, + { name = "numpy", specifier = ">=2.0" }, { name = "onnx", specifier = ">=1.14.0" }, { name = "opencv-python-headless", marker = "extra == 'dev'" }, { name = "parameterized", marker = "extra == 'dev'", specifier = ">=0.8,<0.9" }, @@ -1366,21 +1325,19 @@ requires-dist = [ { name = "pyserial" }, { name = "pytest", marker = "extra == 'testing'" }, { name = "pytest-asyncio", marker = "extra == 'testing'" }, - { name = "pytest-cov", marker = "extra == 'testing'" }, { name = "pytest-cpp", marker = "extra == 'testing'" }, { name = "pytest-mock", marker = "extra == 'testing'" }, { name = "pytest-randomly", marker = "extra == 'testing'" }, { name = "pytest-repeat", marker = "extra == 'testing'" }, { name = "pytest-subtests", marker = "extra == 'testing'" }, { name = "pytest-timeout", marker = "extra == 'testing'" }, - { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=909e97b49d12401c10608f9d777bfc9dab8a4413" }, + { name = "pytest-xdist", marker = "extra == 'testing'", git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da" }, { name = "pytools", marker = "platform_machine != 'aarch64' and extra == 'dev'", specifier = "<2024.1.11" }, { name = "pywinctl", marker = "extra == 'dev'" }, { name = "pyzmq" }, { name = "qrcode" }, { name = "raylib", marker = "extra == 'dev'" }, { name = "requests" }, - { name = "rerun-sdk", marker = "extra == 'tools'", specifier = ">=0.18" }, { name = "ruff", marker = "extra == 'testing'" }, { name = "scons" }, { name = "sentry-sdk" }, @@ -1473,39 +1430,39 @@ wheels = [ [[package]] name = "pillow" -version = "11.2.1" +version = "11.3.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/0d/d0d6dea55cd152ce3d6767bb38a8fc10e33796ba4ba210cbab9354b6d238/pillow-11.3.0.tar.gz", hash = "sha256:3828ee7586cd0b2091b6209e5ad53e20d0649bbe87164a459d0676e035e8f523", size = 47113069, upload-time = "2025-07-01T09:16:30.666Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/68/08/3fbf4b98924c73037a8e8b4c2c774784805e0fb4ebca6c5bb60795c40125/pillow-11.2.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:35ca289f712ccfc699508c4658a1d14652e8033e9b69839edf83cbdd0ba39e70", size = 3198450, upload-time = "2025-04-12T17:47:37.135Z" }, - { url = "https://files.pythonhosted.org/packages/84/92/6505b1af3d2849d5e714fc75ba9e69b7255c05ee42383a35a4d58f576b16/pillow-11.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0409af9f829f87a2dfb7e259f78f317a5351f2045158be321fd135973fff7bf", size = 3030550, upload-time = "2025-04-12T17:47:39.345Z" }, - { url = "https://files.pythonhosted.org/packages/3c/8c/ac2f99d2a70ff966bc7eb13dacacfaab57c0549b2ffb351b6537c7840b12/pillow-11.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d4e5c5edee874dce4f653dbe59db7c73a600119fbea8d31f53423586ee2aafd7", size = 4415018, upload-time = "2025-04-12T17:47:41.128Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e3/0a58b5d838687f40891fff9cbaf8669f90c96b64dc8f91f87894413856c6/pillow-11.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b93a07e76d13bff9444f1a029e0af2964e654bfc2e2c2d46bfd080df5ad5f3d8", size = 4498006, upload-time = "2025-04-12T17:47:42.912Z" }, - { url = "https://files.pythonhosted.org/packages/21/f5/6ba14718135f08fbfa33308efe027dd02b781d3f1d5c471444a395933aac/pillow-11.2.1-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:e6def7eed9e7fa90fde255afaf08060dc4b343bbe524a8f69bdd2a2f0018f600", size = 4517773, upload-time = "2025-04-12T17:47:44.611Z" }, - { url = "https://files.pythonhosted.org/packages/20/f2/805ad600fc59ebe4f1ba6129cd3a75fb0da126975c8579b8f57abeb61e80/pillow-11.2.1-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:8f4f3724c068be008c08257207210c138d5f3731af6c155a81c2b09a9eb3a788", size = 4607069, upload-time = "2025-04-12T17:47:46.46Z" }, - { url = "https://files.pythonhosted.org/packages/71/6b/4ef8a288b4bb2e0180cba13ca0a519fa27aa982875882392b65131401099/pillow-11.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0a6709b47019dff32e678bc12c63008311b82b9327613f534e496dacaefb71e", size = 4583460, upload-time = "2025-04-12T17:47:49.255Z" }, - { url = "https://files.pythonhosted.org/packages/62/ae/f29c705a09cbc9e2a456590816e5c234382ae5d32584f451c3eb41a62062/pillow-11.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f6b0c664ccb879109ee3ca702a9272d877f4fcd21e5eb63c26422fd6e415365e", size = 4661304, upload-time = "2025-04-12T17:47:51.067Z" }, - { url = "https://files.pythonhosted.org/packages/6e/1a/c8217b6f2f73794a5e219fbad087701f412337ae6dbb956db37d69a9bc43/pillow-11.2.1-cp311-cp311-win32.whl", hash = "sha256:cc5d875d56e49f112b6def6813c4e3d3036d269c008bf8aef72cd08d20ca6df6", size = 2331809, upload-time = "2025-04-12T17:47:54.425Z" }, - { url = "https://files.pythonhosted.org/packages/e2/72/25a8f40170dc262e86e90f37cb72cb3de5e307f75bf4b02535a61afcd519/pillow-11.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:0f5c7eda47bf8e3c8a283762cab94e496ba977a420868cb819159980b6709193", size = 2676338, upload-time = "2025-04-12T17:47:56.535Z" }, - { url = "https://files.pythonhosted.org/packages/06/9e/76825e39efee61efea258b479391ca77d64dbd9e5804e4ad0fa453b4ba55/pillow-11.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:4d375eb838755f2528ac8cbc926c3e31cc49ca4ad0cf79cff48b20e30634a4a7", size = 2414918, upload-time = "2025-04-12T17:47:58.217Z" }, - { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, - { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, - { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, - { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, - { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, - { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, - { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, - { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, - { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, - { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ad/2613c04633c7257d9481ab21d6b5364b59fc5d75faafd7cb8693523945a3/pillow-11.2.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:80f1df8dbe9572b4b7abdfa17eb5d78dd620b1d55d9e25f834efdbee872d3aed", size = 3181734, upload-time = "2025-04-12T17:49:46.789Z" }, - { url = "https://files.pythonhosted.org/packages/a4/fd/dcdda4471ed667de57bb5405bb42d751e6cfdd4011a12c248b455c778e03/pillow-11.2.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:ea926cfbc3957090becbcbbb65ad177161a2ff2ad578b5a6ec9bb1e1cd78753c", size = 2999841, upload-time = "2025-04-12T17:49:48.812Z" }, - { url = "https://files.pythonhosted.org/packages/ac/89/8a2536e95e77432833f0db6fd72a8d310c8e4272a04461fb833eb021bf94/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:738db0e0941ca0376804d4de6a782c005245264edaa253ffce24e5a15cbdc7bd", size = 3437470, upload-time = "2025-04-12T17:49:50.831Z" }, - { url = "https://files.pythonhosted.org/packages/9d/8f/abd47b73c60712f88e9eda32baced7bfc3e9bd6a7619bb64b93acff28c3e/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db98ab6565c69082ec9b0d4e40dd9f6181dab0dd236d26f7a50b8b9bfbd5076", size = 3460013, upload-time = "2025-04-12T17:49:53.278Z" }, - { url = "https://files.pythonhosted.org/packages/f6/20/5c0a0aa83b213b7a07ec01e71a3d6ea2cf4ad1d2c686cc0168173b6089e7/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:036e53f4170e270ddb8797d4c590e6dd14d28e15c7da375c18978045f7e6c37b", size = 3527165, upload-time = "2025-04-12T17:49:55.164Z" }, - { url = "https://files.pythonhosted.org/packages/58/0e/2abab98a72202d91146abc839e10c14f7cf36166f12838ea0c4db3ca6ecb/pillow-11.2.1-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:14f73f7c291279bd65fda51ee87affd7c1e097709f7fdd0188957a16c264601f", size = 3571586, upload-time = "2025-04-12T17:49:57.171Z" }, - { url = "https://files.pythonhosted.org/packages/21/2c/5e05f58658cf49b6667762cca03d6e7d85cededde2caf2ab37b81f80e574/pillow-11.2.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:208653868d5c9ecc2b327f9b9ef34e0e42a4cdd172c2988fd81d62d2bc9bc044", size = 2674751, upload-time = "2025-04-12T17:49:59.628Z" }, + { url = "https://files.pythonhosted.org/packages/db/26/77f8ed17ca4ffd60e1dcd220a6ec6d71210ba398cfa33a13a1cd614c5613/pillow-11.3.0-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:1cd110edf822773368b396281a2293aeb91c90a2db00d78ea43e7e861631b722", size = 5316531, upload-time = "2025-07-01T09:13:59.203Z" }, + { url = "https://files.pythonhosted.org/packages/cb/39/ee475903197ce709322a17a866892efb560f57900d9af2e55f86db51b0a5/pillow-11.3.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:9c412fddd1b77a75aa904615ebaa6001f169b26fd467b4be93aded278266b288", size = 4686560, upload-time = "2025-07-01T09:14:01.101Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/442068a160fd179938ba55ec8c97050a612426fae5ec0a764e345839f76d/pillow-11.3.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1aa4de119a0ecac0a34a9c8bde33f34022e2e8f99104e47a3ca392fd60e37d", size = 5870978, upload-time = "2025-07-03T13:09:55.638Z" }, + { url = "https://files.pythonhosted.org/packages/13/92/dcdd147ab02daf405387f0218dcf792dc6dd5b14d2573d40b4caeef01059/pillow-11.3.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:91da1d88226663594e3f6b4b8c3c8d85bd504117d043740a8e0ec449087cc494", size = 7641168, upload-time = "2025-07-03T13:10:00.37Z" }, + { url = "https://files.pythonhosted.org/packages/6e/db/839d6ba7fd38b51af641aa904e2960e7a5644d60ec754c046b7d2aee00e5/pillow-11.3.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:643f189248837533073c405ec2f0bb250ba54598cf80e8c1e043381a60632f58", size = 5973053, upload-time = "2025-07-01T09:14:04.491Z" }, + { url = "https://files.pythonhosted.org/packages/f2/2f/d7675ecae6c43e9f12aa8d58b6012683b20b6edfbdac7abcb4e6af7a3784/pillow-11.3.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:106064daa23a745510dabce1d84f29137a37224831d88eb4ce94bb187b1d7e5f", size = 6640273, upload-time = "2025-07-01T09:14:06.235Z" }, + { url = "https://files.pythonhosted.org/packages/45/ad/931694675ede172e15b2ff03c8144a0ddaea1d87adb72bb07655eaffb654/pillow-11.3.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cd8ff254faf15591e724dc7c4ddb6bf4793efcbe13802a4ae3e863cd300b493e", size = 6082043, upload-time = "2025-07-01T09:14:07.978Z" }, + { url = "https://files.pythonhosted.org/packages/3a/04/ba8f2b11fc80d2dd462d7abec16351b45ec99cbbaea4387648a44190351a/pillow-11.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:932c754c2d51ad2b2271fd01c3d121daaa35e27efae2a616f77bf164bc0b3e94", size = 6715516, upload-time = "2025-07-01T09:14:10.233Z" }, + { url = "https://files.pythonhosted.org/packages/48/59/8cd06d7f3944cc7d892e8533c56b0acb68399f640786313275faec1e3b6f/pillow-11.3.0-cp311-cp311-win32.whl", hash = "sha256:b4b8f3efc8d530a1544e5962bd6b403d5f7fe8b9e08227c6b255f98ad82b4ba0", size = 6274768, upload-time = "2025-07-01T09:14:11.921Z" }, + { url = "https://files.pythonhosted.org/packages/f1/cc/29c0f5d64ab8eae20f3232da8f8571660aa0ab4b8f1331da5c2f5f9a938e/pillow-11.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:1a992e86b0dd7aeb1f053cd506508c0999d710a8f07b4c791c63843fc6a807ac", size = 6986055, upload-time = "2025-07-01T09:14:13.623Z" }, + { url = "https://files.pythonhosted.org/packages/c6/df/90bd886fabd544c25addd63e5ca6932c86f2b701d5da6c7839387a076b4a/pillow-11.3.0-cp311-cp311-win_arm64.whl", hash = "sha256:30807c931ff7c095620fe04448e2c2fc673fcbb1ffe2a7da3fb39613489b1ddd", size = 2423079, upload-time = "2025-07-01T09:14:15.268Z" }, + { url = "https://files.pythonhosted.org/packages/40/fe/1bc9b3ee13f68487a99ac9529968035cca2f0a51ec36892060edcc51d06a/pillow-11.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fdae223722da47b024b867c1ea0be64e0df702c5e0a60e27daad39bf960dd1e4", size = 5278800, upload-time = "2025-07-01T09:14:17.648Z" }, + { url = "https://files.pythonhosted.org/packages/2c/32/7e2ac19b5713657384cec55f89065fb306b06af008cfd87e572035b27119/pillow-11.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:921bd305b10e82b4d1f5e802b6850677f965d8394203d182f078873851dada69", size = 4686296, upload-time = "2025-07-01T09:14:19.828Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1e/b9e12bbe6e4c2220effebc09ea0923a07a6da1e1f1bfbc8d7d29a01ce32b/pillow-11.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:eb76541cba2f958032d79d143b98a3a6b3ea87f0959bbe256c0b5e416599fd5d", size = 5871726, upload-time = "2025-07-03T13:10:04.448Z" }, + { url = "https://files.pythonhosted.org/packages/8d/33/e9200d2bd7ba00dc3ddb78df1198a6e80d7669cce6c2bdbeb2530a74ec58/pillow-11.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:67172f2944ebba3d4a7b54f2e95c786a3a50c21b88456329314caaa28cda70f6", size = 7644652, upload-time = "2025-07-03T13:10:10.391Z" }, + { url = "https://files.pythonhosted.org/packages/41/f1/6f2427a26fc683e00d985bc391bdd76d8dd4e92fac33d841127eb8fb2313/pillow-11.3.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f07ed9f56a3b9b5f49d3661dc9607484e85c67e27f3e8be2c7d28ca032fec7", size = 5977787, upload-time = "2025-07-01T09:14:21.63Z" }, + { url = "https://files.pythonhosted.org/packages/e4/c9/06dd4a38974e24f932ff5f98ea3c546ce3f8c995d3f0985f8e5ba48bba19/pillow-11.3.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:676b2815362456b5b3216b4fd5bd89d362100dc6f4945154ff172e206a22c024", size = 6645236, upload-time = "2025-07-01T09:14:23.321Z" }, + { url = "https://files.pythonhosted.org/packages/40/e7/848f69fb79843b3d91241bad658e9c14f39a32f71a301bcd1d139416d1be/pillow-11.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3e184b2f26ff146363dd07bde8b711833d7b0202e27d13540bfe2e35a323a809", size = 6086950, upload-time = "2025-07-01T09:14:25.237Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1a/7cff92e695a2a29ac1958c2a0fe4c0b2393b60aac13b04a4fe2735cad52d/pillow-11.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6be31e3fc9a621e071bc17bb7de63b85cbe0bfae91bb0363c893cbe67247780d", size = 6723358, upload-time = "2025-07-01T09:14:27.053Z" }, + { url = "https://files.pythonhosted.org/packages/26/7d/73699ad77895f69edff76b0f332acc3d497f22f5d75e5360f78cbcaff248/pillow-11.3.0-cp312-cp312-win32.whl", hash = "sha256:7b161756381f0918e05e7cb8a371fff367e807770f8fe92ecb20d905d0e1c149", size = 6275079, upload-time = "2025-07-01T09:14:30.104Z" }, + { url = "https://files.pythonhosted.org/packages/8c/ce/e7dfc873bdd9828f3b6e5c2bbb74e47a98ec23cc5c74fc4e54462f0d9204/pillow-11.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:a6444696fce635783440b7f7a9fc24b3ad10a9ea3f0ab66c5905be1c19ccf17d", size = 6986324, upload-time = "2025-07-01T09:14:31.899Z" }, + { url = "https://files.pythonhosted.org/packages/16/8f/b13447d1bf0b1f7467ce7d86f6e6edf66c0ad7cf44cf5c87a37f9bed9936/pillow-11.3.0-cp312-cp312-win_arm64.whl", hash = "sha256:2aceea54f957dd4448264f9bf40875da0415c83eb85f55069d89c0ed436e3542", size = 2423067, upload-time = "2025-07-01T09:14:33.709Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e3/6fa84033758276fb31da12e5fb66ad747ae83b93c67af17f8c6ff4cc8f34/pillow-11.3.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7c8ec7a017ad1bd562f93dbd8505763e688d388cde6e4a010ae1486916e713e6", size = 5270566, upload-time = "2025-07-01T09:16:19.801Z" }, + { url = "https://files.pythonhosted.org/packages/5b/ee/e8d2e1ab4892970b561e1ba96cbd59c0d28cf66737fc44abb2aec3795a4e/pillow-11.3.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:9ab6ae226de48019caa8074894544af5b53a117ccb9d3b3dcb2871464c829438", size = 4654618, upload-time = "2025-07-01T09:16:21.818Z" }, + { url = "https://files.pythonhosted.org/packages/f2/6d/17f80f4e1f0761f02160fc433abd4109fa1548dcfdca46cfdadaf9efa565/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe27fb049cdcca11f11a7bfda64043c37b30e6b91f10cb5bab275806c32f6ab3", size = 4874248, upload-time = "2025-07-03T13:11:20.738Z" }, + { url = "https://files.pythonhosted.org/packages/de/5f/c22340acd61cef960130585bbe2120e2fd8434c214802f07e8c03596b17e/pillow-11.3.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:465b9e8844e3c3519a983d58b80be3f668e2a7a5db97f2784e7079fbc9f9822c", size = 6583963, upload-time = "2025-07-03T13:11:26.283Z" }, + { url = "https://files.pythonhosted.org/packages/31/5e/03966aedfbfcbb4d5f8aa042452d3361f325b963ebbadddac05b122e47dd/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5418b53c0d59b3824d05e029669efa023bbef0f3e92e75ec8428f3799487f361", size = 4957170, upload-time = "2025-07-01T09:16:23.762Z" }, + { url = "https://files.pythonhosted.org/packages/cc/2d/e082982aacc927fc2cab48e1e731bdb1643a1406acace8bed0900a61464e/pillow-11.3.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:504b6f59505f08ae014f724b6207ff6222662aab5cc9542577fb084ed0676ac7", size = 5581505, upload-time = "2025-07-01T09:16:25.593Z" }, + { url = "https://files.pythonhosted.org/packages/34/e7/ae39f538fd6844e982063c3a5e4598b8ced43b9633baa3a85ef33af8c05c/pillow-11.3.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:c84d689db21a1c397d001aa08241044aa2069e7587b398c8cc63020390b1c1b8", size = 6984598, upload-time = "2025-07-01T09:16:27.732Z" }, ] [[package]] @@ -1528,14 +1485,14 @@ wheels = [ [[package]] name = "pre-commit-hooks" -version = "5.0.0" +version = "6.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "ruamel-yaml" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/ac/7d/3299241a753c738d114600c360d754550b28c285281dc6a5132c4ccfae65/pre_commit_hooks-5.0.0.tar.gz", hash = "sha256:10626959a9eaf602fbfc22bc61b6e75801436f82326bfcee82bb1f2fc4bc646e", size = 29747, upload-time = "2024-10-05T18:43:11.225Z" } +sdist = { url = "https://files.pythonhosted.org/packages/36/4d/93e63e48f8fd16d6c1e4cef5dabadcade4d1325c7fd6f29f075a4d2284f3/pre_commit_hooks-6.0.0.tar.gz", hash = "sha256:76d8370c006f5026cdd638a397a678d26dda735a3c88137e05885a020f824034", size = 28293, upload-time = "2025-08-09T19:25:04.6Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/1e/29/db1d855a661c02dbde5cab3057969133fcc62e7a0c6393e48fe9d0e81679/pre_commit_hooks-5.0.0-py2.py3-none-any.whl", hash = "sha256:8d71cfb582c5c314a5498d94e0104b6567a8b93fb35903ea845c491f4e290a7a", size = 41245, upload-time = "2024-10-05T18:43:09.901Z" }, + { url = "https://files.pythonhosted.org/packages/12/46/eba9be9daa403fa94854ce16a458c29df9a01c6c047931c3d8be6016cd9a/pre_commit_hooks-6.0.0-py2.py3-none-any.whl", hash = "sha256:76161b76d321d2f8ee2a8e0b84c30ee8443e01376121fd1c90851e33e3bd7ee2", size = 41338, upload-time = "2025-08-09T19:25:03.513Z" }, ] [[package]] @@ -1587,16 +1544,16 @@ wheels = [ [[package]] name = "protobuf" -version = "6.31.1" +version = "6.32.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c0/df/fb4a8eeea482eca989b51cffd274aac2ee24e825f0bf3cbce5281fa1567b/protobuf-6.32.0.tar.gz", hash = "sha256:a81439049127067fc49ec1d36e25c6ee1d1a2b7be930675f919258d03c04e7d2", size = 440614, upload-time = "2025-08-14T21:21:25.015Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, - { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, - { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, - { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, - { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, - { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, + { url = "https://files.pythonhosted.org/packages/33/18/df8c87da2e47f4f1dcc5153a81cd6bca4e429803f4069a299e236e4dd510/protobuf-6.32.0-cp310-abi3-win32.whl", hash = "sha256:84f9e3c1ff6fb0308dbacb0950d8aa90694b0d0ee68e75719cb044b7078fe741", size = 424409, upload-time = "2025-08-14T21:21:12.366Z" }, + { url = "https://files.pythonhosted.org/packages/e1/59/0a820b7310f8139bd8d5a9388e6a38e1786d179d6f33998448609296c229/protobuf-6.32.0-cp310-abi3-win_amd64.whl", hash = "sha256:a8bdbb2f009cfc22a36d031f22a625a38b615b5e19e558a7b756b3279723e68e", size = 435735, upload-time = "2025-08-14T21:21:15.046Z" }, + { url = "https://files.pythonhosted.org/packages/cc/5b/0d421533c59c789e9c9894683efac582c06246bf24bb26b753b149bd88e4/protobuf-6.32.0-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d52691e5bee6c860fff9a1c86ad26a13afbeb4b168cd4445c922b7e2cf85aaf0", size = 426449, upload-time = "2025-08-14T21:21:16.687Z" }, + { url = "https://files.pythonhosted.org/packages/ec/7b/607764ebe6c7a23dcee06e054fd1de3d5841b7648a90fd6def9a3bb58c5e/protobuf-6.32.0-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:501fe6372fd1c8ea2a30b4d9be8f87955a64d6be9c88a973996cef5ef6f0abf1", size = 322869, upload-time = "2025-08-14T21:21:18.282Z" }, + { url = "https://files.pythonhosted.org/packages/40/01/2e730bd1c25392fc32e3268e02446f0d77cb51a2c3a8486b1798e34d5805/protobuf-6.32.0-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:75a2aab2bd1aeb1f5dc7c5f33bcb11d82ea8c055c9becbb41c26a8c43fd7092c", size = 322009, upload-time = "2025-08-14T21:21:19.893Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f2/80ffc4677aac1bc3519b26bc7f7f5de7fce0ee2f7e36e59e27d8beb32dd1/protobuf-6.32.0-py3-none-any.whl", hash = "sha256:ba377e5b67b908c8f3072a57b63e2c6a4cbd18aea4ed98d2584350dbf46f2783", size = 169287, upload-time = "2025-08-14T21:21:23.515Z" }, ] [[package]] @@ -1614,32 +1571,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, ] -[[package]] -name = "pyarrow" -version = "20.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a2/ee/a7810cb9f3d6e9238e61d312076a9859bf3668fd21c69744de9532383912/pyarrow-20.0.0.tar.gz", hash = "sha256:febc4a913592573c8d5805091a6c2b5064c8bd6e002131f01061797d91c783c1", size = 1125187, upload-time = "2025-04-27T12:34:23.264Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/47/a2/b7930824181ceadd0c63c1042d01fa4ef63eee233934826a7a2a9af6e463/pyarrow-20.0.0-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:24ca380585444cb2a31324c546a9a56abbe87e26069189e14bdba19c86c049f0", size = 30856035, upload-time = "2025-04-27T12:28:40.78Z" }, - { url = "https://files.pythonhosted.org/packages/9b/18/c765770227d7f5bdfa8a69f64b49194352325c66a5c3bb5e332dfd5867d9/pyarrow-20.0.0-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:95b330059ddfdc591a3225f2d272123be26c8fa76e8c9ee1a77aad507361cfdb", size = 32309552, upload-time = "2025-04-27T12:28:47.051Z" }, - { url = "https://files.pythonhosted.org/packages/44/fb/dfb2dfdd3e488bb14f822d7335653092dde150cffc2da97de6e7500681f9/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5f0fb1041267e9968c6d0d2ce3ff92e3928b243e2b6d11eeb84d9ac547308232", size = 41334704, upload-time = "2025-04-27T12:28:55.064Z" }, - { url = "https://files.pythonhosted.org/packages/58/0d/08a95878d38808051a953e887332d4a76bc06c6ee04351918ee1155407eb/pyarrow-20.0.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8ff87cc837601532cc8242d2f7e09b4e02404de1b797aee747dd4ba4bd6313f", size = 42399836, upload-time = "2025-04-27T12:29:02.13Z" }, - { url = "https://files.pythonhosted.org/packages/f3/cd/efa271234dfe38f0271561086eedcad7bc0f2ddd1efba423916ff0883684/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:7a3a5dcf54286e6141d5114522cf31dd67a9e7c9133d150799f30ee302a7a1ab", size = 40711789, upload-time = "2025-04-27T12:29:09.951Z" }, - { url = "https://files.pythonhosted.org/packages/46/1f/7f02009bc7fc8955c391defee5348f510e589a020e4b40ca05edcb847854/pyarrow-20.0.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a6ad3e7758ecf559900261a4df985662df54fb7fdb55e8e3b3aa99b23d526b62", size = 42301124, upload-time = "2025-04-27T12:29:17.187Z" }, - { url = "https://files.pythonhosted.org/packages/4f/92/692c562be4504c262089e86757a9048739fe1acb4024f92d39615e7bab3f/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:6bb830757103a6cb300a04610e08d9636f0cd223d32f388418ea893a3e655f1c", size = 42916060, upload-time = "2025-04-27T12:29:24.253Z" }, - { url = "https://files.pythonhosted.org/packages/a4/ec/9f5c7e7c828d8e0a3c7ef50ee62eca38a7de2fa6eb1b8fa43685c9414fef/pyarrow-20.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:96e37f0766ecb4514a899d9a3554fadda770fb57ddf42b63d80f14bc20aa7db3", size = 44547640, upload-time = "2025-04-27T12:29:32.782Z" }, - { url = "https://files.pythonhosted.org/packages/54/96/46613131b4727f10fd2ffa6d0d6f02efcc09a0e7374eff3b5771548aa95b/pyarrow-20.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:3346babb516f4b6fd790da99b98bed9708e3f02e734c84971faccb20736848dc", size = 25781491, upload-time = "2025-04-27T12:29:38.464Z" }, - { url = "https://files.pythonhosted.org/packages/a1/d6/0c10e0d54f6c13eb464ee9b67a68b8c71bcf2f67760ef5b6fbcddd2ab05f/pyarrow-20.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:75a51a5b0eef32727a247707d4755322cb970be7e935172b6a3a9f9ae98404ba", size = 30815067, upload-time = "2025-04-27T12:29:44.384Z" }, - { url = "https://files.pythonhosted.org/packages/7e/e2/04e9874abe4094a06fd8b0cbb0f1312d8dd7d707f144c2ec1e5e8f452ffa/pyarrow-20.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:211d5e84cecc640c7a3ab900f930aaff5cd2702177e0d562d426fb7c4f737781", size = 32297128, upload-time = "2025-04-27T12:29:52.038Z" }, - { url = "https://files.pythonhosted.org/packages/31/fd/c565e5dcc906a3b471a83273039cb75cb79aad4a2d4a12f76cc5ae90a4b8/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4ba3cf4182828be7a896cbd232aa8dd6a31bd1f9e32776cc3796c012855e1199", size = 41334890, upload-time = "2025-04-27T12:29:59.452Z" }, - { url = "https://files.pythonhosted.org/packages/af/a9/3bdd799e2c9b20c1ea6dc6fa8e83f29480a97711cf806e823f808c2316ac/pyarrow-20.0.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c3a01f313ffe27ac4126f4c2e5ea0f36a5fc6ab51f8726cf41fee4b256680bd", size = 42421775, upload-time = "2025-04-27T12:30:06.875Z" }, - { url = "https://files.pythonhosted.org/packages/10/f7/da98ccd86354c332f593218101ae56568d5dcedb460e342000bd89c49cc1/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:a2791f69ad72addd33510fec7bb14ee06c2a448e06b649e264c094c5b5f7ce28", size = 40687231, upload-time = "2025-04-27T12:30:13.954Z" }, - { url = "https://files.pythonhosted.org/packages/bb/1b/2168d6050e52ff1e6cefc61d600723870bf569cbf41d13db939c8cf97a16/pyarrow-20.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:4250e28a22302ce8692d3a0e8ec9d9dde54ec00d237cff4dfa9c1fbf79e472a8", size = 42295639, upload-time = "2025-04-27T12:30:21.949Z" }, - { url = "https://files.pythonhosted.org/packages/b2/66/2d976c0c7158fd25591c8ca55aee026e6d5745a021915a1835578707feb3/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:89e030dc58fc760e4010148e6ff164d2f44441490280ef1e97a542375e41058e", size = 42908549, upload-time = "2025-04-27T12:30:29.551Z" }, - { url = "https://files.pythonhosted.org/packages/31/a9/dfb999c2fc6911201dcbf348247f9cc382a8990f9ab45c12eabfd7243a38/pyarrow-20.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:6102b4864d77102dbbb72965618e204e550135a940c2534711d5ffa787df2a5a", size = 44557216, upload-time = "2025-04-27T12:30:36.977Z" }, - { url = "https://files.pythonhosted.org/packages/a0/8e/9adee63dfa3911be2382fb4d92e4b2e7d82610f9d9f668493bebaa2af50f/pyarrow-20.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:96d6a0a37d9c98be08f5ed6a10831d88d52cac7b13f5287f1e0f625a0de8062b", size = 25660496, upload-time = "2025-04-27T12:30:42.809Z" }, -] - [[package]] name = "pyaudio" version = "0.2.14" @@ -1845,164 +1776,164 @@ name = "pyobjc" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-accessibility", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-accounts", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-addressbook" }, - { name = "pyobjc-framework-adservices", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-adsupport", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-applescriptkit" }, - { name = "pyobjc-framework-applescriptobjc", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-applicationservices" }, - { name = "pyobjc-framework-apptrackingtransparency", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-audiovideobridging", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-authenticationservices", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-automaticassessmentconfiguration", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-automator" }, - { name = "pyobjc-framework-avfoundation", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-avkit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-avrouting", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-backgroundassets", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-browserenginekit", marker = "platform_release >= '23.4'" }, - { name = "pyobjc-framework-businesschat", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-calendarstore", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-callkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-carbon" }, - { name = "pyobjc-framework-cfnetwork" }, - { name = "pyobjc-framework-cinematic", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-classkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-cloudkit", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-collaboration", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-colorsync", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-contacts", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-contactsui", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coreaudiokit" }, - { name = "pyobjc-framework-corebluetooth", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-coredata" }, - { name = "pyobjc-framework-corehaptics", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-corelocation", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-coremedia", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-coremediaio", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-coremidi" }, - { name = "pyobjc-framework-coreml", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-coremotion", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-coreservices" }, - { name = "pyobjc-framework-corespotlight", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-coretext" }, - { name = "pyobjc-framework-corewlan", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-cryptotokenkit", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-datadetection", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-devicecheck", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-devicediscoveryextension", marker = "platform_release >= '24.0'" }, - { name = "pyobjc-framework-dictionaryservices", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-discrecording" }, - { name = "pyobjc-framework-discrecordingui" }, - { name = "pyobjc-framework-diskarbitration" }, - { name = "pyobjc-framework-dvdplayback" }, - { name = "pyobjc-framework-eventkit", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-exceptionhandling" }, - { name = "pyobjc-framework-executionpolicy", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-extensionkit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-externalaccessory", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-fileprovider", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-fileproviderui", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-findersync", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-fsevents", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-fskit", marker = "platform_release >= '24.4'" }, - { name = "pyobjc-framework-gamecenter", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-gamecontroller", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-gamekit", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-gameplaykit", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-healthkit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-imagecapturecore", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-inputmethodkit", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-installerplugins" }, - { name = "pyobjc-framework-instantmessage", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-intents", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-intentsui", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-iobluetooth" }, - { name = "pyobjc-framework-iobluetoothui" }, - { name = "pyobjc-framework-iosurface", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-ituneslibrary", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-kernelmanagement", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-latentsemanticmapping" }, - { name = "pyobjc-framework-launchservices" }, - { name = "pyobjc-framework-libdispatch", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-libxpc", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-linkpresentation", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-localauthentication", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-localauthenticationembeddedui", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mailkit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mapkit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaaccessibility", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaextension", marker = "platform_release >= '24.0'" }, - { name = "pyobjc-framework-medialibrary", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-mediaplayer", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-mediatoolbox", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-metal", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-metalfx", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-metalkit", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-metalperformanceshaders", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-metalperformanceshadersgraph", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-metrickit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-mlcompute", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-modelio", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-multipeerconnectivity", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-naturallanguage", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-netfs", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-network", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-networkextension", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-notificationcenter", marker = "platform_release >= '14.0'" }, - { name = "pyobjc-framework-opendirectory", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-osakit" }, - { name = "pyobjc-framework-oslog", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-passkit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-pencilkit", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-phase", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-photos", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-photosui", marker = "platform_release >= '15.0'" }, - { name = "pyobjc-framework-preferencepanes" }, - { name = "pyobjc-framework-pushkit", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-quartz" }, - { name = "pyobjc-framework-quicklookthumbnailing", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-replaykit", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-safariservices", marker = "platform_release >= '16.0'" }, - { name = "pyobjc-framework-safetykit", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-scenekit", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-screencapturekit", marker = "platform_release >= '21.4'" }, - { name = "pyobjc-framework-screensaver" }, - { name = "pyobjc-framework-screentime", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-scriptingbridge", marker = "platform_release >= '9.0'" }, - { name = "pyobjc-framework-searchkit" }, - { name = "pyobjc-framework-security" }, - { name = "pyobjc-framework-securityfoundation" }, - { name = "pyobjc-framework-securityinterface" }, - { name = "pyobjc-framework-securityui", marker = "platform_release >= '24.4'" }, - { name = "pyobjc-framework-sensitivecontentanalysis", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-servicemanagement", marker = "platform_release >= '10.0'" }, - { name = "pyobjc-framework-sharedwithyou", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-sharedwithyoucore", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-shazamkit", marker = "platform_release >= '21.0'" }, - { name = "pyobjc-framework-social", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-soundanalysis", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-speech", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-spritekit", marker = "platform_release >= '13.0'" }, - { name = "pyobjc-framework-storekit", marker = "platform_release >= '11.0'" }, - { name = "pyobjc-framework-symbols", marker = "platform_release >= '23.0'" }, - { name = "pyobjc-framework-syncservices" }, - { name = "pyobjc-framework-systemconfiguration" }, - { name = "pyobjc-framework-systemextensions", marker = "platform_release >= '19.0'" }, - { name = "pyobjc-framework-threadnetwork", marker = "platform_release >= '22.0'" }, - { name = "pyobjc-framework-uniformtypeidentifiers", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-usernotifications", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-usernotificationsui", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-videosubscriberaccount", marker = "platform_release >= '18.0'" }, - { name = "pyobjc-framework-videotoolbox", marker = "platform_release >= '12.0'" }, - { name = "pyobjc-framework-virtualization", marker = "platform_release >= '20.0'" }, - { name = "pyobjc-framework-vision", marker = "platform_release >= '17.0'" }, - { name = "pyobjc-framework-webkit" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-accessibility", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-accounts", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-addressbook", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-adservices", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-adsupport", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-applescriptkit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-applescriptobjc", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-applicationservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-apptrackingtransparency", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-audiovideobridging", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-authenticationservices", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-automaticassessmentconfiguration", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-automator", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avkit", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avrouting", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-backgroundassets", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-browserenginekit", marker = "platform_release >= '23.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-businesschat", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-calendarstore", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-callkit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-carbon", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cfnetwork", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cinematic", marker = "platform_release >= '23.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-classkit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cloudkit", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-collaboration", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-colorsync", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-contacts", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-contactsui", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudiokit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corebluetooth", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coredata", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corehaptics", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corelocation", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremediaio", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremidi", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreml", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremotion", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corespotlight", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corewlan", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cryptotokenkit", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-datadetection", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-devicecheck", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-devicediscoveryextension", marker = "platform_release >= '24.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-dictionaryservices", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-discrecording", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-discrecordingui", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-diskarbitration", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-dvdplayback", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-eventkit", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-exceptionhandling", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-executionpolicy", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-extensionkit", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-externalaccessory", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fileprovider", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fileproviderui", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-findersync", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fsevents", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fskit", marker = "platform_release >= '24.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gamecenter", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gamecontroller", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gamekit", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-gameplaykit", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-healthkit", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-imagecapturecore", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-inputmethodkit", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-installerplugins", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-instantmessage", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-intents", marker = "platform_release >= '16.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-intentsui", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iobluetooth", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iobluetoothui", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iosurface", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-ituneslibrary", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-kernelmanagement", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-latentsemanticmapping", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-launchservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libdispatch", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-libxpc", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-linkpresentation", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-localauthentication", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-localauthenticationembeddedui", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mailkit", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mapkit", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediaaccessibility", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediaextension", marker = "platform_release >= '24.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-medialibrary", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediaplayer", marker = "platform_release >= '16.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mediatoolbox", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalfx", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalkit", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalperformanceshaders", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalperformanceshadersgraph", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metrickit", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-mlcompute", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-modelio", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-multipeerconnectivity", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-naturallanguage", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-netfs", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-network", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-networkextension", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-notificationcenter", marker = "platform_release >= '14.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-opendirectory", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-osakit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-oslog", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-passkit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-pencilkit", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-phase", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-photos", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-photosui", marker = "platform_release >= '15.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-preferencepanes", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-pushkit", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quicklookthumbnailing", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-replaykit", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-safariservices", marker = "platform_release >= '16.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-safetykit", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-scenekit", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-screencapturekit", marker = "platform_release >= '21.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-screensaver", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-screentime", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-scriptingbridge", marker = "platform_release >= '9.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-searchkit", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-securityfoundation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-securityinterface", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-securityui", marker = "platform_release >= '24.4' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sensitivecontentanalysis", marker = "platform_release >= '23.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-servicemanagement", marker = "platform_release >= '10.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sharedwithyou", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sharedwithyoucore", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-shazamkit", marker = "platform_release >= '21.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-social", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-soundanalysis", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-speech", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-spritekit", marker = "platform_release >= '13.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-storekit", marker = "platform_release >= '11.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-symbols", marker = "platform_release >= '23.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-syncservices", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-systemconfiguration", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-systemextensions", marker = "platform_release >= '19.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-threadnetwork", marker = "platform_release >= '22.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-uniformtypeidentifiers", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-usernotifications", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-usernotificationsui", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-videosubscriberaccount", marker = "platform_release >= '18.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-videotoolbox", marker = "platform_release >= '12.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-virtualization", marker = "platform_release >= '20.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-vision", marker = "platform_release >= '17.0' and sys_platform == 'darwin'" }, + { name = "pyobjc-framework-webkit", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/5e/16bc372806790d295c76b5c7851767cc9ee3787b3e581f5d7cc44158e4e0/pyobjc-11.1.tar.gz", hash = "sha256:a71b14389657811d658526ba4d5faba4ef7eadbddcf9fe8bf4fb3a6261effba3", size = 11161, upload-time = "2025-06-14T20:56:32.819Z" } wheels = [ @@ -2024,9 +1955,9 @@ name = "pyobjc-framework-accessibility" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b4/10c16e9d48568a68da2f61866b19468d4ac7129c377d4b1333ee936ae5d0/pyobjc_framework_accessibility-11.1.tar.gz", hash = "sha256:c0fa5f1e00906ec002f582c7d3d80463a46d19f672bf5ec51144f819eeb40656", size = 45098, upload-time = "2025-06-14T20:56:35.287Z" } wheels = [ @@ -2039,8 +1970,8 @@ name = "pyobjc-framework-accounts" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/12/45/ca21003f68ad0f13b5a9ac1761862ad2ddd83224b4314a2f7d03ca437c8d/pyobjc_framework_accounts-11.1.tar.gz", hash = "sha256:384fec156e13ff75253bb094339013f4013464f6dfd47e2f7de3e2ae7441c030", size = 17086, upload-time = "2025-06-14T20:56:36.035Z" } wheels = [ @@ -2052,8 +1983,8 @@ name = "pyobjc-framework-addressbook" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/d3/f5bb5c72be5c6e52224f43e23e5a44e86d2c35ee9af36939e5514c6c7a0f/pyobjc_framework_addressbook-11.1.tar.gz", hash = "sha256:ce2db3be4a3128bf79d5c41319a6d16b73754785ce75ac694d0d658c690922fc", size = 97609, upload-time = "2025-06-14T20:56:37.324Z" } wheels = [ @@ -2066,8 +1997,8 @@ name = "pyobjc-framework-adservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/3f/af76eab6eee0a405a4fdee172e7181773040158476966ecd757b0a98bfc5/pyobjc_framework_adservices-11.1.tar.gz", hash = "sha256:44c72f8163705c9aa41baca938fdb17dde257639e5797e6a5c3a2b2d8afdade9", size = 12473, upload-time = "2025-06-14T20:56:38.147Z" } wheels = [ @@ -2079,8 +2010,8 @@ name = "pyobjc-framework-adsupport" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7f/03/9c51edd964796a97def4e1433d76a128dd7059b685fb4366081bf4e292ba/pyobjc_framework_adsupport-11.1.tar.gz", hash = "sha256:78b9667c275785df96219d205bd4309731869c3298d0931e32aed83bede29096", size = 12556, upload-time = "2025-06-14T20:56:38.741Z" } wheels = [ @@ -2092,8 +2023,8 @@ name = "pyobjc-framework-applescriptkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bc/63/1bcfcdca53bf5bba3a7b4d73d24232ae1721a378a32fd4ebc34a35549df2/pyobjc_framework_applescriptkit-11.1.tar.gz", hash = "sha256:477707352eaa6cc4a5f8c593759dc3227a19d5958481b1482f0d59394a4601c3", size = 12392, upload-time = "2025-06-14T20:56:39.331Z" } wheels = [ @@ -2105,8 +2036,8 @@ name = "pyobjc-framework-applescriptobjc" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a3/27/687b55b575367df045879b786f358355e40e41f847968e557d0718a6c4a4/pyobjc_framework_applescriptobjc-11.1.tar.gz", hash = "sha256:c8a0ec975b64411a4f16a1280c5ea8dbe949fd361e723edd343102f0f95aba6e", size = 12445, upload-time = "2025-06-14T20:56:39.976Z" } wheels = [ @@ -2118,10 +2049,10 @@ name = "pyobjc-framework-applicationservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coretext" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coretext", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/3f/b33ce0cecc3a42f6c289dcbf9ff698b0d9e85f5796db2e9cb5dadccffbb9/pyobjc_framework_applicationservices-11.1.tar.gz", hash = "sha256:03fcd8c0c600db98fa8b85eb7b3bc31491701720c795e3f762b54e865138bbaf", size = 224842, upload-time = "2025-06-14T20:56:40.648Z" } wheels = [ @@ -2134,8 +2065,8 @@ name = "pyobjc-framework-apptrackingtransparency" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/49/68/7aa3afffd038dd6e5af764336bca734eb910121013ca71030457b61e5b99/pyobjc_framework_apptrackingtransparency-11.1.tar.gz", hash = "sha256:796cc5f83346c10973806cfb535d4200b894a5d2626ff2eeb1972d594d14fed4", size = 13135, upload-time = "2025-06-14T20:56:41.494Z" } wheels = [ @@ -2147,8 +2078,8 @@ name = "pyobjc-framework-audiovideobridging" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c3/25/6c5a7b1443d30139cc722029880284ea9dfa575f0436471b9364fcd499f5/pyobjc_framework_audiovideobridging-11.1.tar.gz", hash = "sha256:12756b3aa35083b8ad5c9139b6a0e2f4792e217096b5bf6b702d499038203991", size = 72913, upload-time = "2025-06-14T20:56:42.128Z" } wheels = [ @@ -2161,8 +2092,8 @@ name = "pyobjc-framework-authenticationservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8f/b7/3e9ad0ed3625dc02e495615ea5dbf55ca95cbd25b3e31f25092f5caad640/pyobjc_framework_authenticationservices-11.1.tar.gz", hash = "sha256:8fd801cdb53d426b4e678b0a8529c005d0c44f5a17ccd7052a7c3a1a87caed6a", size = 115266, upload-time = "2025-06-14T20:56:42.889Z" } wheels = [ @@ -2175,8 +2106,8 @@ name = "pyobjc-framework-automaticassessmentconfiguration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/39/d4c94e0245d290b83919854c4f205851cc0b2603f843448fdfb8e74aad71/pyobjc_framework_automaticassessmentconfiguration-11.1.tar.gz", hash = "sha256:70eadbf8600101901a56fcd7014d8941604e14f3b3728bc4fb0178a9a9420032", size = 24933, upload-time = "2025-06-14T20:56:43.984Z" } wheels = [ @@ -2189,8 +2120,8 @@ name = "pyobjc-framework-automator" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/63/9f/097ed9f4de9e9491a1b08bb7d85d35a95d726c9e9f5f5bf203b359a436b6/pyobjc_framework_automator-11.1.tar.gz", hash = "sha256:9b46c55a4f9ae2b3c39ff560f42ced66bdd18c093188f0b5fc4060ad911838e4", size = 201439, upload-time = "2025-06-14T20:56:44.767Z" } wheels = [ @@ -2203,11 +2134,11 @@ name = "pyobjc-framework-avfoundation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3c/1f/90cdbce1d3b4861cbb17c12adf57daeec32477eb1df8d3f9ab8551bdadfb/pyobjc_framework_avfoundation-11.1.tar.gz", hash = "sha256:6663056cc6ca49af8de6d36a7fff498f51e1a9a7f1bde7afba718a8ceaaa7377", size = 832178, upload-time = "2025-06-14T20:56:46.329Z" } wheels = [ @@ -2220,9 +2151,9 @@ name = "pyobjc-framework-avkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/61/ff/9f41f2b8de786871184b48c4e5052cb7c9fcc204e7fee06687fa32b08bed/pyobjc_framework_avkit-11.1.tar.gz", hash = "sha256:d948204a7b94e0e878b19a909f9b33342e19d9ea519571d66a21fce8f72e3263", size = 46825, upload-time = "2025-06-14T20:56:47.494Z" } wheels = [ @@ -2235,8 +2166,8 @@ name = "pyobjc-framework-avrouting" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cf/42/94bc18b968a4ee8b6427257f907ffbfc97f8ba6a6202953da149b649d638/pyobjc_framework_avrouting-11.1.tar.gz", hash = "sha256:7db1291d9f53cc58d34b2a826feb721a85f50ceb5e71952e8762baacd3db3fc0", size = 21069, upload-time = "2025-06-14T20:56:48.57Z" } wheels = [ @@ -2249,8 +2180,8 @@ name = "pyobjc-framework-backgroundassets" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/08/76/21e1632a212f997d7a5f26d53eb997951978916858039b79f43ebe3d10b2/pyobjc_framework_backgroundassets-11.1.tar.gz", hash = "sha256:2e14b50539d96d5fca70c49f21b69fdbad81a22549e3630f5e4f20d5c0204fc2", size = 24803, upload-time = "2025-06-14T20:56:49.566Z" } wheels = [ @@ -2263,11 +2194,11 @@ name = "pyobjc-framework-browserenginekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/30/75/087270d9f81e913b57c7db58eaff8691fa0574b11faf9302340b3b8320f1/pyobjc_framework_browserenginekit-11.1.tar.gz", hash = "sha256:918440cefb10480024f645169de3733e30ede65e41267fa12c7b90c264a0a479", size = 31944, upload-time = "2025-06-14T20:56:50.195Z" } wheels = [ @@ -2280,8 +2211,8 @@ name = "pyobjc-framework-businesschat" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/85/be/9d9d9d9383c411a58323ea510d768443287ca21610af652b815b3205ea80/pyobjc_framework_businesschat-11.1.tar.gz", hash = "sha256:69589d2f0cb4e7892e5ecc6aed79b1abd1ec55c099a7faacae6a326bc921259d", size = 12698, upload-time = "2025-06-14T20:56:51.173Z" } wheels = [ @@ -2293,8 +2224,8 @@ name = "pyobjc-framework-calendarstore" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/41/df/7ca8ee65b16d5fc862d7e8664289472eed918cf4d76921de6bdaa1461c65/pyobjc_framework_calendarstore-11.1.tar.gz", hash = "sha256:858ee00e6a380d9c086c2d7db82c116a6c406234038e0ec8fc2ad02e385dc437", size = 68215, upload-time = "2025-06-14T20:56:51.799Z" } wheels = [ @@ -2306,8 +2237,8 @@ name = "pyobjc-framework-callkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/51/d5/4f0b62ab35be619e8c8d96538a03cf56fde6fd53540e1837e0fa588b3f6c/pyobjc_framework_callkit-11.1.tar.gz", hash = "sha256:b84d5ea38dff0cbe0754f5f9f6f33c742e216f12e7166179a8ec2cf4b0bfca94", size = 46648, upload-time = "2025-06-14T20:56:52.579Z" } wheels = [ @@ -2320,8 +2251,8 @@ name = "pyobjc-framework-carbon" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/a4/d751851865d9a78405cfec0c8b2931b1e96b9914e9788cd441fa4e8290d0/pyobjc_framework_carbon-11.1.tar.gz", hash = "sha256:047f098535479efa3ab89da1ebdf3cf9ec0b439a33a4f32806193886e9fcea71", size = 37291, upload-time = "2025-06-14T20:56:53.642Z" } wheels = [ @@ -2333,8 +2264,8 @@ name = "pyobjc-framework-cfnetwork" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6f/49/7b24172e3d6eb0ddffc33a7498a2bea264aa2958c3fecaeb463bef88f0b8/pyobjc_framework_cfnetwork-11.1.tar.gz", hash = "sha256:ad600163eeadb7bf71abc51a9b6f2b5462a018d3f9bb1510c5ce3fdf2f22959d", size = 79069, upload-time = "2025-06-14T20:56:54.615Z" } wheels = [ @@ -2347,11 +2278,11 @@ name = "pyobjc-framework-cinematic" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/6f/c2d0b49e01e654496a1781bafb9da72a6fbd00f5abb39dc4a3a0045167c7/pyobjc_framework_cinematic-11.1.tar.gz", hash = "sha256:efde39a6a2379e1738dbc5434b2470cd187cf3114ffb81390b3b1abda470b382", size = 25522, upload-time = "2025-06-14T20:56:55.379Z" } wheels = [ @@ -2363,8 +2294,8 @@ name = "pyobjc-framework-classkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7a/8b/5150b4faddd15d5dd795bc62b2256c4f7dafc983cfa694fcf88121ea0016/pyobjc_framework_classkit-11.1.tar.gz", hash = "sha256:ee1e26395eb00b3ed5442e3234cdbfe925d2413185af38eca0477d7166651df4", size = 39831, upload-time = "2025-06-14T20:56:56.036Z" } wheels = [ @@ -2377,11 +2308,11 @@ name = "pyobjc-framework-cloudkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-accounts" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coredata" }, - { name = "pyobjc-framework-corelocation" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-accounts", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coredata", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corelocation", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/58/a6/bfe5be55ed95704efca0e86b218155a9c801735107cedba3af8ea4580a05/pyobjc_framework_cloudkit-11.1.tar.gz", hash = "sha256:40d2dc4bf28c5be9b836b01e4d267a15d847d756c2a65530e1fcd79b2825e86d", size = 122778, upload-time = "2025-06-14T20:56:56.73Z" } wheels = [ @@ -2393,7 +2324,7 @@ name = "pyobjc-framework-cocoa" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4b/c5/7a866d24bc026f79239b74d05e2cf3088b03263da66d53d1b4cf5207f5ae/pyobjc_framework_cocoa-11.1.tar.gz", hash = "sha256:87df76b9b73e7ca699a828ff112564b59251bb9bbe72e610e670a4dc9940d038", size = 5565335, upload-time = "2025-06-14T20:56:59.683Z" } wheels = [ @@ -2406,8 +2337,8 @@ name = "pyobjc-framework-collaboration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/66/49/9dbe8407d5dd663747267c1234d1b914bab66e1878d22f57926261a3063b/pyobjc_framework_collaboration-11.1.tar.gz", hash = "sha256:4564e3931bfc51773623d4f57f2431b58a39b75cb964ae5c48d27ee4dde2f4ea", size = 16839, upload-time = "2025-06-14T20:57:01.101Z" } wheels = [ @@ -2419,8 +2350,8 @@ name = "pyobjc-framework-colorsync" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b5/97/7613b6041f62c52f972e42dd5d79476b56b84d017a8b5e4add4d9cfaca36/pyobjc_framework_colorsync-11.1.tar.gz", hash = "sha256:7a346f71f34b2ccd1b020a34c219b85bf8b6f6e05283d503185aeb7767a269dd", size = 38999, upload-time = "2025-06-14T20:57:01.761Z" } wheels = [ @@ -2432,8 +2363,8 @@ name = "pyobjc-framework-contacts" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a6/85/34868b6447d552adf8674bac226b55c2baacacee0d67ee031e33805d6faa/pyobjc_framework_contacts-11.1.tar.gz", hash = "sha256:752036e7d8952a4122296d7772f274170a5f35a53ee6454a27f3e1d9603222cc", size = 84814, upload-time = "2025-06-14T20:57:02.582Z" } wheels = [ @@ -2446,9 +2377,9 @@ name = "pyobjc-framework-contactsui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-contacts" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-contacts", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3f/57/8765b54a30edaa2a56df62e11e7c32e41b6ea300513256adffa191689368/pyobjc_framework_contactsui-11.1.tar.gz", hash = "sha256:5bc29ea2b10a342018e1b96be6b140c10ebe3cfb6417278770feef5e88026a1f", size = 20031, upload-time = "2025-06-14T20:57:03.603Z" } wheels = [ @@ -2461,8 +2392,8 @@ name = "pyobjc-framework-coreaudio" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/39/c0/4ab6005cf97e534725b0c14b110d4864b367c282b1c5b0d8f42aad74a83f/pyobjc_framework_coreaudio-11.1.tar.gz", hash = "sha256:b7b89540ae7efc6c1e3208ac838ef2acfc4d2c506dd629d91f6b3b3120e55c1b", size = 141032, upload-time = "2025-06-14T20:57:04.348Z" } wheels = [ @@ -2475,9 +2406,9 @@ name = "pyobjc-framework-coreaudiokit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreaudio" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreaudio", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/4e/c49b26c60047c511727efe994b412276c487dfe90f1ee0fced0bddbdf8a3/pyobjc_framework_coreaudiokit-11.1.tar.gz", hash = "sha256:0b461c3d6123fda4da6b6aaa022efc918c1de2e126a5cf07d2189d63fa54ba40", size = 21955, upload-time = "2025-06-14T20:57:05.218Z" } wheels = [ @@ -2490,8 +2421,8 @@ name = "pyobjc-framework-corebluetooth" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/3d/fe/2081dfd9413b7b4d719935c33762fbed9cce9dc06430f322d1e2c9dbcd91/pyobjc_framework_corebluetooth-11.1.tar.gz", hash = "sha256:1deba46e3fcaf5e1c314f4bbafb77d9fe49ec248c493ad00d8aff2df212d6190", size = 60337, upload-time = "2025-06-14T20:57:05.919Z" } wheels = [ @@ -2504,8 +2435,8 @@ name = "pyobjc-framework-coredata" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/00/e3/af497da7a7c895b6ff529d709d855a783f34afcc4b87ab57a1a2afb3f876/pyobjc_framework_coredata-11.1.tar.gz", hash = "sha256:fe9fd985f8e06c70c0fb1e6bbea5b731461f9e76f8f8d8e89c7c72667cdc6adf", size = 260628, upload-time = "2025-06-14T20:57:06.729Z" } wheels = [ @@ -2518,8 +2449,8 @@ name = "pyobjc-framework-corehaptics" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5f/83/cc997ec4687a68214dd3ad1bdf64353305f5c7e827fad211adac4c28b39f/pyobjc_framework_corehaptics-11.1.tar.gz", hash = "sha256:e5da3a97ed6aca9b7268c8c5196c0a339773a50baa72d1502d3435dc1a2a80f1", size = 42722, upload-time = "2025-06-14T20:57:08.019Z" } wheels = [ @@ -2531,8 +2462,8 @@ name = "pyobjc-framework-corelocation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/ef/fbd2e01ec137208af7bfefe222773748d27f16f845b0efa950d65e2bd719/pyobjc_framework_corelocation-11.1.tar.gz", hash = "sha256:46a67b99925ee3d53914331759c6ee110b31bb790b74b05915acfca41074c206", size = 104508, upload-time = "2025-06-14T20:57:08.731Z" } wheels = [ @@ -2545,8 +2476,8 @@ name = "pyobjc-framework-coremedia" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/95/5d/81513acd219df77a89176f1574d936b81ad6f6002225cabb64d55efb7e8d/pyobjc_framework_coremedia-11.1.tar.gz", hash = "sha256:82cdc087f61e21b761e677ea618a575d4c0dbe00e98230bf9cea540cff931db3", size = 216389, upload-time = "2025-06-14T20:57:09.546Z" } wheels = [ @@ -2559,8 +2490,8 @@ name = "pyobjc-framework-coremediaio" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/68/9cef2aefba8e69916049ff43120e8794df8051bdf1f690a55994bbe4eb57/pyobjc_framework_coremediaio-11.1.tar.gz", hash = "sha256:bccd69712578b177144ded398f4695d71a765ef61204da51a21f0c90b4ad4c64", size = 108326, upload-time = "2025-06-14T20:57:10.435Z" } wheels = [ @@ -2573,8 +2504,8 @@ name = "pyobjc-framework-coremidi" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/06/ca/2ae5149966ccd78290444f88fa62022e2b96ed2fddd47e71d9fd249a9f82/pyobjc_framework_coremidi-11.1.tar.gz", hash = "sha256:095030c59d50c23aa53608777102bc88744ff8b10dfb57afe24b428dcd12e376", size = 107817, upload-time = "2025-06-14T20:57:11.245Z" } wheels = [ @@ -2587,8 +2518,8 @@ name = "pyobjc-framework-coreml" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0d/5d/4309f220981d769b1a2f0dcb2c5c104490d31389a8ebea67e5595ce1cb74/pyobjc_framework_coreml-11.1.tar.gz", hash = "sha256:775923eefb9eac2e389c0821b10564372de8057cea89f1ea1cdaf04996c970a7", size = 82005, upload-time = "2025-06-14T20:57:12.004Z" } wheels = [ @@ -2601,8 +2532,8 @@ name = "pyobjc-framework-coremotion" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/95/e469dc7100ea6b9c29a074a4f713d78b32a78d7ec5498c25c83a56744fc2/pyobjc_framework_coremotion-11.1.tar.gz", hash = "sha256:5884a568521c0836fac39d46683a4dea3d259a23837920897042ffb922d9ac3e", size = 67050, upload-time = "2025-06-14T20:57:12.705Z" } wheels = [ @@ -2615,9 +2546,9 @@ name = "pyobjc-framework-coreservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-fsevents" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fsevents", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/a9/141d18019a25776f507992f9e7ffc051ca5a734848d8ea8d848f7c938efc/pyobjc_framework_coreservices-11.1.tar.gz", hash = "sha256:cf8eb5e272c60a96d025313eca26ff2487dcd02c47034cc9db39f6852d077873", size = 1245086, upload-time = "2025-06-14T20:57:13.914Z" } wheels = [ @@ -2630,8 +2561,8 @@ name = "pyobjc-framework-corespotlight" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/31/c7/b67ebfb63b7ccbfda780d583056d1fd4b610ba3839c8ebe3435b86122c61/pyobjc_framework_corespotlight-11.1.tar.gz", hash = "sha256:4dd363c8d3ff7619659b63dd31400f135b03e32435b5d151459ecdacea14e0f2", size = 87161, upload-time = "2025-06-14T20:57:14.934Z" } wheels = [ @@ -2644,9 +2575,9 @@ name = "pyobjc-framework-coretext" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/65/e9/d3231c4f87d07b8525401fd6ad3c56607c9e512c5490f0a7a6abb13acab6/pyobjc_framework_coretext-11.1.tar.gz", hash = "sha256:a29bbd5d85c77f46a8ee81d381b847244c88a3a5a96ac22f509027ceceaffaf6", size = 274702, upload-time = "2025-06-14T20:57:16.059Z" } wheels = [ @@ -2659,8 +2590,8 @@ name = "pyobjc-framework-corewlan" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/d8/03aff3c75485fc999e260946ef1e9adf17640a6e08d7bf603d31cfcf73fc/pyobjc_framework_corewlan-11.1.tar.gz", hash = "sha256:4a8afea75393cc0a6fe696e136233aa0ed54266f35a47b55a3583f4cb078e6ce", size = 65792, upload-time = "2025-06-14T20:57:16.931Z" } wheels = [ @@ -2673,8 +2604,8 @@ name = "pyobjc-framework-cryptotokenkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/92/7fab6fcc6bb659d6946cfb2f670058180bcc4ca1626878b0f7c95107abf0/pyobjc_framework_cryptotokenkit-11.1.tar.gz", hash = "sha256:5f82f44d9ab466c715a7c8ad4d5ec47c68aacd78bd67b5466a7b8215a2265328", size = 59223, upload-time = "2025-06-14T20:57:17.658Z" } wheels = [ @@ -2687,8 +2618,8 @@ name = "pyobjc-framework-datadetection" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7d/4d/65c61d8878b44689e28d5729be9edbb73e20b1b0500d1095172cfd24aea6/pyobjc_framework_datadetection-11.1.tar.gz", hash = "sha256:cbe0080b51e09b2f91eaf2a9babec3dcf2883d7966bc0abd8393ef7abfcfc5db", size = 13485, upload-time = "2025-06-14T20:57:18.829Z" } wheels = [ @@ -2700,8 +2631,8 @@ name = "pyobjc-framework-devicecheck" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f3/f2/b1d263f8231f815a9eeff15809f4b7428dacdc0a6aa267db5ed907445066/pyobjc_framework_devicecheck-11.1.tar.gz", hash = "sha256:8b05973eb2673571144d81346336e749a21cec90bd7fcaade76ffd3b147a0741", size = 13954, upload-time = "2025-06-14T20:57:19.782Z" } wheels = [ @@ -2713,8 +2644,8 @@ name = "pyobjc-framework-devicediscoveryextension" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9a/b8/102863bfa2f1e414c88bb9f51151a9a58b99c268a841b59d46e0dcc5fe6d/pyobjc_framework_devicediscoveryextension-11.1.tar.gz", hash = "sha256:ae160ea40f25d3ee5e7ce80ac9c1b315f94d0a4c7ccb86920396f71c6bf799a0", size = 14298, upload-time = "2025-06-14T20:57:20.738Z" } wheels = [ @@ -2726,8 +2657,8 @@ name = "pyobjc-framework-dictionaryservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d6/13/c46f6db61133fee15e3471f33a679da2af10d63fa2b4369e0cd476988721/pyobjc_framework_dictionaryservices-11.1.tar.gz", hash = "sha256:39c24452d0ddd037afeb73a1742614c94535f15b1c024a8a6cc7ff081e1d22e7", size = 10578, upload-time = "2025-06-14T20:57:21.392Z" } wheels = [ @@ -2739,8 +2670,8 @@ name = "pyobjc-framework-discrecording" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a5/b2/d8d1a28643c2ab681b517647bacb68496c98886336ffbd274f0b2ad28cdc/pyobjc_framework_discrecording-11.1.tar.gz", hash = "sha256:37585458e363b20bb28acdb5cc265dfca934d8a07b7baed2584953c11c927a87", size = 123004, upload-time = "2025-06-14T20:57:22.01Z" } wheels = [ @@ -2753,9 +2684,9 @@ name = "pyobjc-framework-discrecordingui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-discrecording" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-discrecording", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/25/53/d71717f00332b8fc3d8a5c7234fdc270adadfeb5ca9318a55986f5c29c44/pyobjc_framework_discrecordingui-11.1.tar.gz", hash = "sha256:a9f10e2e7ee19582c77f0755ae11a64e3d61c652cbd8a5bf52756f599be24797", size = 19370, upload-time = "2025-06-14T20:57:22.791Z" } wheels = [ @@ -2767,8 +2698,8 @@ name = "pyobjc-framework-diskarbitration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/da/2a/68fa0c99e04ec1ec24b0b7d6f5b7ec735d5e8a73277c5c0671438a69a403/pyobjc_framework_diskarbitration-11.1.tar.gz", hash = "sha256:a933efc6624779a393fafe0313e43378bcae2b85d6d15cff95ac30048c1ef490", size = 19866, upload-time = "2025-06-14T20:57:23.435Z" } wheels = [ @@ -2780,8 +2711,8 @@ name = "pyobjc-framework-dvdplayback" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b8/76/77046325b1957f0cbcdf4f96667496d042ed4758f3413f1d21df5b085939/pyobjc_framework_dvdplayback-11.1.tar.gz", hash = "sha256:b44c36a62c8479e649133216e22941859407cca5796b5f778815ef9340a838f4", size = 64558, upload-time = "2025-06-14T20:57:24.118Z" } wheels = [ @@ -2793,8 +2724,8 @@ name = "pyobjc-framework-eventkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/c4/cbba8f2dce13b9be37ecfd423ba2b92aa3f209dbb58ede6c4ce3b242feee/pyobjc_framework_eventkit-11.1.tar.gz", hash = "sha256:5643150f584243681099c5e9435efa833a913e93fe9ca81f62007e287349b561", size = 75177, upload-time = "2025-06-14T20:57:24.81Z" } wheels = [ @@ -2806,8 +2737,8 @@ name = "pyobjc-framework-exceptionhandling" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/19/0d/c72a885b40d28a99b586447f9ea6f400589f13d554fcd6f13a2c841bb6d2/pyobjc_framework_exceptionhandling-11.1.tar.gz", hash = "sha256:e010f56bf60ab4e9e3225954ebb53e9d7135d37097043ac6dd2a3f35770d4efa", size = 17890, upload-time = "2025-06-14T20:57:25.521Z" } wheels = [ @@ -2819,8 +2750,8 @@ name = "pyobjc-framework-executionpolicy" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0b/cf/54431846508c5d5bb114a415ebb96187da5847105918169e42f4ca3b00e6/pyobjc_framework_executionpolicy-11.1.tar.gz", hash = "sha256:3280ad2f4c5eaf45901f310cee0c52db940c0c63e959ad082efb8df41055d986", size = 13496, upload-time = "2025-06-14T20:57:26.173Z" } wheels = [ @@ -2832,8 +2763,8 @@ name = "pyobjc-framework-extensionkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ce/7d/89adf16c7de4246477714dce8fcffae4242778aecd0c5f0ad9904725f42c/pyobjc_framework_extensionkit-11.1.tar.gz", hash = "sha256:c114a96f13f586dbbab8b6219a92fa4829896a645c8cd15652a6215bc8ff5409", size = 19766, upload-time = "2025-06-14T20:57:27.106Z" } wheels = [ @@ -2846,8 +2777,8 @@ name = "pyobjc-framework-externalaccessory" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d9/a3/519242e6822e1ddc9e64e21f717529079dbc28a353474420da8315d0a8b1/pyobjc_framework_externalaccessory-11.1.tar.gz", hash = "sha256:50887e948b78a1d94646422c243ac2a9e40761675e38b9184487870a31e83371", size = 23123, upload-time = "2025-06-14T20:57:27.845Z" } wheels = [ @@ -2860,8 +2791,8 @@ name = "pyobjc-framework-fileprovider" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1b/80/3ebba2c1e5e3aeae989fe038c259a93e7e7e18fd56666ece514d000d38ea/pyobjc_framework_fileprovider-11.1.tar.gz", hash = "sha256:748ca1c75f84afdf5419346a24bf8eec44dca071986f31f00071dc191b3e9ca8", size = 91696, upload-time = "2025-06-14T20:57:28.546Z" } wheels = [ @@ -2874,8 +2805,8 @@ name = "pyobjc-framework-fileproviderui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-fileprovider" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-fileprovider", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/ed/0f5af06869661822c4a70aacd674da5d1e6b6661240e2883bbc7142aa525/pyobjc_framework_fileproviderui-11.1.tar.gz", hash = "sha256:162a23e67f59e1bb247e84dda88d513d7944d815144901a46be6fe051b6c7970", size = 13163, upload-time = "2025-06-14T20:57:29.568Z" } wheels = [ @@ -2887,8 +2818,8 @@ name = "pyobjc-framework-findersync" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2a/82/c6b670494ac0c4cf14cf2db0dfbe0df71925d20595404939383ddbcc56d3/pyobjc_framework_findersync-11.1.tar.gz", hash = "sha256:692364937f418f0e4e4abd395a09a7d4a0cdd55fd4e0184de85ee59642defb6e", size = 15045, upload-time = "2025-06-14T20:57:30.173Z" } wheels = [ @@ -2900,8 +2831,8 @@ name = "pyobjc-framework-fsevents" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/83/ec0b9ba355dbc34f27ed748df9df4eb6dbfdd9bbd614b0f193752f36f419/pyobjc_framework_fsevents-11.1.tar.gz", hash = "sha256:d29157d04124503c4dfa9dcbbdc8c34d3bab134d3db3a48d96d93f26bd94c14d", size = 29587, upload-time = "2025-06-14T20:57:30.796Z" } wheels = [ @@ -2914,8 +2845,8 @@ name = "pyobjc-framework-fskit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/46/47/d1f04c6115fa78936399a389cc5e0e443f8341c9a6c1c0df7f6fdbe51286/pyobjc_framework_fskit-11.1.tar.gz", hash = "sha256:9ded1eab19b4183cb04381e554bbbe679c1213fd58599d6fc6e135e93b51136f", size = 42091, upload-time = "2025-06-14T20:57:31.504Z" } wheels = [ @@ -2928,8 +2859,8 @@ name = "pyobjc-framework-gamecenter" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1b/8e/b594fd1dc32a59462fc68ad502be2bd87c70e6359b4e879a99bcc4beaf5b/pyobjc_framework_gamecenter-11.1.tar.gz", hash = "sha256:a1c4ed54e11a6e4efba6f2a21ace92bcf186e3fe5c74a385b31f6b1a515ec20c", size = 31981, upload-time = "2025-06-14T20:57:32.192Z" } wheels = [ @@ -2942,8 +2873,8 @@ name = "pyobjc-framework-gamecontroller" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/70/4c/1dd62103092a182f2ab8904c8a8e3922d2b0a80a7adab0c20e5fd0207d75/pyobjc_framework_gamecontroller-11.1.tar.gz", hash = "sha256:4d5346faf90e1ebe5602c0c480afbf528a35a7a1ad05f9b49991fdd2a97f105b", size = 115783, upload-time = "2025-06-14T20:57:32.879Z" } wheels = [ @@ -2956,9 +2887,9 @@ name = "pyobjc-framework-gamekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5b/7b/ba141ec0f85ca816f493d1f6fe68c72d01092e5562e53c470a0111d9c34b/pyobjc_framework_gamekit-11.1.tar.gz", hash = "sha256:9b8db075da8866c4ef039a165af227bc29393dc11a617a40671bf6b3975ae269", size = 165397, upload-time = "2025-06-14T20:57:33.711Z" } wheels = [ @@ -2971,9 +2902,9 @@ name = "pyobjc-framework-gameplaykit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-spritekit" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-spritekit", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/07/f38b1d83eac10ea4f75c605ffc4850585740db89b90842d311e586ee36cd/pyobjc_framework_gameplaykit-11.1.tar.gz", hash = "sha256:9ae2bee69b0cc1afa0e210b4663c7cdbb3cc94be1374808df06f98f992e83639", size = 73399, upload-time = "2025-06-14T20:57:34.538Z" } wheels = [ @@ -2986,8 +2917,8 @@ name = "pyobjc-framework-healthkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/66/fa76f7c8e36e4c10677d42d91a8e220c135c610a06b759571db1abe26a32/pyobjc_framework_healthkit-11.1.tar.gz", hash = "sha256:20f59bd9e1ffafe5893b4eff5867fdfd20bd46c3d03bc4009219d82fc6815f76", size = 202009, upload-time = "2025-06-14T20:57:35.285Z" } wheels = [ @@ -3000,8 +2931,8 @@ name = "pyobjc-framework-imagecapturecore" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7b/3b/f4edbc58a8c7394393f8d00d0e764f655545e743ee4e33917f27b8c68e7b/pyobjc_framework_imagecapturecore-11.1.tar.gz", hash = "sha256:a610ceb6726e385b132a1481a68ce85ccf56f94667b6d6e1c45a2cfab806a624", size = 100398, upload-time = "2025-06-14T20:57:36.503Z" } wheels = [ @@ -3014,8 +2945,8 @@ name = "pyobjc-framework-inputmethodkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/02/32/6a90bba682a31960ba1fc2d3b263e9be26043c4fb7aed273c13647c8b7d9/pyobjc_framework_inputmethodkit-11.1.tar.gz", hash = "sha256:7037579524041dcee71a649293c2660f9359800455a15e6a2f74a17b46d78496", size = 27203, upload-time = "2025-06-14T20:57:37.246Z" } wheels = [ @@ -3028,8 +2959,8 @@ name = "pyobjc-framework-installerplugins" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4d/89/9a881e466476ca21f3ff3e8e87ccfba1aaad9b88f7eea4be6d3f05b07107/pyobjc_framework_installerplugins-11.1.tar.gz", hash = "sha256:363e59c7e05553d881f0facd41884f17b489ff443d7856e33dd0312064c746d9", size = 27451, upload-time = "2025-06-14T20:57:37.915Z" } wheels = [ @@ -3041,9 +2972,9 @@ name = "pyobjc-framework-instantmessage" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/b9/5cec4dd0053b5f63c01211a60a286c47464d9f3e0c81bd682e6542dbff00/pyobjc_framework_instantmessage-11.1.tar.gz", hash = "sha256:c222aa61eb009704b333f6e63df01a0e690136e7e495907e5396882779bf9525", size = 33774, upload-time = "2025-06-14T20:57:38.553Z" } wheels = [ @@ -3055,8 +2986,8 @@ name = "pyobjc-framework-intents" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/4c/af/d7f260d06b79acca8028e373c2fe30bf0be014388ba612f538f40597d929/pyobjc_framework_intents-11.1.tar.gz", hash = "sha256:13185f206493f45d6bd2d4903c2136b1c4f8b9aa37628309ace6ff4a906b4695", size = 448459, upload-time = "2025-06-14T20:57:39.589Z" } wheels = [ @@ -3069,8 +3000,8 @@ name = "pyobjc-framework-intentsui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-intents" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-intents", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/46/20aae4a71efb514b096f36273a6129b48b01535bf501e5719d4a97fcb3a5/pyobjc_framework_intentsui-11.1.tar.gz", hash = "sha256:c8182155af4dce369c18d6e6ed9c25bbd8110c161ed5f1b4fb77cf5cdb99d135", size = 21305, upload-time = "2025-06-14T20:57:40.477Z" } wheels = [ @@ -3083,8 +3014,8 @@ name = "pyobjc-framework-iobluetooth" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/93/e0/74b7b10c567b66c5f38b45ab240336325a4c889f43072d90f2b90aaeb7c0/pyobjc_framework_iobluetooth-11.1.tar.gz", hash = "sha256:094fd4be60cd1371b17cb4b33a3894e0d88a11b36683912be0540a7d51de76f1", size = 300992, upload-time = "2025-06-14T20:57:41.256Z" } wheels = [ @@ -3097,8 +3028,8 @@ name = "pyobjc-framework-iobluetoothui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-iobluetooth" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-iobluetooth", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dd/32/872272faeab6fe471eac6962c75db72ce65c3556e00b4edebdb41aaab7cb/pyobjc_framework_iobluetoothui-11.1.tar.gz", hash = "sha256:060c721f1cd8af4452493e8153b72b572edcd2a7e3b635d79d844f885afee860", size = 22835, upload-time = "2025-06-14T20:57:42.119Z" } wheels = [ @@ -3110,8 +3041,8 @@ name = "pyobjc-framework-iosurface" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/ce/38ec17d860d0ee040bb737aad8ca7c7ff46bef6c9cffa47382d67682bb2d/pyobjc_framework_iosurface-11.1.tar.gz", hash = "sha256:a468b3a31e8cd70a2675a3ddc7176ab13aa521c035f11188b7a3af8fff8b148b", size = 20275, upload-time = "2025-06-14T20:57:42.742Z" } wheels = [ @@ -3123,8 +3054,8 @@ name = "pyobjc-framework-ituneslibrary" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/43/aebefed774b434965752f9001685af0b19c02353aa7a12d2918af0948181/pyobjc_framework_ituneslibrary-11.1.tar.gz", hash = "sha256:e2212a9340e4328056ade3c2f9d4305c71f3f6af050204a135f9fa9aa3ba9c5e", size = 47388, upload-time = "2025-06-14T20:57:43.383Z" } wheels = [ @@ -3136,8 +3067,8 @@ name = "pyobjc-framework-kernelmanagement" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/b6/708f10ac16425834cb5f8b71efdbe39b42c3b1009ac0c1796a42fc98cd36/pyobjc_framework_kernelmanagement-11.1.tar.gz", hash = "sha256:e934d1638cd89e38d6c6c5d4d9901b4295acee2d39cbfe0bd91aae9832961b44", size = 12543, upload-time = "2025-06-14T20:57:44.046Z" } wheels = [ @@ -3149,8 +3080,8 @@ name = "pyobjc-framework-latentsemanticmapping" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/db/8a/4e54ee2bc77d59d770b287daf73b629e2715a2b3b31264d164398131cbad/pyobjc_framework_latentsemanticmapping-11.1.tar.gz", hash = "sha256:c6c3142301e4d375c24a47dfaeebc2f3d0fc33128a1c0a755794865b9a371145", size = 17444, upload-time = "2025-06-14T20:57:44.643Z" } wheels = [ @@ -3162,8 +3093,8 @@ name = "pyobjc-framework-launchservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/0a/a76b13109b8ab563fdb2d7182ca79515f132f82ac6e1c52351a6b02896a8/pyobjc_framework_launchservices-11.1.tar.gz", hash = "sha256:80b55368b1e208d6c2c58395cc7bc12a630a2a402e00e4930493e9bace22b7bb", size = 20446, upload-time = "2025-06-14T20:57:45.258Z" } wheels = [ @@ -3175,8 +3106,8 @@ name = "pyobjc-framework-libdispatch" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/be/89/7830c293ba71feb086cb1551455757f26a7e2abd12f360d375aae32a4d7d/pyobjc_framework_libdispatch-11.1.tar.gz", hash = "sha256:11a704e50a0b7dbfb01552b7d686473ffa63b5254100fdb271a1fe368dd08e87", size = 53942, upload-time = "2025-06-14T20:57:45.903Z" } wheels = [ @@ -3189,8 +3120,8 @@ name = "pyobjc-framework-libxpc" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6a/c9/7e15e38ac23f5bfb4e82bdf3b7ef88e2f56a8b4ad884009bc2d5267d2e1f/pyobjc_framework_libxpc-11.1.tar.gz", hash = "sha256:8fd7468aa520ff19915f6d793070b84be1498cb87224bee2bad1f01d8375273a", size = 49135, upload-time = "2025-06-14T20:57:46.59Z" } wheels = [ @@ -3203,9 +3134,9 @@ name = "pyobjc-framework-linkpresentation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b9/76/22873be73f12a3a11ae57af13167a1d2379e4e7eef584de137156a00f5ef/pyobjc_framework_linkpresentation-11.1.tar.gz", hash = "sha256:a785f393b01fdaada6d7d6d8de46b7173babba205b13b44f1dc884b3695c2fc9", size = 14987, upload-time = "2025-06-14T20:57:47.277Z" } wheels = [ @@ -3217,9 +3148,9 @@ name = "pyobjc-framework-localauthentication" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/27/9e3195f3561574140e9b9071a36f7e0ebd18f50ade9261d23b5b9df8fccd/pyobjc_framework_localauthentication-11.1.tar.gz", hash = "sha256:3cd48907c794bd414ac68b8ac595d83c7e1453b63fc2cfc2d2035b690d31eaa1", size = 40700, upload-time = "2025-06-14T20:57:47.931Z" } wheels = [ @@ -3232,9 +3163,9 @@ name = "pyobjc-framework-localauthenticationembeddedui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-localauthentication" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-localauthentication", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/29/7b/08c1e52487b07e9aee4c24a78f7c82a46695fa883113e3eece40f8e32d40/pyobjc_framework_localauthenticationembeddedui-11.1.tar.gz", hash = "sha256:22baf3aae606e5204e194f02bb205f244e27841ea7b4a4431303955475b4fa56", size = 14076, upload-time = "2025-06-14T20:57:48.557Z" } wheels = [ @@ -3246,8 +3177,8 @@ name = "pyobjc-framework-mailkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/f22d733897e7618bd70a658b0353f5f897c583df04e7c5a2d68b99d43fbb/pyobjc_framework_mailkit-11.1.tar.gz", hash = "sha256:bf97dc44cb09b9eb9d591660dc0a41f077699976144b954caa4b9f0479211fd7", size = 32012, upload-time = "2025-06-14T20:57:49.173Z" } wheels = [ @@ -3259,10 +3190,10 @@ name = "pyobjc-framework-mapkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-corelocation" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-corelocation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/57/f0/505e074f49c783f2e65ca82174fd2d4348568f3f7281c1b81af816cf83bb/pyobjc_framework_mapkit-11.1.tar.gz", hash = "sha256:f3a5016f266091be313a118a42c0ea4f951c399b5259d93639eb643dacc626f1", size = 165614, upload-time = "2025-06-14T20:57:50.362Z" } wheels = [ @@ -3275,8 +3206,8 @@ name = "pyobjc-framework-mediaaccessibility" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8d/81/60412b423c121de0fa0aa3ef679825e1e2fe8b00fceddec7d72333ef564b/pyobjc_framework_mediaaccessibility-11.1.tar.gz", hash = "sha256:52479a998fec3d079d2d4590a945fc78c41fe7ac8c76f1964c9d8156880565a4", size = 18440, upload-time = "2025-06-14T20:57:51.126Z" } wheels = [ @@ -3288,10 +3219,10 @@ name = "pyobjc-framework-mediaextension" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/09/fd214dc0cf3f3bc3f528815af4799c0cb7b4bf4032703b19ea63486a132b/pyobjc_framework_mediaextension-11.1.tar.gz", hash = "sha256:85a1c8a94e9175fb364c453066ef99b95752343fd113f08a3805cad56e2fa709", size = 58489, upload-time = "2025-06-14T20:57:51.796Z" } wheels = [ @@ -3304,9 +3235,9 @@ name = "pyobjc-framework-medialibrary" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/2b/06/11ff622fb5fbdd557998a45cedd2b0a1c7ea5cc6c5cb015dd6e42ebd1c41/pyobjc_framework_medialibrary-11.1.tar.gz", hash = "sha256:102f4326f789734b7b2dfe689abd3840ca75a76fb8058bd3e4f85398ae2ce29d", size = 18706, upload-time = "2025-06-14T20:57:52.474Z" } wheels = [ @@ -3318,8 +3249,8 @@ name = "pyobjc-framework-mediaplayer" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/80/d5/daba26eb8c70af1f3823acfd7925356acc4dd75eeac4fc86dc95d94d0e15/pyobjc_framework_mediaplayer-11.1.tar.gz", hash = "sha256:d07a634b98e1b9eedd82d76f35e616525da096bd341051ea74f0971e0f2f2ddd", size = 93749, upload-time = "2025-06-14T20:57:53.165Z" } wheels = [ @@ -3331,8 +3262,8 @@ name = "pyobjc-framework-mediatoolbox" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e1/68/cc230d2dfdeb974fdcfa828de655a43ce2bf4962023fd55bbb7ab0970100/pyobjc_framework_mediatoolbox-11.1.tar.gz", hash = "sha256:97834addc5179b3165c0d8cd74cc97ad43ed4c89547724216426348aca3b822a", size = 23568, upload-time = "2025-06-14T20:57:53.913Z" } wheels = [ @@ -3345,8 +3276,8 @@ name = "pyobjc-framework-metal" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/af/cf/29fea96fd49bf72946c5dac4c43ef50f26c15e9f76edd6f15580d556aa23/pyobjc_framework_metal-11.1.tar.gz", hash = "sha256:f9fd3b7574a824632ee9b7602973da30f172d2b575dd0c0f5ef76b44cfe9f6f9", size = 446549, upload-time = "2025-06-14T20:57:54.731Z" } wheels = [ @@ -3359,8 +3290,8 @@ name = "pyobjc-framework-metalfx" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/10/20/4c839a356b534c161fb97e06589f418fc78cc5a0808362bdecf4f9a61a8d/pyobjc_framework_metalfx-11.1.tar.gz", hash = "sha256:555c1b895d4ba31be43930f45e219a5d7bb0e531d148a78b6b75b677cc588fd8", size = 27002, upload-time = "2025-06-14T20:57:55.949Z" } wheels = [ @@ -3373,9 +3304,9 @@ name = "pyobjc-framework-metalkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/45/cb/7e01bc61625c7a6fea9c9888c9ed35aa6bbc47cda2fcd02b6525757bc2b8/pyobjc_framework_metalkit-11.1.tar.gz", hash = "sha256:8811cd81ee9583b9330df4f2499a73dcc53f3359cb92767b409acaec9e4faa1e", size = 45135, upload-time = "2025-06-14T20:57:56.601Z" } wheels = [ @@ -3388,8 +3319,8 @@ name = "pyobjc-framework-metalperformanceshaders" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metal" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metal", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d0/11/5df398a158a6efe2c87ac5cae121ef2788242afe5d4302d703147b9fcd91/pyobjc_framework_metalperformanceshaders-11.1.tar.gz", hash = "sha256:8a312d090a0f51651e63d9001e6cc7c1aa04ceccf23b494cbf84b7fd3d122071", size = 302113, upload-time = "2025-06-14T20:57:57.407Z" } wheels = [ @@ -3402,8 +3333,8 @@ name = "pyobjc-framework-metalperformanceshadersgraph" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-metalperformanceshaders" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-metalperformanceshaders", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/c3/8d98661f7eecd1f1b0d80a80961069081b88efd3a82fbbed2d7e6050c0ad/pyobjc_framework_metalperformanceshadersgraph-11.1.tar.gz", hash = "sha256:d25225aab4edc6f786b29fe3d9badc4f3e2d0caeab1054cd4f224258c1b6dbe2", size = 105098, upload-time = "2025-06-14T20:57:58.273Z" } wheels = [ @@ -3415,8 +3346,8 @@ name = "pyobjc-framework-metrickit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/bd/48/8ae969a51a91864000e39c1de74627b12ff587b1dbad9406f7a30dfe71f8/pyobjc_framework_metrickit-11.1.tar.gz", hash = "sha256:a79d37575489916c35840e6a07edd958be578d3be7a3d621684d028d721f0b85", size = 40952, upload-time = "2025-06-14T20:57:58.996Z" } wheels = [ @@ -3429,8 +3360,8 @@ name = "pyobjc-framework-mlcompute" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8b/e6/f064dec650fb1209f41aba0c3074416cb9b975a7cf4d05d93036e3d917f0/pyobjc_framework_mlcompute-11.1.tar.gz", hash = "sha256:f6c4c3ea6a62e4e3927abf9783c40495aa8bb9a8c89def744b0822da58c2354b", size = 89021, upload-time = "2025-06-14T20:57:59.997Z" } wheels = [ @@ -3442,9 +3373,9 @@ name = "pyobjc-framework-modelio" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a0/27/140bf75706332729de252cc4141e8c8afe16a0e9e5818b5a23155aa3473c/pyobjc_framework_modelio-11.1.tar.gz", hash = "sha256:fad0fa2c09d468ac7e49848e144f7bbce6826f2178b3120add8960a83e5bfcb7", size = 123203, upload-time = "2025-06-14T20:58:01.035Z" } wheels = [ @@ -3457,8 +3388,8 @@ name = "pyobjc-framework-multipeerconnectivity" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/73/99/75bf6170e282d9e546b353b65af7859de8b1b27ddc431fc4afbf15423d01/pyobjc_framework_multipeerconnectivity-11.1.tar.gz", hash = "sha256:a3dacca5e6e2f1960dd2d1107d98399ff81ecf54a9852baa8ec8767dbfdbf54b", size = 26149, upload-time = "2025-06-14T20:58:01.793Z" } wheels = [ @@ -3471,8 +3402,8 @@ name = "pyobjc-framework-naturallanguage" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a2/e9/5352fbf09c5d5360405dea49fb77e53ed55acd572a94ce9a0d05f64d2b70/pyobjc_framework_naturallanguage-11.1.tar.gz", hash = "sha256:ab1fc711713aa29c32719774fc623bf2d32168aed21883970d4896e901ff4b41", size = 46120, upload-time = "2025-06-14T20:58:02.808Z" } wheels = [ @@ -3484,8 +3415,8 @@ name = "pyobjc-framework-netfs" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/68/5d/d68cc59a1c1ea61f227ed58e7b185a444d560655320b53ced155076f5b78/pyobjc_framework_netfs-11.1.tar.gz", hash = "sha256:9c49f050c8171dc37e54d05dd12a63979c8b6b565c10f05092923a2250446f50", size = 15910, upload-time = "2025-06-14T20:58:03.811Z" } wheels = [ @@ -3497,8 +3428,8 @@ name = "pyobjc-framework-network" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/0a/ee/5ea93e48eca341b274027e1532bd8629fd55d609cd9c39c2c3acf26158c3/pyobjc_framework_network-11.1.tar.gz", hash = "sha256:f6df7a58a1279bbc976fd7e2efe813afbbb18427df40463e6e2ee28fba07d2df", size = 124670, upload-time = "2025-06-14T20:58:05.491Z" } wheels = [ @@ -3511,8 +3442,8 @@ name = "pyobjc-framework-networkextension" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/71/30/d1eee738d702bbca78effdaa346a2b05359ab8a96d961b7cb44838e236ca/pyobjc_framework_networkextension-11.1.tar.gz", hash = "sha256:2b74b430ca651293e5aa90a1e7571b200d0acbf42803af87306ac8a1c70b0d4b", size = 217252, upload-time = "2025-06-14T20:58:06.311Z" } wheels = [ @@ -3525,8 +3456,8 @@ name = "pyobjc-framework-notificationcenter" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a8/4a/d3529b9bd7aae2c89d258ebc234673c5435e217a5136abd8c0aba37b916b/pyobjc_framework_notificationcenter-11.1.tar.gz", hash = "sha256:0b938053f2d6b1cea9db79313639d7eb9ddd5b2a5436a346be0887e75101e717", size = 23389, upload-time = "2025-06-14T20:58:07.136Z" } wheels = [ @@ -3539,8 +3470,8 @@ name = "pyobjc-framework-opendirectory" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9d/02/ac56c56fdfbc24cdf87f4a624f81bbe2e371d0983529b211a18c6170e932/pyobjc_framework_opendirectory-11.1.tar.gz", hash = "sha256:319ac3424ed0350be458b78148914468a8fc13a069d62e7869e3079108e4f118", size = 188880, upload-time = "2025-06-14T20:58:08.003Z" } wheels = [ @@ -3552,8 +3483,8 @@ name = "pyobjc-framework-osakit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/22/f9cdfb5de255b335f99e61a3284be7cb1552a43ed1dfe7c22cc868c23819/pyobjc_framework_osakit-11.1.tar.gz", hash = "sha256:920987da78b67578367c315d208f87e8fab01dd35825d72242909f29fb43c820", size = 22290, upload-time = "2025-06-14T20:58:09.103Z" } wheels = [ @@ -3565,10 +3496,10 @@ name = "pyobjc-framework-oslog" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/93/3feb7f6150b50165524750a424f5434448392123420cb4673db766c3f54a/pyobjc_framework_oslog-11.1.tar.gz", hash = "sha256:b2af409617e6b68fa1f1467c5a5679ebf59afd0cdc4b4528e1616059959a7979", size = 24689, upload-time = "2025-06-14T20:58:09.739Z" } wheels = [ @@ -3581,8 +3512,8 @@ name = "pyobjc-framework-passkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/05/063db500e7df70e39cbb5518a5a03c2acc06a1ca90b057061daea00129f3/pyobjc_framework_passkit-11.1.tar.gz", hash = "sha256:d2408b58960fca66607b483353c1ffbd751ef0bef394a1853ec414a34029566f", size = 144859, upload-time = "2025-06-14T20:58:10.761Z" } wheels = [ @@ -3595,8 +3526,8 @@ name = "pyobjc-framework-pencilkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/75/d0/bbbe9dadcfc37e33a63d43b381a8d9a64eca27559df38efb74d524fa6260/pyobjc_framework_pencilkit-11.1.tar.gz", hash = "sha256:9c173e0fe70179feadc3558de113a8baad61b584fe70789b263af202bfa4c6be", size = 22570, upload-time = "2025-06-14T20:58:11.538Z" } wheels = [ @@ -3608,8 +3539,8 @@ name = "pyobjc-framework-phase" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-avfoundation" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-avfoundation", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c6/d2/e9384b5b3fbcc79e8176cb39fcdd48b77f60cd1cb64f9ee4353762b037dc/pyobjc_framework_phase-11.1.tar.gz", hash = "sha256:a940d81ac5c393ae3da94144cf40af33932e0a9731244e2cfd5c9c8eb851e3fc", size = 58986, upload-time = "2025-06-14T20:58:12.196Z" } wheels = [ @@ -3621,8 +3552,8 @@ name = "pyobjc-framework-photos" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/78/b0/576652ecd05c26026ab4e75e0d81466edd570d060ce7df3d6bd812eb90d0/pyobjc_framework_photos-11.1.tar.gz", hash = "sha256:c8c3b25b14a2305047f72c7c081ff3655b3d051f7ed531476c03246798f8156d", size = 92569, upload-time = "2025-06-14T20:58:12.939Z" } wheels = [ @@ -3635,8 +3566,8 @@ name = "pyobjc-framework-photosui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/bb/e6de720efde2e9718677c95c6ae3f97047be437cda7a0f050cd1d6d2a434/pyobjc_framework_photosui-11.1.tar.gz", hash = "sha256:1c7ffab4860ce3e2b50feeed4f1d84488a9e38546db0bec09484d8d141c650df", size = 48443, upload-time = "2025-06-14T20:58:13.626Z" } wheels = [ @@ -3649,8 +3580,8 @@ name = "pyobjc-framework-preferencepanes" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/34/ac/9324602daf9916308ebf1935b8a4b91c93b9ae993dcd0da731c0619c2836/pyobjc_framework_preferencepanes-11.1.tar.gz", hash = "sha256:6e4a55195ec9fc921e0eaad6b3038d0ab91f0bb2f39206aa6fccd24b14a0f1d8", size = 26212, upload-time = "2025-06-14T20:58:14.361Z" } wheels = [ @@ -3662,8 +3593,8 @@ name = "pyobjc-framework-pushkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/9f/f0/92d0eb26bf8af8ebf6b5b88df77e70b807de11f01af0162e0a429fcfb892/pyobjc_framework_pushkit-11.1.tar.gz", hash = "sha256:540769a4aadc3c9f08beca8496fe305372501eb28fdbca078db904a07b8e10f4", size = 21362, upload-time = "2025-06-14T20:58:15.642Z" } wheels = [ @@ -3676,8 +3607,8 @@ name = "pyobjc-framework-quartz" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c7/ac/6308fec6c9ffeda9942fef72724f4094c6df4933560f512e63eac37ebd30/pyobjc_framework_quartz-11.1.tar.gz", hash = "sha256:a57f35ccfc22ad48c87c5932818e583777ff7276605fef6afad0ac0741169f75", size = 3953275, upload-time = "2025-06-14T20:58:17.924Z" } wheels = [ @@ -3690,9 +3621,9 @@ name = "pyobjc-framework-quicklookthumbnailing" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/98/6e87f360c2dfc870ae7870b8a25fdea8ddf1d62092c755686cebe7ec1a07/pyobjc_framework_quicklookthumbnailing-11.1.tar.gz", hash = "sha256:1614dc108c1d45bbf899ea84b8691288a5b1d25f2d6f0c57dfffa962b7a478c3", size = 16527, upload-time = "2025-06-14T20:58:20.811Z" } wheels = [ @@ -3704,8 +3635,8 @@ name = "pyobjc-framework-replaykit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c8/4f/014e95f0fd6842d7fcc3d443feb6ee65ac69d06c66ffa9327fc33ceb7c27/pyobjc_framework_replaykit-11.1.tar.gz", hash = "sha256:6919baa123a6d8aad769769fcff87369e13ee7bae11b955a8185a406a651061b", size = 26132, upload-time = "2025-06-14T20:58:21.853Z" } wheels = [ @@ -3718,8 +3649,8 @@ name = "pyobjc-framework-safariservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/1a/fc/c47d2abf3c1de6db21d685cace76a0931d594aa369e3d090260295273f6e/pyobjc_framework_safariservices-11.1.tar.gz", hash = "sha256:39a17df1a8e1c339457f3acbff0dc0eae4681d158f9d783a11995cf484aa9cd0", size = 34905, upload-time = "2025-06-14T20:58:22.492Z" } wheels = [ @@ -3732,9 +3663,9 @@ name = "pyobjc-framework-safetykit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/28/cc/f6aa5d6f45179bd084416511be4e5b0dd0752cb76daa93869e6edb806096/pyobjc_framework_safetykit-11.1.tar.gz", hash = "sha256:c6b44e0cf69e27584ac3ef3d8b771d19a7c2ccd9c6de4138d091358e036322d4", size = 21240, upload-time = "2025-06-14T20:58:23.132Z" } wheels = [ @@ -3747,9 +3678,9 @@ name = "pyobjc-framework-scenekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/64/cf/2d89777120d2812e7ee53c703bf6fc8968606c29ddc1351bc63f0a2a5692/pyobjc_framework_scenekit-11.1.tar.gz", hash = "sha256:82941f1e5040114d6e2c9fd35507244e102ef561c637686091b71a7ad0f31306", size = 214118, upload-time = "2025-06-14T20:58:24.003Z" } wheels = [ @@ -3762,9 +3693,9 @@ name = "pyobjc-framework-screencapturekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/32/a5/9bd1f1ad1773a1304ccde934ff39e0f0a0b0034441bf89166aea649606de/pyobjc_framework_screencapturekit-11.1.tar.gz", hash = "sha256:11443781a30ed446f2d892c9e6642ca4897eb45f1a1411136ca584997fa739e0", size = 53548, upload-time = "2025-06-14T20:58:24.837Z" } wheels = [ @@ -3777,8 +3708,8 @@ name = "pyobjc-framework-screensaver" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/7c/f6/f2d48583b29fc67b64aa1f415fd51faf003d045cdb1f3acab039b9a3f59f/pyobjc_framework_screensaver-11.1.tar.gz", hash = "sha256:d5fbc9dc076cc574ead183d521840b56be0c160415e43cb8e01cfddd6d6372c2", size = 24302, upload-time = "2025-06-14T20:58:25.52Z" } wheels = [ @@ -3791,8 +3722,8 @@ name = "pyobjc-framework-screentime" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/82/33/ebed70a1de134de936bb9a12d5c76f24e1e335ff4964f9bb0af9b09607f1/pyobjc_framework_screentime-11.1.tar.gz", hash = "sha256:9bb8269456bbb674e1421182efe49f9168ceefd4e7c497047c7bf63e2f510a34", size = 14875, upload-time = "2025-06-14T20:58:26.179Z" } wheels = [ @@ -3804,8 +3735,8 @@ name = "pyobjc-framework-scriptingbridge" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/8e/c1/5b1dd01ff173df4c6676f97405113458918819cb2064c1735b61948e8800/pyobjc_framework_scriptingbridge-11.1.tar.gz", hash = "sha256:604445c759210a35d86d3e0dfcde0aac8e5e3e9d9e35759e0723952138843699", size = 23155, upload-time = "2025-06-14T20:58:26.812Z" } wheels = [ @@ -3818,8 +3749,8 @@ name = "pyobjc-framework-searchkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-coreservices" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreservices", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/6e/20/61b73fddae0d1a94f5defb0cd4b4f391ec03bfcce7ebe830cb827d5e208a/pyobjc_framework_searchkit-11.1.tar.gz", hash = "sha256:13a194eefcf1359ce9972cd92f2aadddf103f3efb1b18fd578ba5367dff3c10c", size = 30918, upload-time = "2025-06-14T20:58:27.447Z" } wheels = [ @@ -3831,8 +3762,8 @@ name = "pyobjc-framework-security" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/ee/6f/ba50ed2d9c1192c67590a7cfefa44fc5f85c776d1e25beb224dec32081f6/pyobjc_framework_security-11.1.tar.gz", hash = "sha256:dabcee6987c6bae575e2d1ef0fcbe437678c4f49f1c25a4b131a5e960f31a2da", size = 302291, upload-time = "2025-06-14T20:58:28.506Z" } wheels = [ @@ -3845,9 +3776,9 @@ name = "pyobjc-framework-securityfoundation" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/5c/d4/19591dd0938a45b6d8711ef9ae5375b87c37a55b45d79c52d6f83a8d991f/pyobjc_framework_securityfoundation-11.1.tar.gz", hash = "sha256:b3c4cf70735a93e9df40f3a14478143959c415778f27be8c0dc9ae0c5b696b92", size = 13270, upload-time = "2025-06-14T20:58:29.304Z" } wheels = [ @@ -3859,9 +3790,9 @@ name = "pyobjc-framework-securityinterface" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/a1/be/c846651c3e7f38a637c40ae1bcda9f14237c2395637c3a188df4f733c727/pyobjc_framework_securityinterface-11.1.tar.gz", hash = "sha256:e7aa6373e525f3ae05d71276e821a6348c53fec9f812b90eec1dbadfcb507bc9", size = 37648, upload-time = "2025-06-14T20:58:29.932Z" } wheels = [ @@ -3874,9 +3805,9 @@ name = "pyobjc-framework-securityui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-security" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-security", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/5b/3b5585d56e0bcaba82e0661224bbc7aaf29fba6b10498971dbe08b2b490a/pyobjc_framework_securityui-11.1.tar.gz", hash = "sha256:e80c93e8a56bf89e4c0333047b9f8219752dd6de290681e9e2e2b2e26d69e92d", size = 12179, upload-time = "2025-06-14T20:58:30.928Z" } wheels = [ @@ -3888,9 +3819,9 @@ name = "pyobjc-framework-sensitivecontentanalysis" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/56/7b/e28f6b30d99e9d464427a07ada82b33cd3292f310bf478a1824051d066b9/pyobjc_framework_sensitivecontentanalysis-11.1.tar.gz", hash = "sha256:5b310515c7386f7afaf13e4632d7d9590688182bb7b563f8026c304bdf317308", size = 12796, upload-time = "2025-06-14T20:58:31.488Z" } wheels = [ @@ -3902,8 +3833,8 @@ name = "pyobjc-framework-servicemanagement" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/20/c6/32e11599d9d232311607b79eb2d1d21c52eaaf001599ea85f8771a933fa2/pyobjc_framework_servicemanagement-11.1.tar.gz", hash = "sha256:90a07164da49338480e0e135b445acc6ae7c08549a2037d1e512d2605fedd80a", size = 16645, upload-time = "2025-06-14T20:58:32.062Z" } wheels = [ @@ -3915,8 +3846,8 @@ name = "pyobjc-framework-sharedwithyou" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-sharedwithyoucore" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-sharedwithyoucore", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/fe/a5/e299fbd0c13d4fac9356459f21372f6eef4279d0fbc99ba316d88dfbbfb4/pyobjc_framework_sharedwithyou-11.1.tar.gz", hash = "sha256:ece3a28a3083d0bcad0ac95b01f0eb699b9d2d0c02c61305bfd402678753ff6e", size = 34216, upload-time = "2025-06-14T20:58:32.75Z" } wheels = [ @@ -3929,8 +3860,8 @@ name = "pyobjc-framework-sharedwithyoucore" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/79/a3/1ca6ff1b785772c7c5a38a7c017c6f971b1eda638d6a0aab3bbde18ac086/pyobjc_framework_sharedwithyoucore-11.1.tar.gz", hash = "sha256:790050d25f47bda662a9f008b17ca640ac2460f2559a56b17995e53f2f44ed73", size = 29459, upload-time = "2025-06-14T20:58:33.422Z" } wheels = [ @@ -3943,8 +3874,8 @@ name = "pyobjc-framework-shazamkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/de/08/ba739b97f1e441653bae8da5dd1e441bbbfa43940018d21edb60da7dd163/pyobjc_framework_shazamkit-11.1.tar.gz", hash = "sha256:c6e3c9ab8744d9319a89b78ae6f185bb5704efb68509e66d77bcd1f84a9446d6", size = 25797, upload-time = "2025-06-14T20:58:34.086Z" } wheels = [ @@ -3957,8 +3888,8 @@ name = "pyobjc-framework-social" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/07/2e/cc7707b7a40df392c579087947049f3e1f0e00597e7151ec411f654d8bef/pyobjc_framework_social-11.1.tar.gz", hash = "sha256:fbc09d7b00dad45b547f9b2329f4dcee3f5a50e2348de1870de0bd7be853a5b7", size = 14540, upload-time = "2025-06-14T20:58:35.116Z" } wheels = [ @@ -3970,8 +3901,8 @@ name = "pyobjc-framework-soundanalysis" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e0/d4/b9497dbb57afdf0d22f61bb6e776a6f46cf9294c890448acde5b46dd61f3/pyobjc_framework_soundanalysis-11.1.tar.gz", hash = "sha256:42cd25b7e0f343d8b59367f72b5dae96cf65696bdb8eeead8d7424ed37aa1434", size = 16539, upload-time = "2025-06-14T20:58:35.813Z" } wheels = [ @@ -3983,8 +3914,8 @@ name = "pyobjc-framework-speech" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/67/76/2a1fd7637b2c662349ede09806e159306afeebfba18fb062ad053b41d811/pyobjc_framework_speech-11.1.tar.gz", hash = "sha256:d382977208c3710eacea89e05eae4578f1638bb5a7b667c06971e3d34e96845c", size = 41179, upload-time = "2025-06-14T20:58:36.43Z" } wheels = [ @@ -3997,9 +3928,9 @@ name = "pyobjc-framework-spritekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/16/02/2e253ba4f7fad6efe05fd5fcf44aede093f6c438d608d67c6c6623a1846d/pyobjc_framework_spritekit-11.1.tar.gz", hash = "sha256:914da6e846573cac8db5e403dec9a3e6f6edf5211f9b7e429734924d00f65108", size = 130297, upload-time = "2025-06-14T20:58:37.113Z" } wheels = [ @@ -4012,8 +3943,8 @@ name = "pyobjc-framework-storekit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/44/a0/58cab9ebc9ac9282e1d4734b1987d1c3cd652b415ec3e678fcc5e735d279/pyobjc_framework_storekit-11.1.tar.gz", hash = "sha256:85acc30c0bfa120b37c3c5ac693fe9ad2c2e351ee7a1f9ea6f976b0c311ff164", size = 76421, upload-time = "2025-06-14T20:58:37.86Z" } wheels = [ @@ -4026,8 +3957,8 @@ name = "pyobjc-framework-symbols" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/cd/af/7191276204bd3e7db1d0a3e490a869956606f77f7a303a04d92a5d0c3f7b/pyobjc_framework_symbols-11.1.tar.gz", hash = "sha256:0e09b7813ef2ebdca7567d3179807444dd60f3f393202b35b755d4e1baf99982", size = 13377, upload-time = "2025-06-14T20:58:38.542Z" } wheels = [ @@ -4039,9 +3970,9 @@ name = "pyobjc-framework-syncservices" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coredata" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coredata", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/69/45/cd9fa83ed1d75be7130fb8e41c375f05b5d6621737ec37e9d8da78676613/pyobjc_framework_syncservices-11.1.tar.gz", hash = "sha256:0f141d717256b98c17ec2eddbc983c4bd39dfa00dc0c31b4174742e73a8447fe", size = 57996, upload-time = "2025-06-14T20:58:39.146Z" } wheels = [ @@ -4054,8 +3985,8 @@ name = "pyobjc-framework-systemconfiguration" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e2/3d/41590c0afc72e93d911348fbde0c9c1071ff53c6f86df42df64b21174bb9/pyobjc_framework_systemconfiguration-11.1.tar.gz", hash = "sha256:f30ed0e9a8233fecb06522e67795918ab230ddcc4a18e15494eff7532f4c3ae1", size = 143410, upload-time = "2025-06-14T20:58:39.917Z" } wheels = [ @@ -4068,8 +3999,8 @@ name = "pyobjc-framework-systemextensions" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/57/4609fd9183383616b1e643c2489ad774335f679523a974b9ce346a6d4d5b/pyobjc_framework_systemextensions-11.1.tar.gz", hash = "sha256:8ff9f0aad14dcdd07dd47545c1dd20df7a286306967b0a0232c81fcc382babe6", size = 23062, upload-time = "2025-06-14T20:58:40.686Z" } wheels = [ @@ -4082,8 +4013,8 @@ name = "pyobjc-framework-threadnetwork" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e7/a4/5400a222ced0e4f077a8f4dd0188e08e2af4762e72ed0ed39f9d27feefc9/pyobjc_framework_threadnetwork-11.1.tar.gz", hash = "sha256:73a32782f44b61ca0f8a4a9811c36b1ca1cdcf96c8a3ba4de35d8e8e58a86ad5", size = 13572, upload-time = "2025-06-14T20:58:41.311Z" } wheels = [ @@ -4095,8 +4026,8 @@ name = "pyobjc-framework-uniformtypeidentifiers" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/c5/4f/066ed1c69352ccc29165f45afb302f8c9c2b5c6f33ee3abfa41b873c07e5/pyobjc_framework_uniformtypeidentifiers-11.1.tar.gz", hash = "sha256:86c499bec8953aeb0c95af39b63f2592832384f09f12523405650b5d5f1ed5e9", size = 20599, upload-time = "2025-06-14T20:58:41.945Z" } wheels = [ @@ -4108,8 +4039,8 @@ name = "pyobjc-framework-usernotifications" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/b4/4c/e7e180fcd06c246c37f218bcb01c40ea0213fde5ace3c09d359e60dcaafd/pyobjc_framework_usernotifications-11.1.tar.gz", hash = "sha256:38fc763afa7854b41ddfca8803f679a7305d278af8a7ad02044adc1265699996", size = 55428, upload-time = "2025-06-14T20:58:42.572Z" } wheels = [ @@ -4122,9 +4053,9 @@ name = "pyobjc-framework-usernotificationsui" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-usernotifications" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-usernotifications", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/d2/c4/03d97bd3adcee9b857533cb42967df0d019f6a034adcdbcfca2569d415b2/pyobjc_framework_usernotificationsui-11.1.tar.gz", hash = "sha256:18e0182bddd10381884530d6a28634ebb3280912592f8f2ad5bac2a9308c6a65", size = 14123, upload-time = "2025-06-14T20:58:43.267Z" } wheels = [ @@ -4136,8 +4067,8 @@ name = "pyobjc-framework-videosubscriberaccount" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/aa/00/cd9d93d06204bbb7fe68fb97022b0dd4ecdf8af3adb6d70a41e22c860d55/pyobjc_framework_videosubscriberaccount-11.1.tar.gz", hash = "sha256:2dd78586260fcee51044e129197e8bf2e157176e02babeec2f873afa4235d8c6", size = 28856, upload-time = "2025-06-14T20:58:43.903Z" } wheels = [ @@ -4149,10 +4080,10 @@ name = "pyobjc-framework-videotoolbox" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coremedia" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coremedia", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/e5/e3/df9096f54ae1f27cab8f922ee70cbda5d80f8c1d12734c38580829858133/pyobjc_framework_videotoolbox-11.1.tar.gz", hash = "sha256:a27985656e1b639cdb102fcc727ebc39f71bb1a44cdb751c8c80cc9fe938f3a9", size = 88551, upload-time = "2025-06-14T20:58:44.566Z" } wheels = [ @@ -4165,8 +4096,8 @@ name = "pyobjc-framework-virtualization" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/f1/ff/57214e8f42755eeaad516a7e673dae4341b8742005d368ecc22c7a790b0b/pyobjc_framework_virtualization-11.1.tar.gz", hash = "sha256:4221ee5eb669e43a2ff46e04178bec149af2d65205deb5d4db5fa62ea060e022", size = 78633, upload-time = "2025-06-14T20:58:45.358Z" } wheels = [ @@ -4179,10 +4110,10 @@ name = "pyobjc-framework-vision" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, - { name = "pyobjc-framework-coreml" }, - { name = "pyobjc-framework-quartz" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-coreml", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-quartz", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/40/a8/7128da4d0a0103cabe58910a7233e2f98d18c590b1d36d4b3efaaedba6b9/pyobjc_framework_vision-11.1.tar.gz", hash = "sha256:26590512ee7758da3056499062a344b8a351b178be66d4b719327884dde4216b", size = 133721, upload-time = "2025-06-14T20:58:46.095Z" } wheels = [ @@ -4195,8 +4126,8 @@ name = "pyobjc-framework-webkit" version = "11.1" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pyobjc-core" }, - { name = "pyobjc-framework-cocoa" }, + { name = "pyobjc-core", marker = "sys_platform == 'darwin'" }, + { name = "pyobjc-framework-cocoa", marker = "sys_platform == 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/92/04/fb3d0b68994f7e657ef00c1ac5fc1c04ae2fc7ea581d647f5ae1f6739b14/pyobjc_framework_webkit-11.1.tar.gz", hash = "sha256:27e701c7aaf4f24fc7e601a128e2ef14f2773f4ab071b9db7438dc5afb5053ae", size = 717102, upload-time = "2025-06-14T20:58:47.461Z" } wheels = [ @@ -4302,28 +4233,14 @@ wheels = [ [[package]] name = "pytest-asyncio" -version = "1.0.0" +version = "1.1.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pytest" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/d0/d4/14f53324cb1a6381bef29d698987625d80052bb33932d8e7cbf9b337b17c/pytest_asyncio-1.0.0.tar.gz", hash = "sha256:d15463d13f4456e1ead2594520216b225a16f781e144f8fdf6c5bb4667c48b3f", size = 46960, upload-time = "2025-05-26T04:54:40.484Z" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/51/f8794af39eeb870e87a8c8068642fc07bce0c854d6865d7dd0f2a9d338c2/pytest_asyncio-1.1.0.tar.gz", hash = "sha256:796aa822981e01b68c12e4827b8697108f7205020f24b5793b3c41555dab68ea", size = 46652, upload-time = "2025-07-16T04:29:26.393Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/30/05/ce271016e351fddc8399e546f6e23761967ee09c8c568bbfbecb0c150171/pytest_asyncio-1.0.0-py3-none-any.whl", hash = "sha256:4f024da9f1ef945e680dc68610b52550e36590a67fd31bb3b4943979a1f90ef3", size = 15976, upload-time = "2025-05-26T04:54:39.035Z" }, -] - -[[package]] -name = "pytest-cov" -version = "6.2.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "coverage", extra = ["toml"] }, - { name = "pluggy" }, - { name = "pytest" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/18/99/668cade231f434aaa59bbfbf49469068d2ddd945000621d3d165d2e7dd7b/pytest_cov-6.2.1.tar.gz", hash = "sha256:25cc6cc0a5358204b8108ecedc51a9b57b34cc6b8c967cc2c01a4e00d8a67da2", size = 69432, upload-time = "2025-06-12T10:47:47.684Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/bc/16/4ea354101abb1287856baa4af2732be351c7bee728065aed451b678153fd/pytest_cov-6.2.1-py3-none-any.whl", hash = "sha256:f5bc4c23f42f1cdd23c70b1dab1bbaef4fc505ba950d53e0081d0730dd7e86d5", size = 24644, upload-time = "2025-06-12T10:47:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/c7/9d/bf86eddabf8c6c9cb1ea9a869d6873b46f105a5d292d3a6f7071f5b07935/pytest_asyncio-1.1.0-py3-none-any.whl", hash = "sha256:5fe2d69607b0bd75c656d1211f969cadba035030156745ee09e7d71740e58ecf", size = 15157, upload-time = "2025-07-16T04:29:24.929Z" }, ] [[package]] @@ -4401,8 +4318,8 @@ wheels = [ [[package]] name = "pytest-xdist" -version = "3.7.1.dev3+g909e97b" -source = { git = "https://github.com/sshane/pytest-xdist?rev=909e97b49d12401c10608f9d777bfc9dab8a4413#909e97b49d12401c10608f9d777bfc9dab8a4413" } +version = "3.7.1.dev24+g2b4372bd6" +source = { git = "https://github.com/sshane/pytest-xdist?rev=2b4372bd62699fb412c4fe2f95bf9f01bd2018da#2b4372bd62699fb412c4fe2f95bf9f01bd2018da" } dependencies = [ { name = "execnet" }, { name = "pytest" }, @@ -4425,7 +4342,7 @@ name = "python-xlib" version = "0.33" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "six" }, + { name = "six", marker = "sys_platform != 'darwin'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/86/f5/8c0653e5bb54e0cbdfe27bf32d41f27bc4e12faa8742778c17f2a71be2c0/python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32", size = 269068, upload-time = "2022-12-25T18:53:00.824Z" } wheels = [ @@ -4460,15 +4377,15 @@ sdist = { url = "https://files.pythonhosted.org/packages/79/0c/c16bc93ac2755bac0 [[package]] name = "pywin32" -version = "310" +version = "311" source = { registry = "https://pypi.org/simple" } wheels = [ - { url = "https://files.pythonhosted.org/packages/f7/b1/68aa2986129fb1011dabbe95f0136f44509afaf072b12b8f815905a39f33/pywin32-310-cp311-cp311-win32.whl", hash = "sha256:1e765f9564e83011a63321bb9d27ec456a0ed90d3732c4b2e312b855365ed8bd", size = 8784284, upload-time = "2025-03-17T00:55:53.124Z" }, - { url = "https://files.pythonhosted.org/packages/b3/bd/d1592635992dd8db5bb8ace0551bc3a769de1ac8850200cfa517e72739fb/pywin32-310-cp311-cp311-win_amd64.whl", hash = "sha256:126298077a9d7c95c53823934f000599f66ec9296b09167810eb24875f32689c", size = 9520748, upload-time = "2025-03-17T00:55:55.203Z" }, - { url = "https://files.pythonhosted.org/packages/90/b1/ac8b1ffce6603849eb45a91cf126c0fa5431f186c2e768bf56889c46f51c/pywin32-310-cp311-cp311-win_arm64.whl", hash = "sha256:19ec5fc9b1d51c4350be7bb00760ffce46e6c95eaf2f0b2f1150657b1a43c582", size = 8455941, upload-time = "2025-03-17T00:55:57.048Z" }, - { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, - { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, ] [[package]] @@ -4543,38 +4460,38 @@ wheels = [ [[package]] name = "pyzmq" -version = "27.0.0" +version = "27.0.2" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi", marker = "implementation_name == 'pypy'" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" } +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/44/df/84c630654106d9bd9339cdb564aa941ed41b023a0264251d6743766bb50e/pyzmq-27.0.0-cp311-cp311-macosx_10_15_universal2.whl", hash = "sha256:21457825249b2a53834fa969c69713f8b5a79583689387a5e7aed880963ac564", size = 1332718, upload-time = "2025-06-13T14:07:16.555Z" }, - { url = "https://files.pythonhosted.org/packages/c1/8e/f6a5461a07654d9840d256476434ae0ff08340bba562a455f231969772cb/pyzmq-27.0.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:1958947983fef513e6e98eff9cb487b60bf14f588dc0e6bf35fa13751d2c8251", size = 908248, upload-time = "2025-06-13T14:07:18.033Z" }, - { url = "https://files.pythonhosted.org/packages/7c/93/82863e8d695a9a3ae424b63662733ae204a295a2627d52af2f62c2cd8af9/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0dc628b5493f9a8cd9844b8bee9732ef587ab00002157c9329e4fc0ef4d3afa", size = 668647, upload-time = "2025-06-13T14:07:19.378Z" }, - { url = "https://files.pythonhosted.org/packages/f3/85/15278769b348121eacdbfcbd8c4d40f1102f32fa6af5be1ffc032ed684be/pyzmq-27.0.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7bbe9e1ed2c8d3da736a15694d87c12493e54cc9dc9790796f0321794bbc91f", size = 856600, upload-time = "2025-06-13T14:07:20.906Z" }, - { url = "https://files.pythonhosted.org/packages/d4/af/1c469b3d479bd095edb28e27f12eee10b8f00b356acbefa6aeb14dd295d1/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dc1091f59143b471d19eb64f54bae4f54bcf2a466ffb66fe45d94d8d734eb495", size = 1657748, upload-time = "2025-06-13T14:07:22.549Z" }, - { url = "https://files.pythonhosted.org/packages/8c/f4/17f965d0ee6380b1d6326da842a50e4b8b9699745161207945f3745e8cb5/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:7011ade88c8e535cf140f8d1a59428676fbbce7c6e54fefce58bf117aefb6667", size = 2034311, upload-time = "2025-06-13T14:07:23.966Z" }, - { url = "https://files.pythonhosted.org/packages/e0/6e/7c391d81fa3149fd759de45d298003de6cfab343fb03e92c099821c448db/pyzmq-27.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2c386339d7e3f064213aede5d03d054b237937fbca6dd2197ac8cf3b25a6b14e", size = 1893630, upload-time = "2025-06-13T14:07:25.899Z" }, - { url = "https://files.pythonhosted.org/packages/0e/e0/eaffe7a86f60e556399e224229e7769b717f72fec0706b70ab2c03aa04cb/pyzmq-27.0.0-cp311-cp311-win32.whl", hash = "sha256:0546a720c1f407b2172cb04b6b094a78773491497e3644863cf5c96c42df8cff", size = 567706, upload-time = "2025-06-13T14:07:27.595Z" }, - { url = "https://files.pythonhosted.org/packages/c9/05/89354a8cffdcce6e547d48adaaf7be17007fc75572123ff4ca90a4ca04fc/pyzmq-27.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:15f39d50bd6c9091c67315ceb878a4f531957b121d2a05ebd077eb35ddc5efed", size = 630322, upload-time = "2025-06-13T14:07:28.938Z" }, - { url = "https://files.pythonhosted.org/packages/fa/07/4ab976d5e1e63976719389cc4f3bfd248a7f5f2bb2ebe727542363c61b5f/pyzmq-27.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c5817641eebb391a2268c27fecd4162448e03538387093cdbd8bf3510c316b38", size = 558435, upload-time = "2025-06-13T14:07:30.256Z" }, - { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438, upload-time = "2025-06-13T14:07:31.676Z" }, - { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095, upload-time = "2025-06-13T14:07:33.104Z" }, - { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826, upload-time = "2025-06-13T14:07:34.831Z" }, - { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750, upload-time = "2025-06-13T14:07:36.553Z" }, - { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357, upload-time = "2025-06-13T14:07:38.21Z" }, - { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281, upload-time = "2025-06-13T14:07:39.599Z" }, - { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110, upload-time = "2025-06-13T14:07:41.027Z" }, - { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297, upload-time = "2025-06-13T14:07:42.533Z" }, - { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203, upload-time = "2025-06-13T14:07:43.843Z" }, - { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927, upload-time = "2025-06-13T14:07:45.51Z" }, - { url = "https://files.pythonhosted.org/packages/98/a6/92394373b8dbc1edc9d53c951e8d3989d518185174ee54492ec27711779d/pyzmq-27.0.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:cd1dc59763effd1576f8368047c9c31468fce0af89d76b5067641137506792ae", size = 835948, upload-time = "2025-06-13T14:08:43.516Z" }, - { url = "https://files.pythonhosted.org/packages/56/f3/4dc38d75d9995bfc18773df3e41f2a2ca9b740b06f1a15dbf404077e7588/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:60e8cc82d968174650c1860d7b716366caab9973787a1c060cf8043130f7d0f7", size = 799874, upload-time = "2025-06-13T14:08:45.017Z" }, - { url = "https://files.pythonhosted.org/packages/ab/ba/64af397e0f421453dc68e31d5e0784d554bf39013a2de0872056e96e58af/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:14fe7aaac86e4e93ea779a821967360c781d7ac5115b3f1a171ced77065a0174", size = 567400, upload-time = "2025-06-13T14:08:46.855Z" }, - { url = "https://files.pythonhosted.org/packages/63/87/ec956cbe98809270b59a22891d5758edae147a258e658bf3024a8254c855/pyzmq-27.0.0-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6ad0562d4e6abb785be3e4dd68599c41be821b521da38c402bc9ab2a8e7ebc7e", size = 747031, upload-time = "2025-06-13T14:08:48.419Z" }, - { url = "https://files.pythonhosted.org/packages/be/8a/4a3764a68abc02e2fbb0668d225b6fda5cd39586dd099cee8b2ed6ab0452/pyzmq-27.0.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:9df43a2459cd3a3563404c1456b2c4c69564daa7dbaf15724c09821a3329ce46", size = 544726, upload-time = "2025-06-13T14:08:49.903Z" }, + { 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]] @@ -4615,7 +4532,7 @@ wheels = [ [[package]] name = "requests" -version = "2.32.4" +version = "2.32.5" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, @@ -4623,40 +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" }, -] - -[[package]] -name = "rerun-sdk" -version = "0.23.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "attrs" }, - { name = "numpy" }, - { name = "pillow" }, - { name = "pyarrow" }, - { name = "typing-extensions" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/00/54/710685ea836d91f14422de515262a9db5c2a17777e59ae4c1c19b46c1fa0/rerun_sdk-0.23.3-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:8a048ace69443aeb4040046d4bc28ba17be2c6862df7c2161e21b0608d5d6e0f", size = 59486089, upload-time = "2025-05-26T16:08:08.395Z" }, - { url = "https://files.pythonhosted.org/packages/26/81/b94c23cde1b9ed54900f47e337f8e156d9fdec7494eaa11bbb592c26a5d9/rerun_sdk-0.23.3-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:a3ef05acfa9315d956c69eb680a50e46c802ba27eb8025bab71e14a989299383", size = 55128863, upload-time = "2025-05-26T16:08:13.328Z" }, - { url = "https://files.pythonhosted.org/packages/02/bc/c32f71969a5d981087fded194012ef162391e49dba71ba5115a17c642065/rerun_sdk-0.23.3-cp39-abi3-manylinux_2_31_aarch64.whl", hash = "sha256:0cc9c60f1180edca1a4d70dbe591eb108e4dbe599c31136949b8d4106410cd14", size = 61603766, upload-time = "2025-05-26T16:08:17.476Z" }, - { url = "https://files.pythonhosted.org/packages/fa/ef/93b6a8932747842e5f7c27f3ce7ef82bbc046aad3b7d5b2e905ffc5614a7/rerun_sdk-0.23.3-cp39-abi3-manylinux_2_31_x86_64.whl", hash = "sha256:49833bf3a4854eaf287a8bfe7dfa4cd8f67b0fd7917a469c8bb7061895fb1cf8", size = 64875203, upload-time = "2025-05-26T16:08:21.95Z" }, - { url = "https://files.pythonhosted.org/packages/c5/36/759287d69301a3ada86ee102369a9521dfef0bb91b215e98a6a7b262a311/rerun_sdk-0.23.3-cp39-abi3-win_amd64.whl", hash = "sha256:88639632116b3761e2a45f4e5ac39eaf3406b615f516baccb508d93c6b42cc4e", size = 50862558, upload-time = "2025-05-26T16:08:26.98Z" }, + { 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]] @@ -4687,36 +4585,37 @@ wheels = [ [[package]] name = "rubicon-objc" -version = "0.5.1" +version = "0.5.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/e0/83/e57741dcf862a2581d53eccf8b11749c97f73d9754bbc538fb6c7b527da3/rubicon_objc-0.5.1.tar.gz", hash = "sha256:90bee9fc1de4515e17615e15648989b88bb8d4d2ffc8c7c52748272cd7f30a66", size = 174639, upload-time = "2025-06-03T06:33:50.822Z" } +sdist = { url = "https://files.pythonhosted.org/packages/a7/2f/612049b8601e810080f63f4b85acbf2ed0784349088d3c944eb288e1d487/rubicon_objc-0.5.2.tar.gz", hash = "sha256:1180593935f6a8a39c23b5f4b7baa24aedf9f7285e80804a1d9d6b50a50572f5", size = 175710, upload-time = "2025-08-07T06:12:25.194Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/58/0a/e451c3dbda38dd6abab1fd16c3b35623fc0635dffcbbf97f1acc55a58508/rubicon_objc-0.5.1-py3-none-any.whl", hash = "sha256:17092756241b8370231cfaad45ad6e8ce99534987f2acbc944d65df5bdf8f6cd", size = 63323, upload-time = "2025-06-03T06:33:48.863Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a4/11ad29100060af56408ed084dca76a16d2ce8eb31b75081bfc0eec45d755/rubicon_objc-0.5.2-py3-none-any.whl", hash = "sha256:829b253c579e51fc34f4bb6587c34806e78960dcc1eb24e62b38141a1fe02b39", size = 63512, upload-time = "2025-08-07T06:12:23.766Z" }, ] [[package]] name = "ruff" -version = "0.12.0" +version = "0.12.10" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/24/90/5255432602c0b196a0da6720f6f76b93eb50baef46d3c9b0025e2f9acbf3/ruff-0.12.0.tar.gz", hash = "sha256:4d047db3662418d4a848a3fdbfaf17488b34b62f527ed6f10cb8afd78135bc5c", size = 4376101, upload-time = "2025-06-17T15:19:26.217Z" } +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/e6/fd/b46bb20e14b11ff49dbc74c61de352e0dc07fb650189513631f6fb5fc69f/ruff-0.12.0-py3-none-linux_armv6l.whl", hash = "sha256:5652a9ecdb308a1754d96a68827755f28d5dfb416b06f60fd9e13f26191a8848", size = 10311554, upload-time = "2025-06-17T15:18:45.792Z" }, - { url = "https://files.pythonhosted.org/packages/e7/d3/021dde5a988fa3e25d2468d1dadeea0ae89dc4bc67d0140c6e68818a12a1/ruff-0.12.0-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:05ed0c914fabc602fc1f3b42c53aa219e5736cb030cdd85640c32dbc73da74a6", size = 11118435, upload-time = "2025-06-17T15:18:49.064Z" }, - { url = "https://files.pythonhosted.org/packages/07/a2/01a5acf495265c667686ec418f19fd5c32bcc326d4c79ac28824aecd6a32/ruff-0.12.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:07a7aa9b69ac3fcfda3c507916d5d1bca10821fe3797d46bad10f2c6de1edda0", size = 10466010, upload-time = "2025-06-17T15:18:51.341Z" }, - { url = "https://files.pythonhosted.org/packages/4c/57/7caf31dd947d72e7aa06c60ecb19c135cad871a0a8a251723088132ce801/ruff-0.12.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e7731c3eec50af71597243bace7ec6104616ca56dda2b99c89935fe926bdcd48", size = 10661366, upload-time = "2025-06-17T15:18:53.29Z" }, - { url = "https://files.pythonhosted.org/packages/e9/ba/aa393b972a782b4bc9ea121e0e358a18981980856190d7d2b6187f63e03a/ruff-0.12.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:952d0630eae628250ab1c70a7fffb641b03e6b4a2d3f3ec6c1d19b4ab6c6c807", size = 10173492, upload-time = "2025-06-17T15:18:55.262Z" }, - { url = "https://files.pythonhosted.org/packages/d7/50/9349ee777614bc3062fc6b038503a59b2034d09dd259daf8192f56c06720/ruff-0.12.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c021f04ea06966b02614d442e94071781c424ab8e02ec7af2f037b4c1e01cc82", size = 11761739, upload-time = "2025-06-17T15:18:58.906Z" }, - { url = "https://files.pythonhosted.org/packages/04/8f/ad459de67c70ec112e2ba7206841c8f4eb340a03ee6a5cabc159fe558b8e/ruff-0.12.0-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7d235618283718ee2fe14db07f954f9b2423700919dc688eacf3f8797a11315c", size = 12537098, upload-time = "2025-06-17T15:19:01.316Z" }, - { url = "https://files.pythonhosted.org/packages/ed/50/15ad9c80ebd3c4819f5bd8883e57329f538704ed57bac680d95cb6627527/ruff-0.12.0-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0c0758038f81beec8cc52ca22de9685b8ae7f7cc18c013ec2050012862cc9165", size = 12154122, upload-time = "2025-06-17T15:19:03.727Z" }, - { url = "https://files.pythonhosted.org/packages/76/e6/79b91e41bc8cc3e78ee95c87093c6cacfa275c786e53c9b11b9358026b3d/ruff-0.12.0-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:139b3d28027987b78fc8d6cfb61165447bdf3740e650b7c480744873688808c2", size = 11363374, upload-time = "2025-06-17T15:19:05.875Z" }, - { url = "https://files.pythonhosted.org/packages/db/c3/82b292ff8a561850934549aa9dc39e2c4e783ab3c21debe55a495ddf7827/ruff-0.12.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:68853e8517b17bba004152aebd9dd77d5213e503a5f2789395b25f26acac0da4", size = 11587647, upload-time = "2025-06-17T15:19:08.246Z" }, - { url = "https://files.pythonhosted.org/packages/2b/42/d5760d742669f285909de1bbf50289baccb647b53e99b8a3b4f7ce1b2001/ruff-0.12.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:3a9512af224b9ac4757f7010843771da6b2b0935a9e5e76bb407caa901a1a514", size = 10527284, upload-time = "2025-06-17T15:19:10.37Z" }, - { url = "https://files.pythonhosted.org/packages/19/f6/fcee9935f25a8a8bba4adbae62495c39ef281256693962c2159e8b284c5f/ruff-0.12.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:b08df3d96db798e5beb488d4df03011874aff919a97dcc2dd8539bb2be5d6a88", size = 10158609, upload-time = "2025-06-17T15:19:12.286Z" }, - { url = "https://files.pythonhosted.org/packages/37/fb/057febf0eea07b9384787bfe197e8b3384aa05faa0d6bd844b94ceb29945/ruff-0.12.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:6a315992297a7435a66259073681bb0d8647a826b7a6de45c6934b2ca3a9ed51", size = 11141462, upload-time = "2025-06-17T15:19:15.195Z" }, - { url = "https://files.pythonhosted.org/packages/10/7c/1be8571011585914b9d23c95b15d07eec2d2303e94a03df58294bc9274d4/ruff-0.12.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:1e55e44e770e061f55a7dbc6e9aed47feea07731d809a3710feda2262d2d4d8a", size = 11641616, upload-time = "2025-06-17T15:19:17.6Z" }, - { url = "https://files.pythonhosted.org/packages/6a/ef/b960ab4818f90ff59e571d03c3f992828d4683561095e80f9ef31f3d58b7/ruff-0.12.0-py3-none-win32.whl", hash = "sha256:7162a4c816f8d1555eb195c46ae0bd819834d2a3f18f98cc63819a7b46f474fb", size = 10525289, upload-time = "2025-06-17T15:19:19.688Z" }, - { url = "https://files.pythonhosted.org/packages/34/93/8b16034d493ef958a500f17cda3496c63a537ce9d5a6479feec9558f1695/ruff-0.12.0-py3-none-win_amd64.whl", hash = "sha256:d00b7a157b8fb6d3827b49d3324da34a1e3f93492c1f97b08e222ad7e9b291e0", size = 11598311, upload-time = "2025-06-17T15:19:21.785Z" }, - { url = "https://files.pythonhosted.org/packages/d0/33/4d3e79e4a84533d6cd526bfb42c020a23256ae5e4265d858bd1287831f7d/ruff-0.12.0-py3-none-win_arm64.whl", hash = "sha256:8cd24580405ad8c1cc64d61725bca091d6b6da7eb3d36f72cc605467069d7e8b", size = 10724946, upload-time = "2025-06-17T15:19:23.952Z" }, + { 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]] @@ -4730,15 +4629,15 @@ wheels = [ [[package]] name = "sentry-sdk" -version = "2.30.0" +version = "2.35.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "certifi" }, { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/4c/af31e0201b48469786ddeb1bf6fd3dfa3a291cc613a0fe6a60163a7535f9/sentry_sdk-2.30.0.tar.gz", hash = "sha256:436369b02afef7430efb10300a344fb61a11fe6db41c2b11f41ee037d2dd7f45", size = 326767, upload-time = "2025-06-12T10:34:34.733Z" } +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/5a/99/31ac6faaae33ea698086692638f58d14f121162a8db0039e68e94135e7f1/sentry_sdk-2.30.0-py2.py3-none-any.whl", hash = "sha256:59391db1550662f746ea09b483806a631c3ae38d6340804a1a4c0605044f6877", size = 343149, upload-time = "2025-06-12T10:34:32.896Z" }, + { 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]] @@ -4898,35 +4797,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, ] -[[package]] -name = "tomli" -version = "2.2.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, - { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, - { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, - { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, - { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, - { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, - { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, - { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, - { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, - { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, - { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, - { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, - { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, - { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, - { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, - { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, - { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, - { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, - { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, - { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, -] - [[package]] name = "tqdm" version = "4.67.1" @@ -4941,14 +4811,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.32.4.20250611" +version = "2.32.4.20250809" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/6d/7f/73b3a04a53b0fd2a911d4ec517940ecd6600630b559e4505cc7b68beb5a0/types_requests-2.32.4.20250611.tar.gz", hash = "sha256:741c8777ed6425830bf51e54d6abe245f79b4dcb9019f1622b773463946bf826", size = 23118, upload-time = "2025-06-11T03:11:41.272Z" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/b0/9355adb86ec84d057fea765e4c49cce592aaf3d5117ce5609a95a7fc3dac/types_requests-2.32.4.20250809.tar.gz", hash = "sha256:d8060de1c8ee599311f56ff58010fb4902f462a1470802cf9f6ed27bc46c4df3", size = 23027, upload-time = "2025-08-09T03:17:10.664Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/3d/ea/0be9258c5a4fa1ba2300111aa5a0767ee6d18eb3fd20e91616c12082284d/types_requests-2.32.4.20250611-py3-none-any.whl", hash = "sha256:ad2fe5d3b0cb3c2c902c8815a70e7fb2302c4b8c1f77bdcd738192cdb3878072", size = 20643, upload-time = "2025-06-11T03:11:40.186Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6f/ec0012be842b1d888d46884ac5558fd62aeae1f0ec4f7a581433d890d4b5/types_requests-2.32.4.20250809-py3-none-any.whl", hash = "sha256:f73d1832fb519ece02c85b1f09d5f0dd3108938e7d47e7f94bbfa18a6782b163", size = 20644, upload-time = "2025-08-09T03:17:09.716Z" }, ] [[package]] @@ -4962,11 +4832,11 @@ wheels = [ [[package]] name = "typing-extensions" -version = "4.14.0" +version = "4.15.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +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/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, + { 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]] @@ -5013,31 +4883,31 @@ wheels = [ [[package]] name = "xattr" -version = "1.1.4" +version = "1.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "cffi" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/62/bf/8b98081f9f8fd56d67b9478ff1e0f8c337cde08bcb92f0d592f0a7958983/xattr-1.1.4.tar.gz", hash = "sha256:b7b02ecb2270da5b7e7deaeea8f8b528c17368401c2b9d5f63e91f545b45d372", size = 16729, upload-time = "2025-01-06T19:19:32.557Z" } +sdist = { url = "https://files.pythonhosted.org/packages/50/65/14438ae55acf7f8fc396ee8340d740a3e1d6ef382bf25bf24156cfb83563/xattr-1.2.0.tar.gz", hash = "sha256:a64c8e21eff1be143accf80fd3b8fde3e28a478c37da298742af647ac3e5e0a7", size = 17293, upload-time = "2025-07-14T03:15:44.884Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/78/5b/f64ba0f93e6447e1997068959f22ff99e08d77dd88d9edcf97ddcb9e9016/xattr-1.1.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:bb4bbe37ba95542081890dd34fa5347bef4651e276647adaa802d5d0d7d86452", size = 23920, upload-time = "2025-01-06T19:17:48.234Z" }, - { url = "https://files.pythonhosted.org/packages/c8/54/ad66655f0b1317b0a297aa2d6ed7d6e5d5343495841fad535bee37a56471/xattr-1.1.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:3da489ecef798705f9a39ea8cea4ead0d1eeed55f92c345add89740bd930bab6", size = 18883, upload-time = "2025-01-06T19:17:49.46Z" }, - { url = "https://files.pythonhosted.org/packages/4d/5d/7d5154570bbcb898e6123c292f697c87c33e12258a1a8b9741539f952681/xattr-1.1.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:798dd0cbe696635a6f74b06fc430818bf9c3b24314e1502eadf67027ab60c9b0", size = 19221, upload-time = "2025-01-06T19:17:51.654Z" }, - { url = "https://files.pythonhosted.org/packages/b9/b7/135cf3018278051f57bb5dde944cb1ca4f7ad4ec383465a08c6a5c7f7152/xattr-1.1.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7b2b6361626efad5eb5a6bf8172c6c67339e09397ee8140ec41258737bea9681", size = 39098, upload-time = "2025-01-06T19:17:53.099Z" }, - { url = "https://files.pythonhosted.org/packages/a5/62/577e2eb0108158b78cd93ea3782c7a8d464693f1338a5350a1db16f69a89/xattr-1.1.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6e7fa20a0c9ce022d19123b1c5b848d00a68b837251835a7929fe041ee81dcd0", size = 36982, upload-time = "2025-01-06T19:17:54.493Z" }, - { url = "https://files.pythonhosted.org/packages/59/cc/ab3bd7a4bedf445be4b35de4a4627ef2944786724d18eaf28d05c1238c7c/xattr-1.1.4-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e20eeb08e2c57fc7e71f050b1cfae35cbb46105449853a582bf53fd23c5379e", size = 38891, upload-time = "2025-01-06T19:17:55.853Z" }, - { url = "https://files.pythonhosted.org/packages/45/e8/2285651d92f1460159753fe6628af259c943fcc5071e48a0540fa11dc34d/xattr-1.1.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:477370e75821bded901487e5e752cffe554d1bd3bd4839b627d4d1ee8c95a093", size = 38362, upload-time = "2025-01-06T19:17:57.078Z" }, - { url = "https://files.pythonhosted.org/packages/5f/af/7856c0b1970272a53a428bb20dc125f9fd350fb1b40ebca4e54610af1b79/xattr-1.1.4-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:a8682091cd34a9f4a93c8aaea4101aae99f1506e24da00a3cc3dd2eca9566f21", size = 36724, upload-time = "2025-01-06T19:17:58.534Z" }, - { url = "https://files.pythonhosted.org/packages/5d/34/087e02b32d6288a40b7f6573e97a119016e6c3713d4f4b866bbf56cfb803/xattr-1.1.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:2e079b3b1a274ba2121cf0da38bbe5c8d2fb1cc49ecbceb395ce20eb7d69556d", size = 37945, upload-time = "2025-01-06T19:17:59.764Z" }, - { url = "https://files.pythonhosted.org/packages/f0/2a/d0f9e46de4cec5e4aa45fd939549b977c49dd68202fa844d07cb24ce5f17/xattr-1.1.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ae6579dea05bf9f335a082f711d5924a98da563cac72a2d550f5b940c401c0e9", size = 23917, upload-time = "2025-01-06T19:18:00.868Z" }, - { url = "https://files.pythonhosted.org/packages/83/e0/a5764257cd9c9eb598f4648a3658d915dd3520ec111ecbd251b685de6546/xattr-1.1.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd6038ec9df2e67af23c212693751481d5f7e858156924f14340376c48ed9ac7", size = 18891, upload-time = "2025-01-06T19:18:02.029Z" }, - { url = "https://files.pythonhosted.org/packages/8b/83/a81a147987387fd2841a28f767efedb099cf90e23553ead458f2330e47c5/xattr-1.1.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:608b2877526674eb15df4150ef4b70b7b292ae00e65aecaae2f192af224be200", size = 19213, upload-time = "2025-01-06T19:18:03.303Z" }, - { url = "https://files.pythonhosted.org/packages/4b/52/bf093b4eb9873ffc9e9373dcb38ec8a9b5cd4e6a9f681c4c5cf6bf067a42/xattr-1.1.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c54dad1a6a998c6a23edfd25e99f4d38e9b942d54e518570044edf8c767687ea", size = 39302, upload-time = "2025-01-06T19:18:05.846Z" }, - { url = "https://files.pythonhosted.org/packages/2d/d8/9d7315ebae76a7f48bc5e1aecc7e592eb43376a0f6cf470a854d895d2093/xattr-1.1.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c0dab6ff72bb2b508f3850c368f8e53bd706585012676e1f71debba3310acde8", size = 37224, upload-time = "2025-01-06T19:18:07.226Z" }, - { url = "https://files.pythonhosted.org/packages/c8/b2/10eb17bea7e378b2bcd76fc8c2e5158318e2c08e774b13f548f333d7318a/xattr-1.1.4-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a3c54c6af7cf09432b2c461af257d5f4b1cb2d59eee045f91bacef44421a46d", size = 39145, upload-time = "2025-01-06T19:18:08.403Z" }, - { url = "https://files.pythonhosted.org/packages/74/fb/95bbc28116b3c19a21acc34ec0a5973e9cc97fe49d3f47a65775af3760a8/xattr-1.1.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e346e05a158d554639fbf7a0db169dc693c2d2260c7acb3239448f1ff4a9d67f", size = 38469, upload-time = "2025-01-06T19:18:09.602Z" }, - { url = "https://files.pythonhosted.org/packages/af/03/23db582cb271ed47f2d62956e112501d998b5493f892a77104b5795ae2fc/xattr-1.1.4-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:3ff6d9e2103d0d6e5fcd65b85a2005b66ea81c0720a37036445faadc5bbfa424", size = 36797, upload-time = "2025-01-06T19:18:10.709Z" }, - { url = "https://files.pythonhosted.org/packages/90/c4/b631d0174e097cf8c44d4f70c66545d91dc8ba15bbfa5054dd7da8371461/xattr-1.1.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7a2ee4563c6414dfec0d1ac610f59d39d5220531ae06373eeb1a06ee37cd193f", size = 38128, upload-time = "2025-01-06T19:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e2/bf74df197a415f25e07378bfa301788e3bf2ac029c3a6c7bd56a900934ff/xattr-1.2.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:00c26c14c90058338993bb2d3e1cebf562e94ec516cafba64a8f34f74b9d18b4", size = 24246, upload-time = "2025-07-14T03:14:34.873Z" }, + { url = "https://files.pythonhosted.org/packages/a5/51/922df424556ff35b20ca043da5e4dcf0f99cbcb674f59046d08ceff3ebc7/xattr-1.2.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:b4f43dc644db87d5eb9484a9518c34a864cb2e588db34cffc42139bf55302a1c", size = 19212, upload-time = "2025-07-14T03:14:35.905Z" }, + { url = "https://files.pythonhosted.org/packages/7c/72/1ed37812e8285c8002b8834395c53cc89a2d83aa088db642b217be439017/xattr-1.2.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7602583fc643ca76576498e2319c7cef0b72aef1936701678589da6371b731b", size = 19546, upload-time = "2025-07-14T03:14:37.242Z" }, + { url = "https://files.pythonhosted.org/packages/d4/b8/ec75db23d81beec68e3be20ea176c11f125697d3bbb5e118b9de9ea7a9ab/xattr-1.2.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90c3ad4a9205cceb64ec54616aa90aa42d140c8ae3b9710a0aaa2843a6f1aca7", size = 39426, upload-time = "2025-07-14T03:14:38.264Z" }, + { url = "https://files.pythonhosted.org/packages/d4/9f/c24950641b138072eda7f34d86966dd15cfe3af9a111b5e77b85ee55f99c/xattr-1.2.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:83d87cfe19cd606fc0709d45a4d6efc276900797deced99e239566926a5afedf", size = 37311, upload-time = "2025-07-14T03:14:39.347Z" }, + { url = "https://files.pythonhosted.org/packages/d0/d5/3b7e0dab706d09c6cdb2f05384610e6c5693c72e3794d54a4cad8c838373/xattr-1.2.0-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c67dabd9ddc04ead63fbc85aed459c9afcc24abfc5bb3217fff7ec9a466faacb", size = 39222, upload-time = "2025-07-14T03:14:40.768Z" }, + { url = "https://files.pythonhosted.org/packages/0e/16/80cf8ec7d92d20b2860c96a1eca18d25e27fa4770f32c9e8250ff32e7386/xattr-1.2.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9a18ee82d8ba2c17f1e8414bfeb421fa763e0fb4acbc1e124988ca1584ad32d5", size = 38694, upload-time = "2025-07-14T03:14:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/b154b254e6e4596aed3210dd48b2e82d958b16d9a7f65346b9154968d2d0/xattr-1.2.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:38de598c47b85185e745986a061094d2e706e9c2d9022210d2c738066990fe91", size = 37055, upload-time = "2025-07-14T03:14:43.435Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/3a615660849ef9bdf46d04f9c6d40ee082f7427678013ff85452ed9497db/xattr-1.2.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:15e754e854bdaac366ad3f1c8fbf77f6668e8858266b4246e8c5f487eeaf1179", size = 38275, upload-time = "2025-07-14T03:14:45.18Z" }, + { url = "https://files.pythonhosted.org/packages/37/e5/b048a5f6c5a489915026b70b9133242a2a368383ddab24e4e3a5bdba7ebd/xattr-1.2.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:daff0c1f5c5e4eaf758c56259c4f72631fa9619875e7a25554b6077dc73da964", size = 24240, upload-time = "2025-07-14T03:14:46.173Z" }, + { url = "https://files.pythonhosted.org/packages/cc/f5/d795774f719a0be6137041d4833ca00b178f816e538948548dff79530f34/xattr-1.2.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:109b11fb3f73a0d4e199962f11230ab5f462e85a8021874f96c1732aa61148d5", size = 19218, upload-time = "2025-07-14T03:14:47.412Z" }, + { url = "https://files.pythonhosted.org/packages/cb/8b/65f3bed09ca9ced27bbba8d4a3326f14a58b98ac102163d85b545f81d9c2/xattr-1.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7c7c12968ce0bf798d8ba90194cef65de768bee9f51a684e022c74cab4218305", size = 19539, upload-time = "2025-07-14T03:14:48.413Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/01ecfdf41ce70f7e29c8a21e730de3c157fb1cb84391923581af81a44c45/xattr-1.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d37989dabf25ff18773e4aaeebcb65604b9528f8645f43e02bebaa363e3ae958", size = 39631, upload-time = "2025-07-14T03:14:49.665Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e9/15cbf9c59cf1117e3c45dd429c52f9dab25d95e65ac245c5ad9532986bec/xattr-1.2.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:165de92b0f2adafb336f936931d044619b9840e35ba01079f4dd288747b73714", size = 37552, upload-time = "2025-07-14T03:14:50.718Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f5/cb4dad87843fe79d605cf5d10caad80e2c338a06f0363f1443449185f489/xattr-1.2.0-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82191c006ae4c609b22b9aea5f38f68fff022dc6884c4c0e1dba329effd4b288", size = 39472, upload-time = "2025-07-14T03:14:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d9/012df7b814cc4a0ae41afb59ac31d0469227397b29f58c1377e8db0f34ba/xattr-1.2.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2b2e9c87dc643b09d86befad218e921f6e65b59a4668d6262b85308de5dbd1dd", size = 38802, upload-time = "2025-07-14T03:14:52.801Z" }, + { url = "https://files.pythonhosted.org/packages/d8/08/e107a5d294a816586f274c33aea480fe740fd446276efc84c067e6c82de2/xattr-1.2.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:14edd5d47d0bb92b23222c0bb6379abbddab01fb776b2170758e666035ecf3aa", size = 37125, upload-time = "2025-07-14T03:14:54.313Z" }, + { url = "https://files.pythonhosted.org/packages/3e/6c/a6f9152e10543af67ea277caae7c5a6400a581e407c42156ffce71dd8242/xattr-1.2.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:12183d5eb104d4da787638c7dadf63b718472d92fec6dbe12994ea5d094d7863", size = 38456, upload-time = "2025-07-14T03:14:55.383Z" }, ] [[package]] @@ -5102,43 +4972,42 @@ wheels = [ [[package]] name = "zstandard" -version = "0.23.0" +version = "0.24.0" source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "cffi", marker = "platform_python_implementation == 'PyPy'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ed/f6/2ac0287b442160a89d726b17a9184a4c615bb5237db763791a7fd16d9df1/zstandard-0.23.0.tar.gz", hash = "sha256:b2d8c62d08e7255f68f7a740bae85b3c9b8e5466baa9cbf7f57f1cde0ac6bc09", size = 681701, upload-time = "2024-07-15T00:18:06.141Z" } +sdist = { url = "https://files.pythonhosted.org/packages/09/1b/c20b2ef1d987627765dcd5bf1dadb8ef6564f00a87972635099bb76b7a05/zstandard-0.24.0.tar.gz", hash = "sha256:fe3198b81c00032326342d973e526803f183f97aa9e9a98e3f897ebafe21178f", size = 905681, upload-time = "2025-08-17T18:36:36.352Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/40/f67e7d2c25a0e2dc1744dd781110b0b60306657f8696cafb7ad7579469bd/zstandard-0.23.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:34895a41273ad33347b2fc70e1bff4240556de3c46c6ea430a7ed91f9042aa4e", size = 788699, upload-time = "2024-07-15T00:14:04.909Z" }, - { url = "https://files.pythonhosted.org/packages/e8/46/66d5b55f4d737dd6ab75851b224abf0afe5774976fe511a54d2eb9063a41/zstandard-0.23.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:77ea385f7dd5b5676d7fd943292ffa18fbf5c72ba98f7d09fc1fb9e819b34c23", size = 633681, upload-time = "2024-07-15T00:14:13.99Z" }, - { url = "https://files.pythonhosted.org/packages/63/b6/677e65c095d8e12b66b8f862b069bcf1f1d781b9c9c6f12eb55000d57583/zstandard-0.23.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:983b6efd649723474f29ed42e1467f90a35a74793437d0bc64a5bf482bedfa0a", size = 4944328, upload-time = "2024-07-15T00:14:16.588Z" }, - { url = "https://files.pythonhosted.org/packages/59/cc/e76acb4c42afa05a9d20827116d1f9287e9c32b7ad58cc3af0721ce2b481/zstandard-0.23.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:80a539906390591dd39ebb8d773771dc4db82ace6372c4d41e2d293f8e32b8db", size = 5311955, upload-time = "2024-07-15T00:14:19.389Z" }, - { url = "https://files.pythonhosted.org/packages/78/e4/644b8075f18fc7f632130c32e8f36f6dc1b93065bf2dd87f03223b187f26/zstandard-0.23.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:445e4cb5048b04e90ce96a79b4b63140e3f4ab5f662321975679b5f6360b90e2", size = 5344944, upload-time = "2024-07-15T00:14:22.173Z" }, - { url = "https://files.pythonhosted.org/packages/76/3f/dbafccf19cfeca25bbabf6f2dd81796b7218f768ec400f043edc767015a6/zstandard-0.23.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd30d9c67d13d891f2360b2a120186729c111238ac63b43dbd37a5a40670b8ca", size = 5442927, upload-time = "2024-07-15T00:14:24.825Z" }, - { url = "https://files.pythonhosted.org/packages/0c/c3/d24a01a19b6733b9f218e94d1a87c477d523237e07f94899e1c10f6fd06c/zstandard-0.23.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d20fd853fbb5807c8e84c136c278827b6167ded66c72ec6f9a14b863d809211c", size = 4864910, upload-time = "2024-07-15T00:14:26.982Z" }, - { url = "https://files.pythonhosted.org/packages/1c/a9/cf8f78ead4597264f7618d0875be01f9bc23c9d1d11afb6d225b867cb423/zstandard-0.23.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ed1708dbf4d2e3a1c5c69110ba2b4eb6678262028afd6c6fbcc5a8dac9cda68e", size = 4935544, upload-time = "2024-07-15T00:14:29.582Z" }, - { url = "https://files.pythonhosted.org/packages/2c/96/8af1e3731b67965fb995a940c04a2c20997a7b3b14826b9d1301cf160879/zstandard-0.23.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:be9b5b8659dff1f913039c2feee1aca499cfbc19e98fa12bc85e037c17ec6ca5", size = 5467094, upload-time = "2024-07-15T00:14:40.126Z" }, - { url = "https://files.pythonhosted.org/packages/ff/57/43ea9df642c636cb79f88a13ab07d92d88d3bfe3e550b55a25a07a26d878/zstandard-0.23.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:65308f4b4890aa12d9b6ad9f2844b7ee42c7f7a4fd3390425b242ffc57498f48", size = 4860440, upload-time = "2024-07-15T00:14:42.786Z" }, - { url = "https://files.pythonhosted.org/packages/46/37/edb78f33c7f44f806525f27baa300341918fd4c4af9472fbc2c3094be2e8/zstandard-0.23.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:98da17ce9cbf3bfe4617e836d561e433f871129e3a7ac16d6ef4c680f13a839c", size = 4700091, upload-time = "2024-07-15T00:14:45.184Z" }, - { url = "https://files.pythonhosted.org/packages/c1/f1/454ac3962671a754f3cb49242472df5c2cced4eb959ae203a377b45b1a3c/zstandard-0.23.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:8ed7d27cb56b3e058d3cf684d7200703bcae623e1dcc06ed1e18ecda39fee003", size = 5208682, upload-time = "2024-07-15T00:14:47.407Z" }, - { url = "https://files.pythonhosted.org/packages/85/b2/1734b0fff1634390b1b887202d557d2dd542de84a4c155c258cf75da4773/zstandard-0.23.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:b69bb4f51daf461b15e7b3db033160937d3ff88303a7bc808c67bbc1eaf98c78", size = 5669707, upload-time = "2024-07-15T00:15:03.529Z" }, - { url = "https://files.pythonhosted.org/packages/52/5a/87d6971f0997c4b9b09c495bf92189fb63de86a83cadc4977dc19735f652/zstandard-0.23.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:034b88913ecc1b097f528e42b539453fa82c3557e414b3de9d5632c80439a473", size = 5201792, upload-time = "2024-07-15T00:15:28.372Z" }, - { url = "https://files.pythonhosted.org/packages/79/02/6f6a42cc84459d399bd1a4e1adfc78d4dfe45e56d05b072008d10040e13b/zstandard-0.23.0-cp311-cp311-win32.whl", hash = "sha256:f2d4380bf5f62daabd7b751ea2339c1a21d1c9463f1feb7fc2bdcea2c29c3160", size = 430586, upload-time = "2024-07-15T00:15:32.26Z" }, - { url = "https://files.pythonhosted.org/packages/be/a2/4272175d47c623ff78196f3c10e9dc7045c1b9caf3735bf041e65271eca4/zstandard-0.23.0-cp311-cp311-win_amd64.whl", hash = "sha256:62136da96a973bd2557f06ddd4e8e807f9e13cbb0bfb9cc06cfe6d98ea90dfe0", size = 495420, upload-time = "2024-07-15T00:15:34.004Z" }, - { url = "https://files.pythonhosted.org/packages/7b/83/f23338c963bd9de687d47bf32efe9fd30164e722ba27fb59df33e6b1719b/zstandard-0.23.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b4567955a6bc1b20e9c31612e615af6b53733491aeaa19a6b3b37f3b65477094", size = 788713, upload-time = "2024-07-15T00:15:35.815Z" }, - { url = "https://files.pythonhosted.org/packages/5b/b3/1a028f6750fd9227ee0b937a278a434ab7f7fdc3066c3173f64366fe2466/zstandard-0.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e172f57cd78c20f13a3415cc8dfe24bf388614324d25539146594c16d78fcc8", size = 633459, upload-time = "2024-07-15T00:15:37.995Z" }, - { url = "https://files.pythonhosted.org/packages/26/af/36d89aae0c1f95a0a98e50711bc5d92c144939efc1f81a2fcd3e78d7f4c1/zstandard-0.23.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b0e166f698c5a3e914947388c162be2583e0c638a4703fc6a543e23a88dea3c1", size = 4945707, upload-time = "2024-07-15T00:15:39.872Z" }, - { url = "https://files.pythonhosted.org/packages/cd/2e/2051f5c772f4dfc0aae3741d5fc72c3dcfe3aaeb461cc231668a4db1ce14/zstandard-0.23.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:12a289832e520c6bd4dcaad68e944b86da3bad0d339ef7989fb7e88f92e96072", size = 5306545, upload-time = "2024-07-15T00:15:41.75Z" }, - { url = "https://files.pythonhosted.org/packages/0a/9e/a11c97b087f89cab030fa71206963090d2fecd8eb83e67bb8f3ffb84c024/zstandard-0.23.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d50d31bfedd53a928fed6707b15a8dbeef011bb6366297cc435accc888b27c20", size = 5337533, upload-time = "2024-07-15T00:15:44.114Z" }, - { url = "https://files.pythonhosted.org/packages/fc/79/edeb217c57fe1bf16d890aa91a1c2c96b28c07b46afed54a5dcf310c3f6f/zstandard-0.23.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:72c68dda124a1a138340fb62fa21b9bf4848437d9ca60bd35db36f2d3345f373", size = 5436510, upload-time = "2024-07-15T00:15:46.509Z" }, - { url = "https://files.pythonhosted.org/packages/81/4f/c21383d97cb7a422ddf1ae824b53ce4b51063d0eeb2afa757eb40804a8ef/zstandard-0.23.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:53dd9d5e3d29f95acd5de6802e909ada8d8d8cfa37a3ac64836f3bc4bc5512db", size = 4859973, upload-time = "2024-07-15T00:15:49.939Z" }, - { url = "https://files.pythonhosted.org/packages/ab/15/08d22e87753304405ccac8be2493a495f529edd81d39a0870621462276ef/zstandard-0.23.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:6a41c120c3dbc0d81a8e8adc73312d668cd34acd7725f036992b1b72d22c1772", size = 4936968, upload-time = "2024-07-15T00:15:52.025Z" }, - { url = "https://files.pythonhosted.org/packages/eb/fa/f3670a597949fe7dcf38119a39f7da49a8a84a6f0b1a2e46b2f71a0ab83f/zstandard-0.23.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:40b33d93c6eddf02d2c19f5773196068d875c41ca25730e8288e9b672897c105", size = 5467179, upload-time = "2024-07-15T00:15:54.971Z" }, - { url = "https://files.pythonhosted.org/packages/4e/a9/dad2ab22020211e380adc477a1dbf9f109b1f8d94c614944843e20dc2a99/zstandard-0.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9206649ec587e6b02bd124fb7799b86cddec350f6f6c14bc82a2b70183e708ba", size = 4848577, upload-time = "2024-07-15T00:15:57.634Z" }, - { url = "https://files.pythonhosted.org/packages/08/03/dd28b4484b0770f1e23478413e01bee476ae8227bbc81561f9c329e12564/zstandard-0.23.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:76e79bc28a65f467e0409098fa2c4376931fd3207fbeb6b956c7c476d53746dd", size = 4693899, upload-time = "2024-07-15T00:16:00.811Z" }, - { url = "https://files.pythonhosted.org/packages/2b/64/3da7497eb635d025841e958bcd66a86117ae320c3b14b0ae86e9e8627518/zstandard-0.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:66b689c107857eceabf2cf3d3fc699c3c0fe8ccd18df2219d978c0283e4c508a", size = 5199964, upload-time = "2024-07-15T00:16:03.669Z" }, - { url = "https://files.pythonhosted.org/packages/43/a4/d82decbab158a0e8a6ebb7fc98bc4d903266bce85b6e9aaedea1d288338c/zstandard-0.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:9c236e635582742fee16603042553d276cca506e824fa2e6489db04039521e90", size = 5655398, upload-time = "2024-07-15T00:16:06.694Z" }, - { url = "https://files.pythonhosted.org/packages/f2/61/ac78a1263bc83a5cf29e7458b77a568eda5a8f81980691bbc6eb6a0d45cc/zstandard-0.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8fffdbd9d1408006baaf02f1068d7dd1f016c6bcb7538682622c556e7b68e35", size = 5191313, upload-time = "2024-07-15T00:16:09.758Z" }, - { url = "https://files.pythonhosted.org/packages/e7/54/967c478314e16af5baf849b6ee9d6ea724ae5b100eb506011f045d3d4e16/zstandard-0.23.0-cp312-cp312-win32.whl", hash = "sha256:dc1d33abb8a0d754ea4763bad944fd965d3d95b5baef6b121c0c9013eaf1907d", size = 430877, upload-time = "2024-07-15T00:16:11.758Z" }, - { url = "https://files.pythonhosted.org/packages/75/37/872d74bd7739639c4553bf94c84af7d54d8211b626b352bc57f0fd8d1e3f/zstandard-0.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:64585e1dba664dc67c7cdabd56c1e5685233fbb1fc1966cfba2a340ec0dfff7b", size = 495595, upload-time = "2024-07-15T00:16:13.731Z" }, + { url = "https://files.pythonhosted.org/packages/01/1f/5c72806f76043c0ef9191a2b65281dacdf3b65b0828eb13bb2c987c4fb90/zstandard-0.24.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:addfc23e3bd5f4b6787b9ca95b2d09a1a67ad5a3c318daaa783ff90b2d3a366e", size = 795228, upload-time = "2025-08-17T18:21:46.978Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ba/3059bd5cd834666a789251d14417621b5c61233bd46e7d9023ea8bc1043a/zstandard-0.24.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6b005bcee4be9c3984b355336283afe77b2defa76ed6b89332eced7b6fa68b68", size = 640520, upload-time = "2025-08-17T18:21:48.162Z" }, + { url = "https://files.pythonhosted.org/packages/57/07/f0e632bf783f915c1fdd0bf68614c4764cae9dd46ba32cbae4dd659592c3/zstandard-0.24.0-cp311-cp311-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:3f96a9130171e01dbb6c3d4d9925d604e2131a97f540e223b88ba45daf56d6fb", size = 5347682, upload-time = "2025-08-17T18:21:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/a6/4c/63523169fe84773a7462cd090b0989cb7c7a7f2a8b0a5fbf00009ba7d74d/zstandard-0.24.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cd0d3d16e63873253bad22b413ec679cf6586e51b5772eb10733899832efec42", size = 5057650, upload-time = "2025-08-17T18:21:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/c6/16/49013f7ef80293f5cebf4c4229535a9f4c9416bbfd238560edc579815dbe/zstandard-0.24.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:b7a8c30d9bf4bd5e4dcfe26900bef0fcd9749acde45cdf0b3c89e2052fda9a13", size = 5404893, upload-time = "2025-08-17T18:21:54.54Z" }, + { url = "https://files.pythonhosted.org/packages/4d/38/78e8bcb5fc32a63b055f2b99e0be49b506f2351d0180173674f516cf8a7a/zstandard-0.24.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:52cd7d9fa0a115c9446abb79b06a47171b7d916c35c10e0c3aa6f01d57561382", size = 5452389, upload-time = "2025-08-17T18:21:56.822Z" }, + { url = "https://files.pythonhosted.org/packages/55/8a/81671f05619edbacd49bd84ce6899a09fc8299be20c09ae92f6618ccb92d/zstandard-0.24.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a0f6fc2ea6e07e20df48752e7700e02e1892c61f9a6bfbacaf2c5b24d5ad504b", size = 5558888, upload-time = "2025-08-17T18:21:58.68Z" }, + { url = "https://files.pythonhosted.org/packages/49/cc/e83feb2d7d22d1f88434defbaeb6e5e91f42a4f607b5d4d2d58912b69d67/zstandard-0.24.0-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:e46eb6702691b24ddb3e31e88b4a499e31506991db3d3724a85bd1c5fc3cfe4e", size = 5048038, upload-time = "2025-08-17T18:22:00.642Z" }, + { url = "https://files.pythonhosted.org/packages/08/c3/7a5c57ff49ef8943877f85c23368c104c2aea510abb339a2dc31ad0a27c3/zstandard-0.24.0-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:d5e3b9310fd7f0d12edc75532cd9a56da6293840c84da90070d692e0bb15f186", size = 5573833, upload-time = "2025-08-17T18:22:02.402Z" }, + { url = "https://files.pythonhosted.org/packages/f9/00/64519983cd92535ba4bdd4ac26ac52db00040a52d6c4efb8d1764abcc343/zstandard-0.24.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:76cdfe7f920738ea871f035568f82bad3328cbc8d98f1f6988264096b5264efd", size = 4961072, upload-time = "2025-08-17T18:22:04.384Z" }, + { url = "https://files.pythonhosted.org/packages/72/ab/3a08a43067387d22994fc87c3113636aa34ccd2914a4d2d188ce365c5d85/zstandard-0.24.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:3f2fe35ec84908dddf0fbf66b35d7c2878dbe349552dd52e005c755d3493d61c", size = 5268462, upload-time = "2025-08-17T18:22:06.095Z" }, + { url = "https://files.pythonhosted.org/packages/49/cf/2abb3a1ad85aebe18c53e7eca73223f1546ddfa3bf4d2fb83fc5a064c5ca/zstandard-0.24.0-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:aa705beb74ab116563f4ce784fa94771f230c05d09ab5de9c397793e725bb1db", size = 5443319, upload-time = "2025-08-17T18:22:08.572Z" }, + { url = "https://files.pythonhosted.org/packages/40/42/0dd59fc2f68f1664cda11c3b26abdf987f4e57cb6b6b0f329520cd074552/zstandard-0.24.0-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:aadf32c389bb7f02b8ec5c243c38302b92c006da565e120dfcb7bf0378f4f848", size = 5822355, upload-time = "2025-08-17T18:22:10.537Z" }, + { url = "https://files.pythonhosted.org/packages/99/c0/ea4e640fd4f7d58d6f87a1e7aca11fb886ac24db277fbbb879336c912f63/zstandard-0.24.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e40cd0fc734aa1d4bd0e7ad102fd2a1aefa50ce9ef570005ffc2273c5442ddc3", size = 5365257, upload-time = "2025-08-17T18:22:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/27/a9/92da42a5c4e7e4003271f2e1f0efd1f37cfd565d763ad3604e9597980a1c/zstandard-0.24.0-cp311-cp311-win32.whl", hash = "sha256:cda61c46343809ecda43dc620d1333dd7433a25d0a252f2dcc7667f6331c7b61", size = 435559, upload-time = "2025-08-17T18:22:17.29Z" }, + { url = "https://files.pythonhosted.org/packages/e2/8e/2c8e5c681ae4937c007938f954a060fa7c74f36273b289cabdb5ef0e9a7e/zstandard-0.24.0-cp311-cp311-win_amd64.whl", hash = "sha256:3b95fc06489aa9388400d1aab01a83652bc040c9c087bd732eb214909d7fb0dd", size = 505070, upload-time = "2025-08-17T18:22:14.808Z" }, + { url = "https://files.pythonhosted.org/packages/52/10/a2f27a66bec75e236b575c9f7b0d7d37004a03aa2dcde8e2decbe9ed7b4d/zstandard-0.24.0-cp311-cp311-win_arm64.whl", hash = "sha256:ad9fd176ff6800a0cf52bcf59c71e5de4fa25bf3ba62b58800e0f84885344d34", size = 461507, upload-time = "2025-08-17T18:22:15.964Z" }, + { url = "https://files.pythonhosted.org/packages/26/e9/0bd281d9154bba7fc421a291e263911e1d69d6951aa80955b992a48289f6/zstandard-0.24.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a2bda8f2790add22773ee7a4e43c90ea05598bffc94c21c40ae0a9000b0133c3", size = 795710, upload-time = "2025-08-17T18:22:19.189Z" }, + { url = "https://files.pythonhosted.org/packages/36/26/b250a2eef515caf492e2d86732e75240cdac9d92b04383722b9753590c36/zstandard-0.24.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:cc76de75300f65b8eb574d855c12518dc25a075dadb41dd18f6322bda3fe15d5", size = 640336, upload-time = "2025-08-17T18:22:20.466Z" }, + { url = "https://files.pythonhosted.org/packages/79/bf/3ba6b522306d9bf097aac8547556b98a4f753dc807a170becaf30dcd6f01/zstandard-0.24.0-cp312-cp312-manylinux2010_i686.manylinux2014_i686.manylinux_2_12_i686.manylinux_2_17_i686.whl", hash = "sha256:d2b3b4bda1a025b10fe0269369475f420177f2cb06e0f9d32c95b4873c9f80b8", size = 5342533, upload-time = "2025-08-17T18:22:22.326Z" }, + { url = "https://files.pythonhosted.org/packages/ea/ec/22bc75bf054e25accdf8e928bc68ab36b4466809729c554ff3a1c1c8bce6/zstandard-0.24.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b84c6c210684286e504022d11ec294d2b7922d66c823e87575d8b23eba7c81f", size = 5062837, upload-time = "2025-08-17T18:22:24.416Z" }, + { url = "https://files.pythonhosted.org/packages/48/cc/33edfc9d286e517fb5b51d9c3210e5bcfce578d02a675f994308ca587ae1/zstandard-0.24.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c59740682a686bf835a1a4d8d0ed1eefe31ac07f1c5a7ed5f2e72cf577692b00", size = 5393855, upload-time = "2025-08-17T18:22:26.786Z" }, + { url = "https://files.pythonhosted.org/packages/73/36/59254e9b29da6215fb3a717812bf87192d89f190f23817d88cb8868c47ac/zstandard-0.24.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:6324fde5cf5120fbf6541d5ff3c86011ec056e8d0f915d8e7822926a5377193a", size = 5451058, upload-time = "2025-08-17T18:22:28.885Z" }, + { url = "https://files.pythonhosted.org/packages/9a/c7/31674cb2168b741bbbe71ce37dd397c9c671e73349d88ad3bca9e9fae25b/zstandard-0.24.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51a86bd963de3f36688553926a84e550d45d7f9745bd1947d79472eca27fcc75", size = 5546619, upload-time = "2025-08-17T18:22:31.115Z" }, + { url = "https://files.pythonhosted.org/packages/e6/01/1a9f22239f08c00c156f2266db857545ece66a6fc0303d45c298564bc20b/zstandard-0.24.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:d82ac87017b734f2fb70ff93818c66f0ad2c3810f61040f077ed38d924e19980", size = 5046676, upload-time = "2025-08-17T18:22:33.077Z" }, + { url = "https://files.pythonhosted.org/packages/a7/91/6c0cf8fa143a4988a0361380ac2ef0d7cb98a374704b389fbc38b5891712/zstandard-0.24.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:92ea7855d5bcfb386c34557516c73753435fb2d4a014e2c9343b5f5ba148b5d8", size = 5576381, upload-time = "2025-08-17T18:22:35.391Z" }, + { url = "https://files.pythonhosted.org/packages/e2/77/1526080e22e78871e786ccf3c84bf5cec9ed25110a9585507d3c551da3d6/zstandard-0.24.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3adb4b5414febf074800d264ddf69ecade8c658837a83a19e8ab820e924c9933", size = 4953403, upload-time = "2025-08-17T18:22:37.266Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d0/a3a833930bff01eab697eb8abeafb0ab068438771fa066558d96d7dafbf9/zstandard-0.24.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6374feaf347e6b83ec13cc5dcfa70076f06d8f7ecd46cc71d58fac798ff08b76", size = 5267396, upload-time = "2025-08-17T18:22:39.757Z" }, + { url = "https://files.pythonhosted.org/packages/f3/5e/90a0db9a61cd4769c06374297ecfcbbf66654f74cec89392519deba64d76/zstandard-0.24.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:13fc548e214df08d896ee5f29e1f91ee35db14f733fef8eabea8dca6e451d1e2", size = 5433269, upload-time = "2025-08-17T18:22:42.131Z" }, + { url = "https://files.pythonhosted.org/packages/ce/58/fc6a71060dd67c26a9c5566e0d7c99248cbe5abfda6b3b65b8f1a28d59f7/zstandard-0.24.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:0a416814608610abf5488889c74e43ffa0343ca6cf43957c6b6ec526212422da", size = 5814203, upload-time = "2025-08-17T18:22:44.017Z" }, + { url = "https://files.pythonhosted.org/packages/5c/6a/89573d4393e3ecbfa425d9a4e391027f58d7810dec5cdb13a26e4cdeef5c/zstandard-0.24.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0d66da2649bb0af4471699aeb7a83d6f59ae30236fb9f6b5d20fb618ef6c6777", size = 5359622, upload-time = "2025-08-17T18:22:45.802Z" }, + { url = "https://files.pythonhosted.org/packages/60/ff/2cbab815d6f02a53a9d8d8703bc727d8408a2e508143ca9af6c3cca2054b/zstandard-0.24.0-cp312-cp312-win32.whl", hash = "sha256:ff19efaa33e7f136fe95f9bbcc90ab7fb60648453b03f95d1de3ab6997de0f32", size = 435968, upload-time = "2025-08-17T18:22:49.493Z" }, + { url = "https://files.pythonhosted.org/packages/ce/a3/8f96b8ddb7ad12344218fbd0fd2805702dafd126ae9f8a1fb91eef7b33da/zstandard-0.24.0-cp312-cp312-win_amd64.whl", hash = "sha256:bc05f8a875eb651d1cc62e12a4a0e6afa5cd0cc231381adb830d2e9c196ea895", size = 505195, upload-time = "2025-08-17T18:22:47.193Z" }, + { url = "https://files.pythonhosted.org/packages/a3/4a/bfca20679da63bfc236634ef2e4b1b4254203098b0170e3511fee781351f/zstandard-0.24.0-cp312-cp312-win_arm64.whl", hash = "sha256:b04c94718f7a8ed7cdd01b162b6caa1954b3c9d486f00ecbbd300f149d2b2606", size = 461605, upload-time = "2025-08-17T18:22:48.317Z" }, ]